| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- import AVFoundation
- import Foundation
- /// Decodes OGG Vorbis files to AVAudioPCMBuffer using stb_vorbis.
- /// This enables playback of .ogg files through AVAudioEngine.
- struct OGGDecoder {
- enum OGGError: LocalizedError {
- case failedToOpen(String)
- case failedToCreateBuffer
- case decodingFailed
- var errorDescription: String? {
- switch self {
- case .failedToOpen(let path): return "Failed to open OGG file: \(path)"
- case .failedToCreateBuffer: return "Failed to create audio buffer"
- case .decodingFailed: return "OGG decoding failed"
- }
- }
- }
- /// Decode an OGG file to an AVAudioPCMBuffer ready for AVAudioPlayerNode.
- static func decode(url: URL) throws -> (buffer: AVAudioPCMBuffer, format: AVAudioFormat) {
- // Open the OGG file
- var error: Int32 = 0
- guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else {
- throw OGGError.failedToOpen(url.lastPathComponent)
- }
- defer { stb_vorbis_close(vorbis) }
- // Get file info
- let info = stb_vorbis_get_info(vorbis)
- let channels = Int(info.channels)
- let sampleRate = Double(info.sample_rate)
- let totalSamples = Int(stb_vorbis_stream_length_in_samples(vorbis))
- // Create the output format (non-interleaved float for AVAudioPlayerNode)
- guard let format = AVAudioFormat(
- commonFormat: .pcmFormatFloat32,
- sampleRate: sampleRate,
- channels: AVAudioChannelCount(channels),
- interleaved: false
- ) else {
- throw OGGError.failedToCreateBuffer
- }
- guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(totalSamples)) else {
- throw OGGError.failedToCreateBuffer
- }
- // Decode all samples — stb_vorbis gives interleaved floats,
- // so we decode to a temp buffer then deinterleave
- var tempInterleaved = [Float](repeating: 0, count: totalSamples * channels)
- let decoded = stb_vorbis_get_samples_float_interleaved(
- vorbis,
- Int32(channels),
- &tempInterleaved,
- Int32(totalSamples * channels)
- )
- if decoded <= 0 {
- throw OGGError.decodingFailed
- }
- // Deinterleave into the PCM buffer's channel pointers
- let frameCount = Int(decoded)
- for frame in 0..<frameCount {
- for ch in 0..<channels {
- buffer.floatChannelData![ch][frame] = tempInterleaved[frame * channels + ch]
- }
- }
- buffer.frameLength = AVAudioFrameCount(decoded)
- return (buffer, format)
- }
- /// Get the duration of an OGG file in seconds.
- static func duration(url: URL) -> TimeInterval {
- var error: Int32 = 0
- guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else { return 0 }
- defer { stb_vorbis_close(vorbis) }
- let totalSamples = stb_vorbis_stream_length_in_samples(vorbis)
- let info = stb_vorbis_get_info(vorbis)
- guard info.sample_rate > 0 else { return 0 }
- return Double(totalSamples) / Double(info.sample_rate)
- }
- /// Get basic info about an OGG file.
- static func fileInfo(url: URL) -> (sampleRate: Double, channels: Int, duration: TimeInterval)? {
- var error: Int32 = 0
- guard let vorbis = stb_vorbis_open_filename(url.path, &error, nil) else { return nil }
- defer { stb_vorbis_close(vorbis) }
- let info = stb_vorbis_get_info(vorbis)
- let totalSamples = stb_vorbis_stream_length_in_samples(vorbis)
- let duration = info.sample_rate > 0 ? Double(totalSamples) / Double(info.sample_rate) : 0
- return (
- sampleRate: Double(info.sample_rate),
- channels: Int(info.channels),
- duration: duration
- )
- }
- /// Check if a file is an OGG Vorbis file.
- static func isOGGFile(_ url: URL) -> Bool {
- url.pathExtension.lowercased() == "ogg"
- }
- /// Convert an OGG file to a CAF file (for compatibility with AVAudioFile).
- /// Returns the URL of the converted file.
- static func convertToCAF(url: URL) throws -> URL {
- let (buffer, format) = try decode(url: url)
- let cafURL = url.deletingPathExtension().appendingPathExtension("caf")
- // Write to CAF format
- let outputFile = try AVAudioFile(
- forWriting: cafURL,
- settings: format.settings,
- commonFormat: .pcmFormatFloat32,
- interleaved: false
- )
- try outputFile.write(from: buffer)
- return cafURL
- }
- }
|