From d3566305dfd41c23739a0e44d48c58c6327469a9 Mon Sep 17 00:00:00 2001 From: Vanilagy <1696106+Vanilagy@users.noreply.github.com> Date: Sun, 26 Jan 2025 11:45:56 +0100 Subject: [PATCH] Implement MP3 muxer --- dev/demux.html | 34 +++++++++++ src/index.ts | 1 + src/input-format.ts | 66 ++++++++++----------- src/mp3/mp3-demuxer.ts | 14 ++--- src/mp3/mp3-misc.ts | 130 +++++++++++++++++++++++++++++++++++++++++ src/mp3/mp3-muxer.ts | 125 +++++++++++++++++++++++++++++++++++++++ src/mp3/mp3-reader.ts | 101 ++------------------------------ src/mp3/mp3-writer.ts | 104 +++++++++++++++++++++++++++++++++ src/muxer.ts | 4 +- src/output-format.ts | 35 +++++++++++ src/wave/wave-muxer.ts | 16 +++-- 11 files changed, 482 insertions(+), 148 deletions(-) create mode 100644 src/mp3/mp3-misc.ts create mode 100644 src/mp3/mp3-muxer.ts create mode 100644 src/mp3/mp3-writer.ts 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); } }