vignette/apps/web/public/worklets/voice-capture-worklet.js
2026-07-01 12:10:52 +09:00

91 lines
2.5 KiB
JavaScript

class VoiceCaptureProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.pending = [];
this.pendingLength = 0;
this.flushSamples = Math.max(1024, Math.floor(sampleRate * 0.25));
this.totalSamples = 0;
this.voiceSamples = 0;
this.silentSamples = 0;
this.trailingSilentSamples = 0;
this.sumSquares = 0;
this.peak = 0;
this.voiceThreshold = 0.018;
this.port.onmessage = (event) => {
if (event.data && event.data.type === "flush") {
this.flush();
}
};
}
process(inputs) {
const input = inputs[0];
const channel = input && input[0];
if (!channel || channel.length === 0) return true;
const pcm = new Int16Array(channel.length);
for (let i = 0; i < channel.length; i += 1) {
const sample = Math.max(-1, Math.min(1, channel[i]));
const abs = Math.abs(sample);
this.totalSamples += 1;
this.sumSquares += sample * sample;
if (abs > this.peak) this.peak = abs;
if (abs >= this.voiceThreshold) {
this.voiceSamples += 1;
this.trailingSilentSamples = 0;
} else {
this.silentSamples += 1;
this.trailingSilentSamples += 1;
}
pcm[i] = sample < 0 ? sample * 0x8000 : sample * 0x7fff;
}
this.pending.push(pcm);
this.pendingLength += pcm.length;
if (this.pendingLength >= this.flushSamples) {
this.flush();
}
return true;
}
flush() {
if (this.pendingLength <= 0) return;
const merged = new Int16Array(this.pendingLength);
let offset = 0;
for (const chunk of this.pending) {
merged.set(chunk, offset);
offset += chunk.length;
}
this.pending = [];
this.pendingLength = 0;
this.port.postMessage(
{
type: "chunk",
sampleRate,
channels: 1,
sampleWidth: 2,
metrics: this.metrics(),
pcm: merged.buffer,
},
[merged.buffer],
);
}
metrics() {
const durationMs = (this.totalSamples / sampleRate) * 1000;
const voiceMs = (this.voiceSamples / sampleRate) * 1000;
const silenceMs = (this.silentSamples / sampleRate) * 1000;
const trailingSilenceMs = (this.trailingSilentSamples / sampleRate) * 1000;
const rms = this.totalSamples > 0 ? Math.sqrt(this.sumSquares / this.totalSamples) : 0;
return {
durationMs,
voiceMs,
silenceMs,
trailingSilenceMs,
rms,
peak: this.peak,
};
}
}
registerProcessor("voice-capture-processor", VoiceCaptureProcessor);