// // BuddyAudioConversionSupport.swift // leanring-buddy // // Shared audio conversion helpers for voice transcription providers. // import AVFoundation import Foundation final class BuddyPCM16AudioConverter { private let targetAudioFormat: AVAudioFormat private var audioConverter: AVAudioConverter? private var currentInputFormatDescription: String? init(targetSampleRate: Double) { self.targetAudioFormat = AVAudioFormat( commonFormat: .pcmFormatInt16, sampleRate: targetSampleRate, channels: 1, interleaved: true )! } func convertToPCM16Data(from audioBuffer: AVAudioPCMBuffer) -> Data? { let inputFormatDescription = audioBuffer.format.settings.description if currentInputFormatDescription != inputFormatDescription { audioConverter = AVAudioConverter(from: audioBuffer.format, to: targetAudioFormat) currentInputFormatDescription = inputFormatDescription } guard let audioConverter else { return nil } let sampleRateRatio = targetAudioFormat.sampleRate / audioBuffer.format.sampleRate let outputFrameCapacity = AVAudioFrameCount( (Double(audioBuffer.frameLength) * sampleRateRatio).rounded(.up) + 32 ) guard let outputBuffer = AVAudioPCMBuffer( pcmFormat: targetAudioFormat, frameCapacity: outputFrameCapacity ) else { return nil } var hasProvidedSourceBuffer = false var conversionError: NSError? let conversionStatus = audioConverter.convert(to: outputBuffer, error: &conversionError) { _, outStatus in if hasProvidedSourceBuffer { outStatus.pointee = .noDataNow return nil } hasProvidedSourceBuffer = true outStatus.pointee = .haveData return audioBuffer } guard conversionStatus != .error else { return nil } guard let pcmDataPointer = outputBuffer.audioBufferList.pointee.mBuffers.mData else { return nil } let bytesPerFrame = Int(targetAudioFormat.streamDescription.pointee.mBytesPerFrame) let byteCount = Int(outputBuffer.frameLength) * bytesPerFrame guard byteCount > 0 else { return nil } return Data(bytes: pcmDataPointer, count: byteCount) } } enum BuddyWAVFileBuilder { static func buildWAVData( fromPCM16MonoAudio pcm16AudioData: Data, sampleRate: Int, channelCount: Int = 1, bitsPerSample: Int = 16 ) -> Data { let byteRate = sampleRate * channelCount * bitsPerSample / 8 let blockAlign = channelCount * bitsPerSample / 8 let dataChunkSize = UInt32(pcm16AudioData.count) let fileSize = UInt32(36) + dataChunkSize var wavData = Data() wavData.append("RIFF".data(using: .ascii)!) wavData.append(littleEndianData(from: fileSize)) wavData.append("WAVE".data(using: .ascii)!) wavData.append("fmt ".data(using: .ascii)!) wavData.append(littleEndianData(from: UInt32(16))) wavData.append(littleEndianData(from: UInt16(1))) wavData.append(littleEndianData(from: UInt16(channelCount))) wavData.append(littleEndianData(from: UInt32(sampleRate))) wavData.append(littleEndianData(from: UInt32(byteRate))) wavData.append(littleEndianData(from: UInt16(blockAlign))) wavData.append(littleEndianData(from: UInt16(bitsPerSample))) wavData.append("data".data(using: .ascii)!) wavData.append(littleEndianData(from: dataChunkSize)) wavData.append(pcm16AudioData) return wavData } private static func littleEndianData(from value: T) -> Data { var littleEndianValue = value.littleEndian return Data(bytes: &littleEndianValue, count: MemoryLayout.size) } }