You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, durationMs = 8000, triggerMode = "dark", scaleType = "pentatonic") {
// Validate and parse parameters
const duration = Number(durationMs) || 8000;
const mode = String(triggerMode).toLowerCase();
const scale = String(scaleType).toLowerCase();
// Create the interactive canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.width;
canvas.height = originalImg.height;
// Draw the image and cache the original pixel data for performance
ctx.drawImage(originalImg, 0, 0);
let originalImageData;
try {
originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
// Handle CORS restriction issues
ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "red";
ctx.font = "bold 20px sans-serif";
ctx.textAlign = "center";
ctx.fillText("Cannot scan image due to CORS restrictions.", canvas.width / 2, canvas.height / 2);
return canvas;
}
const pixels = originalImageData.data;
// Audio states & constants
let audioCtx = null;
let masterGain = null;
let gains = [];
let oscillators = [];
const NUM_BINS = 48; // Number of vertical scanning bins / notes
// Playback state
let isPlaying = false;
let startTime = 0;
let animFrame = null;
let currentX = 0;
// Available scales (in semitone offsets)
const scales = {
pentatonic: [0, 3, 5, 7, 10], // Minor pentatonic (default, very musical)
major: [0, 2, 4, 5, 7, 9, 11],
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
};
function drawOverlay(text) {
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, canvas.height - 60, canvas.width, 60);
ctx.fillStyle = "white";
ctx.font = "bold 22px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, canvas.width / 2, canvas.height - 30);
}
drawOverlay("Click to Start Audio Music Scanner");
function initAudio() {
if (audioCtx) return;
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
masterGain = audioCtx.createGain();
masterGain.gain.value = 1.0;
// Use a compressor to prevent ear-hurting clipping when many notes play
const compressor = audioCtx.createDynamicsCompressor();
compressor.threshold.value = -15;
compressor.knee.value = 10;
compressor.ratio.value = 12;
compressor.attack.value = 0;
compressor.release.value = 0.25;
masterGain.connect(compressor);
compressor.connect(audioCtx.destination);
const intervals = scales[scale] || scales.pentatonic;
const baseMidi = 36; // C2
for (let i = 0; i < NUM_BINS; i++) {
const octave = Math.floor(i / intervals.length);
const note = i % intervals.length;
const midiNote = baseMidi + (octave * 12) + intervals[note];
const freq = 440 * Math.pow(2, (midiNote - 69) / 12);
const osc = audioCtx.createOscillator();
osc.type = 'sine'; // clean tone
osc.frequency.value = freq;
const gain = audioCtx.createGain();
gain.gain.value = 0; // initially silent
osc.connect(gain);
gain.connect(masterGain);
osc.start();
oscillators.push(osc);
gains.push(gain);
}
}
function stopAllAudio() {
if (!audioCtx) return;
for (let i = 0; i < NUM_BINS; i++) {
gains[i].gain.setTargetAtTime(0, audioCtx.currentTime, 0.02);
}
}
function scan() {
// Stop automatically if canvas is removed from DOM to avoid zombie audio
if (!canvas.isConnected && audioCtx) {
isPlaying = false;
audioCtx.close();
audioCtx = null;
return;
}
if (!isPlaying) return;
const now = performance.now();
const elapsed = now - startTime;
let progress = (elapsed % duration) / duration;
// End of scan detection line
currentX = Math.max(0, Math.min(canvas.width - 1, Math.floor(progress * canvas.width)));
// Quickly restore the base frame image
ctx.putImageData(originalImageData, 0, 0);
// Max intensity in each vertical bin
let binMax = new Float32Array(NUM_BINS);
for (let y = 0; y < canvas.height; y++) {
const idx = (y * canvas.width + currentX) * 4;
const r = pixels[idx];
const g = pixels[idx + 1];
const b = pixels[idx + 2];
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
// By default "dark" looks for black/dark sheet music notes over white paper
const intensity = (mode === 'bright') ? (lum / 255) : ((255 - lum) / 255);
// Bottom of the image corresponds to bin 0 (lowest notes)
// Top of the image corresponds to bin NUM_BINS-1 (highest notes)
const binIdx = Math.floor(((canvas.height - 1 - y) / canvas.height) * NUM_BINS);
const safeBinIdx = Math.max(0, Math.min(NUM_BINS - 1, binIdx));
if (intensity > binMax[safeBinIdx]) {
binMax[safeBinIdx] = intensity;
}
}
// Draw the moving red scanner line
ctx.fillStyle = "rgba(255, 0, 0, 0.8)";
ctx.fillRect(currentX, 0, 2, canvas.height);
// Trigger audio notes and visual "Finds"
ctx.fillStyle = "rgba(0, 255, 0, 0.9)";
for (let i = 0; i < NUM_BINS; i++) {
let peak = binMax[i];
// 0.45 serves as an empirical contrast threshold for standard imagery / sheet music
if (peak > 0.45) {
// Apply a quadratic curve for dynamic expression
const targetVolume = Math.min(1.0, Math.pow(peak, 2));
gains[i].gain.setTargetAtTime(targetVolume, audioCtx.currentTime, 0.02);
// Visually highlight the "Found" note at this bin coordinate
let yPos = canvas.height - 1 - (i + 0.5) * (canvas.height / NUM_BINS);
ctx.fillRect(currentX - 3, yPos - 3, 8, 6);
} else {
gains[i].gain.setTargetAtTime(0, audioCtx.currentTime, 0.02);
}
}
animFrame = requestAnimationFrame(scan);
}
// Interaction handler to manage pausing and resuming
canvas.addEventListener('click', async () => {
if (!audioCtx) {
initAudio();
}
// Browsers require resuming the audio context upon user gesture
if (audioCtx.state === 'suspended') {
await audioCtx.resume();
}
if (isPlaying) {
isPlaying = false;
cancelAnimationFrame(animFrame);
stopAllAudio();
ctx.putImageData(originalImageData, 0, 0);
drawOverlay("Paused - Click to Resume Music Scanner");
} else {
isPlaying = true;
// Subtract previously elapsed time to resume cleanly from the same position
startTime = performance.now() - ((currentX / canvas.width) * duration);
scan();
}
});
// Cursor indication to imply interaction exists
canvas.style.cursor = "pointer";
return canvas;
}
Apply Changes