From 1b537eb3957721c07b59b1907c3fbd2470291fb0 Mon Sep 17 00:00:00 2001 From: Nick Payne Date: Thu, 10 Jul 2025 13:43:44 +0100 Subject: [PATCH] use ring buffer to avoid memory leak --- Sources/CLI/AudioTee.swift | 2 +- Sources/Core/AudioBuffer.swift | 79 ++++++++++++++++++++++++------- Sources/Core/AudioRecorder.swift | 1 + Sources/Core/AudioTeeErrors.swift | 1 + 4 files changed, 64 insertions(+), 19 deletions(-) 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..5c09f23 100644 --- a/Sources/Core/AudioBuffer.swift +++ b/Sources/Core/AudioBuffer.swift @@ -2,17 +2,48 @@ 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 + + // Pre-calculated values for efficiency + 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-allocate 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 + } + + // Simple, clean, fast enough + for byte in data { + buffer[writeIndex] = byte + writeIndex = (writeIndex + 1) % maxBufferSize + } + + availableBytes += data.count } public func processChunks() -> [AudioPacket] { @@ -26,36 +57,48 @@ public class AudioBuffer { } public func flushRemaining() -> AudioPacket? { - guard !buffer.isEmpty else { return nil } + guard availableBytes > 0 else { return nil } + + // Create Data from remaining bytes + var remainingData = Data(capacity: availableBytes) + for _ in 0.. 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) + // Extract chunk data - bounds-checked but still efficient + var chunkData = Data(capacity: bytesPerChunk) + + for _ in 0..