23 lines
1.4 KiB
JavaScript
23 lines
1.4 KiB
JavaScript
import { spawn } from 'node:child_process';
|
|
import { EventEmitter } from 'node:events';
|
|
|
|
export class PipeWirePlayback extends EventEmitter {
|
|
constructor({ command = 'pw-cat', spawnImpl = spawn, sampleRate = 44_100, nodeName = 'Jarvis' } = {}) {
|
|
super(); this.command = command; this.spawnImpl = spawnImpl; this.sampleRate = sampleRate; this.nodeName = nodeName; this.process = null;
|
|
}
|
|
async play(samples) {
|
|
this.stop();
|
|
const child = this.process = this.spawnImpl(this.command, ['--playback', '--raw', '--format', 's16', '--rate', String(this.sampleRate), '--channels', '1', '--properties', `node.name=${this.nodeName}`, '-'], { stdio: ['pipe', 'ignore', 'pipe'] });
|
|
let diagnostic = '';
|
|
child.stderr?.on('data', (chunk) => { diagnostic = (diagnostic + String(chunk)).slice(-2048); this.emit('diagnostic', String(chunk).trim()); });
|
|
await new Promise((resolve, reject) => {
|
|
child.once('close', (code, signal) => code === 0 || signal === 'SIGTERM' ? resolve() : reject(new Error(`Audio playback exited with code ${code}: ${diagnostic.trim()}`)));
|
|
child.once('error', reject);
|
|
child.stdin.on('error', reject);
|
|
child.stdin.end(Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength));
|
|
});
|
|
if (this.process === child) this.process = null;
|
|
}
|
|
stop() { if (this.process) { this.process.kill('SIGTERM'); this.process = null; } }
|
|
}
|