use ring buffer to avoid memory leak

This commit is contained in:
Nick Payne
2025-07-10 13:43:44 +01:00
parent 80d7555b60
commit 1b537eb395
4 changed files with 64 additions and 19 deletions
+61 -18
View File
@@ -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..<availableBytes {
remainingData.append(buffer[readIndex])
readIndex = (readIndex + 1) % maxBufferSize
}
availableBytes = 0
let packet = AudioPacket(
timestamp: Date(),
duration: 0.0, // Unknown duration for final chunk
peakAmplitude: 0.0,
rawAudioData: buffer
rawAudioData: remainingData
)
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)
// Extract chunk data - bounds-checked but still efficient
var chunkData = Data(capacity: bytesPerChunk)
for _ in 0..<bytesPerChunk {
chunkData.append(buffer[readIndex])
readIndex = (readIndex + 1) % maxBufferSize
}
availableBytes -= bytesPerChunk
let packet = AudioPacket(
timestamp: Date(),
duration: Double(samplesPerChunk) / streamFormat.mSampleRate,
duration: chunkDuration,
peakAmplitude: 0.0, // No analysis in raw mode
rawAudioData: Data(chunkData)
rawAudioData: chunkData
)
buffer.removeFirst(bytesPerChunk)
return packet
}
}