AudioTee Swift CLI repo
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
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
|
||||
Reference in New Issue
Block a user