Please bookmark this page to avoid losing your image tool!

Image Virtual Musical Instruments Maker

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
function processImage(originalImg, gridColumns = "8", gridRows = "8", musicalScale = "pentatonic") {
    const cols = parseInt(gridColumns, 10) || 8;
    const rws = parseInt(gridRows, 10) || 8;
    const scaleStr = (musicalScale || 'pentatonic').toLowerCase();

    // Setup wrapper div
    const wrapper = document.createElement('div');
    wrapper.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif';
    wrapper.style.textAlign = 'center';
    wrapper.style.backgroundColor = '#1a1a1a';
    wrapper.style.color = '#fff';
    wrapper.style.padding = '20px';
    wrapper.style.borderRadius = '12px';
    wrapper.style.boxShadow = '0 8px 32px rgba(0,0,0,0.4)';
    wrapper.style.display = 'inline-block';
    wrapper.style.maxWidth = '100%';

    // Title & Instructions
    const title = document.createElement('h2');
    title.textContent = 'Image Virtual Musical Instrument';
    title.style.margin = '0 0 8px 0';
    title.style.fontSize = '24px';
    wrapper.appendChild(title);

    const instructions = document.createElement('p');
    instructions.textContent = 'Touch, click, or drag across the grid to play. Pitch = Hue, Octave = Brightness, Timbre = Saturation';
    instructions.style.fontSize = '13px';
    instructions.style.color = '#aaa';
    instructions.style.margin = '0 0 20px 0';
    wrapper.appendChild(instructions);

    // Canvas setup
    const canvasWrap = document.createElement('div');
    canvasWrap.style.position = 'relative';
    canvasWrap.style.display = 'inline-block';
    canvasWrap.style.touchAction = 'none'; // Prevent scrolling on mobile during interaction
    canvasWrap.style.borderRadius = '8px';
    canvasWrap.style.overflow = 'hidden';
    canvasWrap.style.boxShadow = '0 4px 15px rgba(0,0,0,0.5)';

    const canvas = document.createElement('canvas');
    canvas.style.display = 'block';
    const ctx = canvas.getContext('2d');

    // Scale image reasonably
    const MAX_WIDTH = 800;
    const MAX_HEIGHT = 600;
    let width = originalImg.naturalWidth || originalImg.width;
    let height = originalImg.naturalHeight || originalImg.height;
    
    if (width > MAX_WIDTH || height > MAX_HEIGHT) {
        const ratio = Math.min(MAX_WIDTH / width, MAX_HEIGHT / height);
        width = Math.floor(width * ratio);
        height = Math.floor(height * ratio);
    }
    
    canvas.width = width;
    canvas.height = height;
    ctx.drawImage(originalImg, 0, 0, width, height);

    // Read pixel data
    let imgData;
    try {
        imgData = ctx.getImageData(0, 0, width, height).data;
    } catch (e) {
        // Fallback if CORS prevents pixel reading
        console.warn("CORS issue: generating random pixel data.");
        imgData = new Uint8ClampedArray(width * height * 4);
        for(let i = 0; i < imgData.length; i += 4) {
            imgData[i] = Math.random() * 255;
            imgData[i+1] = Math.random() * 255;
            imgData[i+2] = Math.random() * 255;
            imgData[i+3] = 255;
        }
    }

    canvasWrap.appendChild(canvas);
    wrapper.appendChild(canvasWrap);

    // Music & Scale Math
    const baseOctaveC = 130.81; // C3
    const scaleRatios = {
        pentatonic: [1, 9/8, 5/4, 3/2, 5/3],            // C, D, E, G, A
        major: [1, 9/8, 5/4, 4/3, 3/2, 5/3, 15/8],      // C, D, E, F, G, A, B
        minor: [1, 9/8, 6/5, 4/3, 3/2, 8/5, 9/5],       // C, D, Eb, F, G, Ab, Bb
        chromatic: [1, 1.059, 1.122, 1.189, 1.260, 1.335, 1.414, 1.498, 1.587, 1.682, 1.782, 1.888]
    };
    const ratios = scaleRatios[scaleStr] || scaleRatios.pentatonic;

    function rgbToHsl(r, g, b) {
        r /= 255; g /= 255; b /= 255;
        let max = Math.max(r, g, b), min = Math.min(r, g, b);
        let h = 0, s = 0, l = (max + min) / 2;
        if(max !== min) {
            let d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch(max) {
                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
                case g: h = (b - r) / d + 2; break;
                case b: h = (r - g) / d + 4; break;
            }
            h /= 6;
        }
        return [h, s, l];
    }

    // Grid interpretation
    const pads = [];
    const padW = width / cols;
    const padH = height / rws;

    for (let r = 0; r < rws; r++) {
        for (let c = 0; c < cols; c++) {
            const px1 = Math.floor(c * padW);
            const py1 = Math.floor(r * padH);
            let rSum = 0, gSum = 0, bSum = 0, count = 0;
            
            // Sub-sample pad area for efficiency
            const stepX = Math.max(1, Math.floor(padW / 8));
            const stepY = Math.max(1, Math.floor(padH / 8));
            
            for(let y = 0; y < padH && (py1 + y) < height; y += stepY) {
                for(let x = 0; x < padW && (px1 + x) < width; x += stepX) {
                    const idx = ((py1 + y) * width + (px1 + x)) * 4;
                    rSum += imgData[idx];
                    gSum += imgData[idx+1];
                    bSum += imgData[idx+2];
                    count++;
                }
            }
            
            const avgR = rSum / (count || 1); 
            const avgG = gSum / (count || 1); 
            const avgB = bSum / (count || 1);
            
            const [hue, sat, lum] = rgbToHsl(avgR, avgG, avgB);

            // Mapping hue to note
            const hueIdx = Math.min(Math.floor(hue * ratios.length), ratios.length - 1);
            const noteRatio = ratios[hueIdx];

            // Mapping lightness to octave
            let octave = 3;
            if (lum < 0.2) octave = 1;         // very dark = sub
            else if (lum < 0.4) octave = 2;    // dark = low
            else if (lum < 0.6) octave = 3;    // mid = normal
            else if (lum < 0.8) octave = 4;    // light = high
            else octave = 5;                   // very bright = top

            // Frequency 
            const baseFreq = baseOctaveC * noteRatio * Math.pow(2, octave - 3);

            // Mapping saturation to waveform
            let waveType = 'sine';
            if (sat > 0.7) waveType = 'sawtooth';
            else if (sat > 0.4) waveType = 'square';
            else if (sat > 0.2) waveType = 'triangle';

            pads.push({
                x: px1, y: py1, w: Math.ceil(padW), h: Math.ceil(padH),
                freq: baseFreq,
                wave: waveType,
                active: false,
                osc: null,
                gain: null
            });
        }
    }

    // Draw initial grid
    function redrawAllPads() {
        ctx.drawImage(originalImg, 0, 0, width, height);
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)';
        ctx.lineWidth = 1;
        
        ctx.beginPath();
        for(let r = 1; r < rws; r++) { 
            ctx.moveTo(0, r * padH); 
            ctx.lineTo(width, r * padH); 
        }
        for(let c = 1; c < cols; c++) { 
            ctx.moveTo(c * padW, 0); 
            ctx.lineTo(c * padW, height); 
        }
        ctx.stroke();

        for (let p of pads) {
            if (p.active) {
                ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
                ctx.fillRect(p.x, p.y, p.w, p.h);
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
                ctx.strokeRect(p.x + 1, p.y + 1, p.w - 2, p.h - 2);
            }
        }
    }
    redrawAllPads();

    // Audio Setup
    let audioCtx = null;
    function initAudio() {
        if (!audioCtx) {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (audioCtx.state === 'suspended') {
            audioCtx.resume();
        }
    }

    const PADDING_GAIN = 0.3; // Overall volume reduction to prevent clipping

    function playPad(pad) {
        pad.active = true;
        initAudio();
        
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        
        osc.type = pad.wave;
        osc.frequency.value = pad.freq;
        
        osc.connect(gain);
        gain.connect(audioCtx.destination);
        
        const now = audioCtx.currentTime;
        gain.gain.setValueAtTime(0, now);
        // ADSR Envelope
        gain.gain.linearRampToValueAtTime(1.0 * PADDING_GAIN, now + 0.05); // Attack
        gain.gain.exponentialRampToValueAtTime(0.4 * PADDING_GAIN, now + 0.3); // Decay & Sustain

        osc.start(now);
        pad.osc = osc;
        pad.gain = gain;
        
        redrawAllPads();
    }

    function stopPad(pad) {
        pad.active = false;
        if (pad.osc && pad.gain) {
            const now = audioCtx.currentTime;
            pad.gain.gain.cancelScheduledValues(now);
            pad.gain.gain.setValueAtTime(pad.gain.gain.value, now);
            // Release envelope
            pad.gain.gain.setTargetAtTime(0, now, 0.05); 
            pad.osc.stop(now + 0.3);
            
            pad.osc = null;
            pad.gain = null;
        }
        redrawAllPads();
    }

    // Interaction Map (Multi-touch support)
    const activePointers = new Map(); // pointerId -> pad

    function getPadAtPos(e) {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        if(x < 0 || x >= rect.width || y < 0 || y >= rect.height) return null;

        const scaleX = canvas.width / rect.width;
        const scaleY = canvas.height / rect.height;
        const canvasX = x * scaleX;
        const canvasY = y * scaleY;

        return pads.find(p => canvasX >= p.x && canvasX < p.x + p.w && canvasY >= p.y && canvasY < p.y + p.h);
    }

    canvas.addEventListener('pointerdown', (e) => {
        initAudio();
        canvas.setPointerCapture(e.pointerId);
        const p = getPadAtPos(e);
        if (p) {
            activePointers.set(e.pointerId, p);
            if (!p.active) playPad(p);
        }
    });

    canvas.addEventListener('pointermove', (e) => {
        if (activePointers.has(e.pointerId)) {
            const currentP = activePointers.get(e.pointerId);
            const newP = getPadAtPos(e);
            
            if (newP !== currentP) {
                // Check if old pad is held by other pointer
                let anotherHoldsOld = false;
                for (const [id, pad] of activePointers.entries()) {
                    if (id !== e.pointerId && pad === currentP) {
                        anotherHoldsOld = true;
                        break;
                    }
                }
                
                if (!anotherHoldsOld && currentP) {
                    stopPad(currentP);
                }

                if (newP) {
                    activePointers.set(e.pointerId, newP);
                    if (!newP.active) playPad(newP);
                } else {
                    activePointers.delete(e.pointerId);
                }
            }
        }
    });

    function handlePointerEnd(e) {
        const currentP = activePointers.get(e.pointerId);
        if (currentP) {
            activePointers.delete(e.pointerId);
            
            let anotherHoldsOld = false;
            for (const [id, pad] of activePointers.entries()) {
                if (pad === currentP) anotherHoldsOld = true;
            }
            if (!anotherHoldsOld) {
                stopPad(currentP);
            }
        }
    }

    canvas.addEventListener('pointerup', handlePointerEnd);
    canvas.addEventListener('pointercancel', handlePointerEnd);

    return wrapper;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Virtual Musical Instruments Maker transforms any uploaded image into an interactive, playable musical interface. By dividing an image into a customizable grid, the tool maps visual properties to musical elements: color hue determines the pitch, brightness controls the octave, and saturation dictates the sound’s timbre (waveform). Users can interact with the resulting grid by clicking, touching, or dragging across the image to perform melodies. This tool is ideal for creative musicians looking for unique generative sound sources, digital artists exploring audiovisual connections, or educators demonstrating the relationship between color theory and music theory.

Leave a Reply

Your email address will not be published. Required fields are marked *