You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, scaleType = 'major', rootNoteName = 'C4', noteDuration = 0.15, scanResolution = 128, instrument = 'sine') {
/**
* Converts a musical note name (e.g., 'A4', 'C#5') to its MIDI number.
* @param {string} note - The note name.
* @returns {number} The MIDI note number, or a default if invalid.
*/
const noteToMidi = (note) => {
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const match = note.match(/([A-G]#?)([0-9])/);
if (!match) return 60; // Default to C4 if format is wrong
const name = match[1];
const octave = parseInt(match[2], 10);
const noteIndex = notes.indexOf(name);
if (noteIndex < 0) return 60; // Default to C4 if note name is invalid
// MIDI note number formula
return 12 + octave * 12 + noteIndex;
};
/**
* Converts a MIDI note number to a frequency in Hz.
* @param {number} midi - The MIDI note number.
* @returns {number} The frequency in Hertz.
*/
const midiToFreq = (midi) => 440 * Math.pow(2, (midi - 69) / 12);
/**
* Generates an array of frequencies for a given musical scale.
* @param {string} rootNote - The starting note, e.g., 'C4'.
* @param {string} scale - The type of scale ('major', 'minor', 'pentatonic').
* @param {number} octaves - The number of octaves to generate.
* @returns {number[]} An array of frequencies.
*/
const getScaleFrequencies = (rootNote, scale, octaves = 3) => {
const scales = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
pentatonic: [0, 2, 4, 7, 9],
};
const intervals = scales[scale] || scales.major;
const startMidi = noteToMidi(rootNote);
const frequencies = [];
for (let o = 0; o < octaves; o++) {
for (const interval of intervals) {
frequencies.push(midiToFreq(startMidi + o * 12 + interval));
}
}
// Add the final root note of the next octave to complete the scale range
frequencies.push(midiToFreq(startMidi + octaves * 12));
return frequencies;
};
// --- Main Logic ---
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'center';
container.style.gap = '10px';
container.style.fontFamily = 'sans-serif';
const vizCanvas = document.createElement('canvas');
const vizCtx = vizCanvas.getContext('2d');
const aspectRatio = originalImg.width / originalImg.height;
const maxWidth = 500;
vizCanvas.width = Math.min(originalImg.width, maxWidth);
vizCanvas.height = vizCanvas.width / aspectRatio;
vizCtx.drawImage(originalImg, 0, 0, vizCanvas.width, vizCanvas.height);
const analysisCanvas = document.createElement('canvas');
const analysisCtx = analysisCanvas.getContext('2d', { willReadFrequently: true });
analysisCanvas.width = scanResolution;
analysisCanvas.height = Math.round(scanResolution / aspectRatio) || 1;
analysisCtx.drawImage(originalImg, 0, 0, analysisCanvas.width, analysisCanvas.height);
const imageData = analysisCtx.getImageData(0, 0, analysisCanvas.width, analysisCanvas.height).data;
const melody = [];
const noteMarkers = [];
const noteFrequencies = getScaleFrequencies(rootNoteName, scaleType).reverse(); // Reverse for high-pitch at top
for (let x = 0; x < analysisCanvas.width; x++) {
let darkestPix = { y: -1, brightness: 256 };
// Find the darkest pixel in the current column
for (let y = 0; y < analysisCanvas.height; y++) {
const i = (y * analysisCanvas.width + x) * 4;
const r = imageData[i];
const g = imageData[i + 1];
const b = imageData[i + 2];
const brightness = (r * 0.299 + g * 0.587 + b * 0.114); // Perceived brightness formula
if (brightness < darkestPix.brightness) {
darkestPix = { y, brightness };
}
}
if (darkestPix.y !== -1) {
const normalizedY = (analysisCanvas.height === 1) ? 0.5 : darkestPix.y / (analysisCanvas.height - 1);
const noteIndex = Math.min(noteFrequencies.length - 1, Math.floor(normalizedY * noteFrequencies.length));
const frequency = noteFrequencies[noteIndex];
const volume = 0.4 * (1 - (darkestPix.brightness / 255)); // Darker = louder
if (frequency) {
melody.push({
freq: frequency,
vol: volume,
time: x * noteDuration,
});
noteMarkers.push({
x: (x / (analysisCanvas.width - 1)) * vizCanvas.width,
y: normalizedY * vizCanvas.height
});
}
}
}
noteMarkers.forEach(marker => {
vizCtx.beginPath();
vizCtx.arc(marker.x, marker.y, 3, 0, 2 * Math.PI, false);
vizCtx.fillStyle = 'rgba(255, 0, 80, 0.8)';
vizCtx.fill();
vizCtx.lineWidth = 1.5;
vizCtx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
vizCtx.stroke();
});
const playButton = document.createElement('button');
playButton.textContent = '▶️ Play Melody';
playButton.style.padding = '10px 20px';
playButton.style.fontSize = '16px';
playButton.style.cursor = 'pointer';
playButton.style.border = '1px solid #ccc';
playButton.style.borderRadius = '5px';
if (melody.length === 0) {
playButton.disabled = true;
playButton.textContent = 'No melody found in image';
}
let audioContext;
playButton.onclick = () => {
if (!audioContext || audioContext.state === 'closed') {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
} else if (audioContext.state === 'suspended') {
audioContext.resume();
}
if (audioContext.state !== 'running') return;
playButton.disabled = true;
playButton.textContent = 'Playing...';
const startTime = audioContext.currentTime;
const attackTime = 0.01;
const releaseTime = noteDuration * 0.9;
melody.forEach(note => {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.type = instrument;
oscillator.frequency.value = note.freq;
gainNode.connect(audioContext.destination);
oscillator.connect(gainNode);
// Simple attack/release envelope for each note
gainNode.gain.setValueAtTime(0, startTime + note.time);
gainNode.gain.linearRampToValueAtTime(note.vol, startTime + note.time + attackTime);
gainNode.gain.setValueAtTime(note.vol, startTime + note.time + noteDuration - releaseTime);
gainNode.gain.linearRampToValueAtTime(0, startTime + note.time + noteDuration);
oscillator.start(startTime + note.time);
oscillator.stop(startTime + note.time + noteDuration + 0.01);
});
const totalDurationMs = (scanResolution * noteDuration * 1000) + 100;
setTimeout(() => {
playButton.disabled = false;
playButton.textContent = '▶️ Play Melody';
}, totalDurationMs);
};
container.appendChild(vizCanvas);
container.appendChild(playButton);
return container;
}
Apply Changes