Web midi Api 를 사용해서 Launchpad Mk2 제어하기

    이번 글에서는 Web midi Api 를 사용해서 Launchpad Mk2 제어하는 방법에 대해 알아봅니다.

    웹 MIDI API는 웹 브라우저에서 MIDI 기기와 상호작용할 수 있는 기능을 제공합니다. MIDI(Musical Instrument Digital Interface)는 음악을 디지털 형태로 전송하고 제어하기 위한 프로토콜로, 다양한 MIDI 호환 기기를 통해 음악 정보를 주고 받을 수 있습니다. Launchpad MK 2는 MIDI 컨트롤러 중 하나로, 버튼을 통해 MIDI 입출력을 처리할 수 있는 장치입니다.


    브라우저에서 MIDI 장치 불러오기

    먼저 HTML에서 사용할 입출력 MIDI 장치를 선택하는 코드를 작성해보자.

    <p>Select Launchpad from the MIDI device list.</p>
    <span class="clickable">
        <b>Inputs:</b>
        <select id="selectIn" class="focus">
            <option value="">Please select</option></select
        ><br />
    </span>
    <span class="clickable">
        <b>Outputs:</b>
        <select id="selectOut" class="focus">
            <option value="">Please select</option>
        </select>
    </span>

    이 코드는 너무 간단해서 딱히 설명할 게 없다.

    이제 Javascript에서 MIDI 장치를 가져오고, 입출력 처리를 할 차례다.

    let midiIn = [];
    let midiOut = [];
    let notesOn = new Map();
    let listeningMidiDevice = null;
    function initMidiConnection() {
        midiIn = [];
        midiOut = [];
        notesOn = new Map();
        listeningMidiDevice = null;
        navigator.requestMIDIAccess().then(
            (midi) => midiReady(midi),
            (err) => console.log("Something went wrong", err)
        );
    }
    
    function midiReady(midi) {
        initDevices(midi);
        //midiDeviceChangeEventListner();
    }
    function initDevices(midi) {
        midiIn = [];
        midiOut = [];
    
        const inputs = midi.inputs.values();
        for (let input = inputs.next(); input && !input.done; input = inputs.next()) {
            midiIn.push(input.value);
        }
    
        const outputs = midi.outputs.values();
        for (let output = outputs.next(); output && !output.done; output = outputs.next()) {
            midiOut.push(output.value);
        }
    
        displayDevices();
    }
    function displayDevices() {
        selectIn.innerHTML = "<option>Please Select</option>" + midiIn.map((device) => `<option>${device.name}</option>`).join("");
        selectOut.innerHTML = "<option>Please Select</option>" + midiOut.map((device) => `<option>${device.name}</option>`).join("");
        if (listeningMidiDevice) {
            selectedDeviceList.innerHTML = `<p id="selectedDeviceName">${listeningMidiDevice.name}</p>`;
        } else {
            selectedDeviceList.innerHTML = `<p id="selectedDeviceName">No device selected</p>`;
        }
    }

    이 코드는 MIDI 입출력 장치를 찾아 우리가 만든 HTML에 있는 select 태그 안에 option으로 표시해줍니다.

    1. 초기화
      midiIn, midiOut : MIDI 입출력 장치들을 저장하는 배열입니다.
      listeningMidiDevice: 현재 입력을 받고 있는 MIDI장치를 저장하는 변수입니다.
    2. initMidiConnection()
      MIDI연결을 초기화하는 함수입니다.
      navigator.requestMIDIAccess() 합수를 호출해 MIDI 액세스를 요청합니다.
      액세스 요청이 성공하면 midiReady()함수를 호출하고, 실패하면 오류 메시지를 콘솔에 출력합니다.
    3. midiReady(midi)
      MIDI 액세스 요청 후. 받은 응답을 처리하기 위한 함수입니다.
      midi 파라미터를 그대로 initDevices()로 전달합니다.
    4. initDevices(midi)
      midiInmidiOut배열을 비우고, MIDI액세스를 통해 얻은 장치 이름 값을 배열에 담습니다. 그 후 displayDevices()함수를 호출합니다.
    5. displayDevices()
      midiInmidiOut배열에 있는 MIDI 장치 목록들을 selectInselectOut Element에 option으로 추가합니다.

    브라우저에서 MIDI입력받기

    function midiDeviceChangeEventListner() {
        selectIn.addEventListener("change", () => {
            if (selectIn.selectedIndex === 0) {
                return;
            }
            if (listeningMidiDevice !== null && listeningMidiDevice.id === midiIn[selectIn.selectedIndex - 1].id) {
                return;
            }
            listeningMidiDevice = midiIn[selectIn.selectedIndex - 1];
            displayDevices();
            startListening(listeningMidiDevice);
        });
    }

    먼저 미디 장치를 선택했을때, 이벤트를 받을 리스너 함수를 만듭니다. 이 함수는 selectIn에 EventListner를 추가해 변화를 감지합니다.

    function midiReady(midi) {
        initDevices(midi);
    ++  midiDeviceChangeEventListner();
    }

    위에서 작성해 놓았던 midiReady(midi) 함수에서 midiDeviceChangeEventListner() 함수를 호출합니다.

    function startListening(midiInParam) {
        console.log(`Listening to MIDI IN from ${midiInParam.name}`);
        midiInParam.addEventListener("midimessage", midiMessageReceived);
    }
    
    function midiMessageReceived(event) {
        const NOTE_ON = 9;
        const NOTE_OFF = 8;
    
        const cmd = event.data[0] >> 4;
        const pitch = event.data[1];
        const velocity = event.data.length > 2 ? event.data[2] : 1;
    
        const timestamp = Date.now();
    
        if (cmd === NOTE_OFF || (cmd === NOTE_ON && velocity === 0)) {
            console.log(`Launchpad pull from ${event.srcElement.name} note off: pitch:${pitch}, velocity: ${velocity}`);
    
            const note = notesOn.get(pitch);
            if (note) {
                console.log(`pitch:${pitch}, duration:${timestamp - note} ms.`);
                notesOn.delete(pitch);
            }
        } else if (cmd === NOTE_ON) {
            console.log(`Launchpad push from ${event.srcElement.name} note off: pitch:${pitch}, velocity: ${velocity}`);
    
            notesOn.set(pitch, timestamp);
        }
    }
    1. startListening(midiInParam)
      콘솔에 메시지를 표시한 후, midiInParam에 이벤트 리스너를 추가해 midimessage를 수신하고, midiMessageReceived(event) 함수를 호출합니다.
    2. midiMessageReceived(event)
      미디 메시지를 분석해 pitch, duration, velocity 를 추출합니다.

    코드를 모두 작성한 이후, 런치패드를 연결해 버튼을 눌러 보면 콘솔에 반응이 오는 것을 확인할 수 있다.
    Pitch값을 통해 Launchpad에서 어느 버튼을 눌렀는지 알아낼 수 있다.

    Pitch값과 Launchpad의 버튼 위치 관계

    Launchpad MK2 기준으로, 가장 왼쪽 하단 버튼부터 11번으로 시작하고, 오른쪽으로 한칸 갈 때마다 1씩 Pitch가 증가한다. 위쪽으로 한칸 올라가면 Pitch가 10씩 증가한다.
    아래 이미지를 참고하면 이해가 빠르다.

    Launchpad MK2의 버튼마다 Pitch 숫자를 매겨놓은 사진

    실제로 Launchpad MK2의 경우 오른쪽과 위쪽으로 한줄씩 버튼이 더 있지만 사진에선 표시되어 있지 않다. 하지만 버튼마다 어느 Pitch가 매겨지는지 원리를 이해하기에는 충분할 것이다.

    브라우저에서 MIDI출력하기

    이번에는 Launchpad Mk2 로 MIDI 출력을 보내서 RGB LED를 제어하는 방법을 알아보겠습니다.

    function sendMidiMessage(pitch, velocity, duration) {
        const NOTE_ON = 0x90;
        const NOTE_OFF = 0x80;
    
        const device = midiOut[selectOut.selectedIndex - 1];
        const msgOn = [NOTE_ON, pitch, velocity];
        const msgOff = [NOTE_ON, pitch, velocity];
    
    
        device.send(msgOn);
    
    
        device.send(msgOff, Date.now() + duration);
    
    
    }

    여기서도 Pitch 값을 갖고 버튼의 위치를 지정하는 것은 같지만, velocity를 사용해 색을 지정해야 합니다. duration은 사용하지 않습니다.

    함수 사용 예) sendMidiMessage(11, 3, 1000); -> 11번 버튼에 3번 색(하얀색) 표시

    velocity에 따른 Launchpad MK2 색 팔레트

    런치패드 컬러 팔레트

    Launchpad 에 표시하는 내용을 웹사이트에도 똑같이 표시하기

    <div id="launchpadContainer">
        <div class="launchpadRow">
            <div class="launchpadButton" id="11">11</div>
            <div class="launchpadButton" id="12">12</div>
            <div class="launchpadButton" id="13">13</div>
            <div class="launchpadButton" id="14">14</div>
            <div class="launchpadButton" id="15">15</div>
            <div class="launchpadButton" id="16">16</div>
            <div class="launchpadButton" id="17">17</div>
            <div class="launchpadButton" id="18">18</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="21">21</div>
            <div class="launchpadButton" id="22">22</div>
            <div class="launchpadButton" id="23">23</div>
            <div class="launchpadButton" id="24">24</div>
            <div class="launchpadButton" id="25">25</div>
            <div class="launchpadButton" id="26">26</div>
            <div class="launchpadButton" id="27">27</div>
            <div class="launchpadButton" id="28">28</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="31">31</div>
            <div class="launchpadButton" id="32">32</div>
            <div class="launchpadButton" id="33">33</div>
            <div class="launchpadButton" id="34">34</div>
            <div class="launchpadButton" id="35">35</div>
            <div class="launchpadButton" id="36">36</div>
            <div class="launchpadButton" id="37">37</div>
            <div class="launchpadButton" id="38">38</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="41">41</div>
            <div class="launchpadButton" id="42">42</div>
            <div class="launchpadButton" id="43">43</div>
            <div class="launchpadButton" id="44">44</div>
            <div class="launchpadButton" id="45">45</div>
            <div class="launchpadButton" id="46">46</div>
            <div class="launchpadButton" id="47">47</div>
            <div class="launchpadButton" id="48">48</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="51">51</div>
            <div class="launchpadButton" id="52">52</div>
            <div class="launchpadButton" id="53">53</div>
            <div class="launchpadButton" id="54">54</div>
            <div class="launchpadButton" id="55">55</div>
            <div class="launchpadButton" id="56">56</div>
            <div class="launchpadButton" id="57">57</div>
            <div class="launchpadButton" id="58">58</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="61">61</div>
            <div class="launchpadButton" id="62">62</div>
            <div class="launchpadButton" id="63">63</div>
            <div class="launchpadButton" id="64">64</div>
            <div class="launchpadButton" id="65">65</div>
            <div class="launchpadButton" id="66">66</div>
            <div class="launchpadButton" id="67">67</div>
            <div class="launchpadButton" id="68">68</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="71">71</div>
            <div class="launchpadButton" id="72">72</div>
            <div class="launchpadButton" id="73">73</div>
            <div class="launchpadButton" id="74">74</div>
            <div class="launchpadButton" id="75">75</div>
            <div class="launchpadButton" id="76">76</div>
            <div class="launchpadButton" id="77">77</div>
            <div class="launchpadButton" id="78">78</div>
        </div>
        <div class="launchpadRow">
            <div class="launchpadButton" id="81">81</div>
            <div class="launchpadButton" id="82">82</div>
            <div class="launchpadButton" id="83">83</div>
            <div class="launchpadButton" id="84">84</div>
            <div class="launchpadButton" id="85">85</div>
            <div class="launchpadButton" id="86">86</div>
            <div class="launchpadButton" id="87">87</div>
            <div class="launchpadButton" id="88">88</div>
        </div>
    </div>

    먼저 Launchpad 의 버튼 역활을 할 html을 이렇게 작성했다. 그 이후 Launchpad 배열에 맞게 표시되도록 CSS를 작성했다.

    #launchpadContainer{
        display: flex;
        flex-direction: column-reverse;
        max-width: 800px;
        max-height: 800px;
        width: 80vw;
        height: 80vw;
    }
    .launchpadRow{
        display: flex;
        flex-direction: row;
        width: 100%;
        height: 100%;
        margin: 0 5px 5px 0;
    }
    .launchpadButton{
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
        color: RGBA(255, 255, 255, 0.8);
        background-color: black;
        border-radius: 10px;
        margin: 5px 0 0 5px;
        cursor: pointer;
    }

    상단 코드를 작성하여 실행하면 이와 같은 화면이 표시될 것이다.

    Launchpad MK2의 버튼마다 Pitch 숫자를 매겨놓은 사진

    이제 Launchpad 에 Midi Message를 보낼 때 html에도 똑같이 background-color 스타일을 변경해줘야 한다.

    function displayOnScreenLaunchpad(pitch, velocity) {
        let colorName = getColorNameByCode(velocity);
        document.getElementById(pitch).style.background = colorName;
    }

    여기서 Launchpad의 색 코드 만으로는 html에 색을 표현할 수 없는 문제가 생겨 getColorNameByCode(velocity) 함수를 만들었다.

    function getColorNameByCode(colorCode) {
        switch (colorCode) {
            case 0:
                return "black";
            case 1:
                return "gray";
            case 3:
                return "white";
            case 5:
                return "red";
            case 13:
                return "yellow";
            case 67:
                return "blue";
            default:
                return "transparent";
        }
    }
    

    필자는 이정도의 색만 사용할 예정이라 많이 작성하진 않았다. 필요하다면 컬러 팔레트의 모든 색을 스포이드로 찍어가며 노가다를 하면 되겠다.

    function sendMidiMessage(pitch, velocity, duration) {
    ...
    ++   displayOnScreenLaunchpad(pitch, velocity);
    }

    sendMidiMessage 함수에 html에 색을 표시하게끔 하는 코드를 추가했다.

    웹사이트의 버튼을 눌러 Launchpad 버튼과 똑같이 작동시키기

    function initMidiConnection() {
    ...
        document.querySelectorAll(".launchpadButton").forEach((item) => {
            item.addEventListener("click", () => {
                buttonPushEvent(Number(item.id), 1);
            });
        });
    }

    initMidiConnection() 함수에서 .launchpadButton 엘리먼트를 모두 찾아 Eventlistner를 추가하는 코드를 작성했다. Number(item.id)가 Pitch 값과 같은 값이다. buttonPushEvent함수를 만들어 각자 필요한 곳에 알맞게 사용하도록 하자.

    마치며,

    웹 MIDI API를 통해 Launchpad MK 2와 같은 MIDI 장치를 브라우저에서 제어하는 방법에 대해 알아보았습니다. 웹 MIDI API를 사용하여 MIDI 장치를 브라우저에서 제어하는 방법에 대해 알아보았지만, 이는 출발점에 불과합니다. MIDI 컨트롤러의 다양한 활용 방법에는 한계가 없습니다. 굳이 음악 분야에서만 MIDI 컨트롤러를 사용할 필요도 없습니다. 앞으로 더 많은 개발자들이 웹 MIDI API를 활용하여 창의적이고 혁신적인 프로젝트를 만들어 나갈 것을 기대합니다.


    게시됨

    카테고리

    작성자

    태그:

    Obtuse의 테크 블로그 더 알아보기

    이 블로그에 새 글이 나올 때 마다 이메일로 알림을 받아보는 건 어때요?


    ※구독 버튼을 클릭하면 obtuse.kr의 개인정보 처리방침의 광고성 정보 수신에 동의하는 것으로 간주합니다.

    댓글

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

    이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.