feat(pcm): support any channel number

This commit is contained in:
Simon Chan 2023-10-19 10:45:01 +08:00
parent 758cc1860e
commit d82e7450a4
No known key found for this signature in database
GPG key ID: A8B69F750B9BCEDD
3 changed files with 101 additions and 84 deletions

View file

@ -2,14 +2,16 @@ export abstract class PcmPlayer<T> {
protected abstract sourceName: string; protected abstract sourceName: string;
#context: AudioContext; #context: AudioContext;
#channelCount: number;
#worklet: AudioWorkletNode | undefined; #worklet: AudioWorkletNode | undefined;
#buffers: T[] = []; #buffers: T[] = [];
constructor(sampleRate: number) { constructor(sampleRate: number, channelCount: number) {
this.#context = new AudioContext({ this.#context = new AudioContext({
latencyHint: "interactive", latencyHint: "interactive",
sampleRate, sampleRate,
}); });
this.#channelCount = channelCount;
} }
protected abstract feedCore(worklet: AudioWorkletNode, source: T): void; protected abstract feedCore(worklet: AudioWorkletNode, source: T): void;
@ -31,7 +33,7 @@ export abstract class PcmPlayer<T> {
this.#worklet = new AudioWorkletNode(this.#context, this.sourceName, { this.#worklet = new AudioWorkletNode(this.#context, this.sourceName, {
numberOfInputs: 0, numberOfInputs: 0,
numberOfOutputs: 1, numberOfOutputs: 1,
outputChannelCount: [2], outputChannelCount: [this.#channelCount],
}); });
this.#worklet.connect(this.#context.destination); this.#worklet.connect(this.#context.destination);

View file

@ -3,7 +3,8 @@
"compilerOptions": { "compilerOptions": {
"lib": [ "lib": [
"ESNext", "ESNext",
"DOM" "DOM",
"DOM.Iterable"
], ],
"types": [] "types": []
} }

View file

@ -14,6 +14,9 @@ abstract class SourceProcessor<T>
extends AudioWorkletProcessor extends AudioWorkletProcessor
implements AudioWorkletProcessorImpl implements AudioWorkletProcessorImpl
{ {
channelCount: number;
#readBuffer: Float32Array;
#chunks: T[] = []; #chunks: T[] = [];
#chunkSampleCounts: number[] = []; #chunkSampleCounts: number[] = [];
#totalSampleCount = 0; #totalSampleCount = 0;
@ -23,10 +26,15 @@ abstract class SourceProcessor<T>
#inputOffset = 0; #inputOffset = 0;
#outputOffset = 0; #outputOffset = 0;
constructor() { constructor(options: { outputChannelCount?: number[] }) {
super(); super();
this.channelCount = options.outputChannelCount![0]!;
this.#readBuffer = new Float32Array(this.channelCount);
this.port.onmessage = (event) => { this.port.onmessage = (event) => {
while (this.#totalSampleCount > 16000) { while (this.#totalSampleCount > 16000) {
console.log("[Audio] Buffer overflow, dropping samples");
this.#chunks.shift(); this.#chunks.shift();
const count = this.#chunkSampleCounts.shift()!; const count = this.#chunkSampleCounts.shift()!;
this.#totalSampleCount -= count; this.#totalSampleCount -= count;
@ -39,6 +47,7 @@ abstract class SourceProcessor<T>
this.#totalSampleCount += length; this.#totalSampleCount += length;
if (!this.#speedUp && this.#totalSampleCount > 8000) { if (!this.#speedUp && this.#totalSampleCount > 8000) {
console.log("[Audio] Speeding up");
this.#speedUp = true; this.#speedUp = true;
this.#readOffset = 0; this.#readOffset = 0;
this.#inputOffset = 0; this.#inputOffset = 0;
@ -49,15 +58,14 @@ abstract class SourceProcessor<T>
protected abstract createSource(data: ArrayBuffer[]): [T, number]; protected abstract createSource(data: ArrayBuffer[]): [T, number];
process(_inputs: Float32Array[][], outputs: Float32Array[][]) { process(_inputs: Float32Array[][], [outputs]: Float32Array[][]) {
// Stop speeding up when buffer is below 0.05s // Stop speeding up when buffer is below 0.05s
if (this.#speedUp && this.#totalSampleCount < 3000) { if (this.#speedUp && this.#totalSampleCount < 3000) {
console.log("[Audio] Restoring normal speed");
this.#speedUp = false; this.#speedUp = false;
} }
const outputLeft = outputs[0]![0]!; const outputLength = outputs![0]!.length;
const outputRight = outputs[0]![1]!;
const outputLength = outputLeft.length;
if (this.#speedUp) { if (this.#speedUp) {
for (let i = 0; i < outputLength; i += 1) { for (let i = 0; i < outputLength; i += 1) {
@ -75,23 +83,21 @@ abstract class SourceProcessor<T>
let inputIndex = firstWindow * INPUT_HOP_SIZE + inWindowIndex; let inputIndex = firstWindow * INPUT_HOP_SIZE + inWindowIndex;
while (inputIndex > 0 && inWindowIndex >= 0) { while (inputIndex > 0 && inWindowIndex >= 0) {
const [left, right] = this.#read( this.#read(inputIndex - this.#readOffset);
inputIndex - this.#readOffset,
);
const weight = WINDOW_WEIGHT_TABLE[inWindowIndex]!; const weight = WINDOW_WEIGHT_TABLE[inWindowIndex]!;
outputLeft[i] += left * weight; for (let j = 0; j < this.channelCount; j += 1) {
outputRight[i] += right * weight; outputs![j]![i] += this.#readBuffer[j]! * weight;
}
totalWeight += weight; totalWeight += weight;
inputIndex += INPUT_HOP_SIZE - OUTPUT_HOP_SIZE; inputIndex += INPUT_HOP_SIZE - OUTPUT_HOP_SIZE;
inWindowIndex -= OUTPUT_HOP_SIZE; inWindowIndex -= OUTPUT_HOP_SIZE;
inWindowIndex %= WINDOW_SIZE;
} }
if (totalWeight > 0) { if (totalWeight > 0) {
outputLeft[i] /= totalWeight; for (let j = 0; j < this.channelCount; j += 1) {
outputRight[i] /= totalWeight; outputs![j]![i] /= totalWeight;
}
} }
this.#outputOffset += 1; this.#outputOffset += 1;
@ -115,23 +121,22 @@ abstract class SourceProcessor<T>
this.#inputOffset -= firstChunkSampleCount; this.#inputOffset -= firstChunkSampleCount;
} }
} else { } else {
this.#copyChunks(outputLeft, outputRight); this.#copyChunks(outputs!);
} }
return true; return true;
} }
#copyChunks(outputLeft: Float32Array, outputRight: Float32Array) { #copyChunks(outputs: Float32Array[]) {
let outputIndex = 0; let outputIndex = 0;
const outputLength = outputLeft.length; const outputLength = outputs[0]!.length;
while (this.#chunks.length > 0 && outputIndex < outputLength) { while (this.#chunks.length > 0 && outputIndex < outputLength) {
let source: T | undefined = this.#chunks[0]; let source: T | undefined = this.#chunks[0];
let consumedSampleCount = 0; let consumedSampleCount = 0;
[source, consumedSampleCount, outputIndex] = this.copyChunk( [source, consumedSampleCount, outputIndex] = this.copyChunk(
source!, source!,
outputLeft, outputs,
outputRight,
outputLength, outputLength,
outputIndex, outputIndex,
); );
@ -158,25 +163,30 @@ abstract class SourceProcessor<T>
} }
} }
#read(offset: number): [number, number] { #read(offset: number) {
for (let i = 0; i < this.#chunks.length; i += 1) { for (let i = 0; i < this.#chunks.length; i += 1) {
const length = this.#chunkSampleCounts[i]!; const length = this.#chunkSampleCounts[i]!;
if (offset < length) { if (offset < length) {
return this.read(this.#chunks[i]!, offset); this.read(this.#chunks[i]!, offset, this.#readBuffer);
return;
} }
offset -= length; offset -= length;
} }
return [0, 0];
this.#readBuffer.fill(0);
} }
protected abstract read(source: T, offset: number): [number, number]; protected abstract read(
source: T,
offset: number,
target: Float32Array,
): void;
protected abstract copyChunk( protected abstract copyChunk(
source: T, source: T,
outputLeft: Float32Array, outputs: Float32Array[],
outputRight: Float32Array,
outputLength: number, outputLength: number,
outputIndex: number, outputIndex: number,
): [source: T | undefined, sourceIndex: number, outputIndex: number]; ): [source: T | undefined, sourceIndex: number, outputIndex: number];
@ -188,20 +198,23 @@ class Int16SourceProcessor
{ {
protected override createSource(data: ArrayBuffer[]): [Int16Array, number] { protected override createSource(data: ArrayBuffer[]): [Int16Array, number] {
const source = new Int16Array(data[0]!); const source = new Int16Array(data[0]!);
return [source, source.length / 2]; return [source, source.length / this.channelCount];
} }
protected override read( protected override read(
source: Int16Array, source: Int16Array,
offset: number, offset: number,
): [number, number] { target: Float32Array,
return [source[offset * 2]! / 0x8000, source[offset * 2 + 1]! / 0x8000]; ) {
const sourceOffset = offset * this.channelCount;
for (let i = 0; i < this.channelCount; i += 1) {
target[i] = source[sourceOffset + i]! / 0x8000;
}
} }
protected override copyChunk( protected override copyChunk(
source: Int16Array, source: Int16Array,
outputLeft: Float32Array, outputs: Float32Array[],
outputRight: Float32Array,
outputLength: number, outputLength: number,
outputIndex: number, outputIndex: number,
): [ ): [
@ -210,27 +223,27 @@ class Int16SourceProcessor
outputIndex: number, outputIndex: number,
] { ] {
const sourceLength = source.length; const sourceLength = source.length;
let sourceSampleIndex = 0; let sourceIndex = 0;
while (sourceSampleIndex < sourceLength) { while (sourceIndex < sourceLength) {
outputLeft[outputIndex] = source[sourceSampleIndex]! / 0x8000; for (let i = 0; i < this.channelCount; i += 1) {
outputRight[outputIndex] = source[sourceSampleIndex + 1]! / 0x8000; outputs[i]![outputIndex] = source[sourceIndex]! / 0x8000;
sourceIndex += 1;
sourceSampleIndex += 2; }
outputIndex += 1; outputIndex += 1;
if (outputIndex === outputLength) { if (outputIndex === outputLength) {
return [ return [
sourceSampleIndex < sourceLength sourceIndex < sourceLength
? source.subarray(sourceSampleIndex) ? source.subarray(sourceIndex)
: undefined, : undefined,
sourceSampleIndex / 2, sourceIndex / this.channelCount,
outputIndex, outputIndex,
]; ];
} }
} }
return [undefined, sourceSampleIndex / 2, outputIndex]; return [undefined, sourceIndex / this.channelCount, outputIndex];
} }
} }
@ -239,20 +252,23 @@ class Float32SourceProcessor extends SourceProcessor<Float32Array> {
data: ArrayBuffer[], data: ArrayBuffer[],
): [Float32Array, number] { ): [Float32Array, number] {
const source = new Float32Array(data[0]!); const source = new Float32Array(data[0]!);
return [source, source.length / 2]; return [source, source.length / this.channelCount];
} }
protected override read( protected override read(
source: Float32Array, source: Float32Array,
offset: number, offset: number,
): [number, number] { target: Float32Array,
return [source[offset * 2]!, source[offset * 2 + 1]!]; ) {
const sourceOffset = offset * this.channelCount;
for (let i = 0; i < this.channelCount; i += 1) {
target[i] = source[sourceOffset + i]!;
}
} }
protected override copyChunk( protected override copyChunk(
source: Float32Array, source: Float32Array,
outputLeft: Float32Array, outputs: Float32Array[],
outputRight: Float32Array,
outputLength: number, outputLength: number,
outputIndex: number, outputIndex: number,
): [ ): [
@ -261,27 +277,27 @@ class Float32SourceProcessor extends SourceProcessor<Float32Array> {
outputIndex: number, outputIndex: number,
] { ] {
const sourceLength = source.length; const sourceLength = source.length;
let sourceSampleIndex = 0; let sourceIndex = 0;
while (sourceSampleIndex < sourceLength) { while (sourceIndex < sourceLength) {
outputLeft[outputIndex] = source[sourceSampleIndex]!; for (let i = 0; i < this.channelCount; i += 1) {
outputRight[outputIndex] = source[sourceSampleIndex + 1]!; outputs[i]![outputIndex] = source[sourceIndex]!;
sourceIndex += 1;
sourceSampleIndex += 2; }
outputIndex += 1; outputIndex += 1;
if (outputIndex === outputLength) { if (outputIndex === outputLength) {
return [ return [
sourceSampleIndex < sourceLength sourceIndex < sourceLength
? source.subarray(sourceSampleIndex) ? source.subarray(sourceIndex)
: undefined, : undefined,
sourceSampleIndex / 2, sourceIndex / this.channelCount,
outputIndex, outputIndex,
]; ];
} }
} }
return [undefined, sourceSampleIndex / 2, outputIndex]; return [undefined, sourceIndex / this.channelCount, outputIndex];
} }
} }
@ -296,14 +312,16 @@ class Float32PlanerSourceProcessor extends SourceProcessor<Float32Array[]> {
protected override read( protected override read(
source: Float32Array[], source: Float32Array[],
offset: number, offset: number,
): [number, number] { target: Float32Array,
return [source[0]![offset]!, source[1]![offset]!]; ) {
for (let i = 0; i < target.length; i += 1) {
target[i] = source[i]![offset]!;
}
} }
protected override copyChunk( protected override copyChunk(
source: Float32Array[], source: Float32Array[],
outputLeft: Float32Array, outputs: Float32Array[],
outputRight: Float32Array,
outputLength: number, outputLength: number,
outputIndex: number, outputIndex: number,
): [ ): [
@ -311,32 +329,28 @@ class Float32PlanerSourceProcessor extends SourceProcessor<Float32Array[]> {
sourceIndex: number, sourceIndex: number,
outputIndex: number, outputIndex: number,
] { ] {
const sourceLeft = source[0]!; const sourceLength = source[0]!.length;
const sourceRight = source[1]!; let sourceIndex = 0;
const sourceLength = sourceLeft.length;
let sourceSampleIndex = 0;
while (sourceSampleIndex < sourceLength) { while (sourceIndex < sourceLength) {
outputLeft[outputIndex] = sourceLeft[sourceSampleIndex]!; for (let i = 0; i < this.channelCount; i += 1) {
outputRight[outputIndex] = sourceRight[sourceSampleIndex]!; outputs[i]![outputIndex] = source[i]![sourceIndex]!;
}
sourceSampleIndex += 1; sourceIndex += 1;
outputIndex += 1; outputIndex += 1;
if (outputIndex === outputLength) { if (outputIndex === outputLength) {
return [ return [
sourceSampleIndex < sourceLength sourceIndex < sourceLength
? source.map((channel) => ? source.map((channel) => channel.subarray(sourceIndex))
channel.subarray(sourceSampleIndex),
)
: undefined, : undefined,
sourceSampleIndex, sourceIndex,
outputIndex, outputIndex,
]; ];
} }
} }
return [undefined, sourceSampleIndex, outputIndex]; return [undefined, sourceIndex, outputIndex];
} }
} }