(
onData: (chunk: Buffer) => void,
onEnd: () => void,
options?: { silenceDetection?: boolean },
)
| 424 | } |
| 425 | |
| 426 | function startSoxRecording( |
| 427 | onData: (chunk: Buffer) => void, |
| 428 | onEnd: () => void, |
| 429 | options?: { silenceDetection?: boolean }, |
| 430 | ): boolean { |
| 431 | const useSilenceDetection = options?.silenceDetection !== false |
| 432 | |
| 433 | // Record raw PCM: 16 kHz, 16-bit signed, mono, to stdout. |
| 434 | // --buffer 1024 forces SoX to flush audio in small chunks instead of |
| 435 | // accumulating data in its internal buffer. Without this, SoX may buffer |
| 436 | // several seconds of audio before writing anything to stdout when piped, |
| 437 | // causing zero data flow until the process exits. |
| 438 | const args = [ |
| 439 | '-q', // quiet |
| 440 | '--buffer', |
| 441 | '1024', |
| 442 | '-t', |
| 443 | 'raw', |
| 444 | '-r', |
| 445 | String(RECORDING_SAMPLE_RATE), |
| 446 | '-e', |
| 447 | 'signed', |
| 448 | '-b', |
| 449 | '16', |
| 450 | '-c', |
| 451 | String(RECORDING_CHANNELS), |
| 452 | '-', // stdout |
| 453 | ] |
| 454 | |
| 455 | // Add silence detection filter (auto-stop on silence). |
| 456 | // Omit for push-to-talk where the user manually controls start/stop. |
| 457 | if (useSilenceDetection) { |
| 458 | args.push( |
| 459 | 'silence', // start/stop on silence |
| 460 | '1', |
| 461 | '0.1', |
| 462 | SILENCE_THRESHOLD, |
| 463 | '1', |
| 464 | SILENCE_DURATION_SECS, |
| 465 | SILENCE_THRESHOLD, |
| 466 | ) |
| 467 | } |
| 468 | |
| 469 | const child = spawn('rec', args, { |
| 470 | stdio: ['pipe', 'pipe', 'pipe'], |
| 471 | }) |
| 472 | |
| 473 | activeRecorder = child |
| 474 | |
| 475 | child.stdout?.on('data', (chunk: Buffer) => { |
| 476 | onData(chunk) |
| 477 | }) |
| 478 | |
| 479 | // Consume stderr to prevent backpressure |
| 480 | child.stderr?.on('data', () => {}) |
| 481 | |
| 482 | child.on('close', () => { |
| 483 | activeRecorder = null |
no test coverage detected