You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, pitchShift = 0, duration = 1, effect = 'none') {
/**
* This function interprets "Image Voice Changer" by sonifying the input image.
* It analyzes the image's average color and brightness and maps these visual properties
* to audio properties like pitch, waveform, and volume, generating a sound
* that represents the "voice" of the image.
*
* @param {Image} originalImg - The input Image object.
* @param {number} pitchShift - Shift the base pitch by a number of semitones. Default is 0.
* @param {number} duration - The duration of the generated sound in seconds. Default is 1.
* @param {string} effect - An audio effect to apply. Can be 'none' or 'vibrato'. Default is 'none'.
* @returns {HTMLElement} A div container with the image and a "Play" button.
*/
const container = document.createElement('div');
container.style.position = 'relative';
container.style.display = 'inline-block';
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
ctx.drawImage(originalImg, 0, 0);
const button = document.createElement('button');
button.textContent = '▶️ Play Image Voice';
button.style.position = 'absolute';
button.style.bottom = '10px';
button.style.left = '50%';
button.style.transform = 'translateX(-50%)';
button.style.padding = '10px 15px';
button.style.border = 'none';
button.style.borderRadius = '5px';
button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
button.style.color = 'white';
button.style.cursor = 'pointer';
button.style.fontSize = '16px';
button.style.fontFamily = 'Arial, sans-serif';
button.style.transition = 'background-color 0.2s';
button.onmouseover = () => button.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
button.onmouseout = () => button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
container.appendChild(canvas);
container.appendChild(button);
// This AudioContext can be reused if the button is clicked multiple times.
let audioCtx = null;
button.onclick = () => {
if (!audioCtx) {
audioCtx = new(window.AudioContext || window.webkitAudioContext)();
}
// Resume context if it was suspended
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
// --- 1. Analyze Image Data ---
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
let totalR = 0, totalG = 0, totalB = 0;
for (let i = 0; i < data.length; i += 4) {
totalR += data[i];
totalG += data[i + 1];
totalB += data[i + 2];
}
const pixelCount = data.length / 4;
const avgR = totalR / pixelCount;
const avgG = totalG / pixelCount;
const avgB = totalB / pixelCount;
// --- 2. Map Image Properties to Audio Properties ---
// Helper to convert RGB to HSL (Hue, Saturation, Lightness)
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h * 360, s, l];
}
const [hue, saturation, lightness] = rgbToHsl(avgR, avgG, avgB);
// Map Hue (0-360) to a musical frequency range (e.g., 110Hz to 880Hz)
const baseFrequency = 110 + (hue / 360) * 770;
// Apply pitch shift (1 semitone = 2^(1/12))
const finalFrequency = baseFrequency * Math.pow(2, pitchShift / 12);
// Map Saturation (0-1) to waveform type
let waveform = 'sine'; // Grayscale/low saturation -> pure tone
if (saturation > 0.66) {
waveform = 'sawtooth'; // High saturation -> harsh tone
} else if (saturation > 0.33) {
waveform = 'triangle'; // Medium saturation -> richer tone
}
// Map Lightness (0-1) to volume (Gain)
const volume = Math.max(0, Math.min(1, lightness));
// --- 3. Generate Audio using Web Audio API ---
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.type = waveform;
oscillator.frequency.setValueAtTime(finalFrequency, audioCtx.currentTime);
// Apply a gentle fade in and fade out to prevent clicking
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(volume, audioCtx.currentTime + 0.05);
gainNode.gain.linearRampToValueAtTime(0, audioCtx.currentTime + duration);
if (effect.toLowerCase() === 'vibrato') {
const lfo = audioCtx.createOscillator();
lfo.frequency.setValueAtTime(5, audioCtx.currentTime); // 5hz vibrato
const lfoGain = audioCtx.createGain();
lfoGain.gain.setValueAtTime(finalFrequency * 0.05, audioCtx.currentTime); // Vibrato depth
lfo.connect(lfoGain);
lfoGain.connect(oscillator.frequency); // Modulate the main oscillator's frequency
lfo.start(audioCtx.currentTime);
lfo.stop(audioCtx.currentTime + duration);
}
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
oscillator.start(audioCtx.currentTime);
oscillator.stop(audioCtx.currentTime + duration);
};
return container;
}
Apply Changes