mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-27 02:43:48 +02:00
392 lines
11 KiB
HTML
392 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
|
|
<script src="../dist/metamuxer.js"></script>
|
|
|
|
<script type="module">
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
document.body.append(fileInput);
|
|
|
|
const progress = document.createElement('progress');
|
|
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
|
|
}), {
|
|
chunked: true,
|
|
chunkSize: 2**20
|
|
});
|
|
const outputFormat = new Metamuxer.WavOutputFormat();
|
|
|
|
const button = document.createElement('button');
|
|
button.textContent = 'Cancel';
|
|
button.onclick = () => conversion.cancel();
|
|
document.body.append(button);
|
|
|
|
const conversion = await Metamuxer.Conversion.init({
|
|
input: new Metamuxer.Input({
|
|
formats: Metamuxer.ALL_FORMATS,
|
|
source
|
|
}),
|
|
output: new Metamuxer.Output({
|
|
format: outputFormat,
|
|
target
|
|
}),
|
|
audio: {
|
|
numberOfChannels: 1,
|
|
sampleRate: 16000
|
|
//discard: true
|
|
//forceReencode: true,
|
|
},
|
|
/*
|
|
video: {
|
|
discard: true,
|
|
bitrate: Metamuxer.QUALITY_VERY_HIGH,
|
|
//bitrate: Metamuxer.QUALITY_VERY_HIGH
|
|
codec: 'av1',
|
|
//width: 800,
|
|
//fit: 'fill',
|
|
//rotate: 90,
|
|
},
|
|
audio: {
|
|
bitrate: Metamuxer.QUALITY_HIGH,
|
|
codec: 'aac',
|
|
},
|
|
*/
|
|
/*
|
|
video: {
|
|
codec: 'avc',
|
|
},
|
|
audio: {
|
|
codec: 'mp3',
|
|
bitrate: 320000
|
|
},
|
|
*/
|
|
video: {
|
|
discard: true,
|
|
//width: 640
|
|
//forceReencode: true,
|
|
//rotate: 90
|
|
//width: 720 ?? 2160,
|
|
//height: 1280 ?? 3840,
|
|
//fit: 'contain',
|
|
//rotate: 90,
|
|
//width: 512,
|
|
//height: 512,
|
|
//width: 200,
|
|
//height: 100,
|
|
},
|
|
trim: {
|
|
start: 0,
|
|
end: 40
|
|
},
|
|
computeProgress: true
|
|
});
|
|
console.log(conversion);
|
|
|
|
function updateProgress() {
|
|
progress.value = conversion.progress;
|
|
|
|
if (conversion.progress === 1) {
|
|
return;
|
|
}
|
|
|
|
setTimeout(updateProgress, 1000/60);
|
|
}
|
|
updateProgress();
|
|
|
|
console.time();
|
|
await conversion.execute();
|
|
console.timeEnd()
|
|
|
|
console.log("Done", target.buffer);
|
|
|
|
const video = document.createElement('video');
|
|
video.src = URL.createObjectURL(new Blob([target.buffer], { type: outputFormat.mimeType }));
|
|
video.controls = true;
|
|
document.body.append(video);
|
|
video.play();
|
|
|
|
//download(new Blob([target.buffer]), 'converted' + outputFormat.fileExtension);
|
|
|
|
function download(blob, filename) {
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
});
|
|
</script> |