Web MIDI多设备场景下,如何仅让选中的MIDI输入设备生效?
解决Web MIDI仅响应选中设备的问题
核心问题
你的代码会给所有检测到的MIDI输入设备绑定消息监听事件,导致不管下拉框选了哪个,所有设备的指令都会触发声音。要实现仅响应选中设备,需要动态管理MIDI设备的事件绑定。
修改方案
- 全局存储所有MIDI输入设备,方便后续切换
- 监听下拉选择框的变化,移除未选中设备的消息监听,给选中设备绑定监听
- 处理设备连接/断开的状态变化,同步更新下拉列表和事件绑定
修改后的完整代码
import * as Tone from 'https://cdn.skypack.dev/tone@v14.7.58'; let reverb = new Tone.JCReverb(0).toDestination(); let delay = new Tone.FeedbackDelay(0); const synth = new Tone.Synth().chain(delay, reverb).toDestination(); const now = Tone.now(); const notes = ['A3','A#3','B3','C4','C#4','D4','D#4','E4','F4','F#4']; const keyboardNotesMap = { 'q': 'A3', 'w': 'A#3', 'e': 'B3', 'r': 'C4', 't': 'C#4', 'y': 'D4', 'u': 'D#4', 'i': 'E4', 'o': 'F4', 'p': 'F#4' }; const midiNotesMap = { '57': 'A3', '58': 'A#3', '59': 'B3', '60': 'C4', '61': 'C#4', '62': 'D4', '63': 'D#4', '64': 'E4', '65': 'F4', '66': 'F#4' }; let pianoElement; let keyElements; // 全局存储所有MIDI输入设备,key为设备id let midiInputs = {}; // 当前选中的设备id let selectedMidiDeviceId = null; function getUpDOM() { const pianoElement = document.getElementById('piano'); notes.forEach((note) => { const noteDiv = document.createElement("div"); noteDiv.textContent = note; noteDiv.className = 'note'; noteDiv.setAttribute('data', note); piano.appendChild(noteDiv); }); keyElements = document.getElementsByClassName('note'); } function keyAction() { for (let i = 0; i < keyElements.length; i++) { keyElements[i].onmousedown = async function playNote(key) { const note = keyElements[i].getAttribute('data'); synth.triggerAttackRelease(note, "8n"); }; }; } function startUp() { document.querySelector('button.start') .addEventListener('click', async () => { await Tone.start(); synth.triggerAttackRelease("C4", "8n", now) synth.triggerAttackRelease("E4", "8n", now + 0.5) synth.triggerAttackRelease("G4", "8n", now + 1) }); } function keyUpHandler(event) { for (let i = 0; i < keyElements.length; i++) { keyElements[i].classList.remove('active'); } } function keyDownHandler(event) { if (event.key && !keyboardNotesMap[event.key]) { return; } if (event.midiKey && !midiNotesMap[event.midiKey]) { return; } const key = keyboardNotesMap[event.key] || midiNotesMap[event.midiKey]; synth.triggerAttackRelease(key, "8n"); // 修正键盘事件的高亮逻辑 if(event.key) { const targetElement = Array.from(keyElements).find(el => el.getAttribute('data') === key); if(targetElement) { targetElement.classList.add('active'); } } } function keyboardPress() { document.addEventListener('keydown', keyDownHandler, false); document.addEventListener('keyup', keyUpHandler, false); } function delayListner() { const delayElement = document.querySelector('.controls .delay'); delayElement.addEventListener('change', (event) => { delay.disconnect(); delay = new Tone.FeedbackDelay(event.target.value); synth.chain(delay, reverb); }); } // 更新下拉列表选项 function updateMidiSelect() { const select = document.querySelector('.midi select'); select.innerHTML = ''; // 清空现有选项 // 添加默认选项 const defaultOpt = document.createElement('option'); defaultOpt.value = ''; defaultOpt.innerHTML = '选择MIDI设备'; select.appendChild(defaultOpt); // 遍历所有设备添加选项 Object.values(midiInputs).forEach(input => { const opt = document.createElement('option'); opt.value = input.id; opt.innerHTML = input.name; // 如果是当前选中的设备,设置为选中状态 if(input.id === selectedMidiDeviceId) { opt.selected = true; } select.appendChild(opt); }); } // 绑定选中设备的消息监听,移除其他设备的监听 function bindSelectedMidiListener() { // 先移除所有设备的监听 Object.values(midiInputs).forEach(input => { input.onmidimessage = null; }); // 如果有选中的设备,绑定监听 if(selectedMidiDeviceId && midiInputs[selectedMidiDeviceId]) { midiInputs[selectedMidiDeviceId].onmidimessage = function(message) { const { command, note } = parseMidiMessage(message); if(command === 9) { keyDownHandler({ midiKey: note.toString() }) } if(command === 8) { keyUpHandler({ midiKey: note.toString() }) } } } } // 监听下拉选择框变化 function setupMidiSelectListener() { const select = document.querySelector('.midi select'); select.addEventListener('change', (e) => { selectedMidiDeviceId = e.target.value; bindSelectedMidiListener(); }); } function parseMidiMessage(message) { return { command: message.data[0] >> 4, channel: message.data[0] & 0xf, note: message.data[1], velocity: message.data[2] / 127 } } startUp(); getUpDOM(); keyAction(); keyboardPress(); delayListner(); setupMidiSelectListener(); // 初始化下拉选择监听 navigator.requestMIDIAccess() .then(function(access) { // 初始化存储现有设备 access.inputs.forEach(input => { midiInputs[input.id] = input; }); updateMidiSelect(); // 监听设备状态变化 access.onstatechange = function(e) { const port = e.port; if(port.type === 'input') { if(port.state === 'connected') { midiInputs[port.id] = port; } else if(port.state === 'disconnected') { delete midiInputs[port.id]; // 如果断开的是当前选中的设备,清空选中状态 if(port.id === selectedMidiDeviceId) { selectedMidiDeviceId = null; } } updateMidiSelect(); bindSelectedMidiListener(); } }; });
关键修改点说明
- 全局存储MIDI设备:用
midiInputs对象存储所有输入设备,通过设备id快速访问 - 动态切换事件绑定:
bindSelectedMidiListener函数负责移除所有设备的监听,仅给选中设备绑定消息处理 - 下拉选择监听:
setupMidiSelectListener监听下拉框变化,更新选中设备id并触发事件绑定切换 - 设备状态同步:在
onstatechange中处理设备连接/断开,同步更新下拉列表和事件绑定 - 修正键盘高亮逻辑:原代码中键盘事件的高亮只处理了键盘按键,现在改为根据找到的对应音符元素高亮
内容的提问来源于stack exchange,提问作者Pranshu Kumar Chaudhary
相关产品推荐
相关产品推荐

