Skip to content

Instantly share code, notes, and snippets.

@599316527
Last active March 7, 2025 08:38
Show Gist options
  • Save 599316527/c513b58f46afb15ee3d8a4f10b3dc707 to your computer and use it in GitHub Desktop.
Save 599316527/c513b58f46afb15ee3d8a4f10b3dc707 to your computer and use it in GitHub Desktop.
Fix chrome's `AudioContext.decodeAudioData` time drift for m4a/aac
export function getAudioDuration(file: File) {
const url = URL.createObjectURL(file);
const promise = new Promise<number>((resolve, reject) => {
const audio = new Audio(url);
audio.onloadedmetadata = function () {
resolve(audio.duration);
};
audio.onerror = function (error) {
reject(error);
};
});
promise.finally(() => {
URL.revokeObjectURL(url);
});
return promise;
}
async function decodeAudio(file: File) {
const audioContext = new AudioContext();
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await audioContext
.decodeAudioData(arrayBuffer)
.finally(() => {
audioContext.close();
});
return audioBuffer;
}
// Duplicate some frames intermittently to match the total frame count of duration
function fixAudioBufferDuration(audioBuffer: AudioBuffer, duration: number) {
const expectedFrameNumber = Math.round(duration * audioBuffer.sampleRate);
const audioContext = new AudioContext();
const newAudioBuffer = audioContext.createBuffer(
audioBuffer.numberOfChannels,
expectedFrameNumber,
audioBuffer.sampleRate,
);
const missedFrameNumber = expectedFrameNumber - audioBuffer.length;
const freq = Math.floor(expectedFrameNumber / missedFrameNumber);
for (
let channelIndex = 0;
channelIndex < audioBuffer.numberOfChannels;
channelIndex++
) {
let oldFrameIndex = 0;
const oldChannelData = audioBuffer.getChannelData(channelIndex);
const newChannelData = newAudioBuffer.getChannelData(channelIndex);
for (let frameIndex = 0; frameIndex < expectedFrameNumber; frameIndex++) {
newChannelData[frameIndex] = oldChannelData[oldFrameIndex];
if (oldFrameIndex % freq === 0) {
newChannelData[++frameIndex] = oldChannelData[oldFrameIndex];
}
oldFrameIndex++;
}
}
audioContext.close();
// console.log('fix buffer', audioBuffer, newAudioBuffer);
return newAudioBuffer;
}
export async function decodeAudioFile(file: File) {
const [duration, audioBuffer] = await Promise.all([
getAudioDuration(file),
decodeAudio(file),
]);
if (duration - audioBuffer.duration < 0.01) {
return audioBuffer;
}
console.log('will fix audio duration', audioBuffer.duration, duration);
return fixAudioBufferDuration(audioBuffer, duration);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment