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.. 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" } /// Decode an OGG file to mono float samples (for waveform/BPM/key analysis). static func readMonoSamples(url: URL, maxSeconds: Double? = nil) throws -> (samples: [Float], sampleRate: Double) { 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) } let info = stb_vorbis_get_info(vorbis) let channels = Int(info.channels) let sampleRate = Double(info.sample_rate) var totalSamples = Int(stb_vorbis_stream_length_in_samples(vorbis)) if let maxSec = maxSeconds { totalSamples = min(totalSamples, Int(maxSec * sampleRate)) } // Decode interleaved var tempInterleaved = [Float](repeating: 0, count: totalSamples * channels) let decoded = stb_vorbis_get_samples_float_interleaved( vorbis, Int32(channels), &tempInterleaved, Int32(totalSamples * channels) ) guard decoded > 0 else { throw OGGError.decodingFailed } let frameCount = Int(decoded) // Mix to mono if channels == 1 { return (Array(tempInterleaved.prefix(frameCount)), sampleRate) } var mono = [Float](repeating: 0, count: frameCount) let divisor = Float(channels) for frame in 0..