feat: 前端迁移至 TypeScript,集成 Biome 格式化与代码检查
- app.js → app.ts:添加完整类型标注、接口定义 - audio-processor.js → audio-processor.ts:AudioWorklet 类型化 - vite.config.js → vite.config.ts - 添加 tsconfig.json、vite-env.d.ts - 集成 Biome 默认配置(lint + format),通过全部检查 - package.json 添加 check/typecheck 脚本 - index.html 修复无障碍问题(button type、SVG title)
This commit is contained in:
88
web/audio-processor.ts
Normal file
88
web/audio-processor.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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 }
|
||||
*/
|
||||
|
||||
// AudioWorkletGlobalScope globals (not in standard lib)
|
||||
declare const sampleRate: number;
|
||||
declare class AudioWorkletProcessor {
|
||||
readonly port: MessagePort;
|
||||
constructor();
|
||||
process(
|
||||
inputs: Float32Array[][],
|
||||
outputs: Float32Array[][],
|
||||
parameters: Record<string, Float32Array>,
|
||||
): boolean;
|
||||
}
|
||||
declare function registerProcessor(
|
||||
name: string,
|
||||
ctor: new () => AudioWorkletProcessor,
|
||||
): void;
|
||||
|
||||
class VoicePasteProcessor extends AudioWorkletProcessor {
|
||||
private recording = false;
|
||||
private buffer: Float32Array[] = [];
|
||||
private bufferLen = 0;
|
||||
private readonly frameSize: number;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// ~200ms worth of samples at current sample rate
|
||||
this.frameSize = Math.floor(sampleRate * 0.2);
|
||||
|
||||
this.port.onmessage = (e: MessageEvent) => {
|
||||
if (e.data.command === "start") {
|
||||
this.recording = true;
|
||||
this.buffer = [];
|
||||
this.bufferLen = 0;
|
||||
} else if (e.data.command === "stop") {
|
||||
if (this.bufferLen > 0) {
|
||||
this.flush();
|
||||
}
|
||||
this.recording = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
process(inputs: Float32Array[][]): boolean {
|
||||
if (!this.recording) return true;
|
||||
|
||||
const input = inputs[0];
|
||||
if (!input || !input[0]) return true;
|
||||
|
||||
const channelData = input[0];
|
||||
this.buffer.push(new Float32Array(channelData));
|
||||
this.bufferLen += channelData.length;
|
||||
|
||||
if (this.bufferLen >= this.frameSize) {
|
||||
this.flush();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private flush(): void {
|
||||
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],
|
||||
);
|
||||
|
||||
this.buffer = [];
|
||||
this.bufferLen = 0;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("audio-processor", VoicePasteProcessor);
|
||||
Reference in New Issue
Block a user