diff --git a/dev/convert.html b/dev/convert.html
index 6f1a27c..9118417 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -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
diff --git a/docs/guide/output-formats.md b/docs/guide/output-formats.md
index cd871ec..cc116a8 100644
--- a/docs/guide/output-formats.md
+++ b/docs/guide/output-formats.md
@@ -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).
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 6300110..c6ae14b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 8ac74a2..da4720a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/input-format.ts b/src/input-format.ts
index 8ed94f8..54235e3 100644
--- a/src/input-format.ts
+++ b/src/input-format.ts
@@ -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;
}
diff --git a/src/output-format.ts b/src/output-format.ts
index e26d182..5f22e3d 100644
--- a/src/output-format.ts
+++ b/src/output-format.ts
@@ -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.');
}
diff --git a/src/wave/riff-reader.ts b/src/wave/riff-reader.ts
index 58c5bd4..21ccee4 100644
--- a/src/wave/riff-reader.ts
+++ b/src/wave/riff-reader.ts
@@ -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;
diff --git a/src/wave/riff-writer.ts b/src/wave/riff-writer.ts
index cbfc102..d89512e 100644
--- a/src/wave/riff-writer.ts
+++ b/src/wave/riff-writer.ts
@@ -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) {
diff --git a/src/wave/wave-demuxer.ts b/src/wave/wave-demuxer.ts
index b5a446f..b70a464 100644
--- a/src/wave/wave-demuxer.ts
+++ b/src/wave/wave-demuxer.ts
@@ -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) {
diff --git a/src/wave/wave-muxer.ts b/src/wave/wave-muxer.ts
index c4bfb4e..878939f 100644
--- a/src/wave/wave-muxer.ts
+++ b/src/wave/wave-muxer.ts
@@ -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);