JavaScript Web MIDI API
Web MIDI API в JavaScript: запрос доступа, перечисление входов и выходов, чтение MIDIMessageEvent, отправка сообщений Note On/Off, горячее подключение устройств и требования безопасного контекста.
Web MIDI API позволяет веб-странице напрямую взаимодействовать с MIDI-оборудованием — клавиатурами, пэд-контроллерами, поверхностями управления и синтезаторами — без плагинов и нативных приложений. Вы можете в реальном времени читать то, что играет музыкант, и отправлять ноты и команды управления обратно на звуковой модуль.
В этом руководстве рассматривается весь процесс: запрос доступа, перечисление входных и выходных портов, чтение сообщений с помощью MIDIMessageEvent, отправка сообщений Note On/Off и Control Change, обработка подключения и отключения устройств во время работы страницы, а также правила безопасного контекста и разрешений, которые применяет API.
Что такое MIDI
MIDI (Musical Instrument Digital Interface) не передаёт звук. Он передаёт небольшие события — «эта клавиша нажата с такой-то силой», «этот регулятор повёрнут в такое-то положение». Браузер предоставляет эти события в виде сырых байтов; превращение их в звук — задача синтезатора, будь то внешнее аппаратное обеспечение или ваш собственный код (например, Web Audio API).
Стандартное сообщение канала состоит из трёх байтов:
| Байт | Название | Значение |
|---|---|---|
| 1 | Status | Тип сообщения (старший ниббл) + канал 0–15 (младший ниббл) |
| 2 | Data 1 | Номер ноты (0–127) или номер контроллера |
| 3 | Data 2 | Скорость (0–127) или значение контроллера |
Распространённые байты статуса (канал 1, где ниббл канала равен 0):
0x90— Note On (скорость0интерпретируется как Note Off)0x80— Note Off0xB0— Control Change (педаль сустейна, модуляция, громкость, …)0xE0— Pitch Bend
Номер ноты 60 соответствует средней ноте До (Middle C); скорость 0–127 описывает силу нажатия клавиши.
Запрос доступа
Всё начинается с navigator.requestMIDIAccess(). Метод возвращает Promise, который разрешается в объект MIDIAccess, содержащий доступные порты. При первом использовании браузер может запросить у пользователя разрешение.
async function initMIDI() {
// Feature-detect before calling — support is not universal.
if (!navigator.requestMIDIAccess) {
console.warn('Web MIDI API is not supported in this browser.');
return;
}
try {
// Pass { sysex: true } only if you genuinely need System Exclusive messages —
// it triggers a stricter, separate permission prompt.
const midiAccess = await navigator.requestMIDIAccess({ sysex: false });
console.log('MIDI access granted', midiAccess);
return midiAccess;
} catch (err) {
console.error('Could not access MIDI devices:', err);
}
}Безопасный контекст и разрешения
Web MIDI API работает только в безопасном контексте — на страницах, обслуживаемых по https:// (или http://localhost во время разработки). Вызов requestMIDIAccess() на незащищённой странице завершится отклонением промиса.
Доступ также ограничен Permissions Policy и пользовательским запросом разрешения. Если пользователь отклонит его (или заголовок Permissions-Policy: midi=() заблокирует функцию), промис будет отклонён — именно поэтому вызов обёрнут в try/catch. Запрос sysex: true требует более высокого уровня привилегий и отдельного подтверждения, поскольку SysEx-сообщения могут перепрограммировать устройство — запрашивайте его только при необходимости.
Перечисление входов и выходов
MIDIAccess предоставляет две коллекции в виде Map — inputs (устройства, которые отправляют данные в браузер) и outputs (устройства, которым браузер может отправлять данные). Обе представляют собой MIDIInputMap / MIDIOutputMap, поэтому их можно перебирать как Map, с ключами по стабильному id порта.
function listPorts(midiAccess) {
console.log('Inputs:');
for (const input of midiAccess.inputs.values()) {
console.log(` ${input.name} (${input.manufacturer}) — ${input.state}`);
}
console.log('Outputs:');
for (const output of midiAccess.outputs.values()) {
console.log(` ${output.name} (${output.manufacturer}) — ${output.state}`);
}
}Каждый порт содержит полезные метаданные: id, name, manufacturer, type ("input" или "output"), state ("connected" / "disconnected") и connection ("open", "closed" или "pending").
Чтение MIDI-входа
Прикрепите обработчик onmidimessage к входному порту. Каждое событие является MIDIMessageEvent, свойство data которого представляет собой Uint8Array из сырых байтов (см. Бинарные массивы, чтобы узнать, как работают типизированные массивы). Это тот же паттерн обратного вызова, который используется в событиях JavaScript.
function startListening(midiAccess) {
midiAccess.inputs.forEach((input) => {
input.onmidimessage = onMIDIMessage;
});
}
function onMIDIMessage(event) {
// event.data is a Uint8Array; channel messages are usually 3 bytes.
const [status, data1, data2] = event.data;
const command = status & 0xf0; // high nibble = message type
const channel = status & 0x0f; // low nibble = channel 0–15
switch (command) {
case 0x90: // Note On
if (data2 > 0) {
console.log(`Note On — note ${data1}, velocity ${data2}, ch ${channel}`);
} else {
console.log(`Note Off — note ${data1} (velocity 0)`);
}
break;
case 0x80: // Note Off
console.log(`Note Off — note ${data1}, ch ${channel}`);
break;
case 0xb0: // Control Change
console.log(`Control Change — controller ${data1}, value ${data2}`);
break;
default:
console.log('Other message:', Array.from(event.data));
}
}Маскирование байта статуса с помощью & 0xf0 и & 0x0f разделяет тип сообщения и канал, поэтому один обработчик работает независимо от того, по какому из 16 MIDI-каналов передаёт устройство.
Отправка MIDI-выхода
Чтобы управлять внешним оборудованием или программным обеспечением, получите выходной порт и вызовите output.send(data), где data — это массив (или Uint8Array) байтов.
function sendNote(midiAccess) {
const output = midiAccess.outputs.values().next().value; // first available port
if (!output) {
console.log('No MIDI outputs available.');
return;
}
output.send([0x90, 60, 100]); // Note On: Middle C, velocity 100, channel 1
output.send([0x80, 60, 0], performance.now() + 500); // Note Off scheduled 500 ms later
}send() принимает необязательный временной штамп (DOMHighResTimeStamp из performance.now()). Планирование Note Off в будущем надёжнее, чем setTimeout, потому что синхронизацией занимается подсистема MIDI, а не цикл событий JavaScript. Передача 0 без временного штампа означает «прямо сейчас».
Предотвращение зависших нот
Самая распространённая ошибка — зависшая нота: Note On без соответствующего Note Off, из-за чего звук продолжает воспроизводиться бесконечно. Всегда работайте в парах: отслеживайте, какие ноты активны, и отправляйте Note Off при отпускании клавиши или выгрузке страницы.
const activeNotes = new Set();
function noteOn(output, note, velocity = 100) {
output.send([0x90, note, velocity]);
activeNotes.add(note);
}
function noteOff(output, note) {
output.send([0x80, note, 0]);
activeNotes.delete(note);
}
// Panic: silence everything (e.g. on window 'pagehide')
function allNotesOff(output) {
for (const note of activeNotes) output.send([0x80, note, 0]);
activeNotes.clear();
}Обработка горячего подключения
USB-MIDI-устройства могут подключаться и отключаться во время работы страницы. Подписывайтесь на событие statechange объекта MIDIAccess, чтобы прикреплять обработчики к новым входным устройствам и обновлять интерфейс при отключении устройства.
async function setupMIDI() {
const midiAccess = await navigator.requestMIDIAccess();
function attachInputHandlers() {
midiAccess.inputs.forEach((input) => {
input.onmidimessage = onMIDIMessage;
});
}
attachInputHandlers();
midiAccess.onstatechange = (event) => {
const port = event.port;
console.log(`${port.type} "${port.name}" is now ${port.state}`);
if (port.type === 'input' && port.state === 'connected') {
attachInputHandlers(); // wire up the device that just appeared
}
};
}Поддержка браузерами и лучшие практики
- Проверяйте наличие функции с помощью
if (navigator.requestMIDIAccess)перед вызовом — Safari добавил поддержку сравнительно недавно, а в некоторых окружениях она отключена. - Обслуживайте страницы по HTTPS (или через
localhost); требование безопасного контекста обязательно. - Запрашивайте
sysex: trueтолько при необходимости, так как это вызывает более строгий запрос разрешения. - Всегда используйте пары Note On / Note Off и заглушайте все ноты при
pagehide/beforeunload. - Используйте временные штампы в
send()для точной синхронизации вместоsetTimeout. - Для генерации звука в браузере (а не через внешнее оборудование) комбинируйте MIDI-вход с Web Audio API.
Резюме
Web MIDI API предоставляет веб-приложениям прямой низколатентный канал к музыкальному оборудованию. Запросите объект MIDIAccess с помощью navigator.requestMIDIAccess(), переберите его inputs и outputs, читайте входящие события из MIDIMessageEvent.data и отправляйте трёхбайтовые сообщения через port.send(). Соблюдайте правила безопасного контекста и разрешений, обрабатывайте горячее подключение устройств через statechange и всегда завершайте ноты, чтобы избежать классической ошибки зависших нот. На этой основе можно создавать виртуальные клавиатуры, MIDI-контроллеры и инструменты, работающие прямо в браузере.