You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, durationSec = 10, timeResolution = 200, frequencyBands = 80, minFrequency = 65, maxFrequency = 4000) {
// Parse parameters
const W = parseInt(timeResolution, 10) || 200;
const H = parseInt(frequencyBands, 10) || 80;
const duration = parseFloat(durationSec) || 10;
const minFreq = parseFloat(minFrequency) || 65;
const maxFreq = parseFloat(maxFrequency) || 4000;
// Create main container
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.fontFamily = 'system-ui, -apple-system, sans-serif';
container.style.padding = '25px';
container.style.gap = '20px';
container.style.backgroundColor = '#1e1e24';
container.style.color = '#ffffff';
container.style.borderRadius = '12px';
container.style.boxShadow = '0 6px 12px rgba(0,0,0,0.3)';
container.style.maxWidth = '100%';
container.style.boxSizing = 'border-box';
// Title
const title = document.createElement('h3');
title.innerText = 'Photo To Music Track Converter';
title.style.margin = '0';
title.style.color = '#00ffcc';
// Figure out optimal canvas dimensions for visualizer
const maxW = 800;
let vw = originalImg.naturalWidth || originalImg.width || 400;
let vh = originalImg.naturalHeight || originalImg.height || 300;
if (vw > maxW) {
vh = Math.floor(vh * (maxW / vw));
vw = maxW;
}
// Image & Playhead Wrapper
const wrapper = document.createElement('div');
wrapper.style.position = 'relative';
wrapper.style.display = 'inline-block';
wrapper.style.maxWidth = '100%';
wrapper.style.borderRadius = '8px';
wrapper.style.overflow = 'hidden';
wrapper.style.lineHeight = '0';
wrapper.style.boxShadow = '0 4px 10px rgba(0,0,0,0.5)';
const visualizer = document.createElement('canvas');
visualizer.width = vw;
visualizer.height = vh;
visualizer.style.display = 'block';
visualizer.style.width = '100%';
visualizer.style.height = 'auto';
const vCtx = visualizer.getContext('2d');
vCtx.drawImage(originalImg, 0, 0, vw, vh);
const playhead = document.createElement('div');
playhead.style.position = 'absolute';
playhead.style.top = '0';
playhead.style.left = '0';
playhead.style.width = '3px';
playhead.style.height = '100%';
playhead.style.backgroundColor = 'rgba(0, 255, 204, 0.8)';
playhead.style.boxShadow = '0 0 10px #00ffcc';
playhead.style.pointerEvents = 'none';
playhead.style.display = 'none';
playhead.style.transition = 'left 0.1s linear';
wrapper.appendChild(visualizer);
wrapper.appendChild(playhead);
// Status / Loading Indicator
const status = document.createElement('div');
status.innerText = 'Analyzing photo and synthesizing audio... 0%';
status.style.fontSize = '1.1em';
status.style.fontWeight = 'bold';
status.style.color = '#ffcc00';
container.appendChild(title);
container.appendChild(wrapper);
container.appendChild(status);
// Kick off asynchronous rendering to not completely block the UI thread immediately
setTimeout(async () => {
try {
// Resample image down to Time(W) vs Frequency(H) grid
const gridCanvas = document.createElement('canvas');
gridCanvas.width = W;
gridCanvas.height = H;
const ctx = gridCanvas.getContext('2d');
ctx.fillStyle = '#000000'; // Remove alpha gaps
ctx.fillRect(0, 0, W, H);
ctx.drawImage(originalImg, 0, 0, W, H);
const imgData = ctx.getImageData(0, 0, W, H).data;
// Extract brightness amplitudes matrix
const amplitudes = [];
for (let y = 0; y < H; y++) {
const row = [];
for (let x = 0; x < W; x++) {
const idx = (y * W + x) * 4;
const r = imgData[idx];
const g = imgData[idx + 1];
const b = imgData[idx + 2];
// Non-linear brightness mappings for punchier isolated notes vs noise
const brightness = Math.pow((0.299 * r + 0.587 * g + 0.114 * b) / 255.0, 2);
row.push(brightness);
}
amplitudes.push(row);
}
// Audio generation
const sampleRate = 44100;
const numSamples = Math.floor(duration * sampleRate);
const audioData = new Float32Array(numSamples);
for (let y = 0; y < H; y++) {
// Pitch mapping: Logarithmic scale from bottom to top of image
const freq = minFreq * Math.pow(maxFreq / minFreq, (H - 1 - y) / (H - 1));
const A = amplitudes[y];
let phase = 0;
const phaseIncrement = 2 * Math.PI * freq / sampleRate;
for (let i = 0; i < numSamples; i++) {
const target_x = (i / numSamples) * (W - 1);
const x_idx = Math.floor(target_x);
let amp = 0;
// Linear interpolation of amplitude along the timeline
if (x_idx >= W - 1) {
amp = A[W - 1];
} else {
const x_frac = target_x - x_idx;
amp = A[x_idx] * (1 - x_frac) + A[x_idx + 1] * x_frac;
}
audioData[i] += amp * Math.sin(phase);
phase += phaseIncrement;
}
// Yield to visually update the status
if (y % 5 === 0 || y === H - 1) {
status.innerText = `Synthesizing audio... ${Math.round(((y + 1) / H) * 100)}%`;
await new Promise(r => setTimeout(r, 0));
}
}
status.innerText = 'Normalizing output...';
await new Promise(r => setTimeout(r, 0));
// Anti-clicking envelope at ends and finding max value to normalize
const fadeSamples = Math.floor(0.05 * sampleRate);
let maxVal = 0;
for (let i = 0; i < numSamples; i++) {
let envelope = 1;
if (i < fadeSamples) envelope = i / fadeSamples;
else if (i > numSamples - fadeSamples) envelope = (numSamples - i) / fadeSamples;
audioData[i] *= envelope;
if (Math.abs(audioData[i]) > maxVal) maxVal = Math.abs(audioData[i]);
}
// Normalize audio signal to avoid distortion
if (maxVal > 0) {
for (let i = 0; i < numSamples; i++) {
audioData[i] = (audioData[i] / maxVal) * 0.9;
}
}
status.innerText = 'Finalizing WAV file...';
await new Promise(r => setTimeout(r, 0));
// Construct valid WAV ArrayBuffer
const arrayBuffer = new ArrayBuffer(44 + numSamples * 2);
const view = new DataView(arrayBuffer);
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + numSamples * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, numSamples * 2, true);
// Convert Float32 mapped track into 16-bit PCM Audio formatting
let offset = 44;
for (let i = 0; i < numSamples; i++) {
let s = audioData[i];
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
offset += 2;
}
// Creating playable asset
const blob = new Blob([view], { type: 'audio/wav' });
const url = URL.createObjectURL(blob);
// Audio Player Controls structure
const controlsDiv = document.createElement('div');
controlsDiv.style.display = 'flex';
controlsDiv.style.flexDirection = 'column';
controlsDiv.style.alignItems = 'center';
controlsDiv.style.width = '100%';
controlsDiv.style.gap = '15px';
const audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = url;
audioPlayer.style.width = '100%';
audioPlayer.style.maxWidth = '400px';
// Connect playhead visual logic to the audio player
audioPlayer.addEventListener('play', () => {
playhead.style.display = 'block';
});
audioPlayer.addEventListener('timeupdate', () => {
const progress = audioPlayer.currentTime / audioPlayer.duration;
if (!isNaN(progress)) {
playhead.style.left = `${progress * 100}%`;
}
});
audioPlayer.addEventListener('ended', () => {
playhead.style.display = 'none';
playhead.style.left = '0';
});
// Music track downloader
const dlBtn = document.createElement('a');
dlBtn.innerText = 'Download Music Track';
dlBtn.download = 'Photo_Music_Track.wav';
dlBtn.href = url;
dlBtn.style.padding = '12px 24px';
dlBtn.style.backgroundColor = '#00ffcc';
dlBtn.style.color = '#1e1e24';
dlBtn.style.textDecoration = 'none';
dlBtn.style.borderRadius = '6px';
dlBtn.style.fontWeight = 'bold';
dlBtn.style.transition = 'opacity 0.2s';
dlBtn.onmouseenter = () => dlBtn.style.opacity = '0.8';
dlBtn.onmouseleave = () => dlBtn.style.opacity = '1';
controlsDiv.appendChild(audioPlayer);
controlsDiv.appendChild(dlBtn);
// Swap out status text for interactive elements
container.replaceChild(controlsDiv, status);
} catch (error) {
status.innerText = 'An error occurred during conversion.';
status.style.color = '#ff4444';
console.error('Audio generation failed:', error);
}
}, 50);
return container;
}
Apply Changes