mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 05:43:50 +02:00
Add WAVE muxer
This commit is contained in:
+6
-4
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
const target = new Metamuxer.BufferTarget();
|
const target = new Metamuxer.BufferTarget();
|
||||||
const output = new Metamuxer.Output({
|
const output = new Metamuxer.Output({
|
||||||
format: new Metamuxer.MkvOutputFormat(),
|
format: new Metamuxer.WaveOutputFormat(),
|
||||||
target
|
target
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,10 +59,10 @@
|
|||||||
|
|
||||||
const decoderConfig = await audioTrack.getDecoderConfig();
|
const decoderConfig = await audioTrack.getDecoderConfig();
|
||||||
const sampleSource = new Metamuxer.EncodedAudioSampleSource(await audioTrack.getCodec());
|
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());
|
const videoSampleSource = new Metamuxer.EncodedVideoSampleSource(await videoTrack.getCodec());
|
||||||
output.addAudioTrack(audioDataSource ?? sampleSource);
|
output.addAudioTrack(audioDataSource ?? sampleSource);
|
||||||
output.addVideoTrack(videoSampleSource);
|
//output.addVideoTrack(videoSampleSource);
|
||||||
|
|
||||||
output.start();
|
output.start();
|
||||||
|
|
||||||
@@ -85,10 +85,12 @@
|
|||||||
//console.log(sample)
|
//console.log(sample)
|
||||||
await audioDataSource.digest(data);
|
await audioDataSource.digest(data);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
for await (const sample of new Metamuxer.EncodedVideoSampleDrain(videoTrack).samples()) {
|
for await (const sample of new Metamuxer.EncodedVideoSampleDrain(videoTrack).samples()) {
|
||||||
//console.log(sample)
|
//console.log(sample)
|
||||||
await videoSampleSource.digest(sample, { decoderConfig: videoDecoderConfig });
|
await videoSampleSource.digest(sample, { decoderConfig: videoDecoderConfig });
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
await output.finalize();
|
await output.finalize();
|
||||||
|
|
||||||
@@ -102,7 +104,7 @@
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
download(new Blob([target.buffer]), 'converted.mkv');
|
download(new Blob([target.buffer]), 'converted.wav');
|
||||||
|
|
||||||
document.body.textContent = performance.now() - start;
|
document.body.textContent = performance.now() - start;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export {
|
|||||||
MkvOutputFormatOptions,
|
MkvOutputFormatOptions,
|
||||||
WebMOutputFormat,
|
WebMOutputFormat,
|
||||||
WebMOutputFormatOptions,
|
WebMOutputFormatOptions,
|
||||||
|
WaveOutputFormat,
|
||||||
} from './output-format';
|
} from './output-format';
|
||||||
export {
|
export {
|
||||||
VideoEncodingConfig,
|
VideoEncodingConfig,
|
||||||
@@ -53,11 +54,13 @@ export {
|
|||||||
Mp4InputFormat,
|
Mp4InputFormat,
|
||||||
QuickTimeInputFormat as MovInputFormat,
|
QuickTimeInputFormat as MovInputFormat,
|
||||||
MatroskaInputFormat,
|
MatroskaInputFormat,
|
||||||
|
WaveInputFormat,
|
||||||
ALL_FORMATS,
|
ALL_FORMATS,
|
||||||
MP4,
|
MP4,
|
||||||
QTFF,
|
QTFF,
|
||||||
MATROSKA,
|
MATROSKA,
|
||||||
WEBM,
|
WEBM,
|
||||||
|
WAVE,
|
||||||
} from './input-format';
|
} from './input-format';
|
||||||
export { Input, InputOptions } from './input';
|
export { Input, InputOptions } from './input';
|
||||||
export { InputTrack, InputVideoTrack, InputAudioTrack, SampleStats } from './input-track';
|
export { InputTrack, InputVideoTrack, InputAudioTrack, SampleStats } from './input-track';
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { IsobmffMuxer } from './isobmff/isobmff-muxer';
|
|||||||
import { MatroskaMuxer } from './matroska/matroska-muxer';
|
import { MatroskaMuxer } from './matroska/matroska-muxer';
|
||||||
import { Muxer } from './muxer';
|
import { Muxer } from './muxer';
|
||||||
import { Output } from './output';
|
import { Output } from './output';
|
||||||
|
import { WaveMuxer } from './wave/wave-muxer';
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export abstract class OutputFormat {
|
export abstract class OutputFormat {
|
||||||
@@ -197,3 +198,26 @@ export class WebMOutputFormat extends MkvOutputFormat {
|
|||||||
return '';
|
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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user