You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Creates an audio representation of an image through a process called sonification.
* It scans the image from left to right, treating the vertical axis as a frequency
* spectrum and pixel brightness as amplitude. The result is an audio file that can be played.
*
* @param {Image} originalImg The input JavaScript Image object.
* @param {number} duration The desired length of the audio in seconds. Default is 5.
* @param {number} minFreq The lowest frequency (in Hz) mapped to the bottom of the image. Default is 220 (A3).
* @param {number} maxFreq The highest frequency (in Hz) mapped to the top of the image. Default is 1200.
* @param {string} waveType The shape of the audio wave, affecting the timbre. Options: 'sine', 'square', 'sawtooth', 'triangle'. Default is 'sine'.
* @returns {Promise<HTMLAudioElement>} A promise that resolves with an HTML <audio> element containing the generated sound.
*/
async function processImage(originalImg, duration = 5, minFreq = 220, maxFreq = 1200, waveType = 'sine') {
/**
* Converts an AudioBuffer to a WAV file Blob.
* @param {AudioBuffer} buffer The audio buffer to convert.
* @returns {Blob} A blob representing the WAV file.
*/
const bufferToWav = (buffer) => {
const numOfChan = buffer.numberOfChannels;
const length = buffer.length * numOfChan * 2 + 44;
const bufferArr = new ArrayBuffer(length);
const view = new DataView(bufferArr);
let pos = 0;
const writeString = (view, offset, string) => {
for (let i = 0; i < string.length; i++) {
view.setUint8(offset + i, string.charCodeAt(i));
}
};
// RIFF header
writeString(view, pos, 'RIFF'); pos += 4;
view.setUint32(pos, length - 8, true); pos += 4;
writeString(view, pos, 'WAVE'); pos += 4;
// FMT sub-chunk
writeString(view, pos, 'fmt '); pos += 4;
view.setUint32(pos, 16, true); pos += 4;
view.setUint16(pos, 1, true); pos += 2;
view.setUint16(pos, numOfChan, true); pos += 2;
view.setUint32(pos, buffer.sampleRate, true); pos += 4;
view.setUint32(pos, buffer.sampleRate * 2 * numOfChan, true); pos += 4;
view.setUint16(pos, numOfChan * 2, true); pos += 2;
view.setUint16(pos, 16, true); pos += 2;
// Data sub-chunk
writeString(view, pos, 'data'); pos += 4;
view.setUint32(pos, length - pos - 4, true); pos += 4;
// Write samples
const channelData = buffer.getChannelData(0); // Assuming mono
for (let i = 0; i < channelData.length; i++) {
const sample = Math.max(-1, Math.min(1, channelData[i]));
view.setInt16(pos, sample < 0 ? sample * 32768 : sample * 32767, true);
pos += 2;
}
return new Blob([view], { type: 'audio/wav' });
};
/**
* Generates a waveform value for a given phase and type.
* @param {string} type - 'sine', 'square', 'sawtooth', 'triangle'.
* @param {number} phase - The current phase of the wave.
* @returns {number} The wave value (-1 to 1).
*/
const getWaveValue = (type, phase) => {
switch (type.toLowerCase()) {
case 'square':
return Math.sign(Math.sin(phase));
case 'sawtooth':
return 2 * (phase / (2 * Math.PI) - Math.floor(0.5 + phase / (2 * Math.PI)));
case 'triangle':
return (2 / Math.PI) * Math.asin(Math.sin(phase));
case 'sine':
default:
return Math.sin(phase);
}
};
return new Promise((resolve, reject) => {
try {
// 1. Setup Canvas and scale image for performance
// For performance, we cap the width and scale the image down if necessary.
const MAX_WIDTH = 800;
const aspectRatio = originalImg.naturalHeight / originalImg.naturalWidth;
const width = Math.min(originalImg.naturalWidth, MAX_WIDTH);
const height = Math.floor(width * aspectRatio);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
// 2. Setup Web Audio API
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
const sampleRate = audioCtx.sampleRate;
const numSamples = Math.floor(duration * sampleRate);
const audioBuffer = audioCtx.createBuffer(1, numSamples, sampleRate); // 1 = mono channel
const channelData = audioBuffer.getChannelData(0);
let maxAmplitude = 0;
// 3. Main sonification loop (this can be computationally intensive)
for (let i = 0; i < numSamples; i++) {
const time = i / sampleRate;
const x = Math.min(width - 1, Math.floor((i / numSamples) * width));
let sampleValue = 0;
for (let y = 0; y < height; y++) {
const pixelIndex = (y * width + x) * 4;
const r = data[pixelIndex];
const g = data[pixelIndex + 1];
const b = data[pixelIndex + 2];
const brightness = (r + g + b) / (3 * 255);
if (brightness > 0.05) { // Threshold to ignore pure black
// Map y-position to frequency (invert y so top is high freq)
const freq = minFreq + ((height - 1 - y) / (height - 1)) * (maxFreq - minFreq);
const phase = 2 * Math.PI * freq * time;
sampleValue += getWaveValue(waveType, phase) * brightness;
}
}
channelData[i] = sampleValue;
if (Math.abs(sampleValue) > maxAmplitude) {
maxAmplitude = Math.abs(sampleValue);
}
}
// 4. Normalize the audio to prevent clipping and use full dynamic range
if (maxAmplitude > 1.0) { // Only normalize if clipping would occur
for (let i = 0; i < numSamples; i++) {
channelData[i] /= maxAmplitude;
}
}
// 5. Convert buffer to WAV and create an audio element
const wavBlob = bufferToWav(audioBuffer);
const audioUrl = URL.createObjectURL(wavBlob);
const audioElement = document.createElement('audio');
audioElement.controls = true;
audioElement.src = audioUrl;
// 6. Clean up and resolve
audioCtx.close();
resolve(audioElement);
} catch (error) {
console.error("Error processing image into audio:", error);
const errorElement = document.createElement('p');
errorElement.textContent = 'Could not generate audio. See console for details.';
errorElement.style.color = 'red';
reject(errorElement);
}
});
}
Apply Changes