168 lines
5.7 KiB
Swift
168 lines
5.7 KiB
Swift
import AVFoundation
|
|
import CoreAudio
|
|
import Foundation
|
|
|
|
/// Simple audio format converter using AVFoundation
|
|
public class AudioFormatConverter {
|
|
private let avConverter: AVAudioConverter
|
|
private let sourceFormat: AVAudioFormat
|
|
private let targetFormat: AVAudioFormat
|
|
|
|
public init(sourceFormat: AudioStreamBasicDescription, targetFormat: AudioStreamBasicDescription)
|
|
throws
|
|
{
|
|
var mutableSourceFormat = sourceFormat
|
|
var mutableTargetFormat = targetFormat
|
|
|
|
guard let sourceAVFormat = AVAudioFormat(streamDescription: &mutableSourceFormat),
|
|
let targetAVFormat = AVAudioFormat(streamDescription: &mutableTargetFormat)
|
|
else {
|
|
throw AudioConverterError.invalidFormat
|
|
}
|
|
|
|
guard let converter = AVAudioConverter(from: sourceAVFormat, to: targetAVFormat) else {
|
|
throw AudioConverterError.creationFailed
|
|
}
|
|
|
|
self.sourceFormat = sourceAVFormat
|
|
self.targetFormat = targetAVFormat
|
|
self.avConverter = converter
|
|
|
|
Logger.debug(
|
|
"Audio converter created",
|
|
context: [
|
|
"source_sample_rate": String(sourceAVFormat.sampleRate),
|
|
"target_sample_rate": String(targetAVFormat.sampleRate),
|
|
"source_channels": String(sourceAVFormat.channelCount),
|
|
"target_channels": String(targetAVFormat.channelCount),
|
|
])
|
|
|
|
// Warn about upsampling once during initialization
|
|
if targetAVFormat.sampleRate > sourceAVFormat.sampleRate {
|
|
Logger.info(
|
|
"Upsampling audio - this doesn't add frequency content above the original Nyquist limit",
|
|
context: [
|
|
"source_rate": String(sourceAVFormat.sampleRate),
|
|
"target_rate": String(targetAVFormat.sampleRate),
|
|
])
|
|
}
|
|
}
|
|
|
|
/// Get the target format as AudioStreamBasicDescription
|
|
public var targetFormatDescription: AudioStreamBasicDescription {
|
|
return targetFormat.streamDescription.pointee
|
|
}
|
|
|
|
public func transform(_ packet: AudioPacket) -> AudioPacket {
|
|
let inputData = packet.rawAudioData
|
|
|
|
// Short-circuit if no conversion needed
|
|
if sourceFormat.sampleRate == targetFormat.sampleRate {
|
|
return packet
|
|
}
|
|
|
|
// Calculate frame counts
|
|
let inputFrameCount =
|
|
inputData.count / Int(sourceFormat.streamDescription.pointee.mBytesPerFrame)
|
|
let outputFrameCount = Int(
|
|
Double(inputFrameCount) * (targetFormat.sampleRate / sourceFormat.sampleRate))
|
|
|
|
// Create input buffer
|
|
guard
|
|
let inputBuffer = AVAudioPCMBuffer(
|
|
pcmFormat: sourceFormat, frameCapacity: AVAudioFrameCount(inputFrameCount))
|
|
else {
|
|
Logger.error("Failed to create input buffer")
|
|
return packet
|
|
}
|
|
|
|
// Copy input data to buffer
|
|
inputData.withUnsafeBytes { bytes in
|
|
let dest = inputBuffer.audioBufferList.pointee.mBuffers.mData!
|
|
dest.copyMemory(from: bytes.baseAddress!, byteCount: inputData.count)
|
|
}
|
|
inputBuffer.frameLength = AVAudioFrameCount(inputFrameCount)
|
|
|
|
// Create output buffer
|
|
guard
|
|
let outputBuffer = AVAudioPCMBuffer(
|
|
pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(outputFrameCount))
|
|
else {
|
|
Logger.error("Failed to create output buffer")
|
|
return packet
|
|
}
|
|
|
|
// Perform conversion - simpler approach
|
|
var error: NSError?
|
|
|
|
let status = avConverter.convert(to: outputBuffer, error: &error) {
|
|
requestedPackets, outStatus in
|
|
// Always provide our input buffer and let converter manage it
|
|
outStatus.pointee = .haveData
|
|
return inputBuffer
|
|
}
|
|
|
|
// Check if conversion produced output (regardless of status code)
|
|
guard outputBuffer.frameLength > 0 else {
|
|
Logger.error(
|
|
"Audio conversion produced no output",
|
|
context: [
|
|
"status": String(describing: status),
|
|
"error": String(describing: error),
|
|
"input_frames": String(inputBuffer.frameLength),
|
|
"output_capacity": String(outputBuffer.frameCapacity),
|
|
])
|
|
return packet
|
|
}
|
|
|
|
// Extract converted data
|
|
let outputData = Data(
|
|
bytes: outputBuffer.audioBufferList.pointee.mBuffers.mData!,
|
|
count: Int(outputBuffer.frameLength * targetFormat.streamDescription.pointee.mBytesPerFrame))
|
|
|
|
// Return new packet with converted audio (keeping original metadata for simplicity)
|
|
return AudioPacket(
|
|
timestamp: packet.timestamp,
|
|
duration: packet.duration,
|
|
peakAmplitude: packet.peakAmplitude,
|
|
rawAudioData: outputData
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience Constructors
|
|
|
|
extension AudioFormatConverter {
|
|
/// Create a converter to a specific sample rate with mono PCM 16-bit output
|
|
/// Since the tap already converts to mono, we hardcode channels to 1
|
|
public static func toSampleRate(
|
|
_ sampleRate: Double, from sourceFormat: AudioStreamBasicDescription
|
|
) throws -> AudioFormatConverter {
|
|
var targetFormat = AudioStreamBasicDescription()
|
|
targetFormat.mSampleRate = sampleRate
|
|
targetFormat.mFormatID = kAudioFormatLinearPCM
|
|
targetFormat.mFormatFlags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger
|
|
targetFormat.mBytesPerPacket = 2
|
|
targetFormat.mFramesPerPacket = 1
|
|
targetFormat.mBytesPerFrame = 2
|
|
targetFormat.mChannelsPerFrame = 1 // Always mono since tap handles this
|
|
targetFormat.mBitsPerChannel = 16
|
|
|
|
return try AudioFormatConverter(sourceFormat: sourceFormat, targetFormat: targetFormat)
|
|
}
|
|
|
|
/// Common sample rates for validation
|
|
public static let supportedSampleRates: [Double] = [
|
|
8000, 16000, 22050, 24000, 32000, 44100, 48000,
|
|
]
|
|
|
|
/// Validate if a sample rate is supported
|
|
public static func isValidSampleRate(_ sampleRate: Double) -> Bool {
|
|
return supportedSampleRates.contains(sampleRate)
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Types
|
|
|
|
// AudioConverterError moved to Sources/Core/Errors/AudioTeeErrors.swift
|