| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- #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..<frameCount {
- for ch in 0..<channels {
- let bufferOffset = Int(totalDecoded) + frame
- if bufferOffset < Int(frameCapacity) {
- buffer.floatChannelData![ch][bufferOffset] = tempInterleaved[frame * channels + ch]
- }
- }
- }
- totalDecoded += Int64(frameCount)
- }
- guard totalDecoded > 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
|