mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-03 09:49:24 +02:00
feat(pcm): support any channel number
This commit is contained in:
parent
758cc1860e
commit
d82e7450a4
3 changed files with 101 additions and 84 deletions
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
{
|
{
|
||||||
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
"extends": "./node_modules/@yume-chan/tsconfig/tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": [
|
"lib": [
|
||||||
"ESNext",
|
"ESNext",
|
||||||
"DOM"
|
"DOM",
|
||||||
],
|
"DOM.Iterable"
|
||||||
"types": []
|
],
|
||||||
}
|
"types": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue