diff --git a/Sources/CLI/AudioTee.swift b/Sources/CLI/AudioTee.swift index a4fc6f2..51da1d7 100644 --- a/Sources/CLI/AudioTee.swift +++ b/Sources/CLI/AudioTee.swift @@ -200,7 +200,7 @@ struct AudioTee { // Helper for stderr output var standardError = FileHandle.standardError -extension FileHandle: @retroactive TextOutputStream { +extension FileHandle: TextOutputStream { public func write(_ string: String) { let data = Data(string.utf8) self.write(data) diff --git a/Sources/Core/AudioBuffer.swift b/Sources/Core/AudioBuffer.swift index a73c366..4d4bfc7 100644 --- a/Sources/Core/AudioBuffer.swift +++ b/Sources/Core/AudioBuffer.swift @@ -2,17 +2,62 @@ import CoreAudio import Foundation public class AudioBuffer { - private var buffer = Data() - private let targetChunkDuration: Double - private let streamFormat: AudioStreamBasicDescription + private var buffer: [UInt8] + private var writeIndex: Int = 0 + private var readIndex: Int = 0 + private var availableBytes: Int = 0 + private let maxBufferSize: Int + + private let bytesPerChunk: Int + private let chunkDuration: Double public init(format: AudioStreamBasicDescription, chunkDuration: Double = 0.2) { - self.streamFormat = format - self.targetChunkDuration = chunkDuration + + // Pre-calculate chunk parameters + let bytesPerFrame = Int(format.mBytesPerFrame) + let samplesPerChunk = Int(format.mSampleRate * chunkDuration) + self.bytesPerChunk = samplesPerChunk * bytesPerFrame + self.chunkDuration = Double(samplesPerChunk) / format.mSampleRate + + // Calculate max buffer size to hold ~10 seconds of audio, way more than the maximum we allow + let bytesPerSecond = Int(format.mSampleRate) * bytesPerFrame + self.maxBufferSize = bytesPerSecond * 10 + + // Pre-allocated ring buffer + self.buffer = Array(repeating: 0, count: maxBufferSize) } public func append(_ data: Data) { - buffer.append(data) + guard availableBytes + data.count <= maxBufferSize else { + Logger.error("Audio buffer overflow", context: [ + "requested": String(data.count), + "available": String(maxBufferSize - availableBytes) + ]) + return + } + + data.withUnsafeBytes { bytes in + let sourceBytes = bytes.bindMemory(to: UInt8.self) + let dataSize = sourceBytes.count + + // Check if we can copy in one block (no wrap-around) + if writeIndex + dataSize <= maxBufferSize { + // only one write needed + buffer.replaceSubrange(writeIndex.. [AudioPacket] { @@ -25,37 +70,37 @@ public class AudioBuffer { return packets } - public func flushRemaining() -> AudioPacket? { - guard !buffer.isEmpty else { return nil } - - let packet = AudioPacket( - timestamp: Date(), - duration: 0.0, // Unknown duration for final chunk - peakAmplitude: 0.0, - rawAudioData: buffer - ) - - buffer.removeAll() - return packet - } - private func nextChunk() -> AudioPacket? { - let bytesPerFrame = Int(streamFormat.mBytesPerFrame) - let samplesPerChunk = Int(streamFormat.mSampleRate * targetChunkDuration) - let bytesPerChunk = samplesPerChunk * bytesPerFrame + // Check if we have enough data for a complete chunk + guard availableBytes >= bytesPerChunk else { return nil } - guard buffer.count >= bytesPerChunk else { return nil } - - let chunkData = buffer.prefix(bytesPerChunk) + var chunkData = Data(capacity: bytesPerChunk) + + // Check if we can copy in one block (no wrap-around) + if readIndex + bytesPerChunk <= maxBufferSize { + // one copy needed + chunkData.append(contentsOf: buffer[readIndex..