chore: checkpoint 第一版
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-06 17:04:11 +08:00
parent a8cdec98d1
commit 0c2151ace9
659 changed files with 185855 additions and 554 deletions
@@ -0,0 +1,115 @@
class PCM16PlaybackProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
const config = options.processorOptions || {};
this.sourceSampleRate = Number(config.sourceSampleRate || 16000);
this.prebufferSamples = Math.max(1, Math.round(this.sourceSampleRate * Number(config.prebufferMs || 120) / 1000));
this.capacity = Math.max(this.sourceSampleRate * 30, this.prebufferSamples * 4);
this.ring = new Float32Array(this.capacity);
this.readPosition = 0;
this.writePosition = 0;
this.playing = false;
this.ended = false;
this.drained = false;
this.lastOutput = 0;
this.fadeInRemaining = 0;
this.fadeFrames = Math.max(1, Math.round(sampleRate * 0.005));
this.underflows = 0;
this.port.onmessage = (message) => this.onMessage(message.data || {});
}
onMessage(message) {
if (message.type === "reset") {
this.reset();
return;
}
if (message.type === "end") {
this.ended = true;
return;
}
if (message.type !== "push" || !(message.samples instanceof ArrayBuffer)) return;
const samples = new Float32Array(message.samples);
if (this.drained) this.reset();
this.ended = false;
for (let index = 0; index < samples.length; index += 1) {
if (this.writePosition - Math.floor(this.readPosition) >= this.capacity - 1) {
this.readPosition = this.writePosition - this.capacity + 2;
}
this.ring[this.writePosition % this.capacity] = samples[index];
this.writePosition += 1;
}
}
reset() {
this.readPosition = 0;
this.writePosition = 0;
this.playing = false;
this.ended = false;
this.drained = false;
this.lastOutput = 0;
this.fadeInRemaining = 0;
}
available() {
return this.writePosition - this.readPosition;
}
startWhenReady() {
if (this.playing) return true;
const available = this.available();
if (available >= this.prebufferSamples || (this.ended && available > 1)) {
this.playing = true;
this.fadeInRemaining = this.fadeFrames;
this.port.postMessage({ type: "started", bufferedSamples: Math.floor(available) });
return true;
}
return false;
}
finish(output, fromIndex, eventType) {
const remaining = Math.min(this.fadeFrames, output.length - fromIndex);
for (let index = 0; index < remaining; index += 1) {
output[fromIndex + index] = this.lastOutput * (1 - (index + 1) / remaining);
}
output.fill(0, fromIndex + remaining);
this.lastOutput = 0;
this.playing = false;
if (eventType === "drained") {
if (!this.drained) this.port.postMessage({ type: "drained" });
this.drained = true;
} else {
this.underflows += 1;
this.port.postMessage({ type: "underflow", count: this.underflows });
}
}
process(_inputs, outputs) {
const output = outputs[0]?.[0];
if (!output) return true;
output.fill(0);
if (!this.startWhenReady()) return true;
const sourceStep = this.sourceSampleRate / sampleRate;
for (let index = 0; index < output.length; index += 1) {
const base = Math.floor(this.readPosition);
if (base + 1 >= this.writePosition) {
this.finish(output, index, this.ended ? "drained" : "underflow");
break;
}
const fraction = this.readPosition - base;
const first = this.ring[base % this.capacity];
const second = this.ring[(base + 1) % this.capacity];
let value = first + (second - first) * fraction;
if (this.fadeInRemaining > 0) {
value *= 1 - this.fadeInRemaining / this.fadeFrames;
this.fadeInRemaining -= 1;
}
output[index] = value;
this.lastOutput = value;
this.readPosition += sourceStep;
}
return true;
}
}
registerProcessor("pcm16-playback", PCM16PlaybackProcessor);