You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Web MIDI多设备场景下,如何仅让选中的MIDI输入设备生效?

解决Web MIDI仅响应选中设备的问题

核心问题

你的代码会给所有检测到的MIDI输入设备绑定消息监听事件,导致不管下拉框选了哪个,所有设备的指令都会触发声音。要实现仅响应选中设备,需要动态管理MIDI设备的事件绑定。

修改方案

  1. 全局存储所有MIDI输入设备,方便后续切换
  2. 监听下拉选择框的变化,移除未选中设备的消息监听,给选中设备绑定监听
  3. 处理设备连接/断开的状态变化,同步更新下拉列表和事件绑定

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 15:25:17