You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, carrierAudioUrl = '', modulatorAudioUrl = '') {
/**
* Creates an AudioBuffer containing white noise.
* @param {AudioContext} context - The audio context.
* @param {number} duration - The duration of the noise in seconds.
* @returns {AudioBuffer} The generated audio buffer.
*/
const createWhiteNoise = (context, duration) => {
const bufferSize = context.sampleRate * duration;
const buffer = context.createBuffer(1, bufferSize, context.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
return buffer;
};
/**
* Creates an AudioBuffer containing a sine wave.
* @param {AudioContext} context - The audio context.
* @param {number} duration - The duration of the wave in seconds.
* @param {number} freq - The frequency of the sine wave.
* @returns {AudioBuffer} The generated audio buffer.
*/
const createSineWave = (context, duration, freq) => {
const bufferSize = context.sampleRate * duration;
const buffer = context.createBuffer(1, bufferSize, context.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.sin(2 * Math.PI * freq * i / context.sampleRate);
}
return buffer;
};
/**
* Processes an audio source to generate raw spectrogram data. It uses an OfflineAudioContext
* to process the entire audio file non-real-time.
* @param {string} audioUrl - URL of the audio file.
* @param {Function} defaultGenerator - Function to generate a default AudioBuffer if the URL is not provided.
* @returns {Promise<number[][]>} A promise that resolves to the raw spectrogram data (2D array).
*/
const getRawSpectrogram = async (audioUrl, defaultGenerator) => {
// A transient AudioContext is needed to create/decode audio buffers.
const tempAudioCtx = new(window.AudioContext || window.webkitAudioContext)();
let audioBuffer;
try {
if (audioUrl) {
const response = await fetch(audioUrl);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const arrayBuffer = await response.arrayBuffer();
audioBuffer = await tempAudioCtx.decodeAudioData(arrayBuffer);
} else {
audioBuffer = defaultGenerator(tempAudioCtx, 3); // Default 3 second audio
}
} catch (e) {
console.error("Error loading or decoding audio. Using fallback.", e);
audioBuffer = defaultGenerator(tempAudioCtx, 3);
}
await tempAudioCtx.close();
const offlineCtx = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
const source = offlineCtx.createBufferSource();
source.buffer = audioBuffer;
const analyser = offlineCtx.createAnalyser();
analyser.fftSize = 2048;
const frequencyBinCount = analyser.frequencyBinCount;
// Using the deprecated ScriptProcessorNode for simplicity. AudioWorklet is the modern replacement.
const scriptProcessor = offlineCtx.createScriptProcessor(analyser.fftSize, 1, 1);
const spectrogram = [];
scriptProcessor.onaudioprocess = () => {
const frequencyData = new Uint8Array(frequencyBinCount);
analyser.getByteFrequencyData(frequencyData);
spectrogram.push(Array.from(frequencyData));
};
source.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(offlineCtx.destination);
source.start(0);
await offlineCtx.startRendering();
return spectrogram;
};
/**
* Resamples a 2D array (spectrogram) to new dimensions using bilinear interpolation.
* @param {number[][]} data - The input 2D array [width][height].
* @param {number} newWidth - The target width.
* @param {number} newHeight - The target height.
* @returns {number[][]} The resampled 2D array.
*/
const resampleSpectrogram = (data, newWidth, newHeight) => {
if (!data || !data.length || !data[0].length) return [];
const oldWidth = data.length;
const oldHeight = data[0].length;
const newData = Array(newWidth).fill(0).map(() => Array(newHeight).fill(0));
for (let x = 0; x < newWidth; x++) {
for (let y = 0; y < newHeight; y++) {
const srcX = (newWidth === 1) ? 0 : x * (oldWidth - 1) / (newWidth - 1);
const srcY = (newHeight === 1) ? 0 : y * (oldHeight - 1) / (newHeight - 1);
const x1 = Math.floor(srcX);
const y1 = Math.floor(srcY);
const x2 = Math.min(x1 + 1, oldWidth - 1);
const y2 = Math.min(y1 + 1, oldHeight - 1);
const val11 = data[x1][y1];
const val21 = data[x2][y1];
const val12 = data[x1][y2];
const val22 = data[x2][y2];
const xDiff = srcX - x1;
const yDiff = srcY - y1;
const interpolated = val11 * (1 - xDiff) * (1 - yDiff) +
val21 * xDiff * (1 - yDiff) +
val12 * (1 - xDiff) * yDiff +
val22 * xDiff * yDiff;
newData[x][y] = interpolated;
}
}
return newData;
};
// --- Main Function Logic ---
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const { width, height } = originalImg;
canvas.width = width;
canvas.height = height;
try {
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const pixels = imageData.data;
// Process both audio sources in parallel to get their spectrograms
const [carrierRawSpectrogram, modulatorRawSpectrogram] = await Promise.all([
getRawSpectrogram(carrierAudioUrl, (context, duration) => createSineWave(context, duration, 440)),
getRawSpectrogram(modulatorAudioUrl, (context, duration) => createWhiteNoise(context, duration))
]);
// Resample spectrograms to match the image dimensions
const carrierSpectrogram = resampleSpectrogram(carrierRawSpectrogram, width, height);
const modulatorSpectrogram = resampleSpectrogram(modulatorRawSpectrogram, width, height);
if (!carrierSpectrogram.length || !modulatorSpectrogram.length) {
throw new Error("Failed to generate valid spectrograms.");
}
// Apply the visual vocoder effect pixel by pixel
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const r = pixels[i];
const g = pixels[i + 1];
const b = pixels[i + 2];
const carrierVal = carrierSpectrogram[x][y];
const modulatorVal = modulatorSpectrogram[x][y];
// Create 3 "frequency band" gains from the modulator value (0-255).
// These control the mix between the original image and the carrier signal visualization.
const lowGain = Math.max(0, 1.0 - modulatorVal / 128.0);
const highGain = Math.max(0, (modulatorVal - 128.0) / 128.0);
const midGain = 1.0 - Math.abs(modulatorVal - 128.0) / 128.0;
// Mix original pixel color with the carrier value based on the modulator gains for each channel
pixels[i] = r * highGain + carrierVal * (1 - highGain); // Red channel influenced by "high frequencies"
pixels[i + 1] = g * midGain + carrierVal * (1 - midGain); // Green channel influenced by "mid frequencies"
pixels[i + 2] = b * lowGain + carrierVal * (1 - lowGain); // Blue channel influenced by "low frequencies"
}
}
ctx.putImageData(imageData, 0, 0);
} catch (error) {
console.error("An error occurred during image processing:", error);
// In case of error, draw the original image on the canvas as a fallback.
ctx.clearRect(0, 0, width, height);
ctx.drawImage(originalImg, 0, 0, width, height);
const p = document.createElement('p');
p.textContent = `Error: ${error.message}. Please check audio URLs and browser console.`;
p.style.color = 'red';
p.style.position = 'absolute';
p.style.top = '10px';
p.style.left = '10px';
p.style.backgroundColor = 'rgba(255,255,255,0.7)';
return p;
}
return canvas;
}
Apply Changes