#if !DISABLE_OPUS import AVFoundation import Foundation /// Decodes OGG Opus files using libopusfile (compiled for iOS ARM64). struct OpusDecoder { enum OpusError: LocalizedError { case failedToOpen(String, Int32) case failedToCreateBuffer case decodingFailed var errorDescription: String? { switch self { case .failedToOpen(let name, let code): return "Failed to open Opus file '\(name)' (error \(code))" case .failedToCreateBuffer: return "Failed to create audio buffer" case .decodingFailed: return "Opus decoding failed" } } } static func isOpusFile(_ url: URL) -> Bool { url.pathExtension.lowercased() == "opus" } /// Decode an OGG Opus file to an AVAudioPCMBuffer. static func decode(url: URL) throws -> (buffer: AVAudioPCMBuffer, format: AVAudioFormat) { var error: Int32 = 0 guard let opusFile = op_open_file(url.path, &error) else { throw OpusError.failedToOpen(url.lastPathComponent, error) } defer { op_free(opusFile) } let channels = Int(op_channel_count(opusFile, -1)) let totalSamples = op_pcm_total(opusFile, -1) let sampleRate: Double = 48000 // Opus always decodes at 48kHz guard let format = AVAudioFormat( commonFormat: .pcmFormatFloat32, sampleRate: sampleRate, channels: AVAudioChannelCount(channels), interleaved: false ) else { throw OpusError.failedToCreateBuffer } let frameCapacity = totalSamples > 0 ? AVAudioFrameCount(totalSamples) : AVAudioFrameCount(sampleRate * 600) guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCapacity) else { throw OpusError.failedToCreateBuffer } // Decode in chunks — op_read_float gives interleaved float samples let chunkSize = 5760 * channels // 120ms at 48kHz var tempInterleaved = [Float](repeating: 0, count: chunkSize) var totalDecoded: Int64 = 0 while true { let samplesRead = op_read_float(opusFile, &tempInterleaved, Int32(chunkSize / channels), nil) if samplesRead <= 0 { break } let frameCount = Int(samplesRead) // Deinterleave into the PCM buffer's channel pointers for frame in 0.. 0 else { throw OpusError.decodingFailed } buffer.frameLength = AVAudioFrameCount(min(totalDecoded, Int64(frameCapacity))) return (buffer, format) } /// Get duration of an Opus file in seconds. static func duration(url: URL) -> TimeInterval { var error: Int32 = 0 guard let opusFile = op_open_file(url.path, &error) else { return 0 } defer { op_free(opusFile) } let totalSamples = op_pcm_total(opusFile, -1) return Double(totalSamples) / 48000.0 } /// Get basic info about an Opus file. static func fileInfo(url: URL) -> (sampleRate: Double, channels: Int, duration: TimeInterval)? { var error: Int32 = 0 guard let opusFile = op_open_file(url.path, &error) else { return nil } defer { op_free(opusFile) } let channels = Int(op_channel_count(opusFile, -1)) let totalSamples = op_pcm_total(opusFile, -1) let duration = Double(totalSamples) / 48000.0 return (sampleRate: 48000, channels: channels, duration: duration) } } #endif // !DISABLE_OPUS