Compare commits

...
4 Commits
Author SHA1 Message Date
Vanilagy f877fabc60 Bump patch 2025-12-30 16:20:52 +01:00
Vanilagy a2115b1de7 Add workaround for WebKit AudioData.copyTo bug (fixes #260) 2025-12-30 16:10:53 +01:00
Vanilagy 8d46d50c39 Document that ADTS is append-only 2025-12-30 14:11:32 +01:00
Vanilagy 19219fc1ce Add Polotno sponsor 2025-12-29 20:28:13 +01:00
6 changed files with 144 additions and 55 deletions
+4
View File
@@ -272,6 +272,10 @@ const output = new Output({
}); });
``` ```
::: info
This format ensures [append-only writing](#append-only-writing).
:::
The following options are available: The following options are available:
```ts ```ts
type AdtsOutputFormatOptions = { type AdtsOutputFormatOptions = {
+1
View File
@@ -106,6 +106,7 @@ const sponsors = {
{ image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' }, { image: '/sponsors/jellypod.png', name: 'Jellypod', url: 'https://jellypod.ai/' },
], ],
individual: [ individual: [
{ image: 'https://avatars.githubusercontent.com/u/82552321', name: 'Polotno', url: 'https://github.com/polotno-project' },
{ image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' }, { image: 'https://avatars.githubusercontent.com/u/489051', name: 'Roman Rädle', url: 'https://github.com/raedle' },
{ image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' }, { image: 'https://avatars.githubusercontent.com/u/197597', name: 'Christopher Chedeau', url: 'https://github.com/vjeux' },
{ image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' }, { image: 'https://avatars.githubusercontent.com/u/84167135', name: 'Memenome', url: 'https://github.com/memenome' },
+6 -6
View File
@@ -1,12 +1,12 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"version": "1.27.2", "version": "1.27.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "mediabunny", "name": "mediabunny",
"version": "1.27.2", "version": "1.27.3",
"license": "MPL-2.0", "license": "MPL-2.0",
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -7739,9 +7739,9 @@
} }
}, },
"node_modules/mediabunny": { "node_modules/mediabunny": {
"version": "1.27.1", "version": "1.27.2",
"resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.1.tgz", "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.27.2.tgz",
"integrity": "sha512-XdgXNox1hx2ynQy3jcCbzX1YsJt9en9VnZVidNXTNnQUdl6q2twSoV1GjgTb7Wlw9evCYXcKkgjE8KNU7K5trg==", "integrity": "sha512-0g/vmb6X0xmnzqW0U44weF9CmzcUFFQRHrjzoVpSL2KXuhWIWcW3u9wIcohHiTY78jfr0SauYKLxjOQReaMxXQ==",
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true, "peer": true,
"workspaces": [ "workspaces": [
@@ -12065,7 +12065,7 @@
}, },
"packages/mp3-encoder": { "packages/mp3-encoder": {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"version": "1.27.2", "version": "1.27.3",
"license": "MPL-2.0", "license": "MPL-2.0",
"devDependencies": { "devDependencies": {
"@types/emscripten": "^1.40.1" "@types/emscripten": "^1.40.1"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "mediabunny", "name": "mediabunny",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.27.2", "version": "1.27.3",
"description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.", "description": "Pure TypeScript media toolkit for reading, writing, and converting media files, directly in the browser.",
"type": "module", "type": "module",
"workspaces": [ "workspaces": [
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@mediabunny/mp3-encoder", "name": "@mediabunny/mp3-encoder",
"author": "Vanilagy", "author": "Vanilagy",
"version": "1.27.2", "version": "1.27.3",
"description": "MP3 encoder extension for Mediabunny, based on LAME.", "description": "MP3 encoder extension for Mediabunny, based on LAME.",
"main": "./dist/bundles/mediabunny-mp3-encoder.mjs", "main": "./dist/bundles/mediabunny-mp3-encoder.mjs",
"module": "./dist/bundles/mediabunny-mp3-encoder.mjs", "module": "./dist/bundles/mediabunny-mp3-encoder.mjs",
+131 -47
View File
@@ -18,6 +18,7 @@ import {
isFirefox, isFirefox,
polyfillSymbolDispose, polyfillSymbolDispose,
assertNever, assertNever,
isWebKit,
} from './misc'; } from './misc';
polyfillSymbolDispose(); polyfillSymbolDispose();
@@ -1416,6 +1417,7 @@ export class AudioSample implements Disposable {
const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options; const { planeIndex, format, frameCount: optFrameCount, frameOffset: optFrameOffset } = options;
const srcFormat = this.format;
const destFormat = format ?? this.format; const destFormat = format ?? this.format;
if (!destFormat) throw new Error('Destination format not determined'); if (!destFormat) throw new Error('Destination format not determined');
@@ -1450,58 +1452,31 @@ export class AudioSample implements Disposable {
const writeFn = getWriteFunction(destFormat); const writeFn = getWriteFunction(destFormat);
if (isAudioData(this._data)) { if (isAudioData(this._data)) {
if (destIsPlanar) { if (isWebKit() && numChannels > 2 && destFormat !== srcFormat) {
if (destFormat === 'f32-planar') { // WebKit bug workaround
// Simple, since the browser must support f32-planar, we can just delegate here doAudioDataCopyToWebKitWorkaround(
this._data.copyTo(destination, { this._data,
planeIndex, destView,
frameOffset, srcFormat,
frameCount: copyFrameCount, destFormat,
format: 'f32-planar', numChannels,
}); planeIndex,
} else { frameOffset,
// Allocate temporary buffer for f32-planar data copyFrameCount,
const tempBuffer = new ArrayBuffer(copyFrameCount * 4); );
const tempArray = new Float32Array(tempBuffer);
this._data.copyTo(tempArray, {
planeIndex,
frameOffset,
frameCount: copyFrameCount,
format: 'f32-planar',
});
// Convert each f32 sample to destination format
const tempView = new DataView(tempBuffer);
for (let i = 0; i < copyFrameCount; i++) {
const destOffset = i * destBytesPerSample;
const sample = tempView.getFloat32(i * 4, true);
writeFn(destView, destOffset, sample);
}
}
} else { } else {
// Destination is interleaved. // Per spec, only f32-planar conversion must be supported, but in practice, all browsers support all
// Allocate a temporary Float32Array to hold one channel's worth of data. // destination formats, so let's just delegate here:
const numCh = numChannels; this._data.copyTo(destination, {
const temp = new Float32Array(copyFrameCount); planeIndex,
for (let ch = 0; ch < numCh; ch++) { frameOffset,
this._data.copyTo(temp, { frameCount: copyFrameCount,
planeIndex: ch, format: destFormat,
frameOffset, });
frameCount: copyFrameCount,
format: 'f32-planar',
});
for (let i = 0; i < copyFrameCount; i++) {
const destIndex = i * numCh + ch;
const destOffset = destIndex * destBytesPerSample;
writeFn(destView, destOffset, temp[i]!);
}
}
} }
} else { } else {
const uint8Data = this._data; const uint8Data = this._data;
const srcView = toDataView(uint8Data); const srcView = toDataView(uint8Data);
const srcFormat = this.format;
const readFn = getReadFunction(srcFormat); const readFn = getReadFunction(srcFormat);
const srcBytesPerSample = getBytesPerSample(srcFormat); const srcBytesPerSample = getBytesPerSample(srcFormat);
const srcIsPlanar = formatIsPlanar(srcFormat); const srcIsPlanar = formatIsPlanar(srcFormat);
@@ -1844,3 +1819,112 @@ const getWriteFunction = (format: AudioSampleFormat): (view: DataView, offset: n
const isAudioData = (x: unknown): x is AudioData => { const isAudioData = (x: unknown): x is AudioData => {
return typeof AudioData !== 'undefined' && x instanceof AudioData; return typeof AudioData !== 'undefined' && x instanceof AudioData;
}; };
/**
* WebKit has a bug where calling AudioData.copyTo with a format different from the source format
* crashes the tab when there are more than 2 channels. This function works around that by always
* copying with the source format and then manually converting to the destination format.
*
* See https://bugs.webkit.org/show_bug.cgi?id=302521.
*/
const doAudioDataCopyToWebKitWorkaround = (
audioData: AudioData,
destView: DataView,
srcFormat: AudioSampleFormat,
destFormat: AudioSampleFormat,
numChannels: number,
planeIndex: number,
frameOffset: number,
copyFrameCount: number,
) => {
const readFn = getReadFunction(srcFormat);
const writeFn = getWriteFunction(destFormat);
const srcBytesPerSample = getBytesPerSample(srcFormat);
const destBytesPerSample = getBytesPerSample(destFormat);
const srcIsPlanar = formatIsPlanar(srcFormat);
const destIsPlanar = formatIsPlanar(destFormat);
if (destIsPlanar) {
if (srcIsPlanar) {
// src planar -> dest planar: copy single plane and convert
const data = new ArrayBuffer(copyFrameCount * srcBytesPerSample);
const dataView = toDataView(data);
audioData.copyTo(data, {
planeIndex,
frameOffset,
frameCount: copyFrameCount,
format: srcFormat,
});
for (let i = 0; i < copyFrameCount; i++) {
const srcOffset = i * srcBytesPerSample;
const destOffset = i * destBytesPerSample;
const sample = readFn(dataView, srcOffset);
writeFn(destView, destOffset, sample);
}
} else {
// src interleaved -> dest planar: copy all interleaved data, extract one channel
const data = new ArrayBuffer(copyFrameCount * numChannels * srcBytesPerSample);
const dataView = toDataView(data);
audioData.copyTo(data, {
planeIndex: 0,
frameOffset,
frameCount: copyFrameCount,
format: srcFormat,
});
for (let i = 0; i < copyFrameCount; i++) {
const srcOffset = (i * numChannels + planeIndex) * srcBytesPerSample;
const destOffset = i * destBytesPerSample;
const sample = readFn(dataView, srcOffset);
writeFn(destView, destOffset, sample);
}
}
} else {
if (srcIsPlanar) {
// src planar -> dest interleaved: copy each plane and interleave
const planeSize = copyFrameCount * srcBytesPerSample;
const data = new ArrayBuffer(planeSize);
const dataView = toDataView(data);
for (let ch = 0; ch < numChannels; ch++) {
audioData.copyTo(data, {
planeIndex: ch,
frameOffset,
frameCount: copyFrameCount,
format: srcFormat,
});
for (let i = 0; i < copyFrameCount; i++) {
const srcOffset = i * srcBytesPerSample;
const destOffset = (i * numChannels + ch) * destBytesPerSample;
const sample = readFn(dataView, srcOffset);
writeFn(destView, destOffset, sample);
}
}
} else {
// src interleaved -> dest interleaved: copy all and convert
const data = new ArrayBuffer(copyFrameCount * numChannels * srcBytesPerSample);
const dataView = toDataView(data);
audioData.copyTo(data, {
planeIndex: 0,
frameOffset,
frameCount: copyFrameCount,
format: srcFormat,
});
for (let i = 0; i < copyFrameCount; i++) {
for (let ch = 0; ch < numChannels; ch++) {
const idx = i * numChannels + ch;
const srcOffset = idx * srcBytesPerSample;
const destOffset = idx * destBytesPerSample;
const sample = readFn(dataView, srcOffset);
writeFn(destView, destOffset, sample);
}
}
}
}
};