diff --git a/dev/demux.html b/dev/demux.html
index 1e754ee..d381167 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -46,7 +46,7 @@
const target = new Metamuxer.BufferTarget();
const output = new Metamuxer.Output({
- format: new Metamuxer.MkvOutputFormat(),
+ format: new Metamuxer.WaveOutputFormat(),
target
});
@@ -59,10 +59,10 @@
const decoderConfig = await audioTrack.getDecoderConfig();
const sampleSource = new Metamuxer.EncodedAudioSampleSource(await audioTrack.getCodec());
- const audioDataSource = new Metamuxer.AudioDataSource({ codec: 'vorbis', bitrate: 128e3 });
+ const audioDataSource = new Metamuxer.AudioDataSource({ codec: 'pcm-s16', bitrate: 128e3 });
const videoSampleSource = new Metamuxer.EncodedVideoSampleSource(await videoTrack.getCodec());
output.addAudioTrack(audioDataSource ?? sampleSource);
- output.addVideoTrack(videoSampleSource);
+ //output.addVideoTrack(videoSampleSource);
output.start();
@@ -85,10 +85,12 @@
//console.log(sample)
await audioDataSource.digest(data);
}
+ /*
for await (const sample of new Metamuxer.EncodedVideoSampleDrain(videoTrack).samples()) {
//console.log(sample)
await videoSampleSource.digest(sample, { decoderConfig: videoDecoderConfig });
}
+ */
await output.finalize();
@@ -102,7 +104,7 @@
a.click();
URL.revokeObjectURL(url);
}
- download(new Blob([target.buffer]), 'converted.mkv');
+ download(new Blob([target.buffer]), 'converted.wav');
document.body.textContent = performance.now() - start;
diff --git a/src/index.ts b/src/index.ts
index dc90203..3cfc1b4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -9,6 +9,7 @@ export {
MkvOutputFormatOptions,
WebMOutputFormat,
WebMOutputFormatOptions,
+ WaveOutputFormat,
} from './output-format';
export {
VideoEncodingConfig,
@@ -53,11 +54,13 @@ export {
Mp4InputFormat,
QuickTimeInputFormat as MovInputFormat,
MatroskaInputFormat,
+ WaveInputFormat,
ALL_FORMATS,
MP4,
QTFF,
MATROSKA,
WEBM,
+ WAVE,
} from './input-format';
export { Input, InputOptions } from './input';
export { InputTrack, InputVideoTrack, InputAudioTrack, SampleStats } from './input-track';
diff --git a/src/output-format.ts b/src/output-format.ts
index c75177e..63e3ab7 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -3,6 +3,7 @@ import { IsobmffMuxer } from './isobmff/isobmff-muxer';
import { MatroskaMuxer } from './matroska/matroska-muxer';
import { Muxer } from './muxer';
import { Output } from './output';
+import { WaveMuxer } from './wave/wave-muxer';
/** @public */
export abstract class OutputFormat {
@@ -197,3 +198,26 @@ export class WebMOutputFormat extends MkvOutputFormat {
return '';
}
}
+
+/** @public */
+export class WaveOutputFormat extends OutputFormat {
+ /** @internal */
+ _createMuxer(output: Output) {
+ return new WaveMuxer(output);
+ }
+
+ /** @internal */
+ _getName() {
+ return 'WAVE';
+ }
+
+ getSupportedCodecs() {
+ return WaveOutputFormat.getSupportedCodecs();
+ }
+
+ static getSupportedCodecs(): MediaCodec[] {
+ return [
+ 'pcm-u8', 'pcm-s16', 'pcm-s24', 'pcm-s32', 'pcm-f32', 'ulaw', 'alaw',
+ ];
+ }
+}
diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts
new file mode 100644
index 0000000..1477f53
--- /dev/null
+++ b/src/wave/wave-muxer.ts
@@ -0,0 +1,144 @@
+import { Muxer } from '../muxer';
+import { Output, OutputAudioTrack } from '../output';
+import { EncodedAudioSample } from '../sample';
+import { parsePcmCodec, PcmAudioCodec } from '../codec';
+import { WaveFormat } from './wave-demuxer';
+import { Writer } from '../writer';
+
+export class WaveMuxer extends Muxer {
+ private riffWriter: RiffWriter;
+ private headerWritten = false;
+ private dataSize = 0;
+
+ constructor(output: Output) {
+ super(output);
+ this.riffWriter = new RiffWriter(output._writer);
+ }
+
+ async start() {
+ // Nothing needed here - we'll write the header with the first sample
+ }
+
+ async addEncodedVideoSample() {
+ throw new Error('WAVE format does not support video.');
+ }
+
+ async addEncodedAudioSample(
+ track: OutputAudioTrack,
+ sample: EncodedAudioSample,
+ meta?: EncodedAudioChunkMetadata,
+ ) {
+ const release = await this.mutex.acquire();
+
+ try {
+ if (!this.headerWritten) {
+ if (!meta?.decoderConfig) {
+ throw new Error('Decoder config is required for first audio sample.');
+ }
+
+ this.writeHeader(track, meta.decoderConfig);
+ this.headerWritten = true;
+ }
+
+ this.validateAndNormalizeTimestamp(track, sample.timestamp, sample.type === 'key');
+
+ if (sample.data) {
+ this.output._writer.write(sample.data);
+ this.dataSize += sample.data.byteLength;
+ }
+
+ await this.output._writer.flush();
+ } finally {
+ release();
+ }
+ }
+
+ async addSubtitleCue() {
+ throw new Error('WAVE format does not support subtitles.');
+ }
+
+ private writeHeader(track: OutputAudioTrack, config: AudioDecoderConfig) {
+ const codec = track.source._codec;
+ let format: WaveFormat;
+ let sampleSize: number;
+
+ const pcmInfo = parsePcmCodec(codec as PcmAudioCodec);
+
+ if (pcmInfo.dataType === 'ulaw') {
+ format = WaveFormat.MULAW;
+ sampleSize = 1;
+ } else if (pcmInfo.dataType === 'alaw') {
+ format = WaveFormat.ALAW;
+ sampleSize = 1;
+ } else {
+ format = pcmInfo.dataType === 'float' ? WaveFormat.IEEE_FLOAT : WaveFormat.PCM;
+ sampleSize = pcmInfo.sampleSize;
+ }
+
+ const channels = config.numberOfChannels;
+ const sampleRate = config.sampleRate;
+ const blockSize = sampleSize * channels;
+
+ this.riffWriter.writeHeader(format, channels, sampleRate, blockSize, sampleSize);
+ }
+
+ async finalize() {
+ this.riffWriter.patchSizes(this.dataSize);
+ }
+}
+
+class RiffWriter {
+ private helper = new Uint8Array(8);
+ private helperView = new DataView(this.helper.buffer);
+
+ constructor(private writer: Writer) {}
+
+ writeU32(value: number) {
+ this.helperView.setUint32(0, value, true);
+ this.writer.write(this.helper.subarray(0, 4));
+ }
+
+ writeU16(value: number) {
+ this.helperView.setUint16(0, value, true);
+ this.writer.write(this.helper.subarray(0, 2));
+ }
+
+ writeAscii(text: string) {
+ this.writer.write(new TextEncoder().encode(text));
+ }
+
+ writeHeader(format: WaveFormat, channels: number, sampleRate: number, blockSize: number, bytesPerSample: number) {
+ // RIFF header
+ this.writeAscii('RIFF');
+ this.writeU32(0); // File size placeholder
+ this.writeAscii('WAVE');
+
+ // fmt chunk
+ this.writeAscii('fmt ');
+ this.writeU32(16); // Chunk size
+ this.writeU16(format);
+ this.writeU16(channels);
+ this.writeU32(sampleRate);
+ this.writeU32(sampleRate * blockSize); // Bytes per second
+ this.writeU16(blockSize);
+ this.writeU16(8 * bytesPerSample);
+
+ // data chunk
+ this.writeAscii('data');
+ this.writeU32(0); // Data size placeholder
+ }
+
+ patchSizes(dataSize: number) {
+ const currentPos = this.writer.getPos();
+
+ // Write file size
+ this.writer.seek(4);
+ this.writeU32(dataSize + 36); // File size - 8
+
+ // Write data chunk size
+ this.writer.seek(40);
+ this.writeU32(dataSize);
+
+ this.writer.seek(currentPos);
+ }
+}