You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, durationSec = 5, carrierType = 'sawtooth', carrierFreq = 110, numBands = 40, qFactor = 5, minFreq = 300, maxFreq = 10000) {
/**
* Converts an AudioBuffer to a WAV audio format Blob.
* @param {AudioBuffer} aBuffer The audio buffer to convert.
* @returns {Blob} A blob containing the WAV audio data.
*/
function bufferToWave(aBuffer) {
const numOfChan = aBuffer.numberOfChannels;
const L = aBuffer.length;
const length = L * numOfChan * 2 + 44;
const buffer = new ArrayBuffer(length);
const view = new DataView(buffer);
let pos = 0;
const setUint16 = (data) => {
view.setUint16(pos, data, true);
pos += 2;
};
const setUint32 = (data) => {
view.setUint32(pos, data, true);
pos += 4;
};
// RIFF header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8);
setUint32(0x45564157); // "WAVE"
// "fmt " subchunk
setUint32(0x20746d66); // "fmt "
setUint32(16); // 16 for PCM
setUint16(1); // PCM
setUint16(numOfChan);
setUint32(aBuffer.sampleRate);
setUint32(aBuffer.sampleRate * 2 * numOfChan); // byte rate
setUint16(numOfChan * 2); // block align
setUint16(16); // 16-bit
// "data" subchunk
setUint32(0x61746164); // "data"
setUint32(L * numOfChan * 2);
// write the PCM samples
const channels = [];
for (let i = 0; i < numOfChan; i++) {
channels.push(aBuffer.getChannelData(i));
}
for (let i = 0; i < L; i++) {
for (let j = 0; j < numOfChan; j++) {
let sample = Math.max(-1, Math.min(1, channels[j][i]));
sample = sample < 0 ? sample * 0x8000 : sample * 0x7FFF;
view.setInt16(pos, sample, true);
pos += 2;
}
}
return new Blob([buffer], {
type: 'audio/wav'
});
}
// --- Main Logic ---
const AudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
if (!AudioContext) {
const el = document.createElement('p');
el.textContent = 'Web Audio API is not supported in this browser.';
return el;
}
if (originalImg.width === 0 || originalImg.height === 0) {
const el = document.createElement('p');
el.textContent = 'Image has zero width or height and cannot be processed.';
return el;
}
const validCarrierTypes = ['sine', 'square', 'sawtooth', 'triangle', 'noise'];
if (!validCarrierTypes.includes(carrierType)) {
carrierType = 'sawtooth'; // Default to a safe value
}
const width = originalImg.width;
const height = originalImg.height;
const sampleRate = 44100;
const bands = Math.min(numBads, height);
// 1. Analyze image to create gain automation curves for the vocoder bands
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const ctx = tempCanvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height).data;
const gainCurves = Array.from({ length: bands }, () => new Float32Array(width));
const bandHeight = height / bands;
for (let x = 0; x < width; x++) { // Iterate through time (horizontal axis)
for (let b = 0; b < bands; b++) { // Iterate through frequency bands (vertical axis)
let sumBrightness = 0;
let pixelCount = 0;
const startY = Math.floor(b * bandHeight);
const endY = Math.min(height, Math.floor((b + 1) * bandHeight));
for (let y = startY; y < endY; y++) {
const i = (y * width + x) * 4; // pixel index
const r = imageData[i];
const g = imageData[i + 1];
const b_val = imageData[i + 2];
const brightness = (r + g + b_val) / 3;
sumBrightness += brightness;
pixelCount++;
}
const avgBrightness = pixelCount > 0 ? sumBrightness / pixelCount : 0;
gainCurves[b][x] = avgBrightness / 255.0; // Normalize to 0.0 - 1.0 gain
}
}
// Helper to create a carrier source node for a given AudioContext
const createCarrier = (context) => {
if (carrierType === 'noise') {
const bufferSize = Math.ceil(context.sampleRate * durationSec);
const noiseBuffer = context.createBuffer(1, bufferSize, context.samplerate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1; // white noise
}
const noiseSource = context.createBufferSource();
noiseSource.buffer = noiseBuffer;
noiseSource.loop = true;
return noiseSource;
} else {
const oscillator = context.createOscillator();
oscillator.type = carrierType;
oscillator.frequency.value = carrierFreq;
return oscillator;
}
};
// 2. Render the full vocoded audio using an OfflineAudioContext
const vocoderContext = new AudioContext(1, sampleRate * durationSec, sampleRate);
const carrier = createCarrier(vocoderContext);
// Create a bank of bandpass filters
const logMin = Math.log(Math.max(1, minFreq));
const logMax = Math.log(Math.max(1, maxFreq));
const logRange = logMax - logMin;
for (let i = 0; i < bands; i++) {
let centerFreq;
if (bands > 1) { // Logarithmic spacing for multiple bands
centerFreq = Math.exp(logMin + logRange * (i / (bands - 1)));
} else { // Handle the single-band case
centerFreq = Math.exp(logMin + logRange * 0.5);
}
const bandpass = vocoderContext.createBiquadFilter();
bandpass.type = 'bandpass';
bandpass.frequency.value = centerFreq;
bandpass.Q.value = qFactor;
// Gain node controlled by the image's brightness for this band
const gainNode = vocoderContext.createGain();
gainNode.gain.setValueAtTime(0, 0);
gainNode.gain.setValueCurveAtTime(gainCurves[i], 0, durationSec);
carrier.connect(bandpass);
bandpass.connect(gainNode);
gainNode.connect(vocoderContext.destination);
}
carrier.start(0);
const vocodedBuffer = await vocoderContext.startRendering();
// 3. Render the raw carrier signal audio separately for comparison
const carrierContext = new AudioContext(1, sampleRate * durationSec, sampleRate);
const carrierOnly = createCarrier(carrierContext);
carrierOnly.connect(carrierContext.destination);
carrierOnly.start(0);
const carrierBuffer = await carrierContext.startRendering();
// 4. Create and return the HTML element with audio players
const container = document.createElement('div');
container.style.cssText = 'display: flex; flex-direction: column; gap: 10px; align-items: flex-start;';
const vocoderBlob = bufferToWave(vocodedBuffer);
const vocoderUrl = URL.createObjectURL(vocoderBlob);
const vocoderAudioEl = document.createElement('audio');
vocoderAudioEl.controls = true;
vocoderAudioEl.src = vocoderUrl;
const vocoderHeading = document.createElement('h3');
vocoderHeading.textContent = 'Vocoded Result';
container.appendChild(vocoderHeading);
container.appendChild(vocoderAudioEl);
const carrierBlob = bufferToWave(carrierBuffer);
const carrierUrl = URL.createObjectURL(carrierBlob);
const carrierAudioEl = document.createElement('audio');
carrierAudioEl.controls = true;
carrierAudioEl.src = carrierUrl;
const carrierHeading = document.createElement('h3');
carrierHeading.textContent = 'Carrier Signal';
container.appendChild(carrierHeading);
container.appendChild(carrierAudioEl);
return container;
}
Apply Changes