From 3041989cf3e541769c9b25bf5e5f754c96b6c01b Mon Sep 17 00:00:00 2001
From: Vanilagy <1696106+Vanilagy@users.noreply.github.com>
Date: Tue, 7 Jan 2025 20:57:16 +0100
Subject: [PATCH] Add WAVE demuxer
---
dev/player.html | 2 +-
eslint.config.mjs | 3 +-
src/input-format.ts | 49 ++++++-
src/wave/riff-reader.ts | 40 ++++++
src/wave/wave-demuxer.ts | 292 +++++++++++++++++++++++++++++++++++++++
5 files changed, 378 insertions(+), 8 deletions(-)
create mode 100644 src/wave/riff-reader.ts
create mode 100644 src/wave/wave-demuxer.ts
diff --git a/dev/player.html b/dev/player.html
index 7222e10..0420ecb 100644
--- a/dev/player.html
+++ b/dev/player.html
@@ -1,6 +1,6 @@
-
+
= 18 && formatTag !== 0x0165) {
+ const cbSize = this.riffReader.readU16();
+ const remainingSize = size - 18;
+ const extensionSize = Math.min(remainingSize, cbSize);
+
+ if (extensionSize >= 22 && formatTag === WaveFormat.EXTENSIBLE) {
+ // Parse WAVEFORMATEXTENSIBLE
+ this.riffReader.pos += 2 + 4;
+ const subFormat = this.riffReader.readBytes(16);
+
+ // Get actual format from subFormat GUID
+ formatTag = subFormat[0]! | (subFormat[1]! << 8);
+ }
+ }
+
+ if (formatTag === WaveFormat.MULAW || formatTag === WaveFormat.ALAW) {
+ bitsPerSample = 8;
+ }
+
+ this.audioInfo = {
+ format: formatTag,
+ numberOfChannels: numChannels,
+ sampleRate,
+ sampleSizeInBytes: Math.ceil(bitsPerSample / 8),
+ blockSizeInBytes: blockAlign,
+ };
+ }
+
+ getCodec(): AudioCodec | null {
+ assert(this.audioInfo);
+
+ if (this.audioInfo.format === WaveFormat.MULAW) {
+ return 'ulaw';
+ }
+ if (this.audioInfo.format === WaveFormat.ALAW) {
+ return 'alaw';
+ }
+ if (this.audioInfo.format === WaveFormat.PCM) {
+ if (this.audioInfo.sampleSizeInBytes === 1) {
+ return 'pcm-u8';
+ } else if (this.audioInfo.sampleSizeInBytes === 2) {
+ return this.littleEndian ? 'pcm-s16' : 'pcm-s16be';
+ } else if (this.audioInfo.sampleSizeInBytes === 3) {
+ return this.littleEndian ? 'pcm-s24' : 'pcm-s24be';
+ } else if (this.audioInfo.sampleSizeInBytes === 4) {
+ return this.littleEndian ? 'pcm-s32' : 'pcm-s32be';
+ }
+ }
+ if (this.audioInfo.format === WaveFormat.IEEE_FLOAT) {
+ if (this.audioInfo.sampleSizeInBytes === 4) {
+ return this.littleEndian ? 'pcm-f32' : 'pcm-f32be';
+ }
+ }
+
+ return null;
+ }
+
+ async getMimeType() {
+ return 'audio/wav';
+ }
+
+ async computeDuration() {
+ await this.readMetadata();
+ assert(this.audioInfo);
+
+ const numberOfBlocks = this.dataSize / this.audioInfo.blockSizeInBytes;
+ return numberOfBlocks / this.audioInfo.sampleRate;
+ }
+
+ async getTracks() {
+ await this.readMetadata();
+ return this.tracks;
+ }
+}
+
+const SAMPLE_SIZE_IN_FRAMES = 2048;
+
+class WaveAudioTrackBacking implements InputAudioTrackBacking {
+ constructor(public demuxer: WaveDemuxer) {}
+
+ async getCodec() {
+ return this.demuxer.getCodec();
+ }
+
+ async getDecoderConfig(): Promise
{
+ const codec = this.demuxer.getCodec();
+ if (!codec) {
+ return null;
+ }
+
+ assert(this.demuxer.audioInfo);
+ return {
+ codec,
+ numberOfChannels: this.demuxer.audioInfo.numberOfChannels,
+ sampleRate: this.demuxer.audioInfo.sampleRate,
+ };
+ }
+
+ computeDuration() {
+ return this.demuxer.computeDuration();
+ }
+
+ async getNumberOfChannels() {
+ assert(this.demuxer.audioInfo);
+ return this.demuxer.audioInfo.numberOfChannels;
+ }
+
+ async getSampleRate() {
+ assert(this.demuxer.audioInfo);
+ return this.demuxer.audioInfo.sampleRate;
+ }
+
+ async getFirstTimestamp() {
+ return 0;
+ }
+
+ private async getSampleAtIndex(
+ sampleIndex: number,
+ options: SampleRetrievalOptions,
+ ): Promise {
+ assert(this.demuxer.audioInfo);
+ const startOffset = sampleIndex * SAMPLE_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes;
+ if (startOffset >= this.demuxer.dataSize) {
+ return null;
+ }
+
+ const sizeInBytes = Math.min(
+ SAMPLE_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes,
+ this.demuxer.dataSize - startOffset,
+ );
+
+ let data: Uint8Array;
+ if (options.metadataOnly) {
+ data = PLACEHOLDER_DATA;
+ } else {
+ const sizeOfOneSample = SAMPLE_SIZE_IN_FRAMES * this.demuxer.audioInfo.blockSizeInBytes;
+ const chunkSize = Math.ceil(2 ** 19 / sizeOfOneSample) * sizeOfOneSample;
+ const chunkStart = Math.floor(startOffset / chunkSize) * chunkSize;
+ const chunkEnd = chunkStart + chunkSize;
+
+ // Always load large 0.5 MiB chunks instead of just the required sample
+ await this.demuxer.chunkReader.reader.loadRange(
+ this.demuxer.dataStart + chunkStart,
+ this.demuxer.dataStart + chunkEnd,
+ );
+
+ this.demuxer.chunkReader.pos = this.demuxer.dataStart + startOffset;
+ data = this.demuxer.chunkReader.readBytes(sizeInBytes);
+ }
+
+ const timestamp = sampleIndex * SAMPLE_SIZE_IN_FRAMES / this.demuxer.audioInfo.sampleRate;
+ const duration = sizeInBytes / this.demuxer.audioInfo.blockSizeInBytes / this.demuxer.audioInfo.sampleRate;
+
+ return new EncodedAudioSample(
+ data,
+ 'key',
+ timestamp,
+ duration,
+ );
+ }
+
+ getFirstSample(options: SampleRetrievalOptions) {
+ return this.getSampleAtIndex(0, options);
+ }
+
+ getSample(timestamp: number, options: SampleRetrievalOptions) {
+ assert(this.demuxer.audioInfo);
+ const sampleIndex = Math.floor(timestamp * this.demuxer.audioInfo.sampleRate / SAMPLE_SIZE_IN_FRAMES);
+
+ return this.getSampleAtIndex(sampleIndex, options);
+ }
+
+ getNextSample(sample: EncodedAudioSample, options: SampleRetrievalOptions) {
+ assert(this.demuxer.audioInfo);
+ const sampleIndex = Math.round(sample.timestamp * this.demuxer.audioInfo.sampleRate / SAMPLE_SIZE_IN_FRAMES);
+
+ return this.getSampleAtIndex(sampleIndex + 1, options);
+ }
+
+ getKeySample(timestamp: number, options: SampleRetrievalOptions) {
+ return this.getSample(timestamp, options);
+ }
+
+ getNextKeySample(sample: EncodedAudioSample, options: SampleRetrievalOptions) {
+ return this.getNextSample(sample, options);
+ }
+}