令和時代の Web MIDI 再生

以前のブラウザには、MIDI を再生させるための機能がサポートされていましたが、HTML5 が出たあたりから、そのしようがなくなってしまったそうです。 現代のブラウザで、どうやってがんばるかいろいろ大変で、私自身も試行錯誤しました。

このホームページは、Wordpress を利用しており、Thema として、Cocoon を使っています。 いろいろ試行錯誤して、うまくいったのを忘備録として残しておきます。

外観 → テーマファイルエディター → functions.php に追加したのは、下記のコードです。 当初はローカルでの仕組みがなければ、Microsoft GS Wavetable Synth が勝手になってくれると思ったのですがそうはいきませんでした。 そこで、midijs.net の助けを借りることにしました。 このコードでは Website が表にないと再生がおかしくなりますが、この点は目をつぶることにしました。 令和時代の MIDI 再生 で示したようにすれば、下記のコードで、仮想 MIDI ポートを通して、サウンドフォントを利用した再生が可能です。

// Web MIDI API 演奏システム(MIDIjs ハイブリッド版・表示テキスト最適化版)
function add_local_midi_player_script() {
    echo '<script src="https://midijs.net"></script>';
    ?>
    <script>
    document.addEventListener('DOMContentLoaded', () => {
        let midiOutputPort = null;
        let currentMidiBuffer = null;
        let activeTimeouts = [];
        let isWebAudioMode = true;
        let midiUrl = "";

        const playBtn = document.getElementById('localMidiPlayBtn');
        const stopBtn = document.getElementById('localMidiStopBtn');
        const statusText = document.getElementById('midi-status-text');
        
        if (!playBtn || !stopBtn) return;

        if (navigator.requestMIDIAccess) {
            navigator.requestMIDIAccess().then((midiAccess) => {
                const outputs = Array.from(midiAccess.outputs.values());
                
                midiOutputPort = outputs.find(port => 
                    port.name.toLowerCase().includes("loopmidi") || 
                    port.name.toLowerCase().includes("iac")
                );

                if (midiOutputPort) {
                    // ご希望の「🎹 [ポート名] から再生します」に変更
                    statusText.innerText = `🎹 ${midiOutputPort.name} から再生`;
                    isWebAudioMode = false;
                } else {
                    statusText.innerText = "🔊 内部ブラウザ音源(MIDIjs)で再生";
                    isWebAudioMode = true;
                }
                loadMidiFile();
            }).catch(err => {
                statusText.innerText = "🔊 内部ブラウザ音源(MIDIjs)で再生";
                isWebAudioMode = true;
                loadMidiFile();
            });
        } else {
            statusText.innerText = "🔊 内部ブラウザ音源(MIDIjs)で再生";
            isWebAudioMode = true;
            loadMidiFile();
        }

        function loadMidiFile() {
            midiUrl = playBtn.getAttribute('data-midi-url');
            if (!midiUrl) return;

            fetch(midiUrl)
                .then(response => {
                    if (!response.ok) throw new Error('Network error');
                    return response.arrayBuffer();
                })
                .then(arrayBuffer => {
                    currentMidiBuffer = arrayBuffer;
                    playBtn.disabled = false;
                    console.log("MIDIバイナリデータの取得に成功しました。");
                })
                .catch(err => {
                    statusText.innerText = "❌ MIDIファイルの読み込みに失敗しました";
                    console.error(err);
                });
        }

        function resetButtons() {
            playBtn.innerText = "再生する";
            playBtn.disabled = false;
            stopBtn.disabled = true;
        }

        function allNotesOff() {
            activeTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
            activeTimeouts = [];
            
            if (midiOutputPort) {
                for (let ch = 0; ch < 16; ch++) {
                    try {
                        midiOutputPort.send([0xB0 + ch, 0x7B, 0]);
                        midiOutputPort.send([0xB0 + ch, 0x78, 0]);
                        midiOutputPort.send([0xB0 + ch, 0x40, 0]);
                    } catch(e) {}
                }
            }
            
            if (typeof MIDIjs !== 'undefined') {
                try {
                    MIDIjs.stop();
                } catch(e) {}
            }
        }

        function playMidiStream(buffer) {
            const data = new DataView(buffer);
            let p = 0;

            if (data.getUint32(p) !== 0x4D546864) {
                console.error("無効なMIDIファイル形式です");
                return;
            }
            p += 8;
            const format = data.getUint16(p); p += 2;
            const numTracks = data.getUint16(p); p += 2;
            const ticksPerBeat = data.getUint16(p); p += 2;

            let tempo = 500000; 
            let allEvents = [];

            for (let t = 0; t < numTracks; t++) {
                if (p >= data.byteLength) break;
                if (data.getUint32(p) !== 0x4D54726B) {
                    p += 4; let len = data.getUint32(p); p += 4 + len; continue;
                }
                p += 4; let trackLen = data.getUint32(p); p += 4;
                let trackEnd = p + trackLen;

                let currentTicks = 0;
                let runningStatus = 0;

                while (p < trackEnd && p < data.byteLength) {
                    let delta = 0;
                    while (true) {
                        let b = data.getUint8(p++);
                        delta = (delta << 7) | (b & 0x7F);
                        if (!(b & 0x80)) break;
                    }
                    currentTicks += delta;

                    let status = data.getUint8(p);
                    if (status & 0x80) {
                        runningStatus = status;
                        p++;
                    } else {
                        status = runningStatus;
                    }

                    let msgType = status & 0xF0;
                    let channel = status & 0x0F;

                    if (status === 0xFF) {
                        let metaType = data.getUint8(p++);
                        let len = 0;
                        while (true) {
                            let b = data.getUint8(p++);
                            len = (len << 7) | (b & 0x7F);
                            if (!(b & 0x80)) break;
                        }
                        if (metaType === 0x51) {
                            let tVal = (data.getUint8(p) << 16) | (data.getUint8(p+1) << 8) | data.getUint8(p+2);
                            allEvents.push({ ticks: currentTicks, type: 'tempo', val: tVal });
                        }
                        p += len;
                    } else if (status === 0xF0 || status === 0xF7) {
                        let len = 0;
                        while (true) {
                            let b = data.getUint8(p++);
                            len = (len << 7) | (b & 0x7F);
                            if (!(b & 0x80)) break;
                        }
                        let sysexData = [status];
                        for(let i=0; i<len; i++) sysexData.push(data.getUint8(p+i));
                        allEvents.push({ ticks: currentTicks, type: 'midi', bytes: sysexData });
                        p += len;
                    } else {
                        let bytes = [status];
                        if (msgType === 0xC0 || msgType === 0xD0) {
                            bytes.push(data.getUint8(p++));
                        } else {
                            bytes.push(data.getUint8(p++));
                            bytes.push(data.getUint8(p++));
                        }
                        allEvents.push({ ticks: currentTicks, type: 'midi', bytes: bytes });
                    }
                }
            }

            allEvents.sort((a, b) => a.ticks - b.ticks);

            let lastTicks = 0;
            let currentTimeMs = 0;

            allEvents.forEach(ev => {
                let deltaTicks = ev.ticks - lastTicks;
                let deltaTimeMs = (deltaTicks * tempo) / (ticksPerBeat * 1000);
                currentTimeMs += deltaTimeMs;
                lastTicks = ev.ticks;

                if (ev.type === 'tempo') {
                    tempo = ev.val;
                } else if (ev.type === 'midi') {
                    const tId = setTimeout(() => {
                        if (midiOutputPort) {
                            midiOutputPort.send(ev.bytes);
                        }
                    }, currentTimeMs);
                    activeTimeouts.push(tId);
                }
            });

            const endId = setTimeout(() => {
                resetButtons();
            }, currentTimeMs + 500);
            activeTimeouts.push(endId);
        }

        playBtn.addEventListener('click', () => {
            allNotesOff();

            playBtn.innerText = "⏳ 再生中";
            playBtn.disabled = true;
            stopBtn.disabled = false;

            if (isWebAudioMode) {
                if (typeof MIDIjs !== 'undefined' && midiUrl) {
                    try {
                        MIDIjs.play(midiUrl);
                        
                        MIDIjs.get_duration(midiUrl, (duration) => {
                            if (duration > 0) {
                                const endId = setTimeout(() => {
                                    resetButtons();
                                }, (duration * 1000) + 500);
                                activeTimeouts.push(endId);
                            }
                        });
                    } catch (e) {
                        console.error("MIDIjs 再生エラー:", e);
                        resetButtons();
                    }
                } else {
                    resetButtons();
                }
            } else {
                if (currentMidiBuffer) {
                    playMidiStream(currentMidiBuffer);
                } else {
                    resetButtons();
                }
            }
        });

        stopBtn.addEventListener('click', () => {
            allNotesOff();
            resetButtons();
        });
    });
    </script>
    <?php
}
add_action('wp_footer', 'add_local_midi_player_script');

再生のコードは次の通りです。 外観 → カスタマイズ →  ウィジェット → サイドバーとして、カスタムHTML を選んでいます。  なぜか、外観 → ウィジェットとして入力すると、カスタムHTML では保存ができません。 また、テキストにすれば保存できますが、いつのまにか、.mid ファイルのあたりが消えてしまいます。

<div class="midi-player" style="margin: 20px 0; padding: 15px; border: 1px solid #e29fa7; border-radius: 5px; background: #fffafb;">
    <p style="margin: 0 0 0px 0; color: #a1646c; font-weight: bold;">Widmung (Schumann-Liszt)</p>
    <!-- ポート名を表示するエリア(自動で切り替わります) -->
    <p id="midi-status-text" style="margin: 0 0 10px 0; color: #1c1516; font-size: 14px; padding-left: 30px;">サウンドフォント使用推奨(読み込み中...)</p>

    <div style="display: flex; gap: 10px;">
        <!-- 再生ボタン: ' ' の中に実際のMIDIファイルのURLを書き換えてください -->
        <button id="localMidiPlayBtn" 
                data-midi-url="https://schumann.jp/wp-content..............." 
                style="padding: 10px 20px; background: #e29fa7; color: #fff; border: none; border-radius: 3px; cursor: pointer;" 
                disabled>再生する</button>
        <!-- 停止ボタン -->
        <button id="localMidiStopBtn" 
                style="padding: 10px 20px; background: #8e8586; color: #fff; border: none; border-radius: 3px; cursor: pointer;" 
                disabled>停止する</button>
    </div>
</div>

コメント