Files
gnome-jarvis/daemon/audio-pipewire.js
T
2026-09-11 14:09:04 -04:00

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));
}