You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, totalDuration_s = 8, scaleType = 'pentatonicMajor', rootNote = 'C4', octaves = 3, waveform = 'sine', noteDensity = 2, brightnessThreshold = 50) {
// --- Helper Functions for Music Theory ---
/**
* Converts a MIDI note number to a frequency in Hz.
* @param {number} midi - The MIDI note number (e.g., 69 for A4).
* @returns {number} The frequency in Hz.
*/
const midiToFreq = (midi) => {
return 440 * Math.pow(2, (midi - 69) / 12);
};
/**
* Generates an array of frequencies for a given musical scale.
* @param {string} rootNoteStr - The root note of the scale (e.g., 'C4', 'A#3').
* @param {string} scaleType - The type of scale ('major', 'minor', 'pentatonicMajor', etc.).
* @param {number} numOctaves - The number of octaves to generate.
* @returns {number[]} An array of note frequencies.
*/
const generateScaleFrequencies = (rootNoteStr, scaleType, numOctaves) => {
const scaleIntervals = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
pentatonicMajor: [0, 2, 4, 7, 9],
pentatonicMinor: [0, 3, 5, 7, 10],
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
};
const noteOffsets = { C: 0, 'C#': 1, D: 2, 'D#': 3, E: 4, F: 5, 'F#': 6, G: 7, 'G#': 8, A: 9, 'A#': 10, B: 11 };
let rootMidi;
try {
const match = rootNoteStr.toUpperCase().match(/^([A-G]#?)(-?[0-9])$/);
if (!match) throw new Error();
const noteName = match[1];
const octave = parseInt(match[2], 10);
rootMidi = 12 * (octave + 1) + noteOffsets[noteName];
} catch (e) {
console.error(`Invalid root note format: "${rootNoteStr}". Using C4 as default.`);
rootMidi = 60; // Default to C4
}
const intervals = scaleIntervals[scaleType] || scaleIntervals.major;
const frequencies = [];
for (let i = 0; i < numOctaves; i++) {
for (const interval of intervals) {
const midiNote = rootMidi + (i * 12) + interval;
frequencies.push(midiToFreq(midiNote));
}
}
// Add the root of the next octave to complete the scale range
frequencies.push(midiToFreq(rootMidi + numOctaves * 12));
return frequencies;
};
// --- Image Processing and Score Generation ---
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Scale image down for performance and to make the melody less dense
const MAX_WIDTH = 250;
const scaleFactor = originalImg.width > MAX_WIDTH ? MAX_WIDTH / originalImg.width : 1;
canvas.width = originalImg.width * scaleFactor;
canvas.height = originalImg.height * scaleFactor;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const score = [];
const scaleFreqs = generateScaleFrequencies(rootNote, scaleType, octaves);
const noteInterval = totalDuration_s / (canvas.width / noteDensity);
// Scan image columns to create notes
for (let x = 0; x < canvas.width; x += noteDensity) {
let brightestY = -1;
let maxBrightness = 0;
// Find the brightest pixel in the current column
for (let y = 0; y < canvas.height; y++) {
const i = (y * canvas.width + x) * 4;
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const brightness = (r + g + b) / 3;
if (brightness > maxBrightness) {
maxBrightness = brightness;
brightestY = y;
}
}
if (brightestY !== -1 && maxBrightness > brightnessThreshold) {
// Map Y-position (top=high, bottom=low) to a note in the scale
const pitchIndex = Math.floor((1 - (brightestY / canvas.height)) * scaleFreqs.length);
const frequency = scaleFreqs[Math.min(scaleFreqs.length - 1, Math.max(0, pitchIndex))];
// Map brightness to volume
const volume = (maxBrightness / 255) * 0.7; // Max volume at 0.7 to avoid clipping
// Calculate timing
const startTime = (x / canvas.width) * totalDuration_s;
score.push({ frequency, volume, startTime, duration: noteInterval * 0.95 });
}
}
// --- UI and Audio Playback ---
const container = document.createElement('div');
container.style.fontFamily = 'sans-serif';
const playButton = document.createElement('button');
playButton.textContent = `▶️ Play Melody (${score.length} notes)`;
playButton.style.padding = '10px 15px';
playButton.style.fontSize = '16px';
playButton.style.cursor = 'pointer';
container.appendChild(playButton);
let audioCtx = null;
let activeSources = [];
let isPlaying = false;
let playbackTimeout = null;
const stopPlayback = () => {
if (!isPlaying) return;
clearTimeout(playbackTimeout);
activeSources.forEach(source => {
try { source.stop(0); } catch (e) { /* ignore */ }
});
activeSources = [];
if (audioCtx && audioCtx.state === 'running') {
audioCtx.close().then(() => {
audioCtx = null;
});
}
isPlaying = false;
playButton.textContent = `▶️ Play Melody (${score.length} notes)`;
};
playButton.onclick = () => {
if (isPlaying) {
stopPlayback();
return;
}
if (!score.length) {
alert("No notes were generated from the image. Try adjusting the brightness threshold or using a different image.");
return;
}
// Initialize AudioContext on user interaction
if (!audioCtx || audioCtx.state === 'closed') {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
audioCtx.resume();
isPlaying = true;
playButton.textContent = '⏹️ Stop';
const melodyStartTime = audioCtx.currentTime + 0.1;
score.forEach(note => {
const oscillator = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioCtx.destination);
// Set oscillator and gain properties
oscillator.type = waveform;
oscillator.frequency.value = note.frequency;
// Simple ADSR-like envelope for a more pleasant sound
const attackTime = 0.01;
const releaseTime = Math.min(0.2, note.duration * 0.5);
const peakGain = note.volume;
const noteStart = melodyStartTime + note.startTime;
const noteEnd = noteStart + note.duration;
gainNode.gain.setValueAtTime(0, noteStart);
gainNode.gain.linearRampToValueAtTime(peakGain, noteStart + attackTime);
gainNode.gain.setValueAtTime(peakGain, noteEnd - releaseTime);
gainNode.gain.linearRampToValueAtTime(0, noteEnd);
oscillator.start(noteStart);
oscillator.stop(noteEnd);
activeSources.push(oscillator);
});
playbackTimeout = setTimeout(() => {
if (isPlaying) {
stopPlayback();
}
}, (totalDuration_s + 2) * 1000); // Add buffer time
};
return container;
}
Apply Changes