fix chunk empty audio
Browse files- src/audio/AudioPlayer.ts +20 -13
- src/audio/AudioRecorder.ts +12 -10
- src/audio/RecorderWorkletProcessor.js +12 -0
src/audio/AudioPlayer.ts
CHANGED
|
@@ -4,6 +4,7 @@ export class AudioPlayer {
|
|
| 4 |
private context: AudioContext | null = null;
|
| 5 |
private nextPlayTime = 0;
|
| 6 |
private started = false;
|
|
|
|
| 7 |
private pendingBytes = 0;
|
| 8 |
private bufferThresholdBytes = 6400; // 200ms at 16kHz default
|
| 9 |
private pendingBuffers: AudioBuffer[] = [];
|
|
@@ -18,6 +19,7 @@ export class AudioPlayer {
|
|
| 18 |
this.bufferThresholdBytes = Math.floor(sampleRate * 0.2) * 2; // 200ms
|
| 19 |
this.nextPlayTime = 0;
|
| 20 |
this.started = false;
|
|
|
|
| 21 |
this.pendingBytes = 0;
|
| 22 |
this.pendingBuffers = [];
|
| 23 |
}
|
|
@@ -25,7 +27,7 @@ export class AudioPlayer {
|
|
| 25 |
enqueue(rawPcm: ArrayBuffer, onStarted?: () => void): void {
|
| 26 |
if (!this.context) return;
|
| 27 |
|
| 28 |
-
const int16 = new Int16Array(rawPcm);
|
| 29 |
const float32 = new Float32Array(int16.length);
|
| 30 |
for (let i = 0; i < int16.length; i++) {
|
| 31 |
float32[i] = int16[i] / 32768;
|
|
@@ -36,17 +38,27 @@ export class AudioPlayer {
|
|
| 36 |
|
| 37 |
this.pendingBytes += rawPcm.byteLength;
|
| 38 |
|
| 39 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
this.pendingBuffers.push(audioBuffer);
|
| 41 |
-
|
|
|
|
| 42 |
this.started = true;
|
| 43 |
-
const buffered = this.pendingBuffers.splice(0);
|
| 44 |
-
this.pendingBuffers = [];
|
| 45 |
void this.context.resume().then(() => {
|
| 46 |
if (!this.context) return;
|
|
|
|
| 47 |
this.nextPlayTime = this.context.currentTime;
|
| 48 |
onStarted?.();
|
| 49 |
-
|
|
|
|
|
|
|
| 50 |
const source = this.context.createBufferSource();
|
| 51 |
source.buffer = buf;
|
| 52 |
source.connect(this.context.destination);
|
|
@@ -55,12 +67,6 @@ export class AudioPlayer {
|
|
| 55 |
}
|
| 56 |
});
|
| 57 |
}
|
| 58 |
-
} else {
|
| 59 |
-
const source = this.context.createBufferSource();
|
| 60 |
-
source.buffer = audioBuffer;
|
| 61 |
-
source.connect(this.context.destination);
|
| 62 |
-
source.start(this.nextPlayTime);
|
| 63 |
-
this.nextPlayTime += audioBuffer.duration;
|
| 64 |
}
|
| 65 |
}
|
| 66 |
|
|
@@ -74,6 +80,7 @@ export class AudioPlayer {
|
|
| 74 |
this.context = null;
|
| 75 |
this.nextPlayTime = 0;
|
| 76 |
this.started = false;
|
|
|
|
| 77 |
this.pendingBytes = 0;
|
| 78 |
this.pendingBuffers = [];
|
| 79 |
}
|
|
@@ -87,7 +94,7 @@ export function replayAudio(
|
|
| 87 |
let nextTime = ctx.currentTime + 0.05;
|
| 88 |
|
| 89 |
for (const chunk of chunks) {
|
| 90 |
-
const int16 = new Int16Array(chunk);
|
| 91 |
const float32 = new Float32Array(int16.length);
|
| 92 |
for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
|
| 93 |
const buf = ctx.createBuffer(1, float32.length, sampleRate);
|
|
|
|
| 4 |
private context: AudioContext | null = null;
|
| 5 |
private nextPlayTime = 0;
|
| 6 |
private started = false;
|
| 7 |
+
private resumed = false;
|
| 8 |
private pendingBytes = 0;
|
| 9 |
private bufferThresholdBytes = 6400; // 200ms at 16kHz default
|
| 10 |
private pendingBuffers: AudioBuffer[] = [];
|
|
|
|
| 19 |
this.bufferThresholdBytes = Math.floor(sampleRate * 0.2) * 2; // 200ms
|
| 20 |
this.nextPlayTime = 0;
|
| 21 |
this.started = false;
|
| 22 |
+
this.resumed = false;
|
| 23 |
this.pendingBytes = 0;
|
| 24 |
this.pendingBuffers = [];
|
| 25 |
}
|
|
|
|
| 27 |
enqueue(rawPcm: ArrayBuffer, onStarted?: () => void): void {
|
| 28 |
if (!this.context) return;
|
| 29 |
|
| 30 |
+
const int16 = new Int16Array(rawPcm, 0, Math.floor(rawPcm.byteLength / 2));
|
| 31 |
const float32 = new Float32Array(int16.length);
|
| 32 |
for (let i = 0; i < int16.length; i++) {
|
| 33 |
float32[i] = int16[i] / 32768;
|
|
|
|
| 38 |
|
| 39 |
this.pendingBytes += rawPcm.byteLength;
|
| 40 |
|
| 41 |
+
if (this.resumed) {
|
| 42 |
+
// AudioContext is running — schedule immediately
|
| 43 |
+
const source = this.context.createBufferSource();
|
| 44 |
+
source.buffer = audioBuffer;
|
| 45 |
+
source.connect(this.context.destination);
|
| 46 |
+
source.start(this.nextPlayTime);
|
| 47 |
+
this.nextPlayTime += audioBuffer.duration;
|
| 48 |
+
} else {
|
| 49 |
+
// Still buffering: waiting for threshold or waiting for ctx.resume() to complete
|
| 50 |
this.pendingBuffers.push(audioBuffer);
|
| 51 |
+
|
| 52 |
+
if (!this.started && this.pendingBytes >= this.bufferThresholdBytes) {
|
| 53 |
this.started = true;
|
|
|
|
|
|
|
| 54 |
void this.context.resume().then(() => {
|
| 55 |
if (!this.context) return;
|
| 56 |
+
this.resumed = true;
|
| 57 |
this.nextPlayTime = this.context.currentTime;
|
| 58 |
onStarted?.();
|
| 59 |
+
const toSchedule = this.pendingBuffers.splice(0);
|
| 60 |
+
this.pendingBuffers = [];
|
| 61 |
+
for (const buf of toSchedule) {
|
| 62 |
const source = this.context.createBufferSource();
|
| 63 |
source.buffer = buf;
|
| 64 |
source.connect(this.context.destination);
|
|
|
|
| 67 |
}
|
| 68 |
});
|
| 69 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
}
|
| 72 |
|
|
|
|
| 80 |
this.context = null;
|
| 81 |
this.nextPlayTime = 0;
|
| 82 |
this.started = false;
|
| 83 |
+
this.resumed = false;
|
| 84 |
this.pendingBytes = 0;
|
| 85 |
this.pendingBuffers = [];
|
| 86 |
}
|
|
|
|
| 94 |
let nextTime = ctx.currentTime + 0.05;
|
| 95 |
|
| 96 |
for (const chunk of chunks) {
|
| 97 |
+
const int16 = new Int16Array(chunk, 0, Math.floor(chunk.byteLength / 2));
|
| 98 |
const float32 = new Float32Array(int16.length);
|
| 99 |
for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
|
| 100 |
const buf = ctx.createBuffer(1, float32.length, sampleRate);
|
src/audio/AudioRecorder.ts
CHANGED
|
@@ -4,7 +4,7 @@ const CHUNK_SAMPLES = 3200; // 100ms at 16kHz
|
|
| 4 |
export class AudioRecorder {
|
| 5 |
private context: AudioContext | null = null;
|
| 6 |
private stream: MediaStream | null = null;
|
| 7 |
-
private
|
| 8 |
private accumulator: Float32Array = new Float32Array(0);
|
| 9 |
micLevel = 0;
|
| 10 |
|
|
@@ -19,13 +19,16 @@ export class AudioRecorder {
|
|
| 19 |
|
| 20 |
this.context = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
|
| 21 |
await this.context.resume();
|
| 22 |
-
const source = this.context.createMediaStreamSource(this.stream);
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
this.
|
| 28 |
-
const input = e.
|
| 29 |
const combined = new Float32Array(this.accumulator.length + input.length);
|
| 30 |
combined.set(this.accumulator);
|
| 31 |
combined.set(input, this.accumulator.length);
|
|
@@ -51,13 +54,12 @@ export class AudioRecorder {
|
|
| 51 |
}
|
| 52 |
};
|
| 53 |
|
| 54 |
-
source.connect(this.
|
| 55 |
-
this.processor.connect(this.context.destination);
|
| 56 |
}
|
| 57 |
|
| 58 |
stop(): void {
|
| 59 |
-
this.
|
| 60 |
-
this.
|
| 61 |
this.stream?.getTracks().forEach((t) => t.stop());
|
| 62 |
this.stream = null;
|
| 63 |
this.context?.close();
|
|
|
|
| 4 |
export class AudioRecorder {
|
| 5 |
private context: AudioContext | null = null;
|
| 6 |
private stream: MediaStream | null = null;
|
| 7 |
+
private workletNode: AudioWorkletNode | null = null;
|
| 8 |
private accumulator: Float32Array = new Float32Array(0);
|
| 9 |
micLevel = 0;
|
| 10 |
|
|
|
|
| 19 |
|
| 20 |
this.context = new AudioContext({ sampleRate: TARGET_SAMPLE_RATE });
|
| 21 |
await this.context.resume();
|
|
|
|
| 22 |
|
| 23 |
+
await this.context.audioWorklet.addModule(
|
| 24 |
+
new URL("./RecorderWorkletProcessor.js", import.meta.url).href
|
| 25 |
+
);
|
| 26 |
+
|
| 27 |
+
const source = this.context.createMediaStreamSource(this.stream);
|
| 28 |
+
this.workletNode = new AudioWorkletNode(this.context, "recorder-processor");
|
| 29 |
|
| 30 |
+
this.workletNode.port.onmessage = (e: MessageEvent<Float32Array>) => {
|
| 31 |
+
const input = e.data;
|
| 32 |
const combined = new Float32Array(this.accumulator.length + input.length);
|
| 33 |
combined.set(this.accumulator);
|
| 34 |
combined.set(input, this.accumulator.length);
|
|
|
|
| 54 |
}
|
| 55 |
};
|
| 56 |
|
| 57 |
+
source.connect(this.workletNode);
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
stop(): void {
|
| 61 |
+
this.workletNode?.disconnect();
|
| 62 |
+
this.workletNode = null;
|
| 63 |
this.stream?.getTracks().forEach((t) => t.stop());
|
| 64 |
this.stream = null;
|
| 65 |
this.context?.close();
|
src/audio/RecorderWorkletProcessor.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class RecorderProcessor extends AudioWorkletProcessor {
|
| 2 |
+
process(inputs) {
|
| 3 |
+
const channel = inputs[0]?.[0];
|
| 4 |
+
if (channel?.length) {
|
| 5 |
+
const copy = channel.slice();
|
| 6 |
+
this.port.postMessage(copy, [copy.buffer]);
|
| 7 |
+
}
|
| 8 |
+
return true;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
registerProcessor("recorder-processor", RecorderProcessor);
|