mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 05:43:50 +02:00
Add workaround for WebKit AudioData.copyTo bug (fixes #260)
This commit is contained in:
+131
-47
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user