Spaces:
Running
Running
<html lang="pt-BR"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Teclado Musical com MIDI</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
.key { | |
transition: all 0.1s ease; | |
user-select: none; | |
} | |
.white-key { | |
background-color: white; | |
border: 1px solid #ccc; | |
z-index: 1; | |
} | |
.black-key { | |
background-color: black; | |
z-index: 2; | |
margin-left: -15px; | |
margin-right: -15px; | |
height: 60%; | |
} | |
.white-key.active { | |
background-color: #ddd; | |
box-shadow: inset 0 0 10px rgba(0,0,0,0.3); | |
} | |
.black-key.active { | |
background-color: #555; | |
box-shadow: inset 0 0 10px rgba(255,255,255,0.2); | |
} | |
.key-label { | |
pointer-events: none; | |
} | |
.keyboard-container { | |
perspective: 1000px; | |
} | |
.keyboard { | |
transform-style: preserve-3d; | |
transform: rotateX(10deg); | |
} | |
.midi-indicator { | |
animation: pulse 0.5s; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.1); } | |
100% { transform: scale(1); } | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white min-h-screen flex flex-col"> | |
<div class="container mx-auto px-4 py-8 flex-grow"> | |
<header class="text-center mb-8"> | |
<h1 class="text-4xl font-bold mb-2">Teclado Musical MIDI</h1> | |
<p class="text-gray-400">Toque no teclado virtual ou conecte um controlador MIDI físico</p> | |
</header> | |
<div class="flex flex-col lg:flex-row gap-8"> | |
<div class="lg:w-1/4 bg-gray-800 p-6 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-cog mr-2"></i> Configurações | |
</h2> | |
<div class="mb-6"> | |
<label class="block text-gray-400 mb-2">Dispositivo MIDI:</label> | |
<select id="midi-input" class="w-full bg-gray-700 text-white p-2 rounded"> | |
<option value="">Nenhum selecionado</option> | |
</select> | |
</div> | |
<div class="mb-6"> | |
<label class="block text-gray-400 mb-2">Oitavas:</label> | |
<input type="range" id="octave-range" min="1" max="6" value="3" class="w-full"> | |
<div class="flex justify-between text-sm text-gray-400"> | |
<span>1</span> | |
<span id="octave-value">3</span> | |
<span>6</span> | |
</div> | |
</div> | |
<div class="mb-6"> | |
<label class="block text-gray-400 mb-2">Instrumento:</label> | |
<select id="instrument-select" class="w-full bg-gray-700 text-white p-2 rounded"> | |
<option value="piano">Piano</option> | |
<option value="synth">Sintetizador</option> | |
<option value="organ">Órgão</option> | |
<option value="guitar">Guitarra</option> | |
<option value="strings">Cordas</option> | |
</select> | |
</div> | |
<div class="mb-6"> | |
<label class="block text-gray-400 mb-2">Volume:</label> | |
<input type="range" id="volume-range" min="0" max="100" value="70" class="w-full"> | |
<div class="flex justify-between text-sm text-gray-400"> | |
<span>0</span> | |
<span id="volume-value">70</span> | |
<span>100</span> | |
</div> | |
</div> | |
<div class="flex items-center mb-6"> | |
<input type="checkbox" id="show-labels" class="mr-2" checked> | |
<label for="show-labels" class="text-gray-400">Mostrar notas</label> | |
</div> | |
<div class="bg-gray-700 p-4 rounded-lg"> | |
<h3 class="font-medium mb-2 flex items-center"> | |
<i class="fas fa-info-circle mr-2"></i> Status MIDI | |
</h3> | |
<div class="flex items-center"> | |
<div id="midi-status" class="w-3 h-3 rounded-full bg-red-500 mr-2"></div> | |
<span id="midi-status-text">Desconectado</span> | |
</div> | |
<div id="midi-activity" class="mt-2 hidden"> | |
<div class="flex items-center mb-1"> | |
<div id="midi-indicator" class="w-3 h-3 rounded-full bg-green-500 mr-2"></div> | |
<span>Atividade MIDI</span> | |
</div> | |
<div class="text-sm text-gray-400"> | |
Nota: <span id="current-note">-</span> | | |
Velocidade: <span id="current-velocity">-</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="lg:w-3/4"> | |
<div class="keyboard-container mb-8"> | |
<div class="keyboard bg-gray-800 p-6 rounded-lg"> | |
<div class="flex relative" id="keyboard"> | |
<!-- Teclas serão geradas pelo JavaScript --> | |
</div> | |
</div> | |
</div> | |
<div class="bg-gray-800 p-6 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 flex items-center"> | |
<i class="fas fa-chart-bar mr-2"></i> Visualização MIDI | |
</h2> | |
<div class="h-40 bg-gray-900 rounded relative overflow-hidden"> | |
<div id="midi-visualizer" class="absolute inset-0"> | |
<!-- Ondas serão geradas pelo JavaScript --> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<footer class="bg-gray-800 py-4 text-center text-gray-400 text-sm"> | |
<p>Teclado Musical MIDI - Pressione as teclas do teclado físico ou clique nas teclas virtuais</p> | |
</footer> | |
<script> | |
document.addEventListener('DOMContentLoaded', async () => { | |
// Configuração inicial | |
const audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
let activeOscillators = {}; | |
let currentInstrument = 'piano'; | |
let volume = 0.7; | |
let octave = 3; | |
let showLabels = true; | |
// Elementos do DOM | |
const keyboardEl = document.getElementById('keyboard'); | |
const midiInputSelect = document.getElementById('midi-input'); | |
const octaveRange = document.getElementById('octave-range'); | |
const octaveValue = document.getElementById('octave-value'); | |
const instrumentSelect = document.getElementById('instrument-select'); | |
const volumeRange = document.getElementById('volume-range'); | |
const volumeValue = document.getElementById('volume-value'); | |
const showLabelsCheckbox = document.getElementById('show-labels'); | |
const midiStatus = document.getElementById('midi-status'); | |
const midiStatusText = document.getElementById('midi-status-text'); | |
const midiActivity = document.getElementById('midi-activity'); | |
const currentNote = document.getElementById('current-note'); | |
const currentVelocity = document.getElementById('current-velocity'); | |
const midiVisualizer = document.getElementById('midi-visualizer'); | |
// Notas musicais | |
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; | |
const whiteKeys = ['C', 'D', 'E', 'F', 'G', 'A', 'B']; | |
const blackKeys = ['C#', 'D#', 'F#', 'G#', 'A#']; | |
// Inicializa o teclado | |
function initKeyboard() { | |
keyboardEl.innerHTML = ''; | |
// Cria 2 oitavas de teclas (14 teclas brancas) | |
for (let i = 0; i < 14; i++) { | |
const noteIndex = i % 7; | |
const noteName = whiteKeys[noteIndex]; | |
const isSharp = false; | |
createKey(noteName, isSharp, i); | |
// Adiciona teclas pretas entre as brancas apropriadas | |
if (noteName !== 'E' && noteName !== 'B' && i < 13) { | |
const nextNoteIndex = (i + 1) % 7; | |
const nextNoteName = whiteKeys[nextNoteIndex]; | |
if (blackKeys.includes(noteName + '#')) { | |
createKey(noteName + '#', true, i); | |
} | |
} | |
} | |
} | |
// Cria uma tecla individual | |
function createKey(noteName, isSharp, position) { | |
const key = document.createElement('div'); | |
const keyClass = isSharp ? 'black-key' : 'white-key'; | |
const width = isSharp ? 'w-8' : 'w-16'; | |
const label = showLabels ? `<span class="key-label absolute bottom-2 text-xs ${isSharp ? 'text-white' : 'text-gray-700'}">${noteName}${octave + Math.floor(position/7)}</span>` : ''; | |
key.className = `key ${keyClass} ${width} h-32 relative cursor-pointer flex items-end justify-center`; | |
key.innerHTML = label; | |
key.dataset.note = noteName; | |
key.dataset.octave = octave + Math.floor(position/7); | |
// Eventos de toque/clique | |
key.addEventListener('mousedown', () => playNote(noteName, octave + Math.floor(position/7), 100)); | |
key.addEventListener('mouseup', () => stopNote(noteName, octave + Math.floor(position/7))); | |
key.addEventListener('mouseleave', () => stopNote(noteName, octave + Math.floor(position/7))); | |
keyboardEl.appendChild(key); | |
} | |
// Toca uma nota | |
function playNote(note, octave, velocity) { | |
const fullNote = `${note}${octave}`; | |
if (activeOscillators[fullNote]) return; | |
const freq = getNoteFrequency(note, octave); | |
const osc = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
// Configura o oscilador baseado no instrumento selecionado | |
configureOscillator(osc, gainNode); | |
gainNode.gain.value = volume * (velocity / 127); | |
osc.frequency.value = freq; | |
osc.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
osc.start(); | |
activeOscillators[fullNote] = { oscillator: osc, gainNode: gainNode }; | |
// Atualiza a interface | |
highlightKey(note, octave, true); | |
updateMidiVisualizer(freq, velocity); | |
// Atualiza o status MIDI | |
currentNote.textContent = `${note}${octave}`; | |
currentVelocity.textContent = velocity; | |
document.getElementById('midi-indicator').classList.add('midi-indicator'); | |
setTimeout(() => { | |
document.getElementById('midi-indicator').classList.remove('midi-indicator'); | |
}, 500); | |
} | |
// Para uma nota | |
function stopNote(note, octave) { | |
const fullNote = `${note}${octave}`; | |
if (!activeOscillators[fullNote]) return; | |
// Adiciona um pequeno release para evitar clicks | |
activeOscillators[fullNote].gainNode.gain.setValueAtTime( | |
activeOscillators[fullNote].gainNode.gain.value, | |
audioContext.currentTime | |
); | |
activeOscillators[fullNote].gainNode.gain.exponentialRampToValueAtTime( | |
0.0001, | |
audioContext.currentTime + 0.03 | |
); | |
setTimeout(() => { | |
activeOscillators[fullNote].oscillator.stop(); | |
delete activeOscillators[fullNote]; | |
}, 30); | |
highlightKey(note, octave, false); | |
} | |
// Configura o oscilador baseado no instrumento selecionado | |
function configureOscillator(osc, gainNode) { | |
switch(currentInstrument) { | |
case 'piano': | |
osc.type = 'sine'; | |
// Simula o ataque rápido e decay do piano | |
gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.01); | |
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 1); | |
break; | |
case 'synth': | |
osc.type = 'sawtooth'; | |
// Filtro para suavizar o som | |
const filter = audioContext.createBiquadFilter(); | |
filter.type = 'lowpass'; | |
filter.frequency.value = 2000; | |
osc.connect(filter); | |
filter.connect(gainNode); | |
break; | |
case 'organ': | |
osc.type = 'sine'; | |
// Adiciona harmônicos para simular órgão | |
const osc2 = audioContext.createOscillator(); | |
osc2.type = 'square'; | |
osc2.frequency.value = osc.frequency.value * 2; | |
osc2.connect(gainNode); | |
osc2.start(); | |
activeOscillators[`${note}${octave}-organ`] = { oscillator: osc2, gainNode: gainNode }; | |
break; | |
case 'guitar': | |
osc.type = 'sine'; | |
// Simula o sustain da guitarra | |
gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.1); | |
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 1.5); | |
break; | |
case 'strings': | |
osc.type = 'sine'; | |
// Simula o ataque lento das cordas | |
gainNode.gain.setValueAtTime(0, audioContext.currentTime); | |
gainNode.gain.linearRampToValueAtTime(volume, audioContext.currentTime + 0.5); | |
gainNode.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 3); | |
break; | |
default: | |
osc.type = 'sine'; | |
} | |
} | |
// Destaca a tecla quando pressionada | |
function highlightKey(note, octave, isActive) { | |
const keys = document.querySelectorAll('.key'); | |
keys.forEach(key => { | |
if (key.dataset.note === note && parseInt(key.dataset.octave) === octave) { | |
if (isActive) { | |
key.classList.add('active'); | |
} else { | |
key.classList.remove('active'); | |
} | |
} | |
}); | |
} | |
// Atualiza o visualizador MIDI | |
function updateMidiVisualizer(frequency, velocity) { | |
// Limpa o visualizador | |
midiVisualizer.innerHTML = ''; | |
// Cria elementos de onda baseados na frequência e velocidade | |
const waveCount = Math.floor(frequency / 100); | |
const waveHeight = (velocity / 127) * 100; | |
for (let i = 0; i < waveCount; i++) { | |
const wave = document.createElement('div'); | |
wave.className = 'absolute bg-blue-500 opacity-50 rounded-full'; | |
// Posiciona e dimensiona a onda | |
const left = Math.random() * 100; | |
const width = 2 + Math.random() * 8; | |
const height = waveHeight * (0.5 + Math.random() * 0.5); | |
const top = 50 - (height / 2) + (Math.random() * 20 - 10); | |
wave.style.left = `${left}%`; | |
wave.style.top = `${top}%`; | |
wave.style.width = `${width}px`; | |
wave.style.height = `${height}px`; | |
midiVisualizer.appendChild(wave); | |
// Anima a onda | |
wave.animate([ | |
{ transform: 'scale(1)', opacity: 0.5 }, | |
{ transform: 'scale(1.5)', opacity: 0 } | |
], { | |
duration: 1000, | |
easing: 'ease-out' | |
}); | |
} | |
} | |
// Obtém a frequência de uma nota | |
function getNoteFrequency(note, octave) { | |
const A4 = 440; | |
let n; | |
if (note.length === 1) { | |
n = (octave - 4) * 12 + notes.indexOf(note); | |
} else { | |
n = (octave - 4) * 12 + notes.indexOf(note); | |
} | |
return A4 * Math.pow(2, n / 12); | |
} | |
// Configuração MIDI | |
async function setupMidi() { | |
try { | |
const midiAccess = await navigator.requestMIDIAccess(); | |
updateMidiDevices(midiAccess); | |
midiAccess.onstatechange = () => updateMidiDevices(midiAccess); | |
midiStatus.classList.remove('bg-red-500'); | |
midiStatus.classList.add('bg-green-500'); | |
midiStatusText.textContent = 'MIDI disponível'; | |
midiActivity.classList.remove('hidden'); | |
} catch (error) { | |
console.error('Erro ao acessar MIDI:', error); | |
midiStatusText.textContent = 'MIDI não suportado'; | |
} | |
} | |
// Atualiza a lista de dispositivos MIDI | |
function updateMidiDevices(midiAccess) { | |
midiInputSelect.innerHTML = '<option value="">Nenhum selecionado</option>'; | |
const inputs = midiAccess.inputs.values(); | |
for (let input = inputs.next(); !input.done; input = inputs.next()) { | |
const option = document.createElement('option'); | |
option.value = input.value.id; | |
option.textContent = input.value.name; | |
midiInputSelect.appendChild(option); | |
// Remove listeners antigos | |
input.value.onmidimessage = null; | |
// Adiciona novo listener se selecionado | |
if (midiInputSelect.value === input.value.id) { | |
input.value.onmidimessage = handleMidiMessage; | |
} | |
} | |
} | |
// Manipula mensagens MIDI | |
function handleMidiMessage(message) { | |
const [command, note, velocity] = message.data; | |
const action = command >> 4; | |
const channel = command & 0xf; | |
// Note on (144) ou Note off (128) | |
if (action === 0x9 || action === 0x8) { | |
const noteName = notes[note % 12]; | |
const noteOctave = Math.floor(note / 12) - 1; | |
if (action === 0x9 && velocity > 0) { | |
playNote(noteName, noteOctave, velocity); | |
} else { | |
stopNote(noteName, noteOctave); | |
} | |
} | |
} | |
// Event listeners | |
octaveRange.addEventListener('input', () => { | |
octave = parseInt(octaveRange.value); | |
octaveValue.textContent = octave; | |
initKeyboard(); | |
}); | |
instrumentSelect.addEventListener('change', () => { | |
currentInstrument = instrumentSelect.value; | |
}); | |
volumeRange.addEventListener('input', () => { | |
volume = parseInt(volumeRange.value) / 100; | |
volumeValue.textContent = volumeRange.value; | |
}); | |
showLabelsCheckbox.addEventListener('change', () => { | |
showLabels = showLabelsCheckbox.checked; | |
initKeyboard(); | |
}); | |
midiInputSelect.addEventListener('change', () => { | |
navigator.requestMIDIAccess().then(midiAccess => { | |
const inputs = midiAccess.inputs.values(); | |
for (let input = inputs.next(); !input.done; input = inputs.next()) { | |
input.value.onmidimessage = null; | |
if (input.value.id === midiInputSelect.value) { | |
input.value.onmidimessage = handleMidiMessage; | |
} | |
} | |
}); | |
}); | |
// Eventos de teclado físico | |
document.addEventListener('keydown', (e) => { | |
const keyMap = { | |
'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E', | |
'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A', | |
'u': 'A#', 'j': 'B', 'k': 'C', 'o': 'C#', 'l': 'D' | |
}; | |
if (keyMap[e.key.toLowerCase()]) { | |
playNote(keyMap[e.key.toLowerCase()], octave, 100); | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
const keyMap = { | |
'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', 'd': 'E', | |
'f': 'F', 't': 'F#', 'g': 'G', 'y': 'G#', 'h': 'A', | |
'u': 'A#', 'j': 'B', 'k': 'C', 'o': 'C#', 'l': 'D' | |
}; | |
if (keyMap[e.key.toLowerCase()]) { | |
stopNote(keyMap[e.key.toLowerCase()], octave); | |
} | |
}); | |
// Inicializa o aplicativo | |
initKeyboard(); | |
setupMidi(); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=mugnatto/midi-keyboard" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |