Skip to content

Commit

Permalink
Fixes audio cutoff on flac files (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
dimitris-c authored Jul 28, 2024
1 parent d24bca4 commit f8f8361
Show file tree
Hide file tree
Showing 10 changed files with 56 additions and 53 deletions.
7 changes: 7 additions & 0 deletions AudioPlayer/AudioPlayer/Common/AudioContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum AudioContent {
case remoteWave
case local
case localWave
case loopBeatFlac
case custom(String)

var title: String {
Expand Down Expand Up @@ -49,6 +50,8 @@ enum AudioContent {
return "Jazzy Frenchy"
case .nonOptimized:
return "Jazzy Frenchy"
case .loopBeatFlac:
return "Beat loop"
case .custom(let url):
return url
}
Expand Down Expand Up @@ -82,6 +85,8 @@ enum AudioContent {
return "Music by: bensound.com - m4a optimized"
case .nonOptimized:
return "Music by: bensound.com - m4a non-optimized"
case .loopBeatFlac:
return "Remote flac"
case .custom:
return ""
}
Expand Down Expand Up @@ -117,6 +122,8 @@ enum AudioContent {
return URL(fileURLWithPath: path)
case .remoteWave:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/5-MB-WAV.wav")!
case .loopBeatFlac:
return URL(string: "https://github.com/dimitris-c/sample-audio/raw/main/drumbeat-loop.flac")!
case .custom(let url):
return URL(string: url)!
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public class AudioPlayerModel {
}

private let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox]
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave]
private let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave, .loopBeatFlac]

func audioTracksProvider() -> [AudioPlaylist] {
[
Expand Down
15 changes: 1 addition & 14 deletions AudioStreaming/Core/Helpers/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,25 +42,21 @@ final class UnfairLock: Lock {
}

@inlinable
@inline(__always)
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
try unfairLock.withLock(body: body)
}

@inlinable
@inline(__always)
func withLock(body: () -> Void) {
unfairLock.withLock(body: body)
}

@inlinable
@inline(__always)
func lock() {
unfairLock.lock()
}

@inlinable
@inline(__always)
func unlock() {
unfairLock.unlock()
}
Expand All @@ -73,13 +69,11 @@ private class OSStorageLock: Lock {
let osLock = OSAllocatedUnfairLock()

@inlinable
@inline(__always)
func lock() {
osLock.lock()
}

@inlinable
@inline(__always)
func unlock() {
osLock.unlock()
}
Expand All @@ -105,38 +99,31 @@ private class UnfairStorageLock: Lock {
unfairLock.initialize(to: os_unfair_lock())
}

deinit {
deallocate()
}

func deallocate() {
unfairLock.deinitialize(count: 1)
unfairLock.deallocate()
}

@inlinable
@inline(__always)
func withLock<Result>(body: () throws -> Result) rethrows -> Result {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
return try body()
}

@inlinable
@inline(__always)
func withLock(body: () -> Void) {
os_unfair_lock_lock(unfairLock)
defer { os_unfair_lock_unlock(unfairLock) }
body()
}

@inlinable
@inline(__always)
func lock() {
os_unfair_lock_lock(unfairLock)
}

@inlinable
@inline(__always)
func unlock() {
os_unfair_lock_unlock(unfairLock)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct AudioPlayerConfiguration: Equatable {
bufferSizeInSeconds: 10,
secondsRequiredToStartPlaying: 1,
gracePeriodAfterSeekInSeconds: 0.5,
secondsRequiredToStartPlayingAfterBufferUnderrun: 1,
secondsRequiredToStartPlayingAfterBufferUnderrun: 7,
enableLogs: false)
/// Initializes the configuration for the `AudioPlayer`
///
Expand Down
10 changes: 5 additions & 5 deletions AudioStreaming/Streaming/AudioPlayer/AudioPlayerState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ extension AudioPlayer {

static let initial = InternalState([])
static let running = InternalState(rawValue: 1)
static let playing = InternalState(rawValue: 1 << 1 | InternalState.running.rawValue)
static let rebuffering = InternalState(rawValue: 1 << 2 | InternalState.running.rawValue)
static let waitingForData = InternalState(rawValue: 1 << 3 | InternalState.running.rawValue)
static let waitingForDataAfterSeek = InternalState(rawValue: 1 << 4 | InternalState.running.rawValue)
static let paused = InternalState(rawValue: 1 << 5 | InternalState.running.rawValue)
static let playing = InternalState(rawValue: (1 << 1) | InternalState.running.rawValue)
static let rebuffering = InternalState(rawValue: (1 << 2) | InternalState.running.rawValue)
static let waitingForData = InternalState(rawValue: (1 << 3) | InternalState.running.rawValue)
static let waitingForDataAfterSeek = InternalState(rawValue: (1 << 4) | InternalState.running.rawValue)
static let paused = InternalState(rawValue: (1 << 5) | InternalState.running.rawValue)
static let stopped = InternalState(rawValue: 1 << 9)
static let pendingNext = InternalState(rawValue: 1 << 10)
static let disposed = InternalState(rawValue: 1 << 30)
Expand Down
12 changes: 6 additions & 6 deletions AudioStreaming/Streaming/AudioPlayer/AudioRendererContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ final class AudioRendererContext {

let packetsSemaphore = DispatchSemaphore(value: 0)

let framesRequiredToStartPlaying: UInt32
let framesRequiredAfterRebuffering: UInt32
let framesRequiredForDataAfterSeekPlaying: UInt32
let framesRequiredToStartPlaying: Double
let framesRequiredAfterRebuffering: Double
let framesRequiredForDataAfterSeekPlaying: Double

let waitingForDataAfterSeekFrameCount = Atomic<Int32>(0)

Expand All @@ -33,9 +33,9 @@ final class AudioRendererContext {

let canonicalStream = outputAudioFormat.basicStreamDescription

framesRequiredToStartPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlaying)
framesRequiredAfterRebuffering = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
framesRequiredForDataAfterSeekPlaying = UInt32(canonicalStream.mSampleRate) * UInt32(configuration.gracePeriodAfterSeekInSeconds)
framesRequiredToStartPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlaying)
framesRequiredAfterRebuffering = Double(canonicalStream.mSampleRate) * Double(configuration.secondsRequiredToStartPlayingAfterBufferUnderrun)
framesRequiredForDataAfterSeekPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.gracePeriodAfterSeekInSeconds)

let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame)
inOutAudioBufferList = allocateBufferList(dataByteSize: dataByteSize)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,10 +228,12 @@ final class AudioFileStreamProcessor {
processAudioDataPacketCount(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_ReadyToProducePackets:
// check converter for discontinuous stream
processReadyToProducePackets(entry: entry, fileStream: fileStream)
processPacketUpperBoundAndMaxPacketSize(entry: entry, fileStream: fileStream)
processReadyToProducePackets(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_FormatList:
processFormatList(entry: entry, fileStream: fileStream)
case kAudioFileStreamProperty_MagicCookieData:
assignMagicCookieToConverterIfNeeded()
default:
break
}
Expand All @@ -242,7 +244,7 @@ final class AudioFileStreamProcessor {
private func processDataOffset(entry: AudioEntry, fileStream: AudioFileStreamID) {
var offset: UInt64 = 0
fileStreamGetProperty(value: &offset, fileStream: fileStream, propertyId: kAudioFileStreamProperty_DataOffset)
entry.lock.lock(); defer { playerContext.audioReadingEntry?.lock.unlock() }
entry.lock.lock(); defer { entry.lock.unlock() }
entry.audioStreamState.processedDataFormat = true
entry.audioStreamState.dataOffset = offset
}
Expand All @@ -253,7 +255,9 @@ final class AudioFileStreamProcessor {
AudioFileStreamGetProperty(fileStream, kAudioFileStreamProperty_AudioDataPacketCount, &packetCountSize, &packetCount)
entry.lock.lock(); defer { entry.lock.unlock() }
entry.audioStreamState.dataPacketCount = Double(packetCount)
if entry.audioStreamFormat.mFormatID != kAudioFormatLinearPCM {
let entryFormatID = entry.audioStreamFormat.mFormatID
let isFLAC = entryFormatID == kAudioFormatFLAC
if entryFormatID != kAudioFormatLinearPCM && !isFLAC {
discontinuous = true
}
}
Expand Down Expand Up @@ -370,6 +374,11 @@ final class AudioFileStreamProcessor {
guard let entry = playerContext.audioReadingEntry else { return }
guard entry.audioStreamState.processedDataFormat else { return }

guard let converter = audioConverter else {
Logger.error("Couldn't find audio converter", category: .audioRendering)
return
}

if let playingEntry = playerContext.audioPlayingEntry,
playingEntry.seekRequest.requested, playingEntry.calculatedBitrate() > 0
{
Expand All @@ -380,33 +389,32 @@ final class AudioFileStreamProcessor {
return
}

guard let converter = audioConverter else {
Logger.error("Couldn't find audio converter", category: .audioRendering)
return
}

// reset discontinuity
discontinuous = false

var convertInfo = AudioConvertInfo(done: false,
numberOfPackets: inNumberPackets,
packDescription: inPacketDescriptions)
var convertInfo = AudioConvertInfo(
done: false,
numberOfPackets: inNumberPackets,
packDescription: inPacketDescriptions
)
convertInfo.audioBuffer.mData = UnsafeMutableRawPointer(mutating: inInputData)
convertInfo.audioBuffer.mDataByteSize = inNumberBytes
if let playingAudioStreamFormat = playerContext.audioPlayingEntry?.audioStreamFormat {
convertInfo.audioBuffer.mNumberChannels = playingAudioStreamFormat.mChannelsPerFrame
}

updateProcessedPackets(inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets)
updateProcessedPackets(
inPacketDescriptions: inPacketDescriptions,
inNumberPackets: inNumberPackets
)

var status: OSStatus = noErr
packetProcess: while status == noErr {
rendererContext.lock.lock()
let bufferContext = rendererContext.bufferContext
var used = bufferContext.frameUsedCount
var start = bufferContext.frameStartIndex
var end = bufferContext.end
var end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount

var framesLeftInBuffer = bufferContext.totalFrameCount - used
rendererContext.lock.unlock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,29 +64,30 @@ final class AudioPlayerRenderProcessor: NSObject {
let frameSizeInBytes = bufferContext.sizeInBytes
let used = bufferContext.frameUsedCount
let start = bufferContext.frameStartIndex
let end = bufferContext.end
let end = (bufferContext.frameStartIndex + bufferContext.frameUsedCount) % bufferContext.totalFrameCount
let signal = rendererContext.waiting.value && used < bufferContext.totalFrameCount / 2

if let playingEntry = playingEntry {
playingEntry.lock.lock()
let framesState = playingEntry.framesState
playingEntry.lock.unlock()

if state == .waitingForData {
var requiredFramesToStart = rendererContext.framesRequiredToStartPlaying
if framesState.lastFrameQueued >= 0 {
requiredFramesToStart = min(requiredFramesToStart, UInt32(playingEntry.framesState.lastFrameQueued))
requiredFramesToStart = min(requiredFramesToStart, Double(playingEntry.framesState.lastFrameQueued))
}
if let readingEntry = readingEntry, readingEntry === playingEntry,
framesState.queued < requiredFramesToStart

if readingEntry === playingEntry, framesState.queued < Int(requiredFramesToStart)
{
waitForBuffer = true
}
} else if state == .rebuffering {
var requiredFramesToStart = rendererContext.framesRequiredAfterRebuffering
if framesState.lastFrameQueued >= 0 {
requiredFramesToStart = min(requiredFramesToStart, UInt32(framesState.lastFrameQueued - framesState.queued))
requiredFramesToStart = min(requiredFramesToStart, Double(framesState.lastFrameQueued - framesState.queued))
}
if used < requiredFramesToStart {
if used < Int(requiredFramesToStart) {
waitForBuffer = true
}
} else if state == .waitingForDataAfterSeek {
Expand All @@ -102,7 +103,7 @@ final class AudioPlayerRenderProcessor: NSObject {
rendererContext.lock.unlock()

var totalFramesCopied: UInt32 = 0
if used > 0 && !waitForBuffer && state.contains(.running) && state != .paused {
if used > 0 && !waitForBuffer && playingEntry != nil && state.contains(.running) && state != .paused {
if end > start {
let framesToCopy = min(inNumberFrames, used)
bufferList.mBuffers.mNumberChannels = 2
Expand Down Expand Up @@ -162,6 +163,7 @@ final class AudioPlayerRenderProcessor: NSObject {
bufferContext.frameUsedCount -= totalFramesCopied
rendererContext.lock.unlock()
}

if playerContext.internalState != .playing {
playerContext.setInternalState(to: .playing, when: { state -> Bool in
state.contains(.running) && state != .paused
Expand All @@ -175,7 +177,7 @@ final class AudioPlayerRenderProcessor: NSObject {
memset(mData + Int(totalFramesCopied * frameSizeInBytes), 0, Int(delta * frameSizeInBytes))
}

if playingEntry != nil || AudioPlayer.InternalState.waiting.contains(state) {
if !(playingEntry == nil || state == .waitingForDataAfterSeek || state == .waitingForData || state == .rebuffering) {
if playerContext.internalState != .rebuffering {
playerContext.setInternalState(to: .rebuffering, when: { state -> Bool in
state.contains(.running) && state != .paused
Expand All @@ -184,7 +186,7 @@ final class AudioPlayerRenderProcessor: NSObject {
} else if state == .waitingForDataAfterSeek {
if totalFramesCopied == 0 {
rendererContext.waitingForDataAfterSeekFrameCount.write { $0 += Int32(inNumberFrames - totalFramesCopied) }
if rendererContext.waitingForDataAfterSeekFrameCount.value > rendererContext.framesRequiredForDataAfterSeekPlaying {
if rendererContext.waitingForDataAfterSeekFrameCount.value > Int(rendererContext.framesRequiredForDataAfterSeekPlaying) {
if playerContext.internalState != .playing {
playerContext.setInternalState(to: .playing) { state -> Bool in
state.contains(.running) && state != .playing
Expand Down
4 changes: 1 addition & 3 deletions AudioStreaming/Streaming/AudioPlayer/UnitDescriptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@

import AVFoundation

private let outputChannels: UInt32 = 2

enum UnitDescriptions {
static var output: AudioComponentDescription = {
static let output: AudioComponentDescription = {
var desc = AudioComponentDescription()
desc.componentType = kAudioUnitType_Output
#if os(iOS)
Expand Down
1 change: 1 addition & 0 deletions AudioStreaming/Streaming/Helpers/AudioFileType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ let fileTypesFromMimeType: [String: AudioFileTypeID] =
"video/3gpp": kAudioFile3GPType,
"audio/3gp2": kAudioFile3GP2Type,
"video/3gp2": kAudioFile3GP2Type,
"audio/flac": kAudioFileFLACType
]

/// Method that converts mime type to AudioFileTypeID
Expand Down

0 comments on commit f8f8361

Please sign in to comment.