feat: add mobile web frontend with AudioWorklet recording

This commit is contained in:
2026-03-01 03:03:24 +08:00
parent 4ebc9226ed
commit 35d645a186
4 changed files with 688 additions and 0 deletions

73
web/audio-processor.js Normal file
View File

@@ -0,0 +1,73 @@
/**
* AudioWorklet processor for VoicePaste.
*
* Captures raw Float32 PCM from the microphone, accumulates samples into
* ~200ms frames, and posts them to the main thread for resampling + WS send.
*
* Communication:
* Main → Processor: { command: "start" | "stop" }
* Processor → Main: { type: "audio", samples: Float32Array, sampleRate: number }
*/
class AudioProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.recording = false;
this.buffer = [];
this.bufferLen = 0;
// ~200ms worth of samples at current sample rate
// sampleRate is a global in AudioWorkletGlobalScope
this.frameSize = Math.floor(sampleRate * 0.2);
this.port.onmessage = (e) => {
if (e.data.command === "start") {
this.recording = true;
this.buffer = [];
this.bufferLen = 0;
} else if (e.data.command === "stop") {
// Flush remaining samples
if (this.bufferLen > 0) {
this._flush();
}
this.recording = false;
}
};
}
process(inputs) {
if (!this.recording) return true;
const input = inputs[0];
if (!input || !input[0]) return true;
// Mono channel 0
const channelData = input[0];
this.buffer.push(new Float32Array(channelData));
this.bufferLen += channelData.length;
if (this.bufferLen >= this.frameSize) {
this._flush();
}
return true;
}
_flush() {
// Merge buffer chunks into a single Float32Array
const merged = new Float32Array(this.bufferLen);
let offset = 0;
for (const chunk of this.buffer) {
merged.set(chunk, offset);
offset += chunk.length;
}
this.port.postMessage(
{ type: "audio", samples: merged, sampleRate: sampleRate },
[merged.buffer] // Transfer ownership for zero-copy
);
this.buffer = [];
this.bufferLen = 0;
}
}
registerProcessor("audio-processor", AudioProcessor);