| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159 |
- 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"
- }
- /// 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..<frameCount {
- var sum: Float = 0
- for ch in 0..<channels {
- sum += tempInterleaved[frame * channels + ch]
- }
- mono[frame] = sum / divisor
- }
- return (mono, sampleRate)
- }
- }
|