168 lines
7.4 KiB
JavaScript
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;
|