50 lines
1.7 KiB
JavaScript
50 lines
1.7 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { EventEmitter } from 'node:events';
|
|
|
|
export const MIC_SAMPLE_RATE = 16_000;
|
|
export const MIC_CHANNELS = 1;
|
|
export const MIC_FORMAT = 's16';
|
|
|
|
/** Raw 16 kHz mono capture from PipeWire. The process is deliberately kept
|
|
* outside gnome-shell and has a stable node name for routing in Helvum. */
|
|
export class PipeWireCapture extends EventEmitter {
|
|
constructor({ command = 'pw-cat', spawnImpl = spawn, sampleRate = MIC_SAMPLE_RATE, nodeName = 'Jarvis' } = {}) {
|
|
super();
|
|
this.command = command;
|
|
this.spawnImpl = spawnImpl;
|
|
this.sampleRate = sampleRate;
|
|
this.nodeName = nodeName;
|
|
this.process = null;
|
|
}
|
|
|
|
start() {
|
|
if (this.process) return this;
|
|
this.process = this.spawnImpl(this.command, [
|
|
'--record', '--raw', '--format', MIC_FORMAT, '--rate', String(this.sampleRate),
|
|
'--channels', String(MIC_CHANNELS), '--name', this.nodeName,
|
|
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
this.process.stdout?.on('data', (chunk) => this.emit('audio', Buffer.from(chunk)));
|
|
this.process.stderr?.on('data', (chunk) => this.emit('diagnostic', String(chunk).trim()));
|
|
this.process.on('error', (error) => this.emit('error', error));
|
|
this.process.on('close', (code, signal) => { this.process = null; this.emit('close', { code, signal }); });
|
|
return this;
|
|
}
|
|
|
|
stop() {
|
|
if (!this.process) return;
|
|
this.process.kill('SIGTERM');
|
|
this.process = null;
|
|
}
|
|
}
|
|
|
|
export function pcmRms(chunk) {
|
|
const bytes = Buffer.from(chunk || '');
|
|
if (bytes.length < 2) return 0;
|
|
let sum = 0;
|
|
for (let i = 0; i + 1 < bytes.length; i += 2) {
|
|
const sample = bytes.readInt16LE(i) / 32768;
|
|
sum += sample * sample;
|
|
}
|
|
return Math.sqrt(sum / Math.floor(bytes.length / 2));
|
|
}
|