Last active
March 7, 2025 08:38
-
-
Save 599316527/c513b58f46afb15ee3d8a4f10b3dc707 to your computer and use it in GitHub Desktop.
Fix chrome's `AudioContext.decodeAudioData` time drift for m4a/aac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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