diff --git a/dev/convert.html b/dev/convert.html
index e58be51..febc93b 100644
--- a/dev/convert.html
+++ b/dev/convert.html
@@ -11,275 +11,9 @@
progress.max = 1;
document.body.append(progress);
-
-
-
-
- /**
- * Naive audio resampler using linear interpolation between samples
- * Much faster but lower quality than windowed sinc - causes aliasing and imaging artifacts
- * @param {AudioBuffer} inputBuffer - The input audio buffer
- * @param {number} targetSampleRate - The desired output sample rate
- * @returns {AudioBuffer} - New resampled audio buffer
- */
-function naiveResample(inputBuffer, targetSampleRate) {
- const inputSampleRate = inputBuffer.sampleRate;
- const ratio = targetSampleRate / inputSampleRate;
- const inputLength = inputBuffer.length;
- const outputLength = Math.floor(inputLength * ratio);
-
- // Create output buffer
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const outputBuffer = audioContext.createBuffer(
- inputBuffer.numberOfChannels,
- outputLength,
- targetSampleRate
- );
-
- // Process each channel independently
- for (let channel = 0; channel < inputBuffer.numberOfChannels; channel++) {
- const inputData = inputBuffer.getChannelData(channel);
- const outputData = outputBuffer.getChannelData(channel);
-
- naiveResampleChannel(inputData, outputData, inputSampleRate, targetSampleRate);
- }
-
- return outputBuffer;
-}
-
-/**
- * Naive resample a single channel using linear interpolation
- */
-function naiveResampleChannel(inputData, outputData, inputSampleRate, targetSampleRate) {
- const inputLength = inputData.length;
- const outputLength = outputData.length;
-
- for (let n = 0; n < outputLength; n++) {
- // Calculate the corresponding position in the input signal
- const inputPosition = n * inputSampleRate / targetSampleRate;
-
- // Get the floor and ceiling indices
- const lowerIndex = Math.floor(inputPosition);
- const upperIndex = Math.ceil(inputPosition);
-
- // Handle edge cases
- if (lowerIndex >= inputLength - 1) {
- // At or past the end - just use the last sample
- outputData[n] = inputData[inputLength - 1];
- } else if (lowerIndex < 0) {
- // Before the start - use first sample (shouldn't happen with our calculation)
- outputData[n] = inputData[0];
- } else if (lowerIndex === upperIndex) {
- // Exact sample alignment - no interpolation needed
- outputData[n] = inputData[lowerIndex];
- } else {
- // Linear interpolation between floor and ceil samples
- const fraction = inputPosition - lowerIndex;
- const lowerSample = inputData[lowerIndex];
- const upperSample = inputData[upperIndex];
-
- // Linear interpolation: lerp(a, b, t) = a + t * (b - a)
- outputData[n] = lowerSample + fraction * (upperSample - lowerSample);
- }
- }
-}
-
-
-
-
-
-
-
-/**
- * Resample an AudioBuffer to a new sample rate using windowed sinc interpolation
- * @param {AudioBuffer} inputBuffer - The input audio buffer
- * @param {number} targetSampleRate - The desired output sample rate
- * @param {number} windowSize - Half-width of the sinc window (default: 6)
- * @returns {AudioBuffer} - New resampled audio buffer
- */
-function resampleAudioBuffer(inputBuffer, targetSampleRate, windowSize = 1) {
- const inputSampleRate = inputBuffer.sampleRate;
- const ratio = targetSampleRate / inputSampleRate;
- const inputLength = inputBuffer.length;
- const outputLength = Math.floor(inputLength * ratio);
-
- // Scale window size for anti-aliasing when downsampling
- const effectiveWindowSize = windowSize * Math.max(1, inputSampleRate / targetSampleRate);
-
- // Create output buffer
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const outputBuffer = audioContext.createBuffer(
- inputBuffer.numberOfChannels,
- outputLength,
- targetSampleRate
- );
-
- // Process each channel independently
- for (let channel = 0; channel < inputBuffer.numberOfChannels; channel++) {
- const inputData = inputBuffer.getChannelData(channel);
- const outputData = outputBuffer.getChannelData(channel);
-
- resampleChannel(inputData, outputData, inputSampleRate, targetSampleRate, effectiveWindowSize);
- }
-
- return outputBuffer;
-}
-
-/**
- * Resample a single channel of audio data
- */
-function resampleChannel(inputData, outputData, inputSampleRate, targetSampleRate, windowSize) {
- const inputLength = inputData.length;
- const outputLength = outputData.length;
-
- for (let n = 0; n < outputLength; n++) {
- // Current output time in input sample units
- const inputTime = n * inputSampleRate / targetSampleRate;
-
- let sum = 0;
- const windowRadius = Math.ceil(windowSize);
-
- // Convolve with windowed sinc kernel
- for (let k = -windowRadius; k <= windowRadius; k++) {
- const inputIndex = Math.floor(inputTime) + k;
-
- // Handle edges with zero padding
- if (inputIndex < 0 || inputIndex >= inputLength) {
- continue;
- }
-
- // Time difference for sinc calculation
- const timeDiff = inputTime - inputIndex;
-
- // Calculate windowed sinc weight
- const weight = windowedSinc(timeDiff, windowSize);
-
- sum += inputData[inputIndex] * weight;
- }
-
- outputData[n] = sum;
- }
-}
-
-/**
- * Windowed sinc function using Kaiser window
- * @param {number} x - Input value
- * @param {number} windowSize - Window size parameter
- * @returns {number} - Windowed sinc value
- */
-function windowedSinc(x, windowSize) {
- if (Math.abs(x) > windowSize) {
- return 0;
- }
-
- // Sinc function
- let sincValue;
- if (Math.abs(x) < 1e-10) {
- sincValue = 1; // lim(x->0) sinc(x) = 1
- } else {
- const piX = Math.PI * x;
- sincValue = Math.sin(piX) / piX;
- }
-
- // Kaiser window (beta = 8 for good balance of main lobe width vs side lobe suppression)
- const beta = 8;
- const windowValue = kaiserWindow(x / windowSize, beta);
-
- return sincValue * windowValue;
-}
-
-/**
- * Kaiser window function
- * @param {number} n - Normalized position (-1 to 1)
- * @param {number} beta - Kaiser beta parameter
- * @returns {number} - Window value
- */
-function kaiserWindow(n, beta) {
- if (Math.abs(n) > 1) {
- return 0;
- }
-
- const arg = beta * Math.sqrt(1 - n * n);
- return modifiedBesselI0(arg) / modifiedBesselI0(beta);
-}
-
-/**
- * Modified Bessel function of the first kind, order 0
- * Using series approximation
- */
-function modifiedBesselI0(x) {
- let sum = 1;
- let term = 1;
- const xSquaredOver4 = (x * x) / 4;
-
- for (let k = 1; k < 50; k++) {
- term *= xSquaredOver4 / (k * k);
- sum += term;
-
- if (term < 1e-12) break; // Convergence check
- }
-
- return sum;
-}
-
-// Example usage:
-// const resampledBuffer = resampleAudioBuffer(originalBuffer, 44100);
-
-// For testing - create a simple test signal
-function createTestBuffer(sampleRate = 48000, duration = 1, frequency = 440) {
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- const length = Math.floor(sampleRate * duration);
- const buffer = audioContext.createBuffer(1, length, sampleRate);
- const data = buffer.getChannelData(0);
-
- for (let i = 0; i < length; i++) {
- data[i] = Math.sin(2 * Math.PI * frequency * i / sampleRate) * 0.5;
- }
-
- return buffer;
-}
-
-// Test example:
-// const testBuffer = createTestBuffer(48000, 1, 440);
-// const resampled = resampleAudioBuffer(testBuffer, 44100);
-// console.log(`Original: ${testBuffer.sampleRate}Hz, ${testBuffer.length} samples`);
-// console.log(`Resampled: ${resampled.sampleRate}Hz, ${resampled.length} samples`);
-
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
- /*
- const context = new AudioContext();
- const buffer = await context.decodeAudioData(await file.arrayBuffer());
-
- const resampled = naiveResample(buffer, 16000);
- console.log(resampled)
-
- const node = context.createBufferSource();
- node.buffer = resampled;
- node.connect(context.destination);
- node.start();
- */
-
- /*
- const cursedOutput = new Metamuxer.Output({
- format: new Metamuxer.WavOutputFormat(),
- target: new Metamuxer.BufferTarget()
- });
- const cursedSource = new Metamuxer.AudioBufferSource({codec: 'pcm-s16'});
- cursedOutput.addAudioTrack(cursedSource);
-
- await cursedOutput.start();
-
- await cursedSource.add(resampled);
- await cursedOutput.finalize();
-
- console.log(cursedOutput.target.buffer);
- download(new Blob([cursedOutput.target.buffer]), 'cursed.wav')
- */
-
- //return;
-
const source = new Metamuxer.BlobSource(file);
const target = new Metamuxer.BufferTarget() ?? new Metamuxer.StreamTarget(new WritableStream({
write: console.log
@@ -305,7 +39,7 @@ function createTestBuffer(sampleRate = 48000, duration = 1, frequency = 440) {
}),
audio: {
numberOfChannels: 1,
- sampleRate: 16000
+ sampleRate: 8000
//discard: true
//forceReencode: true,
},
diff --git a/src/conversion.ts b/src/conversion.ts
index 940f8f7..3b0cb68 100644
--- a/src/conversion.ts
+++ b/src/conversion.ts
@@ -112,18 +112,8 @@ const FALLBACK_NUMBER_OF_CHANNELS = 2;
const FALLBACK_SAMPLE_RATE = 48000;
/**
- * Utility function to convert one media file into another. In addition to conversion, this function can be used to
- * resize and rotate video, resample audio, drop tracks, or trim to a specific time range.
- * @public
- */
-export const convert = async (options: ConversionOptions) => {
- const conversion = await Conversion.init(options);
- await conversion.execute();
- return conversion;
-};
-
-/**
- * Represents a media file conversion process.
+ * Represents a media file conversion process, used to convert one media file into another. In addition to conversion,
+ * this class can be used to resize and rotate video, resample audio, drop tracks, or trim to a specific time range.
* @public
*/
export class Conversion {
@@ -171,14 +161,15 @@ export class Conversion {
/**
* A number between 0 and 1, indicating the completion of the conversion. If the `computeProgress` option is not
- * enabled, this value will be stuck at 0.
+ * enabled, this value will be stuck at 0. Note that a progress of 1 doesn't necessarily mean the conversion is
+ * complete; the conversion is complete once `execute` resolves.
*/
progress = 0;
/**
* A callback that is fired whenever the conversion progresses. Only called if the `computeProgress` option
* is enabled.
*/
- onProgress?: () => unknown = undefined;
+ onProgress?: (progress: number) => unknown = undefined;
/** The list of tracks that are included in the output file. */
utilizedTracks: InputTrack[] = [];
@@ -333,6 +324,22 @@ export class Conversion {
const outputTrackCounts = this._output.format.getSupportedTrackCounts();
for (const track of inputTracks) {
+ if (track.isVideoTrack() && this._options.video?.discard) {
+ this.discardedTracks.push({
+ track,
+ reason: 'discardedByUser',
+ });
+ continue;
+ }
+
+ if (track.isAudioTrack() && this._options.audio?.discard) {
+ this.discardedTracks.push({
+ track,
+ reason: 'discardedByUser',
+ });
+ continue;
+ }
+
if (this._totalTrackCount === outputTrackCounts.total.max) {
this.discardedTracks.push({
track,
@@ -350,24 +357,8 @@ export class Conversion {
}
if (track.isVideoTrack()) {
- if (this._options.video?.discard) {
- this.discardedTracks.push({
- track,
- reason: 'discardedByUser',
- });
- continue;
- }
-
await this._processVideoTrack(track);
} else if (track.isAudioTrack()) {
- if (this._options.audio?.discard) {
- this.discardedTracks.push({
- track,
- reason: 'discardedByUser',
- });
- continue;
- }
-
await this._processAudioTrack(track);
}
}
@@ -383,7 +374,7 @@ export class Conversion {
await this._input.computeDuration() - this._startTimestamp,
this._endTimestamp - this._startTimestamp,
);
- this.onProgress?.();
+ this.onProgress?.(this.progress);
}
}
@@ -406,9 +397,9 @@ export class Conversion {
await this._output.finalize();
- if (this._options.computeProgress) {
+ if (this._options.computeProgress && this.progress !== 1) {
this.progress = 1;
- this.onProgress?.();
+ this.onProgress?.(this.progress);
}
}
@@ -845,9 +836,12 @@ export class Conversion {
}
const averageTimestamp = totalTimestamps / this._totalTrackCount;
+ const newProgress = clamp(averageTimestamp / this._totalDuration, 0, 1);
- this.progress = 0.99 * clamp(averageTimestamp / this._totalDuration, 0, 1);
- this.onProgress?.();
+ if (newProgress !== this.progress) {
+ this.progress = newProgress;
+ this.onProgress?.(this.progress);
+ }
}
}
diff --git a/src/index.ts b/src/index.ts
index bce8770..1022169 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -121,7 +121,7 @@ export {
AudioBufferSink,
WrappedAudioBuffer,
} from './media-sink';
-export { convert, ConversionOptions, Conversion } from './conversion';
+export { ConversionOptions, Conversion } from './conversion';
export {
CustomVideoDecoder,
CustomAudioDecoder,