OpusDecoder.swift 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. #if !DISABLE_OPUS
  2. import AVFoundation
  3. import Foundation
  4. /// Decodes OGG Opus files using libopusfile (compiled for iOS ARM64).
  5. struct OpusDecoder {
  6. enum OpusError: LocalizedError {
  7. case failedToOpen(String, Int32)
  8. case failedToCreateBuffer
  9. case decodingFailed
  10. var errorDescription: String? {
  11. switch self {
  12. case .failedToOpen(let name, let code): return "Failed to open Opus file '\(name)' (error \(code))"
  13. case .failedToCreateBuffer: return "Failed to create audio buffer"
  14. case .decodingFailed: return "Opus decoding failed"
  15. }
  16. }
  17. }
  18. static func isOpusFile(_ url: URL) -> Bool {
  19. url.pathExtension.lowercased() == "opus"
  20. }
  21. /// Decode an OGG Opus file to an AVAudioPCMBuffer.
  22. static func decode(url: URL) throws -> (buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
  23. var error: Int32 = 0
  24. guard let opusFile = op_open_file(url.path, &error) else {
  25. throw OpusError.failedToOpen(url.lastPathComponent, error)
  26. }
  27. defer { op_free(opusFile) }
  28. let channels = Int(op_channel_count(opusFile, -1))
  29. let totalSamples = op_pcm_total(opusFile, -1)
  30. let sampleRate: Double = 48000 // Opus always decodes at 48kHz
  31. guard let format = AVAudioFormat(
  32. commonFormat: .pcmFormatFloat32,
  33. sampleRate: sampleRate,
  34. channels: AVAudioChannelCount(channels),
  35. interleaved: false
  36. ) else {
  37. throw OpusError.failedToCreateBuffer
  38. }
  39. let frameCapacity = totalSamples > 0 ? AVAudioFrameCount(totalSamples) : AVAudioFrameCount(sampleRate * 600)
  40. guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCapacity) else {
  41. throw OpusError.failedToCreateBuffer
  42. }
  43. // Decode in chunks — op_read_float gives interleaved float samples
  44. let chunkSize = 5760 * channels // 120ms at 48kHz
  45. var tempInterleaved = [Float](repeating: 0, count: chunkSize)
  46. var totalDecoded: Int64 = 0
  47. while true {
  48. let samplesRead = op_read_float(opusFile, &tempInterleaved, Int32(chunkSize / channels), nil)
  49. if samplesRead <= 0 { break }
  50. let frameCount = Int(samplesRead)
  51. // Deinterleave into the PCM buffer's channel pointers
  52. for frame in 0..<frameCount {
  53. for ch in 0..<channels {
  54. let bufferOffset = Int(totalDecoded) + frame
  55. if bufferOffset < Int(frameCapacity) {
  56. buffer.floatChannelData![ch][bufferOffset] = tempInterleaved[frame * channels + ch]
  57. }
  58. }
  59. }
  60. totalDecoded += Int64(frameCount)
  61. }
  62. guard totalDecoded > 0 else {
  63. throw OpusError.decodingFailed
  64. }
  65. buffer.frameLength = AVAudioFrameCount(min(totalDecoded, Int64(frameCapacity)))
  66. return (buffer, format)
  67. }
  68. /// Get duration of an Opus file in seconds.
  69. static func duration(url: URL) -> TimeInterval {
  70. var error: Int32 = 0
  71. guard let opusFile = op_open_file(url.path, &error) else { return 0 }
  72. defer { op_free(opusFile) }
  73. let totalSamples = op_pcm_total(opusFile, -1)
  74. return Double(totalSamples) / 48000.0
  75. }
  76. /// Get basic info about an Opus file.
  77. static func fileInfo(url: URL) -> (sampleRate: Double, channels: Int, duration: TimeInterval)? {
  78. var error: Int32 = 0
  79. guard let opusFile = op_open_file(url.path, &error) else { return nil }
  80. defer { op_free(opusFile) }
  81. let channels = Int(op_channel_count(opusFile, -1))
  82. let totalSamples = op_pcm_total(opusFile, -1)
  83. let duration = Double(totalSamples) / 48000.0
  84. return (sampleRate: 48000, channels: channels, duration: duration)
  85. }
  86. }
  87. #endif // !DISABLE_OPUS