Files

168 lines
7.4 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPartialAudioData = void 0;
const media_parser_1 = require("@remotion/media-parser");
const create_audio_decoder_1 = require("./create-audio-decoder");
/**
* Extract the portion of an audio chunk that overlaps with the requested time window
*/
const extractOverlappingAudioSamples = ({ sample, fromSeconds, toSeconds, channelIndex, timescale, }) => {
const chunkStartInSeconds = sample.timestamp / timescale;
const chunkDuration = sample.numberOfFrames / sample.sampleRate;
const chunkEndInSeconds = chunkStartInSeconds + chunkDuration;
// Calculate overlap with the requested window
const overlapStartSecond = Math.max(chunkStartInSeconds, fromSeconds);
const overlapEndSecond = Math.min(chunkEndInSeconds, toSeconds);
// Skip if no overlap with requested window
if (overlapStartSecond >= overlapEndSecond) {
return null;
}
// For multi-channel audio, we need to handle channels properly
const { numberOfChannels } = sample;
const samplesPerChannel = sample.numberOfFrames;
let data;
if (numberOfChannels === 1) {
// Mono audio
data = new Float32Array(samplesPerChannel);
sample.copyTo(data, { format: 'f32', planeIndex: 0 });
}
else {
// Multi-channel audio: extract specific channel
const interleaved = new Float32Array(samplesPerChannel * numberOfChannels);
sample.copyTo(interleaved, { format: 'f32', planeIndex: 0 });
// Extract the specific channel (interleaved audio)
data = new Float32Array(samplesPerChannel);
for (let i = 0; i < samplesPerChannel; i++) {
data[i] = interleaved[i * numberOfChannels + channelIndex];
}
}
// Calculate which samples to keep from this chunk
const startSampleInChunk = Math.floor((overlapStartSecond - chunkStartInSeconds) * sample.sampleRate);
const endSampleInChunk = Math.ceil((overlapEndSecond - chunkStartInSeconds) * sample.sampleRate);
// Only keep the samples we need
return data.slice(startSampleInChunk, endSampleInChunk);
};
// Small buffer to ensure we capture chunks that span across boundaries
// We need this because specified time window is not always aligned with the audio chunks
// so that we fetch a bit more, and then trim it down to the requested time window
const BUFFER_IN_SECONDS = 0.1;
const getPartialAudioData = async ({ src, fromSeconds, toSeconds, channelIndex, signal, }) => {
const controller = (0, media_parser_1.mediaParserController)();
// Collect audio samples
const audioSamples = [];
// Abort if the signal is already aborted
if (signal.aborted) {
throw new Error('Operation was aborted');
}
// Forward abort signal immediately to the controller
const { resolve: resolveAudioDecode, promise: audioDecodePromise } = Promise.withResolvers();
const onAbort = () => {
controller.abort();
resolveAudioDecode();
};
signal.addEventListener('abort', onAbort, { once: true });
try {
// expand decode window slightly to avoid gaps at boundaries
const seekFromSeconds = Math.max(0, fromSeconds - BUFFER_IN_SECONDS);
if (seekFromSeconds > 0) {
controller.seek(seekFromSeconds);
}
await (0, media_parser_1.parseMedia)({
acknowledgeRemotionLicense: true,
src,
controller,
onAudioTrack: async ({ track }) => {
if (signal.aborted) {
return null;
}
const audioDecoder = await (0, create_audio_decoder_1.createAudioDecoder)({
track,
onFrame: (sample) => {
if (signal.aborted) {
sample.close();
return;
}
const trimmedData = extractOverlappingAudioSamples({
sample,
fromSeconds,
toSeconds,
channelIndex,
timescale: track.timescale,
});
if (trimmedData) {
audioSamples.push(trimmedData);
}
sample.close();
},
onError(error) {
resolveAudioDecode();
throw error;
},
});
return async (sample) => {
if (signal.aborted) {
audioDecoder.close();
controller.abort();
return;
}
if (!audioDecoder) {
throw new Error('No audio decoder found');
}
// decode a bit earlier and later than requested, trimming happens later
const fromSecondsWithBuffer = Math.max(0, fromSeconds - BUFFER_IN_SECONDS);
const toSecondsWithBuffer = toSeconds + BUFFER_IN_SECONDS;
// Convert timestamp using the track's timescale
const time = sample.timestamp / track.timescale;
// Skip samples that are before our requested start time (with buffer)
if (time < fromSecondsWithBuffer) {
return;
}
// Stop immediately when we reach our target time (with buffer)
if (time >= toSecondsWithBuffer) {
// wait until decoder is done
audioDecoder.flush().then(() => {
audioDecoder.close();
resolveAudioDecode();
});
controller.abort();
return;
}
await audioDecoder.waitForQueueToBeLessThan(10);
// we're waiting for the queue above anyway, enqueue in sync mode
audioDecoder.decode(sample);
// this is called on the last sample of the track
// so if we have reached the end of the track, resolve the promise
return () => {
audioDecoder.flush().then(() => {
audioDecoder.close();
resolveAudioDecode();
});
};
};
},
});
}
catch (err) {
const isAbortedByTimeCutoff = (0, media_parser_1.hasBeenAborted)(err);
// Don't throw if we stopped the parsing ourselves
if (!isAbortedByTimeCutoff && !signal.aborted) {
throw err;
}
}
finally {
// Clean up the event listener
signal.removeEventListener('abort', onAbort);
}
await audioDecodePromise;
// Simply concatenate all audio data since we've already trimmed each chunk
const totalSamples = audioSamples.reduce((sum, sample) => sum + sample.length, 0);
const result = new Float32Array(totalSamples);
let offset = 0;
for (const audioSample of audioSamples) {
result.set(audioSample, offset);
offset += audioSample.length;
}
return result;
};
exports.getPartialAudioData = getPartialAudioData;