UNPKG

mediabunny

Version:

Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.

150 lines (149 loc) 5.67 kB
/*! * Copyright (c) 2025-present, Vanilagy and contributors * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Muxer } from '../muxer.js'; import { parsePcmCodec, validateAudioChunkMetadata } from '../codec.js'; import { WaveFormat } from './wave-demuxer.js'; import { RiffWriter } from './riff-writer.js'; import { assert } from '../misc.js'; export class WaveMuxer extends Muxer { constructor(output, format) { super(output); this.headerWritten = false; this.dataSize = 0; this.sampleRate = null; this.sampleCount = 0; this.format = format; this.writer = output._writer; this.riffWriter = new RiffWriter(output._writer); this.isRf64 = !!format._options.large; } async start() { // Nothing needed here - we'll write the header with the first sample } async getMimeType() { return 'audio/wav'; } async addEncodedVideoPacket() { throw new Error('WAVE does not support video.'); } async addEncodedAudioPacket(track, packet, meta) { const release = await this.mutex.acquire(); try { if (!this.headerWritten) { validateAudioChunkMetadata(meta); assert(meta); assert(meta.decoderConfig); this.writeHeader(track, meta.decoderConfig); this.sampleRate = meta.decoderConfig.sampleRate; this.headerWritten = true; } this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key'); if (!this.isRf64 && this.writer.getPos() + packet.data.byteLength >= 2 ** 32) { throw new Error('Adding more audio data would exceed the maximum RIFF size of 4 GiB. To write larger files, use' + ' RF64 by setting `large: true` in the WavOutputFormatOptions.'); } this.writer.write(packet.data); this.dataSize += packet.data.byteLength; this.sampleCount += Math.round(packet.duration * this.sampleRate); await this.writer.flush(); } finally { release(); } } async addSubtitleCue() { throw new Error('WAVE does not support subtitles.'); } writeHeader(track, config) { if (this.format._options.onHeader) { this.writer.startTrackingWrites(); } let format; const codec = track.source._codec; const pcmInfo = parsePcmCodec(codec); if (pcmInfo.dataType === 'ulaw') { format = WaveFormat.MULAW; } else if (pcmInfo.dataType === 'alaw') { format = WaveFormat.ALAW; } else if (pcmInfo.dataType === 'float') { format = WaveFormat.IEEE_FLOAT; } else { format = WaveFormat.PCM; } const channels = config.numberOfChannels; const sampleRate = config.sampleRate; const blockSize = pcmInfo.sampleSize * channels; // RIFF header this.riffWriter.writeAscii(this.isRf64 ? 'RF64' : 'RIFF'); if (this.isRf64) { this.riffWriter.writeU32(0xffffffff); // Not used in RF64 } else { this.riffWriter.writeU32(0); // File size placeholder } this.riffWriter.writeAscii('WAVE'); if (this.isRf64) { this.riffWriter.writeAscii('ds64'); this.riffWriter.writeU32(28); // Chunk size this.riffWriter.writeU64(0); // RIFF size placeholder this.riffWriter.writeU64(0); // Data size placeholder this.riffWriter.writeU64(0); // Sample count placeholder this.riffWriter.writeU32(0); // Table length // Empty table } // fmt chunk this.riffWriter.writeAscii('fmt '); this.riffWriter.writeU32(16); // Chunk size this.riffWriter.writeU16(format); this.riffWriter.writeU16(channels); this.riffWriter.writeU32(sampleRate); this.riffWriter.writeU32(sampleRate * blockSize); // Bytes per second this.riffWriter.writeU16(blockSize); this.riffWriter.writeU16(8 * pcmInfo.sampleSize); // data chunk this.riffWriter.writeAscii('data'); if (this.isRf64) { this.riffWriter.writeU32(0xffffffff); // Not used in RF64 } else { this.riffWriter.writeU32(0); // Data size placeholder } if (this.format._options.onHeader) { const { data, start } = this.writer.stopTrackingWrites(); this.format._options.onHeader(data, start); } } async finalize() { const release = await this.mutex.acquire(); const endPos = this.writer.getPos(); if (this.isRf64) { // Write riff size this.writer.seek(20); this.riffWriter.writeU64(endPos - 8); // Write data size this.writer.seek(28); this.riffWriter.writeU64(this.dataSize); // Write sample count this.writer.seek(36); this.riffWriter.writeU64(this.sampleCount); } else { // Write file size this.writer.seek(4); this.riffWriter.writeU32(endPos - 8); // Write data chunk size this.writer.seek(40); this.riffWriter.writeU32(this.dataSize); } this.writer.seek(endPos); release(); } }