OGGDecoder.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import AVFoundation
  2. import Foundation
  3. /// Decodes OGG Vorbis files to AVAudioPCMBuffer using stb_vorbis.
  4. /// This enables playback of .ogg files through AVAudioEngine.
  5. struct OGGDecoder {
  6. enum OGGError: LocalizedError {
  7. case failedToOpen(String)
  8. case failedToCreateBuffer
  9. case decodingFailed
  10. var errorDescription: String? {
  11. switch self {
  12. case .failedToOpen(let path): return "Failed to open OGG file: \(path)"
  13. case .failedToCreateBuffer: return "Failed to create audio buffer"
  14. case .decodingFailed: return "OGG decoding failed"
  15. }
  16. }
  17. }
  18. /// Decode an OGG file to an AVAudioPCMBuffer ready for AVAudioPlayerNode.
  19. static func decode(url: URL) throws -> (buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
  20. // Open the OGG file
  21. var error: Int32 = 0
  22. guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else {
  23. throw OGGError.failedToOpen(url.lastPathComponent)
  24. }
  25. defer { stb_vorbis_close(vorbis) }
  26. // Get file info
  27. let info = stb_vorbis_get_info(vorbis)
  28. let channels = Int(info.channels)
  29. let sampleRate = Double(info.sample_rate)
  30. let totalSamples = Int(stb_vorbis_stream_length_in_samples(vorbis))
  31. // Create the output format (non-interleaved float for AVAudioPlayerNode)
  32. guard let format = AVAudioFormat(
  33. commonFormat: .pcmFormatFloat32,
  34. sampleRate: sampleRate,
  35. channels: AVAudioChannelCount(channels),
  36. interleaved: false
  37. ) else {
  38. throw OGGError.failedToCreateBuffer
  39. }
  40. guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(totalSamples)) else {
  41. throw OGGError.failedToCreateBuffer
  42. }
  43. // Decode all samples — stb_vorbis gives interleaved floats,
  44. // so we decode to a temp buffer then deinterleave
  45. var tempInterleaved = [Float](repeating: 0, count: totalSamples * channels)
  46. let decoded = stb_vorbis_get_samples_float_interleaved(
  47. vorbis,
  48. Int32(channels),
  49. &tempInterleaved,
  50. Int32(totalSamples * channels)
  51. )
  52. if decoded <= 0 {
  53. throw OGGError.decodingFailed
  54. }
  55. // Deinterleave into the PCM buffer's channel pointers
  56. let frameCount = Int(decoded)
  57. for frame in 0..<frameCount {
  58. for ch in 0..<channels {
  59. buffer.floatChannelData![ch][frame] = tempInterleaved[frame * channels + ch]
  60. }
  61. }
  62. buffer.frameLength = AVAudioFrameCount(decoded)
  63. return (buffer, format)
  64. }
  65. /// Get the duration of an OGG file in seconds.
  66. static func duration(url: URL) -> TimeInterval {
  67. var error: Int32 = 0
  68. guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else { return 0 }
  69. defer { stb_vorbis_close(vorbis) }
  70. let totalSamples = stb_vorbis_stream_length_in_samples(vorbis)
  71. let info = stb_vorbis_get_info(vorbis)
  72. guard info.sample_rate > 0 else { return 0 }
  73. return Double(totalSamples) / Double(info.sample_rate)
  74. }
  75. /// Get basic info about an OGG file.
  76. static func fileInfo(url: URL) -> (sampleRate: Double, channels: Int, duration: TimeInterval)? {
  77. var error: Int32 = 0
  78. guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else { return nil }
  79. defer { stb_vorbis_close(vorbis) }
  80. let info = stb_vorbis_get_info(vorbis)
  81. let totalSamples = stb_vorbis_stream_length_in_samples(vorbis)
  82. let duration = info.sample_rate > 0 ? Double(totalSamples) / Double(info.sample_rate) : 0
  83. return (
  84. sampleRate: Double(info.sample_rate),
  85. channels: Int(info.channels),
  86. duration: duration
  87. )
  88. }
  89. /// Check if a file is an OGG Vorbis file.
  90. static func isOGGFile(_ url: URL) -> Bool {
  91. url.pathExtension.lowercased() == "ogg"
  92. }
  93. /// Decode an OGG file to mono float samples (for waveform/BPM/key analysis).
  94. static func readMonoSamples(url: URL, maxSeconds: Double? = nil) throws -> (samples: [Float], sampleRate: Double) {
  95. var error: Int32 = 0
  96. guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else {
  97. throw OGGError.failedToOpen(url.lastPathComponent)
  98. }
  99. defer { stb_vorbis_close(vorbis) }
  100. let info = stb_vorbis_get_info(vorbis)
  101. let channels = Int(info.channels)
  102. let sampleRate = Double(info.sample_rate)
  103. var totalSamples = Int(stb_vorbis_stream_length_in_samples(vorbis))
  104. if let maxSec = maxSeconds {
  105. totalSamples = min(totalSamples, Int(maxSec * sampleRate))
  106. }
  107. // Decode interleaved
  108. var tempInterleaved = [Float](repeating: 0, count: totalSamples * channels)
  109. let decoded = stb_vorbis_get_samples_float_interleaved(
  110. vorbis,
  111. Int32(channels),
  112. &tempInterleaved,
  113. Int32(totalSamples * channels)
  114. )
  115. guard decoded > 0 else { throw OGGError.decodingFailed }
  116. let frameCount = Int(decoded)
  117. // Mix to mono
  118. if channels == 1 {
  119. return (Array(tempInterleaved.prefix(frameCount)), sampleRate)
  120. }
  121. var mono = [Float](repeating: 0, count: frameCount)
  122. let divisor = Float(channels)
  123. for frame in 0..<frameCount {
  124. var sum: Float = 0
  125. for ch in 0..<channels {
  126. sum += tempInterleaved[frame * channels + ch]
  127. }
  128. mono[frame] = sum / divisor
  129. }
  130. return (mono, sampleRate)
  131. }
  132. }