zero-alloc audio pipeline: pointer-based IO from ring buffer to stdout
Replace Data/AudioPacket allocations with raw pointer callbacks through the entire audio pipeline. Ring buffer hands out direct pointers (or linearizes into a pre-allocated scratch buffer on wrap-around), converter accepts/emits pointers via its cached buffers, and output handler writes to stdout via write(2) with EINTR handling. Remove AudioPacket (dead code), --flush flag (no-op with raw write(2)). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,6 @@ struct AudioTee {
|
||||
var stereo: Bool = false
|
||||
var sampleRate: Double?
|
||||
var chunkDuration: Double = 0.2
|
||||
var flush: Bool = false
|
||||
|
||||
init() {}
|
||||
|
||||
@@ -33,7 +32,6 @@ struct AudioTee {
|
||||
audiotee --include-processes 1234 5678 9012 # Tap only these processes
|
||||
audiotee --exclude-processes 1234 5678 # Tap everything except these
|
||||
audiotee --mute # Mute processes being tapped
|
||||
audiotee --flush # Flush stdout after each chunk
|
||||
"""
|
||||
)
|
||||
|
||||
@@ -45,8 +43,6 @@ struct AudioTee {
|
||||
name: "exclude-processes", help: "Process IDs to exclude (space-separated)")
|
||||
parser.addFlag(name: "mute", help: "Mute processes being tapped")
|
||||
parser.addFlag(name: "stereo", help: "Records in stereo")
|
||||
parser.addFlag(
|
||||
name: "flush", help: "Flush stdout after each audio chunk (reduces latency when piping)")
|
||||
parser.addOption(
|
||||
name: "sample-rate",
|
||||
help: "Target sample rate (8000, 16000, 22050, 24000, 32000, 44100, 48000)")
|
||||
@@ -64,7 +60,6 @@ struct AudioTee {
|
||||
audioTee.excludeProcesses = try parser.getArrayValue("exclude-processes", as: Int32.self)
|
||||
audioTee.mute = parser.getFlag("mute")
|
||||
audioTee.stereo = parser.getFlag("stereo")
|
||||
audioTee.flush = parser.getFlag("flush")
|
||||
audioTee.sampleRate = try parser.getOptionalValue("sample-rate", as: Double.self)
|
||||
audioTee.chunkDuration = try parser.getValue("chunk-duration", as: Double.self)
|
||||
|
||||
@@ -142,7 +137,7 @@ struct AudioTee {
|
||||
throw ExitCode.failure
|
||||
}
|
||||
|
||||
let outputHandler = BinaryAudioOutputHandler(flushAfterWrite: flush)
|
||||
let outputHandler = BinaryAudioOutputHandler()
|
||||
let recorder = try AudioRecorder(
|
||||
deviceID: deviceID, outputHandler: outputHandler, convertToSampleRate: sampleRate,
|
||||
chunkDuration: chunkDuration)
|
||||
|
||||
@@ -4,17 +4,19 @@ import Foundation
|
||||
/// CLI-specific output handler that writes raw PCM audio to stdout
|
||||
/// and lifecycle messages to stderr via the logger.
|
||||
class BinaryAudioOutputHandler: AudioOutputHandler {
|
||||
private let flushAfterWrite: Bool
|
||||
private let fd = STDOUT_FILENO
|
||||
|
||||
init(flushAfterWrite: Bool = false) {
|
||||
self.flushAfterWrite = flushAfterWrite
|
||||
}
|
||||
|
||||
func handleAudioPacket(_ packet: AudioPacket) {
|
||||
// Write raw binary audio data directly to stdout
|
||||
FileHandle.standardOutput.write(packet.data)
|
||||
if flushAfterWrite {
|
||||
fflush(stdout)
|
||||
func handleAudioData(_ pointer: UnsafeRawPointer, count: Int) {
|
||||
var written = 0
|
||||
while written < count {
|
||||
let result = write(fd, pointer.advanced(by: written), count - written)
|
||||
if result >= 0 {
|
||||
written += result
|
||||
} else if errno == EINTR {
|
||||
continue
|
||||
} else {
|
||||
break // EPIPE, EIO, etc — consumer gone or real error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user