Add support for reading and writing RF64 WAVE files

This commit is contained in:
Vanilagy
2025-07-18 16:48:23 +02:00
parent 2277810853
commit 4b12468008
10 changed files with 124 additions and 22 deletions
+3 -3
View File
@@ -21,7 +21,7 @@
chunked: true,
chunkSize: 2**20
});
const outputFormat = new Mediabunny.Mp4OutputFormat();
const outputFormat = new Mediabunny.WavOutputFormat({ large: true });
const button = document.createElement('button');
button.textContent = 'Cancel';
@@ -38,8 +38,8 @@
target
}),
audio: {
codec: 'opus',
bitrate: 128000,
//codec: 'opus',
//bitrate: 128000,
//numberOfChannels: 1,
//sampleRate: 4000
//discard: true
+3
View File
@@ -227,8 +227,11 @@ const output = new Output({
The following options are available:
```ts
type WavOutputFormatOptions = {
large?: boolean;
onHeader?: (data: Uint8Array, position: number) => unknown;
};
```
- `large`\
When enabled, an RF64 file be written, allowing for file sizes to exceed 4 GiB, which is otherwise not possible for regular WAVE files.
- `onHeader`\
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).
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mediabunny",
"version": "1.1.1",
"version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mediabunny",
"version": "1.1.1",
"version": "1.2.0",
"license": "MPL-2.0",
"dependencies": {
"@types/dom-mediacapture-transform": "^0.1.11",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "mediabunny",
"author": "Vanilagy",
"version": "1.1.1",
"version": "1.2.0",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module",
"main": "./dist/bundles/mediabunny.cjs",
+1 -1
View File
@@ -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;
}
+9
View File
@@ -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.');
}
+15
View File
@@ -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;
+9 -3
View File
@@ -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) {
+24 -3
View File
@@ -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
View File
@@ -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);