5 changed files with 584 additions and 386 deletions
@ -1,11 +0,0 @@
@@ -1,11 +0,0 @@
|
||||
# Require any additional compass plugins here. |
||||
|
||||
# Set this to the root of your project when deployed: |
||||
http_path = "/" |
||||
css_dir = "css" |
||||
sass_dir = "sass" |
||||
images_dir = "images" |
||||
javascripts_dir = "js" |
||||
|
||||
# You can select your preferred output style here (can be overridden via the command line): |
||||
output_style = :expanded or :nested or :compact or :compressed |
||||
@ -1,184 +1,659 @@
@@ -1,184 +1,659 @@
|
||||
window.onload = function () { |
||||
MIDI.loadPlugin({ |
||||
soundfontUrl: "./soundfont/", |
||||
instrument: "acoustic_grand_piano", |
||||
callback: function() { |
||||
$(window).trigger('ready'); //trigger an event to know when the plugin is loaded.
|
||||
} |
||||
}); |
||||
// Constants
|
||||
var DELAY = 0; // play one note every quarter second
|
||||
var VELOCITY = 127; // how hard the note hits
|
||||
var ACTIVE_FEEDBACK_DURATION = 200; // milliseconds
|
||||
var NOTE_DURATION = 750; // milliseconds - how long a note plays
|
||||
var NOTE_OFF_DELAY = 0.75; // seconds - delay before note stops
|
||||
|
||||
// Cached DOM elements and key mappings
|
||||
var pianoElement = null; |
||||
var keyElementsMap = new Map(); // Maps note number to DOM element
|
||||
var activeNotes = new Set(); // Track currently playing notes to prevent duplicates
|
||||
var pressedKeys = new Set(); // Track currently pressed keyboard keys to prevent repeat
|
||||
var activeTimeouts = new Map(); // Track timeouts for cleanup
|
||||
|
||||
// Web MIDI API support
|
||||
var midiAccess = null; |
||||
var midiInputs = new Map(); // Track connected MIDI input devices
|
||||
var midiDeviceStatus = { |
||||
supported: false, |
||||
connected: false, |
||||
deviceCount: 0 |
||||
}; |
||||
|
||||
$(window).on('ready', function() { //here we are listening for the ready event.
|
||||
// Keyboard mapping: lowercase key -> MIDI note (normalized in parseAction)
|
||||
var KEYBOARD_MAP = { |
||||
'q': 60, // C4
|
||||
'w': 62, // D4
|
||||
'e': 64, // E4
|
||||
'r': 65, // F4
|
||||
't': 67, // G4
|
||||
'y': 69, // A4
|
||||
'u': 71, // B4
|
||||
'i': 72 // C5
|
||||
}; |
||||
|
||||
/** |
||||
* Validate MIDI note number |
||||
* @param {number} note - The MIDI note number |
||||
* @returns {boolean} True if valid |
||||
*/ |
||||
function isValidNote(note) { |
||||
return !isNaN(note) && note >= 0 && note <= 127; |
||||
} |
||||
|
||||
/** |
||||
* Clear a timeout and remove it from tracking |
||||
* @param {number} note - The note associated with the timeout |
||||
*/ |
||||
function clearNoteTimeout(note) { |
||||
if (activeTimeouts.has(note)) { |
||||
clearTimeout(activeTimeouts.get(note)); |
||||
activeTimeouts.delete(note); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Initialize the piano - cache DOM elements and set up event handlers |
||||
*/ |
||||
function initializePiano() { |
||||
pianoElement = document.getElementById('piano'); |
||||
|
||||
if (!pianoElement) { |
||||
console.error('Piano element not found'); |
||||
return; |
||||
} |
||||
|
||||
// Cache all key elements in a Map for O(1) lookup
|
||||
var allKeys = document.querySelectorAll('[data-note]'); |
||||
allKeys.forEach(function(keyElement) { |
||||
var note = parseInt(keyElement.dataset.note, 10); |
||||
if (isValidNote(note)) { |
||||
keyElementsMap.set(note, keyElement); |
||||
} |
||||
}); |
||||
|
||||
// Set up event handlers
|
||||
assignHandlers(); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Get the note number from a clicked/touched element |
||||
* @param {HTMLElement} target - The clicked/touched element |
||||
* @returns {number|null} The MIDI note number or null |
||||
*/ |
||||
function getNoteFromElement(target) { |
||||
if (!target || !target.dataset) { |
||||
return null; |
||||
} |
||||
|
||||
// Check if clicked element has data-note
|
||||
if (target.dataset.note) { |
||||
return parseInt(target.dataset.note, 10); |
||||
} |
||||
|
||||
// Check if clicked element is inside an anchor or black_key
|
||||
var anchor = target.closest('.anchor, .black_key'); |
||||
if (anchor && anchor.dataset && anchor.dataset.note) { |
||||
return parseInt(anchor.dataset.note, 10); |
||||
} |
||||
|
||||
// Check parent li for anchor or black_key
|
||||
var li = target.closest('li'); |
||||
if (li) { |
||||
var keyElement = li.querySelector('.anchor, .black_key'); |
||||
if (keyElement && keyElement.dataset && keyElement.dataset.note) { |
||||
return parseInt(keyElement.dataset.note, 10); |
||||
} |
||||
} |
||||
|
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Add visual feedback to a key element (CSS transitions handle the animation) |
||||
* @param {HTMLElement} keyElement - The key element to highlight |
||||
*/ |
||||
function addKeyFeedback(keyElement) { |
||||
if (keyElement) { |
||||
keyElement.classList.add('active'); |
||||
// CSS transition handles the visual feedback
|
||||
// Remove class after duration for cleanup
|
||||
setTimeout(function() { |
||||
if (keyElement) { |
||||
keyElement.classList.remove('active'); |
||||
} |
||||
}, ACTIVE_FEEDBACK_DURATION); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Play a note with proper cleanup |
||||
* @param {number} note - The MIDI note number |
||||
* @param {number} velocity - The velocity (0-127), defaults to VELOCITY constant |
||||
*/ |
||||
function playNoteInternal(note, velocity) { |
||||
if (!isValidNote(note)) { |
||||
console.warn('Invalid MIDI note:', note); |
||||
return; |
||||
} |
||||
|
||||
// Use provided velocity or default
|
||||
var noteVelocity = (velocity !== undefined && velocity >= 0 && velocity <= 127) ? velocity : VELOCITY; |
||||
|
||||
// Clear any existing timeout for this note
|
||||
clearNoteTimeout(note); |
||||
|
||||
MIDI.setVolume(0, 127); |
||||
MIDI.noteOn(0, note, noteVelocity, DELAY); |
||||
MIDI.noteOff(0, note, DELAY + NOTE_OFF_DELAY); |
||||
|
||||
// Track timeout for cleanup
|
||||
var timeoutId = setTimeout(function() { |
||||
activeNotes.delete(note); |
||||
activeTimeouts.delete(note); |
||||
}, NOTE_DURATION); |
||||
|
||||
activeTimeouts.set(note, timeoutId); |
||||
} |
||||
|
||||
/** |
||||
* Handle piano key clicks |
||||
* @param {Event} event - The click event |
||||
*/ |
||||
function handlePianoClick(event) { |
||||
var note = getNoteFromElement(event.target); |
||||
|
||||
if (note && isValidNote(note)) { |
||||
// Prevent duplicate triggers for the same note
|
||||
if (!activeNotes.has(note)) { |
||||
activeNotes.add(note); |
||||
|
||||
// Get cached key element
|
||||
var keyElement = keyElementsMap.get(note); |
||||
addKeyFeedback(keyElement); |
||||
|
||||
playNoteInternal(note); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle touch events for mobile devices |
||||
* @param {TouchEvent} event - The touch event |
||||
*/ |
||||
function handleTouchStart(event) { |
||||
event.preventDefault(); // Prevent scrolling
|
||||
var touch = event.touches[0] || event.changedTouches[0]; |
||||
if (touch) { |
||||
var target = document.elementFromPoint(touch.clientX, touch.clientY); |
||||
var note = getNoteFromElement(target); |
||||
|
||||
if (note && isValidNote(note)) { |
||||
// Prevent duplicate triggers for the same note
|
||||
if (!activeNotes.has(note)) { |
||||
activeNotes.add(note); |
||||
|
||||
// Get cached key element
|
||||
var keyElement = keyElementsMap.get(note); |
||||
addKeyFeedback(keyElement); |
||||
|
||||
playNoteInternal(note); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @method assignHandlers creates the click, keydown and keyup event handlers when the font is loaded |
||||
*/ |
||||
function assignHandlers() { |
||||
$('#piano').on('click', function(event) { |
||||
var note = $(event.target).data('note'); |
||||
playNote(note); |
||||
}); |
||||
$(document).on('keydown', parseAction); |
||||
$(document).on('keyup', releaseAction); |
||||
if (!pianoElement) { |
||||
console.error('Piano element not found'); |
||||
return; |
||||
} |
||||
|
||||
// Handle clicks on piano keys (both white and black keys)
|
||||
pianoElement.addEventListener('click', handlePianoClick); |
||||
|
||||
// Handle touch events for mobile devices
|
||||
pianoElement.addEventListener('touchstart', handleTouchStart, { passive: false }); |
||||
|
||||
// Keyboard event handlers
|
||||
document.addEventListener('keydown', parseAction); |
||||
document.addEventListener('keyup', releaseAction); |
||||
} |
||||
|
||||
/** |
||||
* @method method to execute whenever the user triggers a keyup event. |
||||
* @param event |
||||
* @method releaseAction executes whenever the user triggers a keyup event. |
||||
* @param {KeyboardEvent} event |
||||
*/ |
||||
function releaseAction(event) { |
||||
$(".anchor").removeClass('active'); //make the piano keys look like they're being pressed when user is using a keyboard
|
||||
var key = event.key ? event.key.toLowerCase() : null; |
||||
|
||||
// Only process if it's a mapped key
|
||||
if (key && key in KEYBOARD_MAP) { |
||||
pressedKeys.delete(key); |
||||
|
||||
// Remove active class from the specific key element
|
||||
var note = KEYBOARD_MAP[key]; |
||||
var keyElement = keyElementsMap.get(note); |
||||
if (keyElement) { |
||||
keyElement.classList.remove('active'); |
||||
} |
||||
|
||||
// Remove from active notes set
|
||||
activeNotes.delete(note); |
||||
clearNoteTimeout(note); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @method parseAction handles keydown events by detecting the user's event keycode and playing the proper note. |
||||
* @param event |
||||
* @method parseAction handles keydown events by detecting the user's key and playing the proper note. |
||||
* @param {KeyboardEvent} event |
||||
*/ |
||||
function parseAction(event) { |
||||
var keycode = event.keyCode; |
||||
|
||||
switch (keycode) { |
||||
case 81: |
||||
triggerAction(60) |
||||
break; |
||||
case 87: |
||||
triggerAction(62) |
||||
break; |
||||
case 69: |
||||
triggerAction(64); |
||||
break; |
||||
case 82: |
||||
triggerAction(65); |
||||
break; |
||||
case 84: |
||||
triggerAction(67); |
||||
break; |
||||
case 89: |
||||
triggerAction(69); |
||||
break; |
||||
case 85: |
||||
triggerAction(71); |
||||
break; |
||||
case 73: |
||||
triggerAction(72); |
||||
break; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @method triggerAction method to trigger UI change to make the key look pressed and to play the note. |
||||
* @param note |
||||
// Normalize key to lowercase
|
||||
var key = event.key ? event.key.toLowerCase() : null; |
||||
|
||||
// Prevent browser shortcuts and handle key repeat
|
||||
if (key && key in KEYBOARD_MAP) { |
||||
event.preventDefault(); |
||||
|
||||
// Prevent key repeat - only trigger if key wasn't already pressed
|
||||
if (!pressedKeys.has(key)) { |
||||
pressedKeys.add(key); |
||||
|
||||
var note = KEYBOARD_MAP[key]; |
||||
triggerAction(note); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* @method triggerAction triggers UI change to make the key look pressed and to play the note. |
||||
* @param {number} note - The MIDI note number |
||||
*/ |
||||
function triggerAction(note) { |
||||
$(".anchor[data-note="+note+"]").addClass('active'); |
||||
playAugmented(note); |
||||
if (!isValidNote(note)) { |
||||
return; |
||||
} |
||||
|
||||
// Get cached key element
|
||||
var keyElement = keyElementsMap.get(note); |
||||
if (keyElement) { |
||||
addKeyFeedback(keyElement); |
||||
} |
||||
|
||||
// Prevent duplicate triggers
|
||||
if (!activeNotes.has(note)) { |
||||
activeNotes.add(note); |
||||
playNoteInternal(note); |
||||
} |
||||
} |
||||
|
||||
var delay = 0; // play one note every quarter second
|
||||
var velocity = 127; // how hard the note hits
|
||||
|
||||
/** |
||||
* @method playNote plays the note. |
||||
* @param note the midi number of the key the user wants to press. |
||||
* @method playNote plays a single note (public API). |
||||
* @param {number} note - The MIDI note number (0-127) |
||||
*/ |
||||
function playNote(note) { |
||||
MIDI.setVolume(0, 127); |
||||
MIDI.noteOn(0, note, velocity, delay); |
||||
MIDI.noteOff(0, note, delay + 0.75); |
||||
if (!isValidNote(note)) { |
||||
console.warn('Invalid MIDI note:', note); |
||||
return; |
||||
} |
||||
|
||||
// Get cached key element for visual feedback
|
||||
var keyElement = keyElementsMap.get(note); |
||||
if (keyElement) { |
||||
addKeyFeedback(keyElement); |
||||
} |
||||
|
||||
// Prevent duplicate triggers
|
||||
if (!activeNotes.has(note)) { |
||||
activeNotes.add(note); |
||||
playNoteInternal(note); |
||||
} |
||||
} |
||||
|
||||
function multinotes() { |
||||
notes = arguments; |
||||
MIDI.noteOn(0, root, velocity, delay); |
||||
MIDI.noteOn(0, third, velocity, delay); |
||||
MIDI.noteOn(0, fifth, velocity, delay); |
||||
/** |
||||
* @method multinotes plays multiple notes simultaneously (for chords). |
||||
* @param {number} root - The root note |
||||
* @param {number} third - The third note |
||||
* @param {number} fifth - The fifth note |
||||
*/ |
||||
function multinotes(root, third, fifth) { |
||||
// Validate all notes
|
||||
if (!isValidNote(root) || !isValidNote(third) || !isValidNote(fifth)) { |
||||
console.warn('Invalid MIDI note in chord:', root, third, fifth); |
||||
return; |
||||
} |
||||
|
||||
MIDI.noteOn(0, root, VELOCITY, DELAY); |
||||
MIDI.noteOn(0, third, VELOCITY, DELAY); |
||||
MIDI.noteOn(0, fifth, VELOCITY, DELAY); |
||||
} |
||||
|
||||
/** |
||||
* @playRootMajor play the major chord in the root position |
||||
* @param note |
||||
* @method playRootMajor plays the major chord in the root position |
||||
* @param {number} note |
||||
*/ |
||||
function playRootMajor(note) { |
||||
var root = note, |
||||
third = note + 4, |
||||
fifth = note +7; |
||||
if (!isValidNote(note)) return; |
||||
var root = note; |
||||
var third = note + 4; |
||||
var fifth = note + 7; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* playFirstMajorInversion play the major chord in the first inversion |
||||
* @param note |
||||
* @method playFirstMajorInversion plays the major chord in the first inversion |
||||
* @param {number} note |
||||
*/ |
||||
function playFirstMajorInversion(note) { |
||||
var root = note+ 4, |
||||
third = root+ 3, |
||||
fifth = root+5; |
||||
if (!isValidNote(note)) return; |
||||
var root = note + 4; |
||||
var third = root + 3; |
||||
var fifth = root + 5; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @playSecondMajorInversion play the major chord in teh second inversion |
||||
* @param note |
||||
* @method playSecondMajorInversion plays the major chord in the second inversion |
||||
* @param {number} note |
||||
*/ |
||||
function playSecondMajorInversion(note) { |
||||
var root = note+ 7, |
||||
third = root+ 5, |
||||
fifth = root+4; |
||||
if (!isValidNote(note)) return; |
||||
var root = note + 7; |
||||
var third = root + 5; |
||||
var fifth = root + 4; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playRootMinor play the minor chord in the root position |
||||
* @param note |
||||
* @method playRootMinor plays the minor chord in the root position |
||||
* @param {number} note |
||||
*/ |
||||
function playRootMinor(note) { |
||||
var root = note, |
||||
third = note + 3, |
||||
fifth = note +7; |
||||
if (!isValidNote(note)) return; |
||||
var root = note; |
||||
var third = note + 3; |
||||
var fifth = note + 7; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playFirstMinorInversion play the minor chord in the 1st inversion |
||||
* @param note |
||||
* @method playFirstMinorInversion plays the minor chord in the 1st inversion |
||||
* @param {number} note |
||||
*/ |
||||
function playFirstMinorInversion(note) { |
||||
var root = note+ 3, |
||||
third = root+ 4, |
||||
fifth = root+5; |
||||
if (!isValidNote(note)) return; |
||||
var root = note + 3; |
||||
var third = root + 4; |
||||
var fifth = root + 5; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playSecondMinorInversion play the minor chord in the 2nd inversion |
||||
* @param note |
||||
* @method playSecondMinorInversion plays the minor chord in the 2nd inversion |
||||
* @param {number} note |
||||
*/ |
||||
function playSecondMinorInversion(note) { |
||||
var root = note+ 7, |
||||
third = root+ 5, |
||||
fifth = root+3; |
||||
if (!isValidNote(note)) return; |
||||
var root = note + 7; |
||||
var third = root + 5; |
||||
var fifth = root + 3; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playAugmented plays an augmented chord |
||||
* @param {number} note |
||||
*/ |
||||
function playAugmented(note) { |
||||
var root = note, |
||||
third = note + 4, |
||||
fifth = root + 8; |
||||
if (!isValidNote(note)) return; |
||||
var root = note; |
||||
var third = note + 4; |
||||
var fifth = root + 8; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playDiminished plays a diminished chord |
||||
* @param {number} note |
||||
*/ |
||||
function playDiminished(note) { |
||||
var root = note, |
||||
third = note + 3, |
||||
fifth = note + 3; |
||||
if (!isValidNote(note)) return; |
||||
var root = note; |
||||
var third = note + 3; |
||||
var fifth = note + 6; // Fixed: was note + 3, should be note + 6 for diminished fifth
|
||||
multinotes(root, third, fifth); |
||||
} |
||||
|
||||
/** |
||||
* @method playSuspended plays a suspended chord |
||||
* @param {number} note |
||||
*/ |
||||
function playSuspended(note) { |
||||
var root = note, |
||||
third = note + 5, |
||||
fifth = note + 7; |
||||
if (!isValidNote(note)) return; |
||||
var root = note; |
||||
var third = note + 5; |
||||
var fifth = note + 7; |
||||
multinotes(root, third, fifth); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Show error message to user |
||||
* @param {string} message - Error message to display |
||||
*/ |
||||
function showError(message) { |
||||
console.error(message); |
||||
// You could add a visual error message here
|
||||
// var errorDiv = document.createElement('div');
|
||||
// errorDiv.className = 'error-message';
|
||||
// errorDiv.textContent = message;
|
||||
// document.body.appendChild(errorDiv);
|
||||
} |
||||
|
||||
/** |
||||
* Update MIDI device status indicator |
||||
*/ |
||||
function updateMIDIStatus() { |
||||
var statusElement = document.getElementById('midi-status'); |
||||
if (!statusElement) { |
||||
// Create status element if it doesn't exist
|
||||
statusElement = document.createElement('div'); |
||||
statusElement.id = 'midi-status'; |
||||
statusElement.style.cssText = 'position: fixed; top: 10px; right: 10px; padding: 8px 12px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; font-size: 12px; z-index: 10000; font-family: Arial, sans-serif;'; |
||||
document.body.appendChild(statusElement); |
||||
} |
||||
|
||||
if (!midiDeviceStatus.supported) { |
||||
statusElement.textContent = 'MIDI: Not supported'; |
||||
statusElement.style.background = 'rgba(200,0,0,0.7)'; |
||||
} else if (midiDeviceStatus.connected && midiDeviceStatus.deviceCount > 0) { |
||||
statusElement.textContent = 'MIDI: ' + midiDeviceStatus.deviceCount + ' device(s) connected'; |
||||
statusElement.style.background = 'rgba(0,150,0,0.7)'; |
||||
} else { |
||||
statusElement.textContent = 'MIDI: No devices connected'; |
||||
statusElement.style.background = 'rgba(150,150,0,0.7)'; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle MIDI message from physical keyboard |
||||
* @param {MIDIMessageEvent} event - MIDI message event |
||||
*/ |
||||
function handleMIDIMessage(event) { |
||||
var data = event.data; |
||||
var command = data[0] & 0xf0; // Upper nibble is command
|
||||
var channel = data[0] & 0x0f; // Lower nibble is channel (we ignore for now)
|
||||
var note = data[1]; |
||||
var velocity = data[2]; |
||||
|
||||
// Note On (0x90) or Note Off (0x80)
|
||||
if (command === 0x90 && velocity > 0) { |
||||
// Note On
|
||||
if (isValidNote(note)) { |
||||
// Prevent duplicate triggers
|
||||
if (!activeNotes.has(note)) { |
||||
activeNotes.add(note); |
||||
|
||||
// Get cached key element for visual feedback
|
||||
var keyElement = keyElementsMap.get(note); |
||||
if (keyElement) { |
||||
addKeyFeedback(keyElement); |
||||
} |
||||
|
||||
// Play note with velocity from MIDI keyboard
|
||||
playNoteInternal(note, velocity); |
||||
} |
||||
} |
||||
} else if (command === 0x80 || (command === 0x90 && velocity === 0)) { |
||||
// Note Off
|
||||
if (isValidNote(note)) { |
||||
// Remove from active notes
|
||||
activeNotes.delete(note); |
||||
clearNoteTimeout(note); |
||||
|
||||
// Remove visual feedback
|
||||
var keyElement = keyElementsMap.get(note); |
||||
if (keyElement) { |
||||
keyElement.classList.remove('active'); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle MIDI input device connection |
||||
* @param {MIDIInput} input - MIDI input device |
||||
*/ |
||||
function handleMIDIInputConnected(input) { |
||||
console.log('MIDI input connected:', input.name, input.manufacturer); |
||||
|
||||
// Set up message handler
|
||||
input.onmidimessage = handleMIDIMessage; |
||||
|
||||
// Track the input
|
||||
midiInputs.set(input.id, input); |
||||
|
||||
// Update status
|
||||
midiDeviceStatus.connected = true; |
||||
midiDeviceStatus.deviceCount = midiInputs.size; |
||||
updateMIDIStatus(); |
||||
} |
||||
|
||||
/** |
||||
* Handle MIDI input device disconnection |
||||
* @param {MIDIInput} input - MIDI input device |
||||
*/ |
||||
function handleMIDIInputDisconnected(input) { |
||||
console.log('MIDI input disconnected:', input.name); |
||||
|
||||
// Remove message handler
|
||||
input.onmidimessage = null; |
||||
|
||||
// Remove from tracking
|
||||
midiInputs.delete(input.id); |
||||
|
||||
// Update status
|
||||
midiDeviceStatus.connected = midiInputs.size > 0; |
||||
midiDeviceStatus.deviceCount = midiInputs.size; |
||||
updateMIDIStatus(); |
||||
} |
||||
|
||||
/** |
||||
* Initialize Web MIDI API support |
||||
*/ |
||||
function initializeMIDI() { |
||||
// Check if Web MIDI API is supported
|
||||
if (!navigator.requestMIDIAccess) { |
||||
console.warn('Web MIDI API is not supported in this browser'); |
||||
midiDeviceStatus.supported = false; |
||||
updateMIDIStatus(); |
||||
return; |
||||
} |
||||
|
||||
midiDeviceStatus.supported = true; |
||||
|
||||
// Request MIDI access
|
||||
navigator.requestMIDIAccess({ sysex: false }) |
||||
.then(function(access) { |
||||
midiAccess = access; |
||||
|
||||
// Handle state changes (devices connecting/disconnecting)
|
||||
access.onstatechange = function(event) { |
||||
if (event.port.state === 'connected' && event.port.type === 'input') { |
||||
handleMIDIInputConnected(event.port); |
||||
} else if (event.port.state === 'disconnected' && event.port.type === 'input') { |
||||
handleMIDIInputDisconnected(event.port); |
||||
} |
||||
}; |
||||
|
||||
// Connect to existing inputs
|
||||
var inputs = access.inputs.values(); |
||||
for (var input = inputs.next(); input && !input.done; input = inputs.next()) { |
||||
if (input.value.state === 'connected') { |
||||
handleMIDIInputConnected(input.value); |
||||
} |
||||
} |
||||
|
||||
updateMIDIStatus(); |
||||
}) |
||||
.catch(function(error) { |
||||
console.error('Error accessing MIDI devices:', error); |
||||
showError('Failed to access MIDI devices: ' + error.message); |
||||
midiDeviceStatus.supported = false; |
||||
updateMIDIStatus(); |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Initialize when DOM is ready |
||||
*/ |
||||
document.addEventListener('DOMContentLoaded', function() { |
||||
// Initialize Web MIDI API for physical keyboard support
|
||||
initializeMIDI(); |
||||
|
||||
try { |
||||
MIDI.loadPlugin({ |
||||
soundfontUrl: "./soundfont/", |
||||
instrument: "acoustic_grand_piano", |
||||
callback: function() { |
||||
// Trigger custom event to know when the plugin is loaded
|
||||
window.dispatchEvent(new Event('ready')); |
||||
}, |
||||
onerror: function(error) { |
||||
showError('Failed to load MIDI plugin: ' + (error || 'Unknown error')); |
||||
} |
||||
}); |
||||
} catch (error) { |
||||
showError('Error initializing MIDI: ' + error.message); |
||||
} |
||||
}); |
||||
|
||||
// Set up handlers when MIDI plugin is ready
|
||||
window.addEventListener('ready', function() { |
||||
try { |
||||
initializePiano(); |
||||
} catch (error) { |
||||
showError('Error initializing piano: ' + error.message); |
||||
} |
||||
}); |
||||
|
||||
// Cleanup on page unload
|
||||
window.addEventListener('beforeunload', function() { |
||||
// Clear all timeouts
|
||||
activeTimeouts.forEach(function(timeoutId) { |
||||
clearTimeout(timeoutId); |
||||
}); |
||||
activeTimeouts.clear(); |
||||
activeNotes.clear(); |
||||
pressedKeys.clear(); |
||||
|
||||
// Disconnect MIDI inputs
|
||||
midiInputs.forEach(function(input) { |
||||
input.onmidimessage = null; |
||||
}); |
||||
midiInputs.clear(); |
||||
}); |
||||
|
||||
@ -1,9 +0,0 @@
@@ -1,9 +0,0 @@
|
||||
/* Welcome to Compass. |
||||
* In this file you should write your main styles. (or centralize your imports) |
||||
* Import this file using the following HTML or equivalent: |
||||
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */ |
||||
|
||||
@import "compass/reset"; |
||||
|
||||
/* MODULES */ |
||||
@import "modules/piano"; |
||||
@ -1,256 +0,0 @@
@@ -1,256 +0,0 @@
|
||||
/* Piano Wrapper */ |
||||
|
||||
#p-wrapper { |
||||
background: #000; |
||||
background: -webkit-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); |
||||
background: -moz-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); |
||||
background: -ms-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); |
||||
background: -o-linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); |
||||
background: linear-gradient(-60deg, black, #333333, black, #666666, #333333 70%); |
||||
width: 100%; |
||||
position: relative; |
||||
-webkit-box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; |
||||
-moz-box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; |
||||
box-shadow: 0 2px 0px #666666, 0 3px 0px #555555, 0 4px 0px #444444, 0 6px 6px black, inset 0 -1px 1px rgba(255, 255, 255, 0.5), inset 0 -4px 5px black; |
||||
border: 2px solid #333; |
||||
-webkit-border-radius: 0 0 5px 5px; |
||||
-moz-border-radius: 0 0 5px 5px; |
||||
border-radius: 0 0 5px 5px; |
||||
-webkit-animation: taufik 2s; |
||||
-moz-animation: taufik 2s; |
||||
animation: taufik 2s; |
||||
margin: 0 auto; |
||||
} |
||||
|
||||
/* Tuts */ |
||||
|
||||
ul#piano { |
||||
display: block; |
||||
width: 1560px; |
||||
height: 240px; |
||||
border-top: 2px solid #222; |
||||
margin: 0 auto; |
||||
li { |
||||
list-style: none; |
||||
float: left; |
||||
display: inline; |
||||
width: 30px; |
||||
position: relative; |
||||
text-align: center; |
||||
color: #fff; |
||||
font-size: 10px; |
||||
font-family: arial, sans-serif; |
||||
line-height: 17px; |
||||
a, div.anchor { |
||||
display: block; |
||||
height: 220px; |
||||
background: #fff; |
||||
background: -webkit-linear-gradient(-30deg, whitesmoke, white); |
||||
background: -moz-linear-gradient(-30deg, whitesmoke, white); |
||||
background: -ms-linear-gradient(-30deg, whitesmoke, white); |
||||
background: -o-linear-gradient(-30deg, whitesmoke, white); |
||||
background: linear-gradient(-30deg, whitesmoke, white); |
||||
border: 1px solid #ccc; |
||||
-webkit-box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); |
||||
-moz-box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); |
||||
box-shadow: inset 0 1px 0px white, inset 0 -1px 0px white, inset 1px 0px 0px white, inset -1px 0px 0px white, 0 4px 3px rgba(0, 0, 0, 0.7); |
||||
-webkit-border-radius: 0 0 3px 3px; |
||||
-moz-border-radius: 0 0 3px 3px; |
||||
border-radius: 0 0 3px 3px; |
||||
} |
||||
a:active, div.anchor:active, div.active { |
||||
-webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); |
||||
-moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); |
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.4); |
||||
position: relative; |
||||
top: 2px; |
||||
height: 216px; |
||||
} |
||||
a:active:before, div.anchor:active:before, div.active:before { |
||||
content: ""; |
||||
width: 0px; |
||||
height: 0px; |
||||
border-width: 216px 5px 0px; |
||||
border-style: solid; |
||||
border-color: transparent transparent transparent rgba(0, 0, 0, 0.1); |
||||
position: absolute; |
||||
left: 0px; |
||||
top: 0px; |
||||
} |
||||
a:active:after, div.anchor:active:after { |
||||
content: ""; |
||||
width: 0px; |
||||
height: 0px; |
||||
border-width: 216px 5px 0px; |
||||
border-style: solid; |
||||
border-color: transparent rgba(0, 0, 0, 0.1) transparent transparent; |
||||
position: absolute; |
||||
right: 0px; |
||||
top: 0px; |
||||
} |
||||
span { |
||||
position: absolute; |
||||
top: 0px; |
||||
left: -12px; |
||||
width: 20px; |
||||
height: 120px; |
||||
background: #333; |
||||
background: -webkit-linear-gradient(-20deg, #333333, black, #333333); |
||||
background: -moz-linear-gradient(-20deg, #333333, black, #333333); |
||||
background: -ms-linear-gradient(-20deg, #333333, black, #333333); |
||||
background: -o-linear-gradient(-20deg, #333333, black, #333333); |
||||
background: linear-gradient(-20deg, #333333, black, #333333); |
||||
z-index: 10; |
||||
border-width: 1px 2px 7px; |
||||
border-style: solid; |
||||
border-color: #666 #222 #111 #555; |
||||
-webkit-box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); |
||||
-moz-box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); |
||||
box-shadow: inset 0px -1px 2px rgba(255, 255, 255, 0.4), 0 2px 3px rgba(0, 0, 0, 0.4); |
||||
-webkit-border-radius: 0 0 2px 2px; |
||||
-moz-border-radius: 0 0 2px 2px; |
||||
border-radius: 0 0 2px 2px; |
||||
&:active { |
||||
border-bottom-width: 2px; |
||||
height: 123px; |
||||
-webkit-box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; |
||||
-moz-box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; |
||||
box-shadow: inset 0px -1px 1px rgba(255, 255, 255, 0.4), 0 1px 0px rgba(0, 0, 0, 0.8), 0 2px 2px rgba(0, 0, 0, 0.4), 0 -1px 0px black; |
||||
} |
||||
} |
||||
b { |
||||
position: absolute; |
||||
top: 0px; |
||||
margin-top: -10px; |
||||
background: #111; |
||||
color: #fff; |
||||
font: bold 14px 'Trebuchet MS',Arial,Sans-Serif; |
||||
border: 2px solid #e6e6e6; |
||||
-webkit-border-radius: 7px; |
||||
-moz-border-radius: 7px; |
||||
border-radius: 7px; |
||||
width: 100px; |
||||
height: 30px; |
||||
padding: 10px; |
||||
left: -40px; |
||||
z-index: 100; |
||||
visibility: hidden; |
||||
opacity: 0; |
||||
-webkit-box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); |
||||
-moz-box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); |
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5); |
||||
-webkit-transition: all 0.2s ease-out; |
||||
-moz-transition: all 0.2s ease-out; |
||||
-ms-transition: all 0.2s ease-out; |
||||
-o-transition: all 0.2s ease-out; |
||||
-transition: all 0.2s ease-out; |
||||
&:before { |
||||
content: ""; |
||||
display: block; |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 50px; |
||||
border-width: 8px; |
||||
border-style: solid; |
||||
border-color: #e6e6e6 transparent transparent transparent; |
||||
} |
||||
&:after { |
||||
content: ""; |
||||
display: block; |
||||
position: absolute; |
||||
top: 100%; |
||||
left: 53px; |
||||
border-width: 5px; |
||||
border-style: solid; |
||||
border-color: #111 transparent transparent transparent; |
||||
} |
||||
} |
||||
&:hover b { |
||||
visibility: visible; |
||||
opacity: 1; |
||||
margin-top: 10px; |
||||
} |
||||
ul { |
||||
position: absolute; |
||||
border: 2px solid #e6e6e6; |
||||
margin-top: -100px; |
||||
top: 100%; |
||||
left: 0px; |
||||
z-index: 1000; |
||||
visibility: hidden; |
||||
opacity: 0; |
||||
-webkit-box-shadow: 0 2px 7px #000; |
||||
-moz-box-shadow: 0 2px 7px #000; |
||||
box-shadow: 0 2px 7px #000; |
||||
-webkit-transition: all 0.2s ease-out 0.2s; |
||||
-moz-transition: all 0.2s ease-out 0.2s; |
||||
-ms-transition: all 0.2s ease-out 0.2s; |
||||
-o-transition: all 0.2s ease-out 0.2s; |
||||
transition: all 0.2s ease-out 0.2s; |
||||
} |
||||
} |
||||
} |
||||
ul#piano li { |
||||
li { |
||||
width: 150px; |
||||
height: auto; |
||||
display: block; |
||||
float: none; |
||||
background: transparent; |
||||
a { |
||||
height: auto; |
||||
display: block; |
||||
padding: 10px 15px; |
||||
background: #333; |
||||
font: normal 12px Arial,Sans-Serif; |
||||
color: #fff; |
||||
text-decoration: none; |
||||
-webkit-box-shadow: none; |
||||
-moz-box-shadow: none; |
||||
box-shadow: none; |
||||
border-radius: 0px; |
||||
-webkit-border-radius: 0px; |
||||
-moz-border-radius: 0px; |
||||
border-width: 1px 0; |
||||
border-style: solid; |
||||
border-color: #444 transparent #222 transparent; |
||||
top: 0px; |
||||
margin-top: 0px; |
||||
&:active { |
||||
height: auto; |
||||
display: block; |
||||
padding: 10px 15px; |
||||
background: #333; |
||||
font: normal 12px Arial,Sans-Serif; |
||||
color: #fff; |
||||
text-decoration: none; |
||||
-webkit-box-shadow: none; |
||||
-moz-box-shadow: none; |
||||
box-shadow: none; |
||||
border-radius: 0px; |
||||
-webkit-border-radius: 0px; |
||||
-moz-border-radius: 0px; |
||||
border-width: 1px 0; |
||||
border-style: solid; |
||||
border-color: #444 transparent #222 transparent; |
||||
top: 0px; |
||||
margin-top: 0px; |
||||
&:before, &:after { |
||||
border: none !important; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
&:hover { |
||||
ul, #search, #contact { |
||||
visibility: visible; |
||||
opacity: 1; |
||||
margin-top: 15px; |
||||
} |
||||
} |
||||
li a:hover { |
||||
background: #111; |
||||
border-top-color: #222; |
||||
border-bottom-color: #000; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue