Created
          May 4, 2025 18:21 
        
      - 
      
- 
        Save Kalvisan/4fc3b5085ea18bc422c5d05c521d3fc0 to your computer and use it in GitHub Desktop. 
Revisions
- 
        Kalvisan created this gist May 4, 2025 .There are no files selected for viewingThis file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,446 @@ 'use client'; import { useState, useEffect } from 'react'; export default function MidiPage() { /* * VARIABLES * padColors: Array of background colors for 16 pads (purple for corners, cyan for others) * blackKeyIdx: Array of indexes for black keys relative to white keys * blackNotes: Array of MIDI note numbers for black keys (C#3-A#3, C#4-A#4) * whiteNotes: Array of MIDI note numbers for white keys (C3-C4) * whiteKeyWidth: Width of white piano keys in pixels * whiteKeyCount: Number of white piano keys (15) * pitch/modulation: State for pitch bend and modulation values * knobPositions: State array tracking 8 knob positions * midiStatus: State for MIDI connection status * midiInputs/Messages/Error: State for MIDI data and errors * showMidiLog: State to toggle MIDI message log visibility * activeNotes: Set of currently pressed MIDI notes */ // C3 = 48, so first white = 48, next = 50, 52, ... // White keys: C D E F G A B C D E F G A B C // MIDI notes for 15 white keys starting from C3 (48): const whiteNotes = [48, 50, 52, 53, 55, 57, 59, 60, 62, 64, 65, 67, 69, 71, 72]; const activeWhiteNotesClass = 'bg-green-600'; const whiteKeyWidth = 58; const whiteKeyHeight = 220; const whiteKeyCount = 15; // Black key positions (indexes above white keys) // C# D# F# G# A# C# D# F# G# A# const blackKeyIdx = [0, 1, 3, 4, 5, 7, 8, 10, 11, 12]; // Black keys: C# D# F# G# A# C# D# F# G# A# // MIDI notes for black keys starting from C#3 (49): const blackNotes = [49, 51, 54, 56, 58, 61, 63, 66, 68, 70]; const activeBlackNotesClass = 'bg-green-600'; const blackKeyWidth = 30; const blackKeyHeight = 120; // Pad colors: 0,7,8,15 purple, others cyan const padColors = [ 'bg-purple-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-purple-400', 'bg-purple-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-cyan-400', 'bg-purple-400', ]; // MIDI note numbers for 16 pads (General MIDI drum notes, e.g. 36 = C1) const padNotes = [60, 62, 63, 65, 67, 68, 70, 72, 48, 50, 51, 53, 55, 56, 58, 60]; const activePadClass = 'bg-green-600'; // MIDI knob controller numbers (21-28) const knobCC = [21, 22, 23, 24, 25, 26, 27, 28]; /////////////////////////// /* END OF VARIABLES */ /////////////////////////// const [midiStatus, setMidiStatus] = useState<'disconnected' | 'connected' | 'error' | 'pending'>('pending'); const [midiInputs, setMidiInputs] = useState<any[]>([]); const [midiMessages, setMidiMessages] = useState<string[]>([]); const [midiError, setMidiError] = useState<string | null>(null); const [showMidiLog, setShowMidiLog] = useState(true); const [pitch, setPitch] = useState(8192); const [modulation, setModulation] = useState(0); const [knobPositions, setKnobPositions] = useState(Array(8).fill(0)); // Map of active notes to their velocity const [activeNotes, setActiveNotes] = useState<Map<number, number>>(new Map()); const [activePads, setActivePads] = useState<Map<number, number>>(new Map()); // Play button state const [playActive, setPlayActive] = useState(false); // Record button state const [recordActive, setRecordActive] = useState(false); // Channel Rack buttons const [channelRackUp, setChannelRackUp] = useState(false); const [channelRackDown, setChannelRackDown] = useState(false); function handleButtonClick(id: string) { // Here you can map by id or perform other actions console.log('Button pressed:', id); } function handleKnobChange(idx: number, value: number) { setKnobPositions(prev => prev.map((v, i) => (i === idx ? value : v))); } const minAngle = -135; const maxAngle = 135; useEffect(() => { let midiAccess: MIDIAccess | null = null; setMidiStatus('pending'); setMidiError(null); if (navigator.requestMIDIAccess) { navigator.requestMIDIAccess({ sysex: false }) .then((access) => { midiAccess = access; setMidiStatus('connected'); setMidiInputs(Array.from(access.inputs.values())); access.onstatechange = (event) => { setMidiInputs(Array.from(access.inputs.values())); if (event.port && event.port.state === 'disconnected') { setMidiStatus('disconnected'); } else if (event.port && event.port.state === 'connected') { setMidiStatus('connected'); } }; access.inputs.forEach((input) => { // MIDI message handler input.onmidimessage = (msg: MIDIMessageEvent) => { const [status, note, velocity] = msg.data ?? []; if (status !== 248) { setMidiMessages((prev) => [ `${input.name}: [${Array.from(msg.data ?? []).join(', ')}]`, ...prev.slice(0, 19) ]); } // Knobs: Control Change (status 176), controller 21-28 if (status === 176 && knobCC.includes(note)) { const idx = note - 21; setKnobPositions(prev => prev.map((v, i) => i === idx ? velocity : v)); } // Modulation wheel: Control Change (status 176), controller 1 if (status === 176 && note === 1) { setModulation(velocity); } // Pitch bend: status 224, value = (velocity << 7) + note if (status === 224) { const value = (velocity << 7) + note; setPitch(value); } // Note on - keyboard if (status === 144 && velocity > 0) { setActiveNotes(prev => new Map(prev).set(note, velocity)); } // Note on - pads if (status === 153 && velocity > 0) { setActivePads(prev => new Map(prev).set(note, velocity)); } // Note off - keyboard if ((status === 128) || (status === 144 && velocity === 0)) { setActiveNotes(prev => { const next = new Map(prev); next.delete(note); return next; }); } // Note off - pads if ((status === 137) || (status === 153 && velocity === 0)) { setActivePads(prev => { const next = new Map(prev); next.delete(note); return next; }); } // Channel Rack: 192, 127 if (status === 191 && note === 104 && velocity === 127) { setChannelRackUp(true); } if (status === 191 && note === 104 && velocity === 0) { setChannelRackUp(false); } // Channel Rack: 192, 127 if (status === 191 && note === 105 && velocity === 127) { setChannelRackDown(true); } if (status === 191 && note === 105 && velocity === 0) { setChannelRackDown(false); } // Play/Stop transport: 250 (play), 252 (stop) if (status === 250) { setPlayActive(true); } if (status === 252) { setPlayActive(false); } // Record: 192, 127 if (status === 191 && note === 117 && velocity === 127) { setRecordActive(true); } if (status === 191 && note === 117 && velocity === 0) { setRecordActive(false); } }; }); }) .catch((err) => { setMidiStatus('error'); setMidiError(err.message || 'MIDI access not available'); }); } else { setMidiStatus('error'); setMidiError('This browser does not support Web MIDI API'); } return () => { if (midiAccess) { midiAccess.onstatechange = null; midiAccess.inputs.forEach((input) => { input.onmidimessage = null; }); } }; }, []); return ( <div className="min-h-screen flex items-center justify-center bg-neutral-500"> <div className="rounded-xl shadow-2xl bg-neutral-700 border-4 border-neutral-900 p-6 pb-0 pt-4 w-[950px] max-w-full flex flex-col items-center relative"> {/* MIDI status bar */} <div className="w-full flex flex-row justify-between items-center mb-2"> <div className="flex flex-row items-center gap-2"> {midiStatus === 'pending' && <span className="text-yellow-400">Connecting...</span>} {midiStatus === 'connected' && <span className="text-green-400">MIDI connected</span>} {midiStatus === 'disconnected' && <span className="text-red-400">MIDI disconnected</span>} {midiStatus === 'error' && <span className="text-red-400">Error: {midiError}</span>} {/* MIDI devices inline */} {midiInputs.length === 0 && <span className="text-gray-400 ml-2">No MIDI device found</span>} {midiInputs.map((input, idx) => ( <span key={input.id || idx} className="text-xs text-gray-200 bg-neutral-800 px-2 py-1 rounded ml-2">{input.name}</span> ))} </div> <div className="text-lg text-gray-100 tracking-widest"><b>FL</b>KEY<sup className="text-[10px]">MINI</sup></div> </div> {/* MIDI message log as floating block */} <button onClick={() => setShowMidiLog((v) => !v)} className="fixed bottom-6 right-6 z-50 bg-neutral-900 text-white px-3 py-2 rounded shadow-lg border border-neutral-700 text-xs hover:bg-neutral-800 transition" > {showMidiLog ? 'Hide MIDI log' : 'Show MIDI log'} </button> {showMidiLog && ( <div className="fixed bottom-20 right-6 z-50 w-80 max-w-[90vw] bg-black/90 rounded-lg p-3 shadow-2xl border border-green-700"> <div className="text-xs text-green-300 mb-1 font-mono">Received MIDI messages:</div> <div className="h-32 overflow-y-auto text-xs text-green-200 font-mono"> {midiMessages.length === 0 ? ( <span className="text-gray-500">No messages</span> ) : ( midiMessages.map((msg, i) => <div key={i}>{msg}</div>) )} </div> </div> )} <div className="flex flex-row w-full mb-2"> {/* Left side: vertical bars and buttons */} <div className="flex flex-row"> {/* Pitch/Modulation bars */} <div className="flex flex-row gap-3 mb-3"> <div className="flex flex-col items-center"> <div className="text-[8px] w-full text-center text-gray-300 font-mono tracking-widest">Pitch</div> <div className="w-14 h-full bg-neutral-600 border-2 border-neutral-900 relative flex items-end justify-center"> {/* Pitch visualization: center is 8192, range 0-16383 */} <div className="absolute left-0 bottom-0 w-full" style={{ background: pitch > 8192 ? '#60a5fa' : pitch < 8192 ? '#f87171' : '#a3a3a3', height: `${Math.abs((pitch - 8192) / 8192) * 100}%`, top: pitch < 8192 ? `0` : 'auto', bottom: pitch > 8192 ? `0` : 'auto', opacity: pitch === 8192 ? 0.3 : 0.7 }} /> {/* Center white line that moves with pitch */} <div className="absolute left-1/2 w-2 h-2 rounded-full bg-white z-10" style={{ top: `calc(${100 - ((pitch / 16383) * 100)}% - 4px)`, transform: 'translateX(-50%)', boxShadow: '0 0 4px 2px rgba(255,255,255,0.7)' }} /> <input type="range" min={0} max={16383} value={pitch} onChange={e => setPitch(Number(e.target.value))} className="absolute left-0 bottom-0 w-full h-full opacity-0 cursor-row-resize" style={{ writingMode: 'vertical-lr', WebkitAppearance: 'slider-vertical' }} /> <div className="absolute top-1 left-1/2 -translate-x-1/2 text-[8px] text-white"> {pitch < 8192 ? `-${Math.round((1 - pitch / 8192) * 100)}%` : pitch > 8192 ? `+${Math.round(((pitch - 8192) / 8191) * 100)}%` : '0%'} </div> </div> </div> <div className="flex flex-col items-center"> <div className="text-[8px] w-full text-center text-gray-300 font-mono tracking-widest">Modulation</div> <div className="w-14 h-full bg-neutral-600 border-2 border-neutral-900 relative flex items-end justify-center"> <div className="absolute left-0 bottom-0 w-full bg-gray-400" style={{ height: `${100 - Math.round((127 - modulation) / 127 * 100)}%` }} /> <input type="range" min={0} max={127} value={modulation} onChange={e => setModulation(Number(e.target.value))} className="absolute left-0 bottom-0 w-full h-full opacity-0 cursor-row-resize" style={{ writingMode: 'vertical-lr', WebkitAppearance: 'slider-vertical' }} /> <div className="absolute top-1 left-1/2 -translate-x-1/2 text-[8px] text-white">{100 - Math.round((127 - modulation) / 127 * 100)}%</div> </div> </div> </div> {/* Left side: buttons */} <div className="flex flex-col gap-2 mt-2 ml-3 mr-3 justify-between"> <div className="border-1 border-neutral-500"> <button onClick={() => handleButtonClick('shift')} className="w-12 h-6 bg-neutral-900 text-white m-0.5 mt-0 border-3 border-neutral-500 text-[7px] font-bold cursor-pointer">Shift</button> </div> <div className="text-center"> <button onClick={() => handleButtonClick('transpose')} className="w-12 h-6 bg-neutral-900 text-white border-3 border-neutral-500 text-[7px] cursor-pointer">Transpose</button> <div className="text-[8px] text-gray-300 font-mono pt-0.5">Channel</div> </div> <div className="flex flex-col text-center justify-center items-center"> <div className="text-[8px] text-gray-300 font-mono">Octava</div> <button onClick={() => handleButtonClick('octave-plus')} className="w-12 h-6 bg-neutral-900 text-white border-3 border-neutral-500 text-[12px] cursor-pointer">+</button> <div className="text-[8px] text-gray-300 font-mono">|</div> <button onClick={() => handleButtonClick('octave-minus')} className="w-12 h-6 bg-neutral-900 text-white border-3 border-neutral-500 text-[12px] cursor-pointer">-</button> <div className="text-[8px] text-gray-300 font-mono">Preset</div> </div> </div> </div> {/* Middle: knob, pad, scale indicators */} <div className="flex-1 flex flex-col items-center"> {/* Knobs */} <div className="flex flex-row gap-8 mb-8 mt-2"> {Array.from({ length: 8 }).map((_, i) => ( <div key={i} className="w-8 h-8 bg-neutral-900 border-2 border-neutral-600 rounded-full flex items-center justify-center shadow-inner relative"> {/* Marķieris */} <div className="w-0.5 h-4 bg-white absolute top-0 left-1/2" style={{ transform: `translateX(-50%) rotate(${minAngle + ((maxAngle - minAngle) * ((knobPositions[i] || 0) / 127))}deg)`, transformOrigin: 'bottom center' }} /> <input type="range" min={0} max={127} value={knobPositions[i]} onChange={e => handleKnobChange(i, Number(e.target.value))} className="absolute left-0 top-0 w-full h-full opacity-0 cursor-pointer" /> <div className="w-3 h-3 bg-neutral-500 rounded-full absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" /> <div className="absolute text-[8px] text-gray-400 -bottom-4 left-1/2 -translate-x-1/2"> {Math.round(((knobPositions[i] || 0) / 127) * 100)}% </div> </div> ))} </div> {/* Pad row */} <div className="grid grid-cols-8 gap-2 mb-2"> {Array.from({ length: 16 }).map((_, i) => { const isActive = activePads.has(padNotes[i]); const velocity = activePads.get(padNotes[i]) || 0; // Velocity 1-127, map to opacity 0.2-1 const opacity = isActive ? (0.2 + 0.8 * (velocity / 127)) : 1; return ( <button key={i} onClick={() => handleButtonClick('pad-' + i)} className={`w-14 h-14 border-2 border-neutral-900 rounded-md shadow-md relative cursor-pointer ${isActive ? activePadClass : padColors[i]}`} style={{ opacity }} > <span className="absolute inset-0 rounded-md bg-white/10" /> </button> ); })} </div> </div> {/* Right side: buttons */} <div className="flex flex-col justify-end gap-2 mb-2 ml-2 mr-4"> <button onClick={() => handleButtonClick('channel-up')} className={`w-14 h-14 bg-neutral-900 text-white border-3 text-[8px] cursor-pointer ${channelRackUp ? 'border-white' : 'border-neutral-500'}`}>▲<br />Channel Rack</button> <button onClick={() => handleButtonClick('channel-down')} className={`w-14 h-14 bg-neutral-900 text-white border-3 text-[8px] cursor-pointer ${channelRackDown ? 'border-white' : 'border-neutral-500'}`}>Channel Rack<br />▼</button> </div> <div className="flex flex-col justify-end gap-2 mb-3"> <div className="flex flex-col gap-2 mb-10" > <div className="flex flex-row gap-2"> <button onClick={() => handleButtonClick('scale')} className="w-12 h-6 bg-neutral-900 text-white border-3 border-neutral-500 text-[8px] cursor-pointer">Scale</button> <button onClick={() => handleButtonClick('note-repeat')} className="w-12 h-6 bg-neutral-900 text-white border-3 border-neutral-500 text-[7px] cursor-pointer">Note Repeat</button> </div> <div className="text-[7px] text-gray-300 font-mono text-center">◄ Mixer Track ►</div> </div> <div className="flex flex-row gap-2"> <button onClick={() => handleButtonClick('play')} className={`w-12 h-6 bg-neutral-900 text-white border-3 text-[10px] cursor-pointer transition-all ${playActive ? 'border-white shadow-[0_0_20px_2px_white]' : 'border-neutral-500'}`} > {'▶'} </button> <button onClick={() => handleButtonClick('record')} className={`w-12 h-6 text-red-700 bg-neutral-900 border-3 text-[14px] cursor-pointer transition-all ${recordActive ? 'border-red-700 shadow-[0_0_20px_2px_red]' : 'border-neutral-500'}`} > {'●'} </button> </div> </div> </div> {/* Keyboard bottom */} <div className="relative flex flex-row items-end justify-center mt-6" style={{ height: `${whiteKeyHeight}px`, width: `${whiteKeyWidth * whiteKeyCount}px` }}> {/* White keys */} {Array.from({ length: whiteKeyCount }).map((_, i) => { const midiNote = whiteNotes[i]; const velocity = activeNotes.get(midiNote) || 0; const isActive = activeNotes.has(midiNote); const opacity = isActive ? (0.2 + 0.8 * (velocity / 127)) : 1; return ( <div key={i} onClick={() => handleButtonClick('white-' + i)} className={`relative h-full border-2 flex items-end justify-center cursor-pointer ${isActive ? activeWhiteNotesClass : 'bg-white border-neutral-900'}`} style={{ width: `${whiteKeyWidth}px` }} /> ); })} {/* Black keys */} {blackKeyIdx.map((i, idx) => { const midiNote = blackNotes[idx]; const velocity = activeNotes.get(midiNote) || 0; const isActive = activeNotes.has(midiNote); const opacity = isActive ? (0.2 + 0.8 * (velocity / 127)) : 1; return ( <div key={i} onClick={() => handleButtonClick('black-' + i)} className={`absolute border-2 top-0 rounded-b cursor-pointer ${isActive ? activeBlackNotesClass : 'bg-black border-neutral-800'}`} style={{ left: `${(i + 1) * whiteKeyWidth - (blackKeyWidth / 2)}px`, width: `${blackKeyWidth}px`, height: `${blackKeyHeight}px` }} /> ); })} </div> </div> </div> ); }