You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, durationSeconds = "5", numRows = "64", minFrequency = "200", maxFrequency = "6000") {
// Dynamically load lamejs if it's not already loaded
if (!window.lamejs) {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/lamejs/1.2.1/lame.min.js';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
const duration = parseFloat(durationSeconds) || 5;
const rows = parseInt(numRows) || 64;
const minFreq = parseFloat(minFrequency) || 200;
const maxFreq = parseFloat(maxFrequency) || 6000;
// Calculate columns based on image aspect ratio
const cols = Math.round((originalImg.width / originalImg.height) * rows);
const safeCols = Math.min(Math.max(cols, 10), 1000);
const safeRows = Math.min(Math.max(rows, 10), 200);
const canvas = document.createElement('canvas');
canvas.width = safeCols;
canvas.height = safeRows;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Draw the image onto the canvas and extract pixel data
ctx.drawImage(originalImg, 0, 0, safeCols, safeRows);
const imageData = ctx.getImageData(0, 0, safeCols, safeRows).data;
const sampleRate = 44100;
const numSamples = Math.floor(sampleRate * duration);
const audioData = new Float32Array(numSamples);
// Map rows to frequencies (Logarithmic scale)
// Top row (y=0) corresponds to highest frequency, bottom row (y=safeRows-1) to minFreq
const frequencies = [];
for (let y = 0; y < safeRows; y++) {
const fraction = (safeRows - 1 - y) / (safeRows - 1);
const freq = minFreq * Math.pow(maxFreq / minFreq, fraction);
frequencies.push(freq);
}
const samplesPerCol = numSamples / safeCols;
// Additive synthesis using pixel brightness as amplitudes sequence for each frequency band
for (let y = 0; y < safeRows; y++) {
const freq = frequencies[y];
let phase = Math.random() * 2 * Math.PI; // Randomize start phase to avoid initial unison spikes
const phaseIncrement = (2 * Math.PI * freq) / sampleRate;
let amplitudes = new Float32Array(safeCols);
for (let x = 0; x < safeCols; x++) {
const index = (y * safeCols + x) * 4;
const r = imageData[index];
const g = imageData[index + 1];
const b = imageData[index + 2];
// Luminance provides intensity/amplitude (Normalized 0.0 - 1.0)
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
amplitudes[x] = lum / 255.0;
}
for (let i = 0; i < numSamples; i++) {
const colPos = i / samplesPerCol;
const x0 = Math.min(Math.floor(colPos), safeCols - 1);
const x1 = Math.min(x0 + 1, safeCols - 1);
const t = colPos - x0;
// Linear interpolation of amplitude for a smooth sound transition between columns
const amp = amplitudes[x0] * (1 - t) + amplitudes[x1] * t;
audioData[i] += amp * Math.sin(phase);
phase += phaseIncrement;
}
}
// Apply fade-in and fade-out to prevent ugly pops at the start and end of the audio track
const fadeLen = Math.min(2000, Math.floor(numSamples / 10)); // max ~45ms
for (let i = 0; i < fadeLen; i++) {
const fadeMultiplier = i / fadeLen;
audioData[i] *= fadeMultiplier;
audioData[numSamples - 1 - i] *= fadeMultiplier;
}
// Normalize generated samples to avoid clipping
let maxAmp = 0;
for (let i = 0; i < numSamples; i++) {
if (Math.abs(audioData[i]) > maxAmp) {
maxAmp = Math.abs(audioData[i]);
}
}
const scale = (maxAmp > 0) ? (0.9 / maxAmp) : 1;
// Fill Int16 buffer suitable for MP3 compression
const int16Samples = new Int16Array(numSamples);
for (let i = 0; i < numSamples; i++) {
audioData[i] *= scale;
const s = Math.max(-1, Math.min(1, audioData[i]));
int16Samples[i] = Math.round(s < 0 ? s * 32768 : s * 32767);
}
// Encode to MP3 using lamejs
const mp3encoder = new lamejs.Mp3Encoder(1, sampleRate, 128); // (Channels=1 Mono, 44100Hz, 128kbps)
const mp3Data = [];
const chunkSize = 1152; // standard encoding frame length
for (let i = 0; i < int16Samples.length; i += chunkSize) {
const chunk = int16Samples.subarray(i, i + chunkSize);
const mp3buf = mp3encoder.encodeBuffer(chunk);
if (mp3buf.length > 0) {
mp3Data.push(mp3buf);
}
}
const mp3bufExt = mp3encoder.flush();
if (mp3bufExt.length > 0) {
mp3Data.push(mp3bufExt);
}
const blob = new Blob(mp3Data, { type: 'audio/mp3' });
const blobUrl = URL.createObjectURL(blob);
// Style the canvas for preview purposes
canvas.style.width = '100%';
canvas.style.maxWidth = '300px';
canvas.style.height = 'auto';
canvas.style.imageRendering = 'pixelated';
canvas.style.border = '2px solid #ccc';
canvas.style.borderRadius = '6px';
canvas.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
// Build the UI Container
const container = document.createElement('div');
Object.assign(container.style, {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
padding: '24px',
gap: '15px',
background: '#ffffff',
border: '1px solid #eaeaea',
borderRadius: '12px',
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
maxWidth: '450px',
margin: '0 auto',
boxSizing: 'border-box'
});
const title = document.createElement('h3');
title.textContent = 'MP3 Sonification Complete';
title.style.margin = '0';
title.style.color = '#333';
const description = document.createElement('p');
description.textContent = 'Your image has been converted into a spectrogram-like audio field (X-axis: time, Y-axis: frequency, Brightness: amplitude).';
Object.assign(description.style, {
fontSize: '13px',
color: '#666',
textAlign: 'center',
marginTop: '-5px',
marginBottom: '5px',
lineHeight: '1.4'
});
const audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = blobUrl;
audioPlayer.style.width = '100%';
audioPlayer.style.marginTop = '10px';
const downloadBtn = document.createElement('a');
downloadBtn.href = blobUrl;
downloadBtn.download = `sonified_image_${Date.now()}.mp3`;
downloadBtn.textContent = 'Download MP3 File';
Object.assign(downloadBtn.style, {
padding: '12px 24px',
background: '#007BFF',
color: '#FFFFFF',
textDecoration: 'none',
borderRadius: '6px',
fontWeight: 'bold',
fontSize: '14px',
width: '100%',
textAlign: 'center',
transition: 'background 0.2s',
boxSizing: 'border-box'
});
// Add hover effect programmatically
downloadBtn.addEventListener('mouseenter', () => downloadBtn.style.background = '#0056b3');
downloadBtn.addEventListener('mouseleave', () => downloadBtn.style.background = '#007BFF');
container.appendChild(title);
container.appendChild(description);
container.appendChild(canvas);
container.appendChild(audioPlayer);
container.appendChild(downloadBtn);
return container;
}
Apply Changes