use ring buffer to avoid memory leak
This commit is contained in:
@@ -200,7 +200,7 @@ struct AudioTee {
|
|||||||
// Helper for stderr output
|
// Helper for stderr output
|
||||||
var standardError = FileHandle.standardError
|
var standardError = FileHandle.standardError
|
||||||
|
|
||||||
extension FileHandle: @retroactive TextOutputStream {
|
extension FileHandle: TextOutputStream {
|
||||||
public func write(_ string: String) {
|
public func write(_ string: String) {
|
||||||
let data = Data(string.utf8)
|
let data = Data(string.utf8)
|
||||||
self.write(data)
|
self.write(data)
|
||||||
|
|||||||
@@ -2,17 +2,48 @@ import CoreAudio
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class AudioBuffer {
|
public class AudioBuffer {
|
||||||
private var buffer = Data()
|
private var buffer: [UInt8]
|
||||||
private let targetChunkDuration: Double
|
private var writeIndex: Int = 0
|
||||||
private let streamFormat: AudioStreamBasicDescription
|
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) {
|
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) {
|
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] {
|
public func processChunks() -> [AudioPacket] {
|
||||||
@@ -26,36 +57,48 @@ public class AudioBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func flushRemaining() -> AudioPacket? {
|
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(
|
let packet = AudioPacket(
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
duration: 0.0, // Unknown duration for final chunk
|
duration: 0.0, // Unknown duration for final chunk
|
||||||
peakAmplitude: 0.0,
|
peakAmplitude: 0.0,
|
||||||
rawAudioData: buffer
|
rawAudioData: remainingData
|
||||||
)
|
)
|
||||||
|
|
||||||
buffer.removeAll()
|
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
|
|
||||||
private func nextChunk() -> AudioPacket? {
|
private func nextChunk() -> AudioPacket? {
|
||||||
let bytesPerFrame = Int(streamFormat.mBytesPerFrame)
|
// Check if we have enough data for a complete chunk
|
||||||
let samplesPerChunk = Int(streamFormat.mSampleRate * targetChunkDuration)
|
guard availableBytes >= bytesPerChunk else { return nil }
|
||||||
let bytesPerChunk = samplesPerChunk * bytesPerFrame
|
|
||||||
|
|
||||||
guard buffer.count >= bytesPerChunk else { return nil }
|
// Extract chunk data - bounds-checked but still efficient
|
||||||
|
var chunkData = Data(capacity: bytesPerChunk)
|
||||||
|
|
||||||
let chunkData = buffer.prefix(bytesPerChunk)
|
for _ in 0..<bytesPerChunk {
|
||||||
|
chunkData.append(buffer[readIndex])
|
||||||
|
readIndex = (readIndex + 1) % maxBufferSize
|
||||||
|
}
|
||||||
|
|
||||||
|
availableBytes -= bytesPerChunk
|
||||||
|
|
||||||
let packet = AudioPacket(
|
let packet = AudioPacket(
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
duration: Double(samplesPerChunk) / streamFormat.mSampleRate,
|
duration: chunkDuration,
|
||||||
peakAmplitude: 0.0, // No analysis in raw mode
|
peakAmplitude: 0.0, // No analysis in raw mode
|
||||||
rawAudioData: Data(chunkData)
|
rawAudioData: chunkData
|
||||||
)
|
)
|
||||||
|
|
||||||
buffer.removeFirst(bytesPerChunk)
|
|
||||||
return packet
|
return packet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ public class AudioRecorder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: note to self, what about installTap? Would require audio engine and a node?
|
// FIXME: 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() {
|
private func setupAndStartIOProc() {
|
||||||
Logger.debug("Creating IO proc")
|
Logger.debug("Creating IO proc")
|
||||||
var status = AudioDeviceCreateIOProcID(
|
var status = AudioDeviceCreateIOProcID(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public enum AudioTeeError: Error {
|
|||||||
case aggregateDeviceCreationFailed(OSStatus)
|
case aggregateDeviceCreationFailed(OSStatus)
|
||||||
case tapAssignmentFailed(OSStatus)
|
case tapAssignmentFailed(OSStatus)
|
||||||
case pidTranslationFailed([Int32])
|
case pidTranslationFailed([Int32])
|
||||||
|
case bufferOverflow(requested: Int, available: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Audio Format Conversion Errors
|
// MARK: - Audio Format Conversion Errors
|
||||||
|
|||||||
Reference in New Issue
Block a user