Make media player AudioContext match audio track sample rate

This commit is contained in:
Vanilagy
2025-05-30 20:12:00 +02:00
parent 218803a9fe
commit 176d21cf50
2 changed files with 21 additions and 19 deletions
+4 -3
View File
@@ -21,7 +21,7 @@
chunked: true, chunked: true,
chunkSize: 2**20 chunkSize: 2**20
}); });
const outputFormat = new Metamuxer.Mp4OutputFormat(); const outputFormat = new Metamuxer.WavOutputFormat();
const button = document.createElement('button'); const button = document.createElement('button');
button.textContent = 'Cancel'; button.textContent = 'Cancel';
@@ -38,8 +38,8 @@
target target
}), }),
audio: { audio: {
numberOfChannels: 2, numberOfChannels: 1,
//sampleRate: 8000 sampleRate: 4000
//discard: true //discard: true
//forceReencode: true, //forceReencode: true,
}, },
@@ -68,6 +68,7 @@
}, },
*/ */
video: { video: {
discard: true,
width: 1280, width: 1280,
//discard: true, //discard: true,
//width: 640 //width: 640
+17 -16
View File
@@ -29,9 +29,9 @@ const fullscreenButton = document.querySelector('#fullscreen-button') as HTMLBut
const errorElement = document.querySelector('#error-element') as HTMLDivElement; const errorElement = document.querySelector('#error-element') as HTMLDivElement;
const context = canvas.getContext('2d')!; const context = canvas.getContext('2d')!;
const audioContext = new AudioContext();
const gainNode = audioContext.createGain(); let audioContext: AudioContext | null = null;
gainNode.connect(audioContext.destination); let gainNode: GainNode | null = null;
let fileLoaded = false; let fileLoaded = false;
let videoSink: CanvasSink | null = null; let videoSink: CanvasSink | null = null;
@@ -79,11 +79,6 @@ const initMediaPlayer = async (file: File) => {
horizontalRule.style.display = ''; horizontalRule.style.display = '';
playerContainer.style.display = 'none'; playerContainer.style.display = 'none';
if (audioContext.state === 'suspended') {
// Resume the audio context now that a user gesture has happened
await audioContext.resume();
}
// Create an Input from the file // Create an Input from the file
const input = new Input({ const input = new Input({
source: new BlobSource(file), source: new BlobSource(file),
@@ -110,6 +105,13 @@ const initMediaPlayer = async (file: File) => {
throw new Error('Media file has no playable video or audio track.'); throw new Error('Media file has no playable video or audio track.');
} }
// We must create the audio context with the matching sample rate for correct acoustic results
// (especially for low-sample rate files)
audioContext = new AudioContext({ sampleRate: audioTrack?.sampleRate });
gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
updateVolume();
// For video, let's use a CanvasSink as it handles rotation and closing video samples for us. // For video, let's use a CanvasSink as it handles rotation and closing video samples for us.
// Pool size of 2: We'll only ever have the current and the next frame around, so we only need two canvases. // Pool size of 2: We'll only ever have the current and the next frame around, so we only need two canvases.
videoSink = videoTrack && new CanvasSink(videoTrack, { poolSize: 2 }); videoSink = videoTrack && new CanvasSink(videoTrack, { poolSize: 2 });
@@ -248,19 +250,19 @@ const runAudioIterator = async () => {
} }
for await (const { buffer, timestamp } of audioBufferIterator!) { for await (const { buffer, timestamp } of audioBufferIterator!) {
const node = audioContext.createBufferSource(); const node = audioContext!.createBufferSource();
node.buffer = buffer; node.buffer = buffer;
node.connect(gainNode); node.connect(gainNode!);
const startTimestamp = audioContextStartTime! + timestamp - playbackTimeAtStart; const startTimestamp = audioContextStartTime! + timestamp - playbackTimeAtStart;
// Two cases: Either, the audio starts in the future or in the past // Two cases: Either, the audio starts in the future or in the past
if (startTimestamp >= audioContext.currentTime) { if (startTimestamp >= audioContext!.currentTime) {
// If the audio starts in the future, easy, we just schedule it // If the audio starts in the future, easy, we just schedule it
node.start(startTimestamp); node.start(startTimestamp);
} else { } else {
// If it starts in the past, then let's only play the audible section that remains from here on out // If it starts in the past, then let's only play the audible section that remains from here on out
node.start(audioContext.currentTime, audioContext.currentTime - startTimestamp); node.start(audioContext!.currentTime, audioContext!.currentTime - startTimestamp);
} }
queuedAudioNodes.add(node); queuedAudioNodes.add(node);
@@ -290,7 +292,7 @@ const getPlaybackTime = () => {
if (playing) { if (playing) {
// To ensure perfect audio-video sync, we always use the audio context's clock to determine playback time, even // To ensure perfect audio-video sync, we always use the audio context's clock to determine playback time, even
// when there is no audio track. // when there is no audio track.
return audioContext.currentTime - audioContextStartTime! + playbackTimeAtStart; return audioContext!.currentTime - audioContextStartTime! + playbackTimeAtStart;
} else { } else {
return playbackTimeAtStart; return playbackTimeAtStart;
} }
@@ -303,7 +305,7 @@ const play = async () => {
await startVideoIterator(); await startVideoIterator();
} }
audioContextStartTime = audioContext.currentTime; audioContextStartTime = audioContext!.currentTime;
playing = true; playing = true;
if (audioSink) { if (audioSink) {
@@ -398,7 +400,7 @@ const updateVolume = () => {
const actualVolume = volumeMuted ? 0 : volume; const actualVolume = volumeMuted ? 0 : volume;
volumeBar.style.width = `${actualVolume * 100}%`; volumeBar.style.width = `${actualVolume * 100}%`;
gainNode.gain.value = actualVolume ** 2; // Quadratic for more fine-grained control gainNode!.gain.value = actualVolume ** 2; // Quadratic for more fine-grained control
const iconNumber = volumeMuted ? 0 : Math.ceil(1 + 3 * volume); const iconNumber = volumeMuted ? 0 : Math.ceil(1 + 3 * volume);
for (let i = 0; i < volumeIconWrapper.children.length; i++) { for (let i = 0; i < volumeIconWrapper.children.length; i++) {
@@ -406,7 +408,6 @@ const updateVolume = () => {
icon.style.display = i === iconNumber ? '' : 'none'; icon.style.display = i === iconNumber ? '' : 'none';
} }
}; };
updateVolume();
volumeBarContainer.addEventListener('pointerdown', (event) => { volumeBarContainer.addEventListener('pointerdown', (event) => {
draggingVolumeBar = true; draggingVolumeBar = true;