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;
#context: AudioContext;
#channelCount: number;
#worklet: AudioWorkletNode | undefined;
#buffers: T[] = [];
constructor(sampleRate: number) {
constructor(sampleRate: number, channelCount: number) {
this.#context = new AudioContext({
latencyHint: "interactive",
sampleRate,
});
this.#channelCount = channelCount;
}
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, {
numberOfInputs: 0,
numberOfOutputs: 1,
outputChannelCount: [2],
outputChannelCount: [this.#channelCount],
});
this.#worklet.connect(this.#context.destination);

View file

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

View file

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