En la sala de Música & Creatividad de Roomly, los participantes pueden tocar un piano polifónico interactivo utilizando tanto el teclado de su computadora o pantalla táctil como un controlador o piano MIDI físico conectado por USB, escuchando e interpretando piezas en vivo junto a otros usuarios conectados en la misma sala. Lograr que la pulsación de una tecla física se traduzca instantáneamente en sonido local y remoto requiere dominar la Web MIDI API y la programación de voces polifónicas.
1. El Protocolo MIDI: Eficiencia Extrema en 3 Bytes
Diseñado originalmente en 1983 y vigente hasta hoy por su extraordinaria simplicidad, un mensaje de nota MIDI estándar no transmite ondas de audio (que pesarían cientos de kilobits por segundo), sino instrucciones de control compactas de exactamente 3 bytes:
- Byte 0 (Status Byte): Define el comando y el canal (del 1 al 16). En hexadecimal,
0x90(144 en decimal) indica Note On en el canal 1, mientras que0x80(128 en decimal) indica Note Off. - Byte 1 (Data Byte 1 - Pitch): El número de nota musical entre
0y127, donde60corresponde al Do central (Middle C / C4 = 261.63 Hz) y69al La de afinación estándar (A4 = 440.00 Hz). - Byte 2 (Data Byte 2 - Velocity): La fuerza o velocidad con la que se presionó la tecla, de
0a127. Un mensaje Note On convelocity = 0equivale por convención a soltar la tecla (Note Off).
2. Captura de Eventos Hardware con navigator.requestMIDIAccess()
El navegador expone el acceso a los controladores MIDI del sistema operativo mediante una promesa asíncrona que enumera todas las entradas (inputs) y permite reaccionar a la conexión en caliente (hot-plugging) de teclados USB:
async function initMidiController(onNoteOn, onNoteOff) {
if (!navigator.requestMIDIAccess) return;
const midiAccess = await navigator.requestMIDIAccess({ sysex: false });
const bindInputs = () => {
for (const input of midiAccess.inputs.values()) {
input.onmidimessage = (event) => {
const [status, midiNote, velocity] = event.data;
const command = status & 0xf0; // Ignorar los 4 bits del canal MIDI
if (command === 0x90 && velocity > 0) {
onNoteOn(midiNote, velocity / 127);
} else if (command === 0x80 || (command === 0x90 && velocity === 0)) {
onNoteOff(midiNote);
}
};
}
};
bindInputs();
midiAccess.onstatechange = () => bindInputs();
}
3. Conversión de Nota MIDI a Frecuencia Acústica (Temperamento Igual)
Para sintetizar cualquier nota en la Web Audio API, convertimos el número de nota MIDI $n$ a su frecuencia fundamental en Hercios ($f$) utilizando la fórmula matemática del temperamento igual de 12 semitonos por octava referenciada a $A_4 = 440\text{ Hz}$:
function midiNoteToFrequency(midiNote) {
return 440 * Math.pow(2, (midiNote - 69) / 12);
}
4. Gestión de Turnos, Polifonía y Prevención de Saturación en Salas Grupales
Si 20 usuarios en una sala de chat presionaran teclas al mismo tiempo sin coordinación, el resultado sería cacofónico y saturaría el bus de mezcla de audio (clipping). Por ello, en Roomly diseñamos un sistema de Escenario y Cola de Turnos en Tiempo Real acompañado de un compresor dinámico (DynamicsCompressorNode) a la salida del sintetizador:
- Los usuarios pueden practicar en modo local silencioso o unirse a la cola del escenario para interpretar una pieza frente a la sala durante su turno activo.
- Cada evento enviado por WebSocket transporta únicamente el identificador de nota, la velocidad normalizada y la marca de tiempo relativa, ocupando menos de 60 bytes en JSON o 4 bytes en binario.
- Un límite de polifonía máxima de 16 voces simultáneas recicla automáticamente la nota más antigua (voice stealing) para garantizar un consumo de CPU constante incluso en teléfonos móviles de gama media.