(
onData: (chunk: Buffer) => void,
onEnd: () => void,
options?: { silenceDetection?: boolean },
)
| 396 | } |
| 397 | |
| 398 | function startSoxRecording( |
| 399 | onData: (chunk: Buffer) => void, |
| 400 | onEnd: () => void, |
| 401 | options?: { silenceDetection?: boolean }, |
| 402 | ): boolean { |
| 403 | const useSilenceDetection = options?.silenceDetection !== false |
| 404 | |
| 405 | // Record raw PCM: 16 kHz, 16-bit signed, mono, to stdout. |
| 406 | // --buffer 1024 forces SoX to flush audio in small chunks instead of |
| 407 | // accumulating data in its internal buffer. Without this, SoX may buffer |
| 408 | // several seconds of audio before writing anything to stdout when piped, |
| 409 | // causing zero data flow until the process exits. |
| 410 | const args = [ |
| 411 | '-q', // quiet |
| 412 | '--buffer', |
| 413 | '1024', |
| 414 | '-t', |
| 415 | 'raw', |
| 416 | '-r', |
| 417 | String(RECORDING_SAMPLE_RATE), |
| 418 | '-e', |
| 419 | 'signed', |
| 420 | '-b', |
| 421 | '16', |
| 422 | '-c', |
| 423 | String(RECORDING_CHANNELS), |
| 424 | '-', // stdout |
| 425 | ] |
| 426 | |
| 427 | // Add silence detection filter (auto-stop on silence). |
| 428 | // Omit for push-to-talk where the user manually controls start/stop. |
| 429 | if (useSilenceDetection) { |
| 430 | args.push( |
| 431 | 'silence', // start/stop on silence |
| 432 | '1', |
| 433 | '0.1', |
| 434 | SILENCE_THRESHOLD, |
| 435 | '1', |
| 436 | SILENCE_DURATION_SECS, |
| 437 | SILENCE_THRESHOLD, |
| 438 | ) |
| 439 | } |
| 440 | |
| 441 | const child = spawn('rec', args, { |
| 442 | stdio: ['pipe', 'pipe', 'pipe'], |
| 443 | }) |
| 444 | |
| 445 | activeRecorder = child |
| 446 | |
| 447 | child.stdout?.on('data', (chunk: Buffer) => { |
| 448 | onData(chunk) |
| 449 | }) |
| 450 | |
| 451 | // Consume stderr to prevent backpressure |
| 452 | child.stderr?.on('data', () => {}) |
| 453 | |
| 454 | child.on('close', () => { |
| 455 | activeRecorder = null |
no test coverage detected