midi-keyboard / index.html
mugnatto's picture
Add 2 files
ed07889 verified
<!DOCTYPE html>
<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>