diff --git a/dev/demux.html b/dev/demux.html
index 3062e52..f0fac30 100644
--- a/dev/demux.html
+++ b/dev/demux.html
@@ -16,11 +16,45 @@
source
});
+ const output = new Metamuxer.Output({
+ format: new Metamuxer.Mp3OutputFormat(),
+ target: new Metamuxer.BufferTarget()
+ });
+
+ const audioTrack = await input.getPrimaryAudioTrack();
+ const sink = new Metamuxer.EncodedAudioSampleSink(audioTrack);
+
+ const mediaSource = new Metamuxer.EncodedAudioSampleSource(await audioTrack.getCodec());
+ output.addAudioTrack(mediaSource);
+
+ output.start();
+
+ for await (const sample of sink.samples()) {
+ await mediaSource.digest(sample);
+ }
+
+ await output.finalize();
+
+ function download(blob, filename) {
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ console.log(output.target.buffer)
+ download(new Blob([output.target.buffer]), 'converted.mp3');
+
+
+ /*
const videoTrack = await input.getPrimaryVideoTrack();
const sink = new Metamuxer.EncodedVideoSampleSink(videoTrack);
console.log(await sink.getSample(16));
console.log(await sink.getFirstSample());
+ */
/*
const source = new Metamuxer.BufferSource(await file.arrayBuffer()) ?? new Metamuxer.BlobSource(file);
diff --git a/src/index.ts b/src/index.ts
index cbac2d9..f7a45cb 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -18,6 +18,7 @@ export {
MkvOutputFormatOptions,
WebMOutputFormat,
WebMOutputFormatOptions,
+ Mp3OutputFormat,
WaveOutputFormat,
TrackCountLimits,
InclusiveRange,
diff --git a/src/input-format.ts b/src/input-format.ts
index ed0fd88..1393031 100644
--- a/src/input-format.ts
+++ b/src/input-format.ts
@@ -179,39 +179,6 @@ export class WebMInputFormat extends MatroskaInputFormat {
}
}
-/** @public */
-export class WaveInputFormat extends InputFormat {
- async _canReadInput(input: Input) {
- const sourceSize = await input._mainReader.source._getSize();
- if (sourceSize < 12) {
- return false;
- }
-
- const riffReader = new RiffReader(input._mainReader);
- const riffType = riffReader.readAscii(4);
- if (riffType !== 'RIFF' && riffType !== 'RIFX') {
- return false;
- }
-
- riffReader.pos = 8;
- const format = riffReader.readAscii(4);
- return format === 'WAVE';
- }
-
- /** @internal */
- _createDemuxer(input: Input) {
- return new WaveDemuxer(input);
- }
-
- getName() {
- return 'WAVE';
- }
-
- getMimeType() {
- return 'audio/wav';
- }
-}
-
/** @public */
export class Mp3InputFormat extends InputFormat {
async _canReadInput(input: Input) {
@@ -248,6 +215,39 @@ export class Mp3InputFormat extends InputFormat {
}
}
+/** @public */
+export class WaveInputFormat extends InputFormat {
+ async _canReadInput(input: Input) {
+ const sourceSize = await input._mainReader.source._getSize();
+ if (sourceSize < 12) {
+ return false;
+ }
+
+ const riffReader = new RiffReader(input._mainReader);
+ const riffType = riffReader.readAscii(4);
+ if (riffType !== 'RIFF' && riffType !== 'RIFX') {
+ return false;
+ }
+
+ riffReader.pos = 8;
+ const format = riffReader.readAscii(4);
+ return format === 'WAVE';
+ }
+
+ /** @internal */
+ _createDemuxer(input: Input) {
+ return new WaveDemuxer(input);
+ }
+
+ getName() {
+ return 'WAVE';
+ }
+
+ getMimeType() {
+ return 'audio/wav';
+ }
+}
+
/** @public */
export const MP4 = new Mp4InputFormat();
/** @public */
diff --git a/src/mp3/mp3-demuxer.ts b/src/mp3/mp3-demuxer.ts
index 583ec23..6997126 100644
--- a/src/mp3/mp3-demuxer.ts
+++ b/src/mp3/mp3-demuxer.ts
@@ -5,7 +5,8 @@ import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
import { SampleRetrievalOptions } from '../media-sink';
import { assert, binarySearchExact, binarySearchLessOrEqual, last, UNDETERMINED_LANGUAGE } from '../misc';
import { EncodedAudioSample, PLACEHOLDER_DATA } from '../sample';
-import { FrameHeader, Mp3Reader } from './mp3-reader';
+import { FrameHeader, getXingOffset, INFO, XING } from './mp3-misc';
+import { Mp3Reader } from './mp3-reader';
const AUDIO_SAMPLES_PER_FRAME = 1152;
@@ -54,13 +55,10 @@ export class Mp3Demuxer extends Demuxer {
break;
}
- const xingOffset = header.mpegVersionId === 3
- ? (header.channelCount === 1 ? 21 : 36)
- : (header.channelCount === 1 ? 13 : 21);
+ const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
this.reader.pos = header.startPos + xingOffset;
const word = this.reader.readU32();
- const isXing = word === 0x58696e67 // 'Xing'
- || word === 0x496e666f; // 'Info'
+ const isXing = word === XING || word === INFO;
this.reader.pos = header.startPos + header.totalSize - 1; // -1 in case the frame is 1 byte too short
@@ -137,7 +135,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
async getNumberOfChannels() {
assert(this.demuxer.firstFrameHeader);
- return this.demuxer.firstFrameHeader.channelCount;
+ return this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2;
}
async getSampleRate() {
@@ -150,7 +148,7 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
return {
codec: 'mp3',
- numberOfChannels: this.demuxer.firstFrameHeader.channelCount,
+ numberOfChannels: this.demuxer.firstFrameHeader.channel === 3 ? 1 : 2,
sampleRate: this.demuxer.firstFrameHeader.sampleRate,
};
}
diff --git a/src/mp3/mp3-misc.ts b/src/mp3/mp3-misc.ts
new file mode 100644
index 0000000..496dac3
--- /dev/null
+++ b/src/mp3/mp3-misc.ts
@@ -0,0 +1,130 @@
+export const FRAME_HEADER_SIZE = 4;
+
+// These are in kbps:
+export const MPEG_V1_BITRATES: Record = {
+ // Layer 3
+ 1: [-1, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1],
+ // Layer 2
+ 2: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1],
+ // Layer 1
+ 3: [-1, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1],
+};
+export const MPEG_V2_BITRATES: Record = {
+ // Layer 3
+ 1: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1],
+ // Layer 2
+ 2: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
+ // Layer 1
+ 3: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
+};
+export const SAMPLING_RATES: Record = {
+ // MPEG Version 2.5
+ 0: [11025, 12000, 8000, -1],
+ // MPEG Version 2 (ISO/IEC 13818-3)
+ 2: [22050, 24000, 16000, -1],
+ // MPEG Version 1 (ISO/IEC 11172-3)
+ 3: [44100, 48000, 32000, -1],
+};
+
+/** 'Xing' */
+export const XING = 0x58696e67;
+/** 'Info' */
+export const INFO = 0x496e666f;
+
+export type FrameHeader = {
+ startPos: number;
+ mpegVersionId: number;
+ layer: number;
+ bitrate: number;
+ frequencyIndex: number;
+ sampleRate: number;
+ channel: number;
+ modeExtension: number;
+ copyright: number;
+ original: number;
+ emphasis: number;
+ totalSize: number;
+ dataStart: number;
+ dataSize: number;
+};
+
+export const computeMp3FrameSize = (bitrate: number, sampleRate: number, padding: number) => {
+ return Math.floor((144 * bitrate / sampleRate) + padding);
+};
+
+export const getXingOffset = (mpegVersionId: number, channel: number) => {
+ return mpegVersionId === 3
+ ? (channel === 3 ? 21 : 36)
+ : (channel === 3 ? 13 : 21);
+};
+
+export const readFrameHeader = (word: number, reader: { pos: number; fileSize: number | null }): FrameHeader | null => {
+ const startPos = reader.pos;
+
+ const firstByte = word >>> 24;
+ const secondByte = (word >>> 16) & 0xff;
+ const thirdByte = (word >>> 8) & 0xff;
+ const fourthByte = word & 0xff;
+
+ if (firstByte !== 0xff && secondByte !== 0xff && thirdByte !== 0xff && fourthByte !== 0xff) {
+ reader.pos += 4;
+ return null;
+ }
+
+ reader.pos += 1;
+
+ if ((secondByte & 0xe0) !== 0xe0) {
+ return null;
+ }
+
+ const mpegVersionId = (secondByte >> 3) & 0x3;
+ const layer = (secondByte >> 1) & 0x3;
+
+ const bitrateIndex = (thirdByte >> 4) & 0xf;
+ const frequencyIndex = (thirdByte >> 2) & 0x3;
+ const padding = (thirdByte >> 1) & 0x1;
+
+ const channel = (fourthByte >> 6) & 0x3;
+ const modeExtension = (fourthByte >> 4) & 0x3;
+ const copyright = (fourthByte >> 3) & 0x1;
+ const original = (fourthByte >> 2) & 0x1;
+ const emphasis = fourthByte & 0x3;
+
+ const kilobitRate = mpegVersionId === 3
+ ? MPEG_V1_BITRATES[layer]?.[bitrateIndex]
+ : MPEG_V2_BITRATES[layer]?.[bitrateIndex];
+ if (!kilobitRate || kilobitRate === -1) {
+ return null;
+ }
+
+ const bitrate = kilobitRate * 1000;
+
+ const sampleRate = SAMPLING_RATES[mpegVersionId]?.[frequencyIndex];
+ if (!sampleRate || sampleRate === -1) {
+ return null;
+ }
+
+ const frameLength = computeMp3FrameSize(bitrate, sampleRate, padding);
+
+ if (reader.fileSize !== null && reader.fileSize - startPos < frameLength) {
+ // The frame doesn't fit into the rest of the file
+ return null;
+ }
+
+ return {
+ startPos: startPos,
+ mpegVersionId,
+ layer,
+ bitrate,
+ frequencyIndex,
+ sampleRate,
+ channel,
+ modeExtension,
+ copyright,
+ original,
+ emphasis,
+ totalSize: frameLength,
+ dataStart: startPos + 4,
+ dataSize: frameLength - 4,
+ };
+};
diff --git a/src/mp3/mp3-muxer.ts b/src/mp3/mp3-muxer.ts
new file mode 100644
index 0000000..47ae310
--- /dev/null
+++ b/src/mp3/mp3-muxer.ts
@@ -0,0 +1,125 @@
+import { assert } from '../misc';
+import { Muxer } from '../muxer';
+import { Output, OutputAudioTrack } from '../output';
+import { EncodedAudioSample } from '../sample';
+import { Writer } from '../writer';
+import { getXingOffset, INFO, readFrameHeader, XING } from './mp3-misc';
+import { Mp3Writer, XingFrameData } from './mp3-writer';
+
+export class Mp3Muxer extends Muxer {
+ private writer: Writer;
+ private mp3Writer: Mp3Writer;
+ private xingFrameData: XingFrameData | null = null;
+ private frameCount = 0;
+ private framePositions: number[] = [];
+
+ constructor(output: Output) {
+ super(output);
+
+ this.writer = output._writer;
+ this.mp3Writer = new Mp3Writer(output._writer);
+ }
+
+ async start() {
+ // Nothing needed here
+ }
+
+ async addEncodedVideoSample() {
+ throw new Error('MP3 does not support video.');
+ }
+
+ async addEncodedAudioSample(
+ track: OutputAudioTrack,
+ sample: EncodedAudioSample,
+ ) {
+ const release = await this.mutex.acquire();
+
+ try {
+ if (!this.xingFrameData) {
+ const view = new DataView(sample.data.buffer, sample.data.byteOffset, sample.data.byteLength);
+ if (view.byteLength < 4) {
+ throw new Error('Invalid MP3 header in sample.');
+ }
+
+ const word = view.getUint32(0, false);
+ const header = readFrameHeader(word, { pos: 0, fileSize: null });
+ if (!header) {
+ throw new Error('Invalid MP3 header in sample.');
+ }
+
+ const xingOffset = getXingOffset(header.mpegVersionId, header.channel);
+ if (view.byteLength >= xingOffset + 4) {
+ const word = view.getUint32(xingOffset, false);
+ const isXing = word === XING || word === INFO;
+
+ if (isXing) {
+ // This is not a data frame, so let's completely ignore this sample
+ return;
+ }
+ }
+
+ this.xingFrameData = {
+ mpegVersionId: header.mpegVersionId,
+ layer: header.layer,
+ frequencyIndex: header.frequencyIndex,
+ channel: header.channel,
+ modeExtension: header.modeExtension,
+ copyright: header.copyright,
+ original: header.original,
+ emphasis: header.emphasis,
+
+ frameCount: null,
+ fileSize: null,
+ toc: null,
+ };
+
+ // Write a Xing frame because this muxer doesn't make any bitrate constraints, meaning we don't know if
+ // this will be a constant or variable bitrate file. Therefore, always write the Xing frame.
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
+
+ this.frameCount++;
+ }
+
+ this.validateAndNormalizeTimestamp(track, sample.timestamp, sample.type === 'key');
+
+ this.framePositions.push(this.writer.getPos());
+ this.writer.write(sample.data);
+ this.frameCount++;
+
+ await this.writer.flush();
+ } finally {
+ release();
+ }
+ }
+
+ async addSubtitleCue() {
+ throw new Error('MP3 does not support subtitles.');
+ }
+
+ async finalize() {
+ if (!this.xingFrameData) {
+ return;
+ }
+
+ const endPos = this.writer.getPos();
+
+ this.writer.seek(0);
+
+ const toc = new Uint8Array(100);
+ for (let i = 0; i < 100; i++) {
+ const index = Math.floor(this.framePositions.length * (i / 100));
+ assert(index !== -1 && index < this.framePositions.length);
+
+ const byteOffset = this.framePositions[index]!;
+ toc[i] = 256 * (byteOffset / endPos);
+ }
+
+ this.xingFrameData.frameCount = this.frameCount;
+ this.xingFrameData.fileSize = endPos;
+ this.xingFrameData.toc = toc;
+
+ this.mp3Writer.writeXingFrame(this.xingFrameData);
+
+ this.writer.seek(endPos);
+ }
+}
diff --git a/src/mp3/mp3-reader.ts b/src/mp3/mp3-reader.ts
index 957fd82..0883163 100644
--- a/src/mp3/mp3-reader.ts
+++ b/src/mp3/mp3-reader.ts
@@ -1,44 +1,6 @@
import { assert } from '../misc';
import { Reader } from '../reader';
-
-const FRAME_HEADER_SIZE = 4;
-
-// These are in kbps:
-const MPEG_V1_BITRATES: Record = {
- // Layer 3
- 1: [-1, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1],
- // Layer 2
- 2: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1],
- // Layer 1
- 3: [-1, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1],
-};
-const MPEG_V2_BITRATES: Record = {
- // Layer 3
- 1: [-1, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, -1],
- // Layer 2
- 2: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
- // Layer 1
- 3: [-1, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1],
-};
-const SAMPLING_RATES: Record = {
- // MPEG Version 2.5
- 0: [11025, 12000, 8000, -1],
- // MPEG Version 2 (ISO/IEC 13818-3)
- 2: [22050, 24000, 16000, -1],
- // MPEG Version 1 (ISO/IEC 11172-3)
- 3: [44100, 48000, 32000, -1],
-};
-
-export type FrameHeader = {
- startPos: number;
- mpegVersionId: number;
- bitrate: number;
- sampleRate: number;
- channelCount: number;
- totalSize: number;
- dataStart: number;
- dataSize: number;
-};
+import { FRAME_HEADER_SIZE, FrameHeader, readFrameHeader } from './mp3-misc';
export class Mp3Reader {
pos = 0;
@@ -96,66 +58,13 @@ export class Mp3Reader {
until ??= this.fileSize;
while (this.pos < until - FRAME_HEADER_SIZE) {
- const startPos = this.pos;
const word = this.readU32();
+ this.pos -= 4;
- const firstByte = word >>> 24;
- const secondByte = (word >>> 16) & 0xff;
- const thirdByte = (word >>> 8) & 0xff;
- const fourthByte = word & 0xff;
-
- if (firstByte !== 0xff && secondByte !== 0xff && thirdByte !== 0xff && fourthByte !== 0xff) {
- continue;
+ const header = readFrameHeader(word, this);
+ if (header) {
+ return header;
}
-
- this.pos -= 3; // For when we continue
-
- if ((secondByte & 0xe0) !== 0xe0) {
- continue;
- }
-
- const mpegVersionId = (secondByte >> 3) & 0x3;
- const layer = (secondByte >> 1) & 0x3;
-
- const bitrateIndex = (thirdByte >> 4) & 0xf;
- const frequencyIndex = (thirdByte >> 2) & 0x3;
- const padding = (thirdByte >> 1) & 0x1;
-
- const channel = (fourthByte >> 6) & 0x3;
-
- const kilobitRate = mpegVersionId === 3
- ? MPEG_V1_BITRATES[layer]?.[bitrateIndex]
- : MPEG_V2_BITRATES[layer]?.[bitrateIndex];
- if (!kilobitRate || kilobitRate === -1) {
- continue;
- }
-
- const bitrate = kilobitRate * 1000;
-
- const sampleRate = SAMPLING_RATES[mpegVersionId]?.[frequencyIndex];
- if (!sampleRate || sampleRate === -1) {
- continue;
- }
-
- const channelCount = channel === 3 ? 1 : 2;
-
- const frameLength = Math.floor((144 * bitrate / sampleRate) + padding);
-
- if (this.fileSize - startPos < frameLength) {
- // The frame doesn't fit into the rest of the file
- return null;
- }
-
- return {
- startPos,
- mpegVersionId,
- bitrate,
- sampleRate,
- channelCount,
- totalSize: frameLength,
- dataStart: startPos + 4,
- dataSize: frameLength - 4,
- };
}
return null;
diff --git a/src/mp3/mp3-writer.ts b/src/mp3/mp3-writer.ts
new file mode 100644
index 0000000..f5a603c
--- /dev/null
+++ b/src/mp3/mp3-writer.ts
@@ -0,0 +1,104 @@
+import { Writer } from '../writer';
+import {
+ computeMp3FrameSize,
+ getXingOffset,
+ MPEG_V1_BITRATES,
+ MPEG_V2_BITRATES,
+ SAMPLING_RATES,
+ XING,
+} from './mp3-misc';
+
+export type XingFrameData = {
+ mpegVersionId: number;
+ layer: number;
+ frequencyIndex: number;
+ channel: number;
+ modeExtension: number;
+ copyright: number;
+ original: number;
+ emphasis: number;
+
+ frameCount: number | null;
+ fileSize: number | null;
+ toc: Uint8Array | null;
+};
+
+export class Mp3Writer {
+ private helper = new Uint8Array(8);
+ private helperView = new DataView(this.helper.buffer);
+
+ constructor(private writer: Writer) {}
+
+ writeU32(value: number) {
+ this.helperView.setUint32(0, value, false);
+ this.writer.write(this.helper.subarray(0, 4));
+ }
+
+ writeXingFrame(data: XingFrameData) {
+ const startPos = this.writer.getPos();
+
+ const firstByte = 0xff;
+ const secondByte = 0xe0 | (data.mpegVersionId << 3) | (data.layer << 1);
+
+ const bitrateGroup = data.mpegVersionId === 3 ? MPEG_V1_BITRATES : MPEG_V2_BITRATES;
+ const bitrates = bitrateGroup?.[data.layer];
+ if (!bitrates) {
+ throw new Error('Invalid MPEG version and layer combination.');
+ }
+
+ const sampleRate = SAMPLING_RATES[data.mpegVersionId]?.[data.frequencyIndex];
+ if (!sampleRate || sampleRate === -1) {
+ throw new Error('Invalid MPEG version and frequency index combination.');
+ }
+
+ const padding = 0;
+ const neededBytes = 155;
+
+ // Let's find the lowest bitrate for which the frame size is sufficiently large to fit all the data
+ const bitrateIndex = bitrates.findIndex((kbr) => {
+ return computeMp3FrameSize(1000 * kbr, sampleRate, padding) >= neededBytes;
+ });
+ if (bitrateIndex === -1) {
+ throw new Error('No suitable bitrate found.');
+ }
+
+ const thirdByte = (bitrateIndex << 4) | (data.frequencyIndex << 2) | padding << 1;
+ const fourthByte = (data.channel << 6)
+ | (data.modeExtension << 4)
+ | (data.copyright << 3)
+ | (data.original << 2)
+ | data.emphasis;
+
+ this.helper[0] = firstByte;
+ this.helper[1] = secondByte;
+ this.helper[2] = thirdByte;
+ this.helper[3] = fourthByte;
+
+ this.writer.write(this.helper.subarray(0, 4));
+
+ const xingOffset = getXingOffset(data.mpegVersionId, data.channel);
+
+ this.writer.seek(startPos + xingOffset);
+ this.writeU32(XING);
+
+ let flags = 0;
+ if (data.frameCount !== null) {
+ flags |= 1;
+ }
+ if (data.fileSize !== null) {
+ flags |= 2;
+ }
+ if (data.toc !== null) {
+ flags |= 4;
+ }
+
+ this.writeU32(flags);
+
+ this.writeU32(data.frameCount ?? 0);
+ this.writeU32(data.fileSize ?? 0);
+ this.writer.write(data.toc ?? new Uint8Array(100));
+
+ const frameSize = computeMp3FrameSize(1000 * bitrates[bitrateIndex]!, sampleRate, padding);
+ this.writer.seek(startPos + frameSize);
+ }
+}
diff --git a/src/muxer.ts b/src/muxer.ts
index 8afc366..aafb3af 100644
--- a/src/muxer.ts
+++ b/src/muxer.ts
@@ -14,12 +14,12 @@ export abstract class Muxer {
abstract start(): Promise;
abstract addEncodedVideoSample(
track: OutputVideoTrack,
- chunk: EncodedVideoSample,
+ sample: EncodedVideoSample,
meta?: EncodedVideoChunkMetadata
): Promise;
abstract addEncodedAudioSample(
track: OutputAudioTrack,
- chunk: EncodedAudioSample,
+ sample: EncodedAudioSample,
meta?: EncodedAudioChunkMetadata
): Promise;
abstract addSubtitleCue(track: OutputSubtitleTrack, cue: SubtitleCue, meta?: SubtitleMetadata): Promise;
diff --git a/src/output-format.ts b/src/output-format.ts
index d70890e..0327d7f 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -11,6 +11,7 @@ import {
} from './codec';
import { IsobmffMuxer } from './isobmff/isobmff-muxer';
import { MatroskaMuxer } from './matroska/matroska-muxer';
+import { Mp3Muxer } from './mp3/mp3-muxer';
import { Muxer } from './muxer';
import { Output, TrackType } from './output';
import { WaveMuxer } from './wave/wave-muxer';
@@ -256,6 +257,40 @@ export class WebMOutputFormat extends MkvOutputFormat {
}
}
+/** @public */
+export class Mp3OutputFormat extends OutputFormat {
+ /** @internal */
+ _createMuxer(output: Output) {
+ return new Mp3Muxer(output);
+ }
+
+ /** @internal */
+ _getName() {
+ return 'MP3';
+ }
+
+ getSupportedTrackCounts(): TrackCountLimits {
+ return {
+ video: { min: 0, max: 0 },
+ audio: { min: 1, max: 1 },
+ subtitle: { min: 0, max: 0 },
+ total: { min: 1, max: 1 },
+ };
+ }
+
+ getFileExtension() {
+ return '.mp3';
+ }
+
+ getSupportedCodecs() {
+ return Mp3OutputFormat.getSupportedCodecs();
+ }
+
+ static getSupportedCodecs(): MediaCodec[] {
+ return ['mp3'];
+ }
+}
+
/** @public */
export class WaveOutputFormat extends OutputFormat {
/** @internal */
diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts
index 5597eb2..174150d 100644
--- a/src/wave/wave-muxer.ts
+++ b/src/wave/wave-muxer.ts
@@ -24,7 +24,7 @@ export class WaveMuxer extends Muxer {
}
async addEncodedVideoSample() {
- throw new Error('WAVE format does not support video.');
+ throw new Error('WAVE does not support video.');
}
async addEncodedAudioSample(
@@ -46,19 +46,17 @@ export class WaveMuxer extends Muxer {
this.validateAndNormalizeTimestamp(track, sample.timestamp, sample.type === 'key');
- if (sample.data) {
- this.output._writer.write(sample.data);
- this.dataSize += sample.data.byteLength;
- }
+ this.writer.write(sample.data);
+ this.dataSize += sample.data.byteLength;
- await this.output._writer.flush();
+ await this.writer.flush();
} finally {
release();
}
}
async addSubtitleCue() {
- throw new Error('WAVE format does not support subtitles.');
+ throw new Error('WAVE does not support subtitles.');
}
private writeHeader(track: OutputAudioTrack, config: AudioDecoderConfig) {
@@ -102,7 +100,7 @@ export class WaveMuxer extends Muxer {
}
async finalize() {
- const currentPos = this.writer.getPos();
+ const endPos = this.writer.getPos();
// Write file size
this.writer.seek(4);
@@ -112,6 +110,6 @@ export class WaveMuxer extends Muxer {
this.writer.seek(40);
this.riffWriter.writeU32(this.dataSize);
- this.writer.seek(currentPos);
+ this.writer.seek(endPos);
}
}