MIDI曲を楽に弾ける「Easy Midi Player」

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Easy Midi Player 1.0</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
    <script src="https://unpkg.com/@tonejs/midi"></script>
    <style>
        body { margin: 0; background: #111; color: white; font-family: sans-serif; overflow: hidden; }
        
        /* カーソルを消さない(操作しやすくするため) */
        canvas { 
            display: block; 
            margin: 0 auto; 
            background: #222; 
            border-left: 1px solid #444; 
            border-right: 1px solid #444;
            cursor: default; 
        }

        #ui { 
            position: absolute; 
            top: 0; 
            left: 0; 
            width: 100%;
            background: rgba(0, 0, 0, 0.9); 
            padding: 8px; 
            box-sizing: border-box;
            border-bottom: 1px solid #555;
            display: flex;
            gap: 15px;
            align-items: center;
            justify-content: center;
            z-index: 100;
        }

        .control-group { display: flex; align-items: center; gap: 5px; }
        
        button, select { 
            background: #444; color: white; border: 1px solid #777; 
            padding: 5px 10px; cursor: pointer; font-size: 14px; border-radius: 4px;
        }
        button:hover, select:hover { background: #666; }
        button:disabled { color: #888; cursor: not-allowed; background: #333; }
        
        #status { font-size: 12px; color: #0ff; margin-left: 10px; min-width: 100px;}
        .instruction { font-size: 11px; color: #aaa; position: absolute; bottom: 5px; right: 10px; }
    </style>
</head>
<body>

<div id="ui">
    <div class="control-group">
        <label>音色:</label>
        <select id="soundMode">
            <option value="piano">🎹 Piano Mode</option>
            <option value="synth" selected>🎹 Synth Mode</option>
            <option value="beep">👾 Beep Mode</option>
        </select>
    </div>

    <div class="control-group">
        <input type="file" id="fileInput" accept=".mid,.midi" style="width: 180px;">
    </div>

    <button id="startBtn" disabled>再生スタート</button>
    <div id="status">待機中</div>
</div>

<div class="instruction">
    [マウス移動] エリア移動 | [左クリック] エリア内再生 | [右クリック] 単音試聴 | [ESC] 停止
</div>

<canvas id="gameCanvas"></canvas>

<script>
    // --- 設定 ---
    const CANVAS_WIDTH = 600;
    // 画面いっぱいに広げる
    const CANVAS_HEIGHT = window.innerHeight; 
    const NOTE_WIDTH = 40;
    const NOTE_HEIGHT = 20;
    const TARGET_HEIGHT = 50; // 判定ラインの太さ

    // --- 変数 ---
    let canvas = document.getElementById('gameCanvas');
    let ctx = canvas.getContext('2d');
    
    // ゲーム状態
    let notes = [];
    let isPlaying = false;
    let startTime = 0;
    
    // 判定ライン(マウス追従用)
    let targetY = CANVAS_HEIGHT - 100; // 初期位置

    // 音源
    let pianoSampler = null;
    let polySynth = null;
    let beepSynth = null;
    let isPianoLoaded = false;

    // 演出
    let areaFlash = 0.0;
    
    // UI要素
    const startBtn = document.getElementById('startBtn');
    const statusDiv = document.getElementById('status');
    const modeSelect = document.getElementById('soundMode');

    // --- 初期化 ---
    window.onload = () => {
        canvas.width = CANVAS_WIDTH;
        canvas.height = CANVAS_HEIGHT;

        // 1. シンセサイザー(標準 & ビープ)
        polySynth = new Tone.PolySynth(Tone.Synth, {
            volume: -8,
            oscillator: { type: "triangle" },
            envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 1 }
        }).toDestination();

        beepSynth = new Tone.PolySynth(Tone.Synth, {
            volume: -10,
            oscillator: { type: "square" },
            envelope: { attack: 0.01, decay: 0.1, sustain: 0.1, release: 0.1 }
        }).toDestination();

        // 2. ピアノサンプラー(非同期ロード)
        pianoSampler = new Tone.Sampler({
            urls: {
                "C4": "C4.mp3", "D#4": "Ds4.mp3", "F#4": "Fs4.mp3", "A4": "A4.mp3",
            },
            release: 1,
            baseUrl: "https://tonejs.github.io/audio/salamander/",
            onload: () => {
                isPianoLoaded = true;
                console.log("Piano loaded");
                if(modeSelect.value === 'piano') statusDiv.innerText = "ピアノ準備完了";
            }
        }).toDestination();

        requestAnimationFrame(loop);
    };

    // --- メインループ ---
    function loop() {
        // 背景
        ctx.fillStyle = "#111";
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        // 1. 判定エリアの描画(targetYを使用)
        if (areaFlash > 0) areaFlash -= 0.1;
        if (areaFlash < 0) areaFlash = 0;

        let r = 0, g = 255, b = 255;
        if (areaFlash > 0) r = 255; // 発光時は白っぽく

        // エリアの描画
        ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${0.2 + areaFlash * 0.3})`;
        ctx.fillRect(0, targetY, CANVAS_WIDTH, TARGET_HEIGHT);
        
        ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.8)`;
        ctx.lineWidth = 2;
        ctx.strokeRect(0, targetY, CANVAS_WIDTH, TARGET_HEIGHT);

        // 2. ノートの計算と描画
        const now = isPlaying ? (Tone.now() - startTime) : 0;
        
        notes.forEach(note => {
            // 【重要】ノートのY座標は、現在のtargetYを基準に計算される
            // targetYが変われば、すべてのノート位置もシフトする(迎えに行く挙動)
            if (isPlaying) {
                // (到達予定時間 - 現在時間) * 速度
                // 時間がピッタリなら targetY の位置に来る
                const distance = (note.time - now) * 200;
                note.y = targetY - distance; 
            } else {
                // 停止中は画面外または初期位置
                if(startTime === 0) note.y = -100;
            }

            // フラッシュ減衰
            if (note.flash > 0) {
                note.flash -= 0.1;
                if (note.flash < 0) note.flash = 0;
            }

            // 画面内描画
            if (note.y > -50 && note.y < CANVAS_HEIGHT) {
                ctx.fillStyle = note.flash > 0 ? "#FFF" : note.color;
                ctx.fillRect(note.x, note.y, note.w, note.h);
                ctx.strokeRect(note.x, note.y, note.w, note.h);
            }
        });

        requestAnimationFrame(loop);
    }

    // --- 入力処理 ---

    // マウス移動(判定ライン移動)
    canvas.addEventListener('mousemove', (e) => {
        const rect = canvas.getBoundingClientRect();
        const mouseY = e.clientY - rect.top;
        
        // エリアの中心をマウス位置にする
        targetY = mouseY - (TARGET_HEIGHT / 2);
        
        // 画面外にはみ出さないように制限
        if(targetY < 50) targetY = 50; // UIとかぶらないように
        if(targetY > CANVAS_HEIGHT - TARGET_HEIGHT) targetY = CANVAS_HEIGHT - TARGET_HEIGHT;
    });

    // クリック処理(左と右を区別)
    canvas.addEventListener('mousedown', async (e) => {
        // 右クリックのメニューを出さないようにする処理は contextmenu イベントで行うが、
        // ここでもボタン判定を行う
        await initAudioContext();

        const rect = canvas.getBoundingClientRect();
        const mouseX = e.clientX - rect.left;
        const mouseY = e.clientY - rect.top;

        // 左クリック (0) : エリア判定
        if (e.button === 0) {
            // 判定エリア内をクリックしたか?
            // マウス位置がエリア内にある、かつ
            if (mouseY >= targetY && mouseY <= targetY + TARGET_HEIGHT) {
                areaFlash = 1.0;
                
                // エリアに重なっているノートを全再生
                notes.forEach(note => {
                    const noteTop = note.y;
                    const noteBottom = note.y + note.h;
                    const areaTop = targetY;
                    const areaBottom = targetY + TARGET_HEIGHT;

                    // 矩形同士の重なり判定
                    if (noteBottom >= areaTop && noteTop <= areaBottom) {
                        playTone(note.name, "8n");
                        note.flash = 1.0;
                    }
                });
            }
        }
        // 右クリック (2) : 単音指定再生
        else if (e.button === 2) {
            // マウス位置にあるノートを個別に探す
            let hit = false;
            notes.forEach(note => {
                if (mouseX >= note.x && mouseX <= note.x + note.w &&
                    mouseY >= note.y && mouseY <= note.y + note.h) {
                    playTone(note.name, "8n");
                    note.flash = 1.0;
                    hit = true;
                }
            });
        }
    });

    // コンテキストメニュー(右クリックメニュー)の無効化
    canvas.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        return false;
    });

    // キーボード入力 (ESCで停止)
    window.addEventListener('keydown', (e) => {
        if (e.key === "Escape") {
            stopGame();
        }
    });

    // --- 音声再生ロジック ---
    function playTone(noteName, duration) {
        const mode = modeSelect.value;

        if (mode === 'piano') {
            if (isPianoLoaded) {
                pianoSampler.triggerAttackRelease(noteName, duration);
            } else {
                // ロードまだならシンセで代用
                polySynth.triggerAttackRelease(noteName, duration);
            }
        } else if (mode === 'beep') {
            beepSynth.triggerAttackRelease(noteName, duration);
        } else {
            // default: synth
            polySynth.triggerAttackRelease(noteName, duration);
        }
    }

    async function initAudioContext() {
        if (Tone.context.state !== 'running') {
            await Tone.start();
        }
    }

    // --- ゲーム進行制御 ---
    function stopGame() {
        isPlaying = false;
        startBtn.disabled = false;
        statusDiv.innerText = "停止中";
        
        // 音を全て止める
        // Tone.Transport.stop(); // 今回はTransportを使っていないので不要だが念のため
    }

    document.getElementById('startBtn').addEventListener('click', async () => {
        await initAudioContext();
        if(notes.length === 0) return;

        startTime = Tone.now() + 1.0;
        isPlaying = true;
        startBtn.disabled = true;
        statusDiv.innerText = "再生中... [ESC]で停止";
    });

    // ファイル読み込み
    document.getElementById('fileInput').addEventListener('change', async (e) => {
        const file = e.target.files[0];
        if (!file) return;
        
        statusDiv.innerText = "解析中...";
        const reader = new FileReader();
        reader.onload = function(ev) {
            try {
                const midiData = new Midi(ev.target.result);
                notes = [];
                const colors = ["#ff5252", "#448aff", "#69f0ae", "#ffd740", "#e040fb", "#536dfe"];
                
                midiData.tracks.forEach((track, i) => {
                    track.notes.forEach(n => {
                        notes.push({
                            name: n.name, time: n.time, 
                            x: 50 + (n.midi % 12) * 42, y: -100, w: 40, h: 20,
                            color: colors[i % colors.length], flash: 0
                        });
                    });
                });
                notes.sort((a,b) => a.time - b.time);
                
                startBtn.disabled = false;
                statusDiv.innerText = `準備完了 (${notes.length}音)`;
            } catch(err) {
                console.error(err);
                statusDiv.innerText = "エラー";
            }
        };
        reader.readAsArrayBuffer(file);
    });

    // モード切替時の通知
    modeSelect.addEventListener('change', () => {
        if (modeSelect.value === 'piano' && !isPianoLoaded) {
            statusDiv.innerText = "ピアノ音源ロード中...";
        } else {
            statusDiv.innerText = "音色変更完了";
        }
        // フォーカスを外す(ESCキーが効かなくなるのを防ぐため)
        modeSelect.blur();
    });

</script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
MIDI曲を楽に弾ける「Easy Midi Player」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1