You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, durationSec = "5", minFreqHz = "100", maxFreqHz = "8000", timeResolution = "100", freqResolution = "64") {
const duration = parseFloat(durationSec) || 5;
const minFreq = parseFloat(minFreqHz) || 100;
const maxFreq = parseFloat(maxFreqHz) || 8000;
const specWidth = parseInt(timeResolution) || 100;
const specHeight = parseInt(freqResolution) || 64;
// Create main container
const container = document.createElement("div");
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.alignItems = "center";
container.style.gap = "15px";
container.style.fontFamily = "system-ui, -apple-system, sans-serif";
container.style.background = "#1e1e2e";
container.style.padding = "20px";
container.style.borderRadius = "12px";
container.style.boxShadow = "0 4px 15px rgba(0,0,0,0.3)";
container.style.color = "#cdd6f4";
container.style.maxWidth = "100%";
container.style.width = "max-content";
container.style.margin = "0 auto";
// Header label
const header = document.createElement("h3");
header.textContent = "Image to Music (Spectrogram)";
header.style.margin = "0";
header.style.color = "#f9e2af";
container.appendChild(header);
// Create canvas to display the image and play head
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// Scale canvas nicely for display
const maxDisplaySize = 600;
const scale = Math.min(maxDisplaySize / originalImg.width, maxDisplaySize / originalImg.height, 1);
canvas.width = originalImg.width * scale;
canvas.height = originalImg.height * scale;
canvas.style.borderRadius = "8px";
canvas.style.boxShadow = "0 4px 10px rgba(0,0,0,0.5)";
canvas.style.maxWidth = "100%";
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
container.appendChild(canvas);
// Play/Pause button
const btn = document.createElement("button");
btn.textContent = "🔊 Generate & Play";
btn.style.padding = "12px 24px";
btn.style.fontSize = "16px";
btn.style.fontWeight = "bold";
btn.style.cursor = "pointer";
btn.style.border = "none";
btn.style.borderRadius = "8px";
btn.style.backgroundColor = "#a6e3a1";
btn.style.color = "#11111b";
btn.style.transition = "transform 0.1s, background-color 0.2s";
// Hover effects for button
btn.onmouseover = () => { if (!btn.disabled) btn.style.transform = "scale(1.05)"; };
btn.onmouseout = () => { if (!btn.disabled) btn.style.transform = "scale(1)"; };
container.appendChild(btn);
// Audio tracking variables
let audioCtx;
let audioBuffer;
let sourceNode;
let isPlaying = false;
let startTime = 0;
let animFrameId;
const generateAudio = async () => {
// Step 1: Reduce image to spectrogram resolution (timeResolution x freqResolution)
const tempCanvas = document.createElement("canvas");
tempCanvas.width = specWidth;
tempCanvas.height = specHeight;
const tempCtx = tempCanvas.getContext("2d", { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0, specWidth, specHeight);
const imgData = tempCtx.getImageData(0, 0, specWidth, specHeight).data;
// Extract amplitudes matching Luma values
const amps = new Float32Array(specWidth * specHeight);
for (let y = 0; y < specHeight; y++) {
for (let x = 0; x < specWidth; x++) {
const idx = (y * specWidth + x) * 4;
const r = imgData[idx];
const g = imgData[idx + 1];
const b = imgData[idx + 2];
// Non-linear luma extraction to reduce noise and emphasize bold features
const luma = Math.pow((0.299 * r + 0.587 * g + 0.114 * b) / 255.0, 1.5);
amps[y * specWidth + x] = luma > 0.05 ? luma : 0;
}
}
// Step 2: Set up AudioBuffer
const sampleRate = audioCtx.sampleRate;
const totalSamples = Math.floor(duration * sampleRate);
audioBuffer = audioCtx.createBuffer(1, totalSamples, sampleRate);
const bufferData = audioBuffer.getChannelData(0);
// Step 3: Additive Synthesis per Y-row (Frequency)
for (let y = 0; y < specHeight; y++) {
// Logarithmic scale for frequencies sounds more natural visually applied
const freq = minFreq * Math.pow(maxFreq / minFreq, (specHeight - 1 - y) / (specHeight - 1));
// Traverse X-columns (Time)
for (let x = 0; x < specWidth - 1; x++) {
const amp1 = amps[y * specWidth + x];
const amp2 = amps[y * specWidth + x + 1];
// Skip processing if this time segment is silent for this frequency
if (amp1 === 0 && amp2 === 0) continue;
const startSample = Math.floor((x * totalSamples) / (specWidth - 1));
const endSample = Math.floor(((x + 1) * totalSamples) / (specWidth - 1));
for (let i = startSample; i < endSample; i++) {
const t = i / sampleRate;
// Linearly interpolate amplitude to prevent audio clicks/flutter between columns
const mix = (i - startSample) / (endSample - startSample);
const amplitude = amp1 + (amp2 - amp1) * mix;
bufferData[i] += amplitude * Math.sin(2 * Math.PI * freq * t);
}
}
}
// Step 4: Normalize audio to prevent clipping
let maxVal = 0.001;
for (let i = 0; i < totalSamples; i++) {
if (Math.abs(bufferData[i]) > maxVal) {
maxVal = Math.abs(bufferData[i]);
}
}
for (let i = 0; i < totalSamples; i++) {
bufferData[i] /= maxVal;
// Drop volume slightly lower than absolute peak to be easy on ears
bufferData[i] *= 0.9;
}
};
const drawLoop = () => {
if (!isPlaying || !audioCtx) return;
const timeElapsed = audioCtx.currentTime - startTime;
if (timeElapsed >= duration) {
stopAudio();
return;
}
// Redraw image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Progress playhead
const progress = timeElapsed / duration;
const x = progress * canvas.width;
// Draw shadow/overlay for played area
ctx.fillStyle = "rgba(0, 0, 0, 0.4)";
ctx.fillRect(0, 0, x, canvas.height);
// Draw sweeping laser line
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, canvas.height);
ctx.strokeStyle = "#f38ba8";
ctx.lineWidth = 3;
ctx.shadowColor = "#f38ba8";
ctx.shadowBlur = 10;
ctx.stroke();
ctx.shadowBlur = 0; // reset
animFrameId = requestAnimationFrame(drawLoop);
};
const playAudio = () => {
sourceNode = audioCtx.createBufferSource();
sourceNode.buffer = audioBuffer;
sourceNode.connect(audioCtx.destination);
sourceNode.start();
isPlaying = true;
startTime = audioCtx.currentTime;
btn.textContent = "⏹ Stop Music";
btn.style.backgroundColor = "#f38ba8";
drawLoop();
};
const stopAudio = () => {
if (sourceNode) {
sourceNode.stop();
sourceNode.disconnect();
sourceNode = null;
}
isPlaying = false;
btn.textContent = "🔊 Play Music";
btn.style.backgroundColor = "#a6e3a1";
cancelAnimationFrame(animFrameId);
// Reset visual state
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
};
btn.addEventListener("click", async () => {
if (isPlaying) {
stopAudio();
return;
}
// Initialize AudioContext synchronously on user interaction (Safari requirement)
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx.state === "suspended") {
await audioCtx.resume();
}
if (!audioBuffer) {
btn.textContent = "⏳ Synthesizing Audio...";
btn.style.backgroundColor = "#89b4fa";
btn.style.color = "#11111b";
btn.disabled = true;
btn.style.cursor = "wait";
// Allow DOM to repaint before heavy synchronous task
await new Promise(resolve => setTimeout(resolve, 50));
await generateAudio();
btn.disabled = false;
btn.style.cursor = "pointer";
}
playAudio();
});
return container;
}
Apply Changes