You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, audioUrl = 'https://cdn.freesound.org/previews/612/612089_11861999-lq.mp3', effectType = 'imageModulation', effectParam1 = 'gain', effectParam2 = 1.0) {
/**
* Applies audio effects to a sound file, with some effects controlled by an input image.
* The function processes the audio offline and returns an HTML element with an audio player and a download link.
*
* @param {Image} originalImg - The input image object to use for modulation effects.
* @param {string} audioUrl - The URL of the audio file to process. Must be CORS-accessible.
* @param {string} effectType - The type of effect to apply. Options: 'imageModulation', 'pitchShift', 'distortion', 'reverb'.
* @param {string|number} effectParam1 - The first parameter for the chosen effect.
* @param {string|number} effectParam2 - The second parameter for the chosen effect.
*
* === Effect-specific parameters ===
* 'imageModulation': Modulates an audio property based on image brightness.
* - effectParam1 (string): Property to modulate. 'gain', 'filter', or 'pitch'. Default: 'gain'.
* - effectParam2 (number): Scan speed multiplier across the image. 1.0 means the image is scanned once over the audio's duration. Default: 1.0.
* 'pitchShift': Changes the audio pitch without changing its speed.
* - effectParam1 (number): Semitones to shift the pitch. Can be a negative value. Default: 0.
* 'distortion': Adds a classic distortion/overdrive effect.
* - effectParam1 (number): Amount of distortion. Recommended range 0-1000. Default: 50.
* 'reverb': Adds reverberation to the sound.
* - effectParam1 (number): Reverb decay time in seconds. Default: 3.
* - effectParam2 (number): Set to 1 for a reversed reverb effect. Default: 0.
*/
// Helper function to convert an AudioBuffer into a WAV audio format Blob.
const bufferToWave = (abuffer) => {
const numOfChan = abuffer.numberOfChannels;
const length = abuffer.length * numOfChan * 2 + 44;
const buffer = new ArrayBuffer(length);
const view = new DataView(buffer);
let pos = 0;
const setUint16 = (data) => {
view.setUint16(pos, data, true);
pos += 2;
};
const setUint32 = (data) => {
view.setUint32(pos, data, true);
pos += 4;
};
// Write WAVE header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8);
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16);
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
setUint32(abuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
setUint16(numOfChan * 2); // block-align
setUint16(16); // 16-bit
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4);
// Get channel data and write interleaved samples
const channels = [];
for (let i = 0; i < abuffer.numberOfChannels; i++) {
channels.push(abuffer.getChannelData(i));
}
for (let offset = 0; offset < abuffer.length; offset++) {
for (let i = 0; i < numOfChan; i++) {
const sample = Math.max(-1, Math.min(1, channels[i][offset] || 0));
view.setInt16(pos, sample < 0 ? sample * 32768 : sample * 32767, true);
pos += 2;
}
}
return new Blob([view], { type: 'audio/wav' });
};
const resultContainer = document.createElement('div');
resultContainer.style.fontFamily = 'sans-serif';
resultContainer.innerHTML = 'Processing audio...';
try {
const response = await fetch(audioUrl);
if (!response.ok) throw new Error(`Audio fetch failed: ${response.statusText}`);
const arrayBuffer = await response.arrayBuffer();
const tempContext = new(window.AudioContext || window.webkitAudioContext)();
const decodedBuffer = await tempContext.decodeAudioData(arrayBuffer);
await tempContext.close();
const offlineContext = new OfflineAudioContext(
decodedBuffer.numberOfChannels,
decodedBuffer.length,
decodedBuffer.sampleRate
);
const source = offlineContext.createBufferSource();
source.buffer = decodedBuffer;
let lastNode = source;
// Apply selected audio effect
switch (effectType) {
case 'imageModulation': {
const modulates = String(effectParam1) || 'gain';
const scanSpeed = Number(effectParam2) || 1.0;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.width;
canvas.height = originalImg.height;
ctx.drawImage(originalImg, 0, 0);
let modulatorNode, automationParam, valueMapper;
if (modulates === 'filter') {
modulatorNode = offlineContext.createBiquadFilter();
modulatorNode.type = 'lowpass';
modulatorNode.Q.value = 5;
automationParam = modulatorNode.frequency;
const minFreq = 40, maxFreq = offlineContext.sampleRate / 2;
const logMin = Math.log(minFreq), logMax = Math.log(maxFreq);
valueMapper = (b) => Math.exp(logMin + (logMax - logMin) * (b / 255));
} else if (modulates === 'pitch') {
automationParam = source.detune; // Modulate source directly
const maxDetune = 2400; // Cents, +/- 2 octaves
valueMapper = (b) => (b / 255 * 2 - 1) * maxDetune;
} else { // default to 'gain'
modulatorNode = offlineContext.createGain();
automationParam = modulatorNode.gain;
valueMapper = (b) => b / 255;
}
// Schedule parameter changes based on image brightness
const updatesPerSecond = 60;
const numSteps = Math.floor(decodedBuffer.duration * updatesPerSecond);
for (let i = 0; i <= numSteps; i++) {
const time = i / updatesPerSecond;
if (time > decodedBuffer.duration) break;
const progress = time / decodedBuffer.duration;
const imageX = Math.floor((progress * canvas.width * scanSpeed) % canvas.width);
const sliceData = ctx.getImageData(imageX, 0, 1, canvas.height).data;
let sum = 0;
for (let j = 0; j < sliceData.length; j += 4) {
sum += (sliceData[j] + sliceData[j + 1] + sliceData[j + 2]) / 3;
}
const avgBrightness = sum / (sliceData.length / 4);
const value = valueMapper(avgBrightness);
if (i === 0) automationParam.setValueAtTime(value, 0);
else automationParam.linearRampToValueAtTime(value, time);
}
if (modulatorNode) {
lastNode.connect(modulatorNode);
lastNode = modulatorNode;
}
break;
}
case 'pitchShift': {
const pitch = Number(effectParam1) || 0;
source.detune.value = pitch * 100; // detune is in cents
break;
}
case 'distortion': {
const amount = Math.max(0, Number(effectParam1) || 50);
const curve = new Float32Array(44100);
for (let i = 0; i < curve.length; ++i) {
const x = i * 2 / curve.length - 1;
curve[i] = (Math.PI + amount) * x / (Math.PI + amount * Math.abs(x));
}
const distortion = offlineContext.createWaveShaper();
distortion.curve = curve;
distortion.oversample = '4x';
lastNode.connect(distortion);
lastNode = distortion;
break;
}
case 'reverb': {
const decay = Math.max(0.1, Number(effectParam1) || 3);
const reverse = Number(effectParam2) || 0;
const len = offlineContext.sampleRate * decay;
const impulse = offlineContext.createBuffer(2, len, offlineContext.sampleRate);
for (let c = 0; c < 2; c++) {
const data = impulse.getChannelData(c);
for (let i = 0; i < len; i++) {
const n = reverse ? len - i : i;
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - n / len, 2.5);
}
}
const convolver = offlineContext.createConvolver();
convolver.buffer = impulse;
lastNode.connect(convolver);
lastNode = convolver;
break;
}
}
lastNode.connect(offlineContext.destination);
source.start(0);
const renderedBuffer = await offlineContext.startRendering();
const wavBlob = bufferToWave(renderedBuffer);
const audioObjectUrl = URL.createObjectURL(wavBlob);
resultContainer.innerHTML = '';
const title = document.createElement('h3');
title.textContent = `Result for Effect: ${effectType}`;
resultContainer.appendChild(title);
const audioElement = document.createElement('audio');
audioElement.controls = true;
audioElement.src = audioObjectUrl;
audioElement.style.width = '100%';
resultContainer.appendChild(audioElement);
const downloadLink = document.createElement('a');
downloadLink.href = audioObjectUrl;
downloadLink.download = `processed_audio_${effectType}.wav`;
downloadLink.textContent = 'Download as .WAV';
downloadLink.style.display = 'block';
downloadLink.style.marginTop = '10px';
resultContainer.appendChild(downloadLink);
} catch (error) {
console.error('ImageAudioEffects error:', error);
resultContainer.innerHTML = `An error occurred: ${error.message}. <br>Check if the audio URL is valid and allows cross-origin requests (CORS).`;
resultContainer.style.color = 'red';
}
return resultContainer;
}
Apply Changes