mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 19:03:46 +02:00
Add support for reading and writing RF64 WAVE files
This commit is contained in:
+1
-1
@@ -291,7 +291,7 @@ export class WaveInputFormat extends InputFormat {
|
||||
|
||||
const riffReader = new RiffReader(input._mainReader);
|
||||
const riffType = riffReader.readAscii(4);
|
||||
if (riffType !== 'RIFF' && riffType !== 'RIFX') {
|
||||
if (riffType !== 'RIFF' && riffType !== 'RIFX' && riffType !== 'RF64') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -546,6 +546,12 @@ export class Mp3OutputFormat extends OutputFormat {
|
||||
* @public
|
||||
*/
|
||||
export type WavOutputFormatOptions = {
|
||||
/**
|
||||
* When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible
|
||||
* for regular WAVE files.
|
||||
*/
|
||||
large?: boolean;
|
||||
|
||||
/**
|
||||
* Will be called once the file header is written. The header consists of the RIFF header, the format chunk, and the
|
||||
* start of the data chunk (with a placeholder size of 0).
|
||||
@@ -565,6 +571,9 @@ export class WavOutputFormat extends OutputFormat {
|
||||
if (!options || typeof options !== 'object') {
|
||||
throw new TypeError('options must be an object.');
|
||||
}
|
||||
if (options.large !== undefined && typeof options.large !== 'boolean') {
|
||||
throw new TypeError('options.large, when provided, must be a boolean.');
|
||||
}
|
||||
if (options.onHeader !== undefined && typeof options.onHeader !== 'function') {
|
||||
throw new TypeError('options.onHeader, when provided, must be a function.');
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ export class RiffReader {
|
||||
return view.getUint32(offset, this.littleEndian);
|
||||
}
|
||||
|
||||
readU64() {
|
||||
let low: number;
|
||||
let high: number;
|
||||
|
||||
if (this.littleEndian) {
|
||||
low = this.readU32();
|
||||
high = this.readU32();
|
||||
} else {
|
||||
high = this.readU32();
|
||||
low = this.readU32();
|
||||
}
|
||||
|
||||
return high * 0x100000000 + low;
|
||||
}
|
||||
|
||||
readAscii(length: number) {
|
||||
const { view, offset } = this.reader.getViewAndOffset(this.pos, this.pos + length);
|
||||
this.pos += length;
|
||||
|
||||
@@ -14,14 +14,20 @@ export class RiffWriter {
|
||||
|
||||
constructor(private writer: Writer) {}
|
||||
|
||||
writeU16(value: number) {
|
||||
this.helperView.setUint16(0, value, true);
|
||||
this.writer.write(this.helper.subarray(0, 2));
|
||||
}
|
||||
|
||||
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));
|
||||
writeU64(value: number) {
|
||||
this.helperView.setUint32(0, value, true);
|
||||
this.helperView.setUint32(4, Math.floor(value / 2 ** 32), true);
|
||||
this.writer.write(this.helper);
|
||||
}
|
||||
|
||||
writeAscii(text: string) {
|
||||
|
||||
@@ -53,9 +53,13 @@ export class WaveDemuxer extends Demuxer {
|
||||
const actualFileSize = await this.metadataReader.reader.source.getSize();
|
||||
|
||||
const riffType = this.metadataReader.readAscii(4);
|
||||
this.metadataReader.littleEndian = riffType === 'RIFF';
|
||||
this.metadataReader.littleEndian = riffType !== 'RIFX';
|
||||
|
||||
const totalFileSize = Math.min(this.metadataReader.readU32() + 8, actualFileSize);
|
||||
const isRf64 = riffType === 'RF64';
|
||||
|
||||
const outerChunkSize = this.metadataReader.readU32();
|
||||
|
||||
let totalFileSize = isRf64 ? actualFileSize : Math.min(outerChunkSize + 8, actualFileSize);
|
||||
const format = this.metadataReader.readAscii(4);
|
||||
|
||||
if (format !== 'WAVE') {
|
||||
@@ -63,6 +67,9 @@ export class WaveDemuxer extends Demuxer {
|
||||
}
|
||||
|
||||
this.metadataReader.pos = 12;
|
||||
let chunksRead = 0;
|
||||
let dataChunkSize: number | null = null;
|
||||
|
||||
while (this.metadataReader.pos < totalFileSize) {
|
||||
await this.metadataReader.reader.loadRange(this.metadataReader.pos, this.metadataReader.pos + 8);
|
||||
|
||||
@@ -70,14 +77,28 @@ export class WaveDemuxer extends Demuxer {
|
||||
const chunkSize = this.metadataReader.readU32();
|
||||
const startPos = this.metadataReader.pos;
|
||||
|
||||
if (isRf64 && chunksRead === 0 && chunkId !== 'ds64') {
|
||||
throw new Error('Invalid RF64 file: First chunk must be "ds64".');
|
||||
}
|
||||
|
||||
if (chunkId === 'fmt ') {
|
||||
await this.parseFmtChunk(chunkSize);
|
||||
} else if (chunkId === 'data') {
|
||||
dataChunkSize ??= chunkSize;
|
||||
|
||||
this.dataStart = this.metadataReader.pos;
|
||||
this.dataSize = Math.min(chunkSize, totalFileSize - this.dataStart);
|
||||
this.dataSize = Math.min(dataChunkSize, totalFileSize - this.dataStart);
|
||||
} else if (chunkId === 'ds64') {
|
||||
// File and data chunk sizes are defined in here instead
|
||||
|
||||
const riffChunkSize = this.metadataReader.readU64();
|
||||
dataChunkSize = this.metadataReader.readU64();
|
||||
|
||||
totalFileSize = Math.min(riffChunkSize + 8, actualFileSize);
|
||||
}
|
||||
|
||||
this.metadataReader.pos = startPos + chunkSize + (chunkSize & 1); // Handle padding
|
||||
chunksRead++;
|
||||
}
|
||||
|
||||
if (!this.audioInfo) {
|
||||
|
||||
+57
-9
@@ -18,10 +18,13 @@ import { assert } from '../misc';
|
||||
|
||||
export class WaveMuxer extends Muxer {
|
||||
private format: WavOutputFormat;
|
||||
private isRf64: boolean;
|
||||
private writer: Writer;
|
||||
private riffWriter: RiffWriter;
|
||||
private headerWritten = false;
|
||||
private dataSize = 0;
|
||||
private sampleRate: number | null = null;
|
||||
private sampleCount = 0;
|
||||
|
||||
constructor(output: Output, format: WavOutputFormat) {
|
||||
super(output);
|
||||
@@ -29,6 +32,7 @@ export class WaveMuxer extends Muxer {
|
||||
this.format = format;
|
||||
this.writer = output._writer;
|
||||
this.riffWriter = new RiffWriter(output._writer);
|
||||
this.isRf64 = !!format._options.large;
|
||||
}
|
||||
|
||||
async start() {
|
||||
@@ -58,13 +62,22 @@ export class WaveMuxer extends Muxer {
|
||||
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 {
|
||||
@@ -101,10 +114,26 @@ export class WaveMuxer extends Muxer {
|
||||
const blockSize = pcmInfo.sampleSize * channels;
|
||||
|
||||
// RIFF header
|
||||
this.riffWriter.writeAscii('RIFF');
|
||||
this.riffWriter.writeU32(0); // File size placeholder
|
||||
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
|
||||
@@ -117,7 +146,12 @@ export class WaveMuxer extends Muxer {
|
||||
|
||||
// data chunk
|
||||
this.riffWriter.writeAscii('data');
|
||||
this.riffWriter.writeU32(0); // Data size placeholder
|
||||
|
||||
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();
|
||||
@@ -130,13 +164,27 @@ export class WaveMuxer extends Muxer {
|
||||
|
||||
const endPos = this.writer.getPos();
|
||||
|
||||
// Write file size
|
||||
this.writer.seek(4);
|
||||
this.riffWriter.writeU32(this.dataSize + 36); // File size - 8
|
||||
if (this.isRf64) {
|
||||
// Write riff size
|
||||
this.writer.seek(20);
|
||||
this.riffWriter.writeU64(endPos - 8);
|
||||
|
||||
// Write data chunk size
|
||||
this.writer.seek(40);
|
||||
this.riffWriter.writeU32(this.dataSize);
|
||||
// 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user