OGGDecoder.swift 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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. /// Convert an OGG file to a CAF file (for compatibility with AVAudioFile).
  94. /// Returns the URL of the converted file.
  95. static func convertToCAF(url: URL) throws -> URL {
  96. let (buffer, format) = try decode(url: url)
  97. let cafURL = url.deletingPathExtension().appendingPathExtension("caf")
  98. // Write to CAF format
  99. let outputFile = try AVAudioFile(
  100. forWriting: cafURL,
  101. settings: format.settings,
  102. commonFormat: .pcmFormatFloat32,
  103. interleaved: false
  104. )
  105. try outputFile.write(from: buffer)
  106. return cafURL
  107. }
  108. }