import AudioToolbox import Foundation import os /// os_unfair_lock in a stable heap slot. Short critical sections only; the render thread takes it. final class UnfairLock: @unchecked Sendable { private let p: UnsafeMutablePointer init() { p = .allocate(capacity: 1) p.initialize(to: os_unfair_lock()) } deinit { p.deallocate() } @inline(__always) func lock() { os_unfair_lock_lock(p) } @inline(__always) func unlock() { os_unfair_lock_unlock(p) } @inline(__always) func with(_ f: () throws -> T) rethrows -> T { os_unfair_lock_lock(p); defer { os_unfair_lock_unlock(p) } return try f() } } /// Single-producer single-consumer ring of interleaved stereo float frames. final class FloatRing: @unchecked Sendable { private let buf: UnsafeMutablePointer private let cap: Int private var readPos: Int64 = 0 private var writePos: Int64 = 0 private let lock = UnfairLock() private let notFull = DispatchSemaphore(value: 0) private var eosFlag = false /// A writer blocked on a full ring is woken once a quarter of it is free, not after every /// read, so decoders run in bursts and sleep in between. private var writerWaiting = false private var lowWater: Int { cap / 4 } init(capacityFrames: Int) { cap = capacityFrames * 2 buf = .allocate(capacity: cap) buf.initialize(repeating: 0, count: cap) } deinit { buf.deallocate() } var eos: Bool { get { lock.with { eosFlag } } set { lock.with { eosFlag = newValue } } } var availableFrames: Int { lock.with { Int((writePos - readPos) / 2) } } var freeFrames: Int { lock.with { Int((Int64(cap) - (writePos - readPos)) / 2) } } var capacityFrames: Int { cap / 2 } /// Blocks while full; returns false if [abort] became true first. [len] counts floats. func write(_ src: UnsafePointer, _ len: Int, abort: () -> Bool) -> Bool { var done = 0 while done < len { lock.lock() let free = Int(Int64(cap) - (writePos - readPos)) if free < min(len - done, lowWater) { writerWaiting = true lock.unlock() if abort() { return false } // seek() and release() signal too, so the timeout is only a backstop. _ = notFull.wait(timeout: .now() + .milliseconds(250)) continue } lock.unlock() if abort() { return false } lock.lock() let n = min(Int(Int64(cap) - (writePos - readPos)), len - done) let w = Int(writePos % Int64(cap)) let first = min(n, cap - w) (buf + w).update(from: src + done, count: first) if n > first { buf.update(from: src + done + first, count: n - first) } writePos += Int64(n) done += n lock.unlock() } return true } /// Copies up to [frames] frames into dst; returns frames copied. Never blocks on data. func read(_ dst: UnsafeMutablePointer, _ frames: Int) -> Int { lock.lock() let n = Int(min(Int64(frames * 2), writePos - readPos)) let r = Int(readPos % Int64(cap)) let first = min(n, cap - r) dst.update(from: buf + r, count: first) if n > first { (dst + first).update(from: buf, count: n - first) } readPos += Int64(n) let wake = writerWaiting && Int(Int64(cap) - (writePos - readPos)) >= lowWater if wake { writerWaiting = false } lock.unlock() if wake { notFull.signal() } return n / 2 } func clear() { lock.with { readPos = writePos eosFlag = false writerWaiting = false } notFull.signal() } func wake() { notFull.signal() } } /// The decoder's view of a cached resource. Retries brief network trouble before giving up. final class ResourceDataSource: @unchecked Sendable { private let res: CachedResource private let onClose: () -> Void private let lock = NSLock() private var failure: Error? private var isClosed = false init(_ res: CachedResource, onClose: @escaping () -> Void = {}) { self.res = res self.onClose = onClose } var failed: Error? { lock.withLock { failure } } var closed: Bool { lock.withLock { isClosed } } /// Bytes read, 0 at end of file. func readAt(_ position: Int64, _ buffer: UnsafeMutableRawPointer, _ size: Int) throws -> Int { var attempt = 0 while true { do { let n = try res.readAt(position, buffer, size) lock.withLock { failure = nil } if n > 0 && attempt == 0 { res.prefetch() } return max(n, 0) } catch { attempt += 1 if closed || attempt > 8 { lock.withLock { failure = error } throw error } Thread.sleep(forTimeInterval: 0.25 * Double(attempt)) } } } func size() -> Int64 { do { return try res.size() } catch { lock.withLock { failure = error } return -1 } } func close() { let first = lock.withLock { () -> Bool in if isClosed { return false } isClosed = true return true } if first { onClose() } } } struct DecodeError: LocalizedError { let message: String init(_ m: String) { message = m } var errorDescription: String? { message } } /// Decodes one stem on its own thread into [ring], as stereo float at the file's own rate. /// FLAC, Ogg Opus and whatever else Core Audio reads all go through ExtAudioFile, whose seeks /// are sample-accurate, so every stem of a track resumes on exactly the same frame. final class StemDecoder: @unchecked Sendable { let name: String let ring: FloatRing private let source: ResourceDataSource private let typeHint: AudioFileTypeID private let prepared = NSCondition() private var isPrepared = false private let state = UnfairLock() private var _sampleRate = 0 private var _durationUs: Int64 = 0 private var _error: Error? private var _readyGen = -1 var sampleRate: Int { state.with { _sampleRate } } var durationUs: Int64 { state.with { _durationUs } } var error: Error? { state.with { _error } } /// Generation whose data is in the ring (or that hit EOS). var readyGen: Int { state.with { _readyGen } } private let seekLock = NSCondition() private var pendingGen = -1 private var pendingFrame: Int64 = 0 private var releasedFlag = false private var released: Bool { seekLock.withLock { releasedFlag } } private var thread: Thread? /// [hint] is the file's extension or format ("flac", "opus", "mp3", ...), so Core Audio /// doesn't have to guess from the bytes. init(name: String, source: ResourceDataSource, ringFrames: Int, hint: String) { self.name = name self.source = source ring = FloatRing(capacityFrames: ringFrames) typeHint = Self.fileType(hint) } static func fileType(_ hint: String) -> AudioFileTypeID { switch hint.lowercased() { case "flac": return kAudioFileFLACType case "opus", "ogg", "oga": return AudioFileTypeID(0x4F67_6773) // 'Oggs' case "mp3": return kAudioFileMP3Type case "m4a", "mp4", "alac": return kAudioFileM4AType case "aac": return kAudioFileAAC_ADTSType case "wav": return kAudioFileWAVEType case "aif", "aiff": return kAudioFileAIFFType case "caf": return kAudioFileCAFType default: return 0 } } func start(gen: Int, frame: Int64) { seek(gen: gen, frame: frame) let t = Thread { [self] in run() } t.name = "stem-\(name)" // Seconds ahead of the mixer, so efficiency cores are fast enough. t.qualityOfService = .userInitiated thread = t t.start() } /// Blocks until the stream's format is known. False on error or timeout. func awaitPrepared(timeoutMs: Int) -> Bool { let deadline = Date().addingTimeInterval(Double(timeoutMs) / 1000) prepared.lock() while !isPrepared { if !prepared.wait(until: deadline) { break } } let ok = isPrepared prepared.unlock() return ok && error == nil } private func markPrepared() { prepared.lock() isPrepared = true prepared.broadcast() prepared.unlock() } func seek(gen: Int, frame: Int64) { seekLock.lock() pendingGen = gen pendingFrame = frame seekLock.broadcast() seekLock.unlock() ring.wake() } private func seekPending(_ current: Int) -> Bool { seekLock.withLock { pendingGen != current || releasedFlag } } func release() { seekLock.lock() releasedFlag = true seekLock.broadcast() seekLock.unlock() source.close() ring.wake() } private static let readProc: AudioFile_ReadProc = { ud, pos, req, buf, actual in let src = Unmanaged.fromOpaque(ud).takeUnretainedValue() do { actual.pointee = UInt32(try src.readAt(pos, buf, Int(req))) return noErr } catch { actual.pointee = 0 return kAudioFileUnspecifiedError } } private static let sizeProc: AudioFile_GetSizeProc = { ud in max(0, Unmanaged.fromOpaque(ud).takeUnretainedValue().size()) } private func run() { var fileId: AudioFileID? var ext: ExtAudioFileRef? let srcRef = Unmanaged.passRetained(source) defer { if let ext { ExtAudioFileDispose(ext) } if let fileId { AudioFileClose(fileId) } srcRef.release() markPrepared() } do { var st = AudioFileOpenWithCallbacks(srcRef.toOpaque(), Self.readProc, nil, Self.sizeProc, nil, typeHint, &fileId) if st != noErr || fileId == nil { throw source.failed ?? DecodeError("Can't read \(name) (\(st))") } st = ExtAudioFileWrapAudioFileID(fileId!, false, &ext) guard st == noErr, let ext else { throw DecodeError("Can't decode \(name) (\(st))") } var fmt = AudioStreamBasicDescription() var sz = UInt32(MemoryLayout.size) st = ExtAudioFileGetProperty(ext, kExtAudioFileProperty_FileDataFormat, &sz, &fmt) guard st == noErr, fmt.mSampleRate > 0 else { throw DecodeError("No audio in \(name)") } let rate = Int(fmt.mSampleRate.rounded()) var frames: Int64 = 0 sz = UInt32(MemoryLayout.size) ExtAudioFileGetProperty(ext, kExtAudioFileProperty_FileLengthFrames, &sz, &frames) // Asking for stereo makes the converter upmix mono and drop channels past the // first two, the same as Android's toStereo. var client = AudioStreamBasicDescription( mSampleRate: fmt.mSampleRate, mFormatID: kAudioFormatLinearPCM, mFormatFlags: kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, mBytesPerPacket: 8, mFramesPerPacket: 1, mBytesPerFrame: 8, mChannelsPerFrame: 2, mBitsPerChannel: 32, mReserved: 0) st = ExtAudioFileSetProperty(ext, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout.size(ofValue: client)), &client) guard st == noErr else { throw DecodeError("Can't convert \(name) (\(st))") } if fmt.mChannelsPerFrame > 2 { // Keep L/R instead of letting the converter downmix surround. var conv: AudioConverterRef? sz = UInt32(MemoryLayout.size) if ExtAudioFileGetProperty(ext, kExtAudioFileProperty_AudioConverter, &sz, &conv) == noErr, let conv { var map: [Int32] = [0, 1] AudioConverterSetProperty(conv, kAudioConverterChannelMap, UInt32(MemoryLayout.size * 2), &map) var nul: Unmanaged? ExtAudioFileSetProperty(ext, kExtAudioFileProperty_ConverterConfig, UInt32(MemoryLayout?>.size), &nul) } } state.with { _sampleRate = rate _durationUs = frames > 0 ? frames * 1_000_000 / Int64(rate) : 0 } markPrepared() let chunk = 4096 let scratch = UnsafeMutablePointer.allocate(capacity: chunk * 2) defer { scratch.deallocate() } var gen = -1 var outputDone = false var pos: Int64 = 0 while !released { seekLock.lock() if pendingGen != gen { gen = pendingGen let target = pendingFrame seekLock.unlock() ring.clear() st = ExtAudioFileSeek(ext, target) if st != noErr { throw source.failed ?? DecodeError("Seek failed in \(name) (\(st))") } pos = target outputDone = false } else if outputDone { ring.eos = true state.with { _readyGen = gen } _ = seekLock.wait(until: Date().addingTimeInterval(0.1)) seekLock.unlock() continue } else { seekLock.unlock() } if released { break } var n = UInt32(chunk) var abl = AudioBufferList(mNumberBuffers: 1, mBuffers: AudioBuffer(mNumberChannels: 2, mDataByteSize: UInt32(chunk * 8), mData: scratch)) st = ExtAudioFileRead(ext, &n, &abl) if st != noErr { if released { break } throw source.failed ?? DecodeError("Decode failed in \(name) (\(st))") } if n == 0 { // A short file is only an error if the source gave up well before the end. if let f = source.failed, frames > 0, pos < frames - Int64(rate) * 2 { throw f } outputDone = true continue } pos += Int64(n) let myGen = gen if ring.write(scratch, Int(n) * 2, abort: { seekPending(myGen) }) { state.with { _readyGen = gen } } } } catch { if !released { Log.shared.error("decoder", "\(name) failed: \(error.localizedDescription)") state.with { _error = error } } ring.eos = true state.with { _readyGen = Int.max } } } }