library improvements
This commit is contained in:
@@ -29,7 +29,7 @@ public class AudioBuffer {
|
||||
|
||||
public func append(_ data: Data) {
|
||||
guard availableBytes + data.count <= maxBufferSize else {
|
||||
Logger.error(
|
||||
AudioTeeLogging.logger.error(
|
||||
"Audio buffer overflow",
|
||||
context: [
|
||||
"requested": String(data.count),
|
||||
|
||||
@@ -28,7 +28,7 @@ public class AudioFormatConverter {
|
||||
self.targetFormat = targetAVFormat
|
||||
self.avConverter = converter
|
||||
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Audio converter created",
|
||||
context: [
|
||||
"source_sample_rate": String(sourceAVFormat.sampleRate),
|
||||
@@ -39,7 +39,7 @@ public class AudioFormatConverter {
|
||||
|
||||
// Warn about upsampling once during initialization
|
||||
if targetAVFormat.sampleRate > sourceAVFormat.sampleRate {
|
||||
Logger.info(
|
||||
AudioTeeLogging.logger.info(
|
||||
"Upsampling audio - this doesn't add frequency content above the original Nyquist limit",
|
||||
context: [
|
||||
"source_rate": String(sourceAVFormat.sampleRate),
|
||||
@@ -48,7 +48,12 @@ public class AudioFormatConverter {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the target format as AudioStreamBasicDescription
|
||||
/// The source format this converter reads from.
|
||||
public var sourceFormatDescription: AudioStreamBasicDescription {
|
||||
return sourceFormat.streamDescription.pointee
|
||||
}
|
||||
|
||||
/// The target format this converter produces.
|
||||
public var targetFormatDescription: AudioStreamBasicDescription {
|
||||
return targetFormat.streamDescription.pointee
|
||||
}
|
||||
@@ -67,7 +72,7 @@ public class AudioFormatConverter {
|
||||
let inputBuffer = AVAudioPCMBuffer(
|
||||
pcmFormat: sourceFormat, frameCapacity: AVAudioFrameCount(inputFrameCount))
|
||||
else {
|
||||
Logger.error("Failed to create input buffer")
|
||||
AudioTeeLogging.logger.error("Failed to create input buffer")
|
||||
return packet
|
||||
}
|
||||
|
||||
@@ -83,7 +88,7 @@ public class AudioFormatConverter {
|
||||
let outputBuffer = AVAudioPCMBuffer(
|
||||
pcmFormat: targetFormat, frameCapacity: AVAudioFrameCount(outputFrameCount))
|
||||
else {
|
||||
Logger.error("Failed to create output buffer")
|
||||
AudioTeeLogging.logger.error("Failed to create output buffer")
|
||||
return packet
|
||||
}
|
||||
|
||||
@@ -99,7 +104,7 @@ public class AudioFormatConverter {
|
||||
|
||||
// Check if conversion produced output (regardless of status code)
|
||||
guard outputBuffer.frameLength > 0 else {
|
||||
Logger.error(
|
||||
AudioTeeLogging.logger.error(
|
||||
"Audio conversion produced no output",
|
||||
context: [
|
||||
"status": String(describing: status),
|
||||
|
||||
@@ -3,25 +3,25 @@ import CoreAudio
|
||||
import Foundation
|
||||
|
||||
public class AudioFormatManager {
|
||||
public static func getDeviceFormat(deviceID: AudioObjectID) -> AudioStreamBasicDescription {
|
||||
public static func getDeviceFormat(deviceID: AudioObjectID) throws -> AudioStreamBasicDescription {
|
||||
// First, wait for the device to become alive/ready
|
||||
let deviceReadyTimeout = 2.0 // 2 seconds max wait
|
||||
let pollInterval = 0.1 // 100ms poll interval
|
||||
let maxPolls = Int(deviceReadyTimeout / pollInterval)
|
||||
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Waiting for audio device to become ready", context: ["device_id": String(deviceID)])
|
||||
|
||||
// Poll device readiness
|
||||
for poll in 1...maxPolls {
|
||||
if isAudioDeviceValid(deviceID) {
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Audio device is ready", context: ["device_id": String(deviceID), "polls": String(poll)])
|
||||
break
|
||||
}
|
||||
|
||||
if poll == maxPolls {
|
||||
Logger.info(
|
||||
AudioTeeLogging.logger.info(
|
||||
"Device did not become ready within timeout, proceeding anyway",
|
||||
context: [
|
||||
"device_id": String(deviceID),
|
||||
@@ -30,7 +30,7 @@ public class AudioFormatManager {
|
||||
break
|
||||
}
|
||||
|
||||
Logger.info("------- not ready; retrying...")
|
||||
AudioTeeLogging.logger.info("------- not ready; retrying...")
|
||||
|
||||
Thread.sleep(forTimeInterval: pollInterval)
|
||||
}
|
||||
@@ -49,11 +49,11 @@ public class AudioFormatManager {
|
||||
deviceID, &propertyAddress, 0, nil, &propertySize, &streamFormat)
|
||||
|
||||
if status == noErr {
|
||||
Logger.debug("Successfully retrieved device format", context: ["attempt": String(attempt)])
|
||||
AudioTeeLogging.logger.debug("Successfully retrieved device format", context: ["attempt": String(attempt)])
|
||||
return streamFormat
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
AudioTeeLogging.logger.info(
|
||||
"------- Failed to get stream format after device ready check, retrying...",
|
||||
context: [
|
||||
"attempt": String(attempt),
|
||||
@@ -69,16 +69,14 @@ public class AudioFormatManager {
|
||||
}
|
||||
|
||||
// If all attempts failed after device readiness confirmation, this is a genuine error
|
||||
Logger.error(
|
||||
AudioTeeLogging.logger.error(
|
||||
"Failed to get device format after device readiness check and retries",
|
||||
context: [
|
||||
"device_id": String(deviceID),
|
||||
"device_was_ready": "true",
|
||||
])
|
||||
|
||||
fatalError(
|
||||
"Failed to get stream format from ready device: \(deviceID). This indicates a Core Audio subsystem error."
|
||||
)
|
||||
throw AudioTeeError.deviceFormatUnavailable(deviceID)
|
||||
}
|
||||
|
||||
static func createMetadata(for format: AudioStreamBasicDescription) -> AudioStreamMetadata {
|
||||
@@ -94,14 +92,8 @@ public class AudioFormatManager {
|
||||
)
|
||||
}
|
||||
|
||||
public static func writeMetadata(for format: AudioStreamBasicDescription) {
|
||||
let metadata = createMetadata(for: format)
|
||||
Logger.writeMessage(.metadata, data: metadata)
|
||||
Logger.writeMessage(.streamStart, data: Optional<String>.none)
|
||||
}
|
||||
|
||||
public static func logFormatInfo(_ format: AudioStreamBasicDescription) {
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Using device's native format",
|
||||
context: [
|
||||
"channels": String(format.mChannelsPerFrame),
|
||||
|
||||
@@ -10,15 +10,25 @@ public class AudioRecorder {
|
||||
private var outputHandler: AudioOutputHandler
|
||||
private var converter: AudioFormatConverter?
|
||||
|
||||
/// The audio format this recorder produces (after any conversion).
|
||||
public var outputFormat: AudioStreamBasicDescription {
|
||||
return finalFormat
|
||||
}
|
||||
|
||||
/// Whether this recorder is performing sample rate conversion.
|
||||
public var isConverting: Bool {
|
||||
return converter != nil
|
||||
}
|
||||
|
||||
public init(
|
||||
deviceID: AudioObjectID, outputHandler: AudioOutputHandler, convertToSampleRate: Double? = nil,
|
||||
chunkDuration: Double = 0.2
|
||||
) {
|
||||
) throws {
|
||||
self.deviceID = deviceID
|
||||
self.outputHandler = outputHandler
|
||||
|
||||
// Get source format and set up conversion if requested
|
||||
let sourceFormat = AudioFormatManager.getDeviceFormat(deviceID: deviceID)
|
||||
let sourceFormat = try AudioFormatManager.getDeviceFormat(deviceID: deviceID)
|
||||
|
||||
// Set up the audio buffer using source format and configurable chunk duration
|
||||
self.audioBuffer = AudioBuffer(format: sourceFormat, chunkDuration: chunkDuration)
|
||||
@@ -26,7 +36,7 @@ public class AudioRecorder {
|
||||
if let targetSampleRate = convertToSampleRate {
|
||||
// Validate sample rate
|
||||
guard AudioFormatConverter.isValidSampleRate(targetSampleRate) else {
|
||||
Logger.error("Invalid sample rate", context: ["sample_rate": String(targetSampleRate)])
|
||||
AudioTeeLogging.logger.error("Invalid sample rate", context: ["sample_rate": String(targetSampleRate)])
|
||||
self.converter = nil
|
||||
self.finalFormat = sourceFormat
|
||||
return
|
||||
@@ -36,10 +46,10 @@ public class AudioRecorder {
|
||||
let converter = try AudioFormatConverter.toSampleRate(targetSampleRate, from: sourceFormat)
|
||||
self.converter = converter
|
||||
self.finalFormat = converter.targetFormatDescription
|
||||
Logger.info(
|
||||
AudioTeeLogging.logger.info(
|
||||
"Audio conversion enabled", context: ["target_sample_rate": String(targetSampleRate)])
|
||||
} catch {
|
||||
Logger.error(
|
||||
AudioTeeLogging.logger.error(
|
||||
"Failed to create audio converter, using original format",
|
||||
context: ["error": String(describing: error)])
|
||||
self.converter = nil
|
||||
@@ -51,8 +61,8 @@ public class AudioRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
public func startRecording() {
|
||||
Logger.debug("Starting audio recording")
|
||||
public func startRecording() throws {
|
||||
AudioTeeLogging.logger.debug("Starting audio recording")
|
||||
|
||||
// Log format info and send metadata for final format
|
||||
AudioFormatManager.logFormatInfo(finalFormat)
|
||||
@@ -60,15 +70,15 @@ public class AudioRecorder {
|
||||
outputHandler.handleMetadata(metadata)
|
||||
outputHandler.handleStreamStart()
|
||||
|
||||
setupAndStartIOProc()
|
||||
try setupAndStartIOProc()
|
||||
|
||||
Logger.info("Audio device started successfully")
|
||||
AudioTeeLogging.logger.info("Audio device started successfully")
|
||||
}
|
||||
|
||||
// Note to self, what about installTap? Would require audio engine and a node?
|
||||
// No; AudioEngine.installTap() can only fire as often as 100ms. too slow for us
|
||||
private func setupAndStartIOProc() {
|
||||
Logger.debug("Creating IO proc")
|
||||
private func setupAndStartIOProc() throws {
|
||||
AudioTeeLogging.logger.debug("Creating IO proc")
|
||||
var status = AudioDeviceCreateIOProcID(
|
||||
deviceID,
|
||||
{
|
||||
@@ -82,15 +92,15 @@ public class AudioRecorder {
|
||||
)
|
||||
|
||||
guard status == noErr else {
|
||||
fatalError("Failed to create IO proc: \(status)")
|
||||
throw AudioTeeError.ioProcCreationFailed(status)
|
||||
}
|
||||
|
||||
Logger.debug("Starting audio device")
|
||||
AudioTeeLogging.logger.debug("Starting audio device")
|
||||
status = AudioDeviceStart(deviceID, ioProcID)
|
||||
|
||||
if status != noErr {
|
||||
cleanupIOProc()
|
||||
fatalError("Failed to start audio device: \(status). Device ID: \(deviceID)")
|
||||
throw AudioTeeError.deviceStartFailed(status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +109,7 @@ public class AudioRecorder {
|
||||
let firstBuffer = bufferList.mBuffers
|
||||
|
||||
guard firstBuffer.mData != nil && firstBuffer.mDataByteSize > 0 else {
|
||||
"Warning: Received empty audio buffer".print(to: .standardError)
|
||||
AudioTeeLogging.logger.error("Received empty audio buffer")
|
||||
return noErr
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ public class AudioTapManager {
|
||||
public init() {}
|
||||
|
||||
deinit {
|
||||
Logger.debug("Cleaning up audio tap manager")
|
||||
AudioTeeLogging.logger.debug("Cleaning up audio tap manager")
|
||||
|
||||
if let tapID = tapID {
|
||||
AudioHardwareDestroyProcessTap(tapID)
|
||||
@@ -25,7 +25,7 @@ public class AudioTapManager {
|
||||
|
||||
/// Sets up the audio tap and aggregate device
|
||||
public func setupAudioTap(with config: TapConfiguration) throws {
|
||||
Logger.debug("Setting up audio tap manager")
|
||||
AudioTeeLogging.logger.debug("Setting up audio tap manager")
|
||||
|
||||
tapID = try createSystemAudioTap(with: config)
|
||||
deviceID = try createAggregateDevice()
|
||||
@@ -36,7 +36,7 @@ public class AudioTapManager {
|
||||
|
||||
try addTapToAggregateDevice(tapID: tapID, deviceID: deviceID)
|
||||
|
||||
Logger.debug("Audio tap manager setup complete")
|
||||
AudioTeeLogging.logger.debug("Audio tap manager setup complete")
|
||||
}
|
||||
|
||||
/// Returns the aggregate device ID for recording
|
||||
@@ -45,7 +45,7 @@ public class AudioTapManager {
|
||||
}
|
||||
|
||||
private func createSystemAudioTap(with config: TapConfiguration) throws -> AudioObjectID {
|
||||
Logger.debug("Creating tap description")
|
||||
AudioTeeLogging.logger.debug("Creating tap description")
|
||||
let description = CATapDescription()
|
||||
|
||||
description.name = "audiotee-tap"
|
||||
@@ -58,7 +58,7 @@ public class AudioTapManager {
|
||||
description.deviceUID = nil // system default
|
||||
description.stream = 0 // first stream of output device
|
||||
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Tap description configured",
|
||||
context: [
|
||||
"name": description.name,
|
||||
@@ -69,14 +69,14 @@ public class AudioTapManager {
|
||||
])
|
||||
|
||||
// Create the tap
|
||||
Logger.debug("Creating tap")
|
||||
AudioTeeLogging.logger.debug("Creating tap")
|
||||
var tapID = AudioObjectID(kAudioObjectUnknown)
|
||||
let status = AudioHardwareCreateProcessTap(description, &tapID)
|
||||
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"AudioHardwareCreateProcessTap completed", context: ["status": String(status)])
|
||||
guard status == kAudioHardwareNoError else {
|
||||
Logger.error("Failed to create audio tap", context: ["status": String(status)])
|
||||
AudioTeeLogging.logger.error("Failed to create audio tap", context: ["status": String(status)])
|
||||
throw AudioTeeError.tapCreationFailed(status)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public class AudioTapManager {
|
||||
tapID, &propertyAddress, 0, nil, &propertySize, &streamDescription)
|
||||
|
||||
if formatStatus == noErr {
|
||||
Logger.debug(
|
||||
AudioTeeLogging.logger.debug(
|
||||
"Tap format retrieved",
|
||||
context: [
|
||||
"channels": String(streamDescription.mChannelsPerFrame),
|
||||
@@ -115,7 +115,7 @@ public class AudioTapManager {
|
||||
let status = AudioHardwareCreateAggregateDevice(description as CFDictionary, &deviceID)
|
||||
|
||||
guard status == kAudioHardwareNoError else {
|
||||
Logger.error("Failed to create aggregate device", context: ["status": String(status)])
|
||||
AudioTeeLogging.logger.error("Failed to create aggregate device", context: ["status": String(status)])
|
||||
throw AudioTeeError.aggregateDeviceCreationFailed(status)
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class AudioTapManager {
|
||||
}
|
||||
|
||||
guard status == kAudioHardwareNoError else {
|
||||
Logger.error(
|
||||
AudioTeeLogging.logger.error(
|
||||
"Failed to add tap to aggregate device", context: ["status": String(status)])
|
||||
throw AudioTeeError.tapAssignmentFailed(status)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CoreAudio
|
||||
import Foundation
|
||||
|
||||
// MARK: - Core AudioTee Errors
|
||||
@@ -8,6 +9,9 @@ public enum AudioTeeError: Error {
|
||||
case aggregateDeviceCreationFailed(OSStatus)
|
||||
case tapAssignmentFailed(OSStatus)
|
||||
case pidTranslationFailed([Int32])
|
||||
case deviceFormatUnavailable(AudioObjectID)
|
||||
case ioProcCreationFailed(OSStatus)
|
||||
case deviceStartFailed(OSStatus)
|
||||
}
|
||||
|
||||
// MARK: - Audio Format Conversion Errors
|
||||
|
||||
Reference in New Issue
Block a user