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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Easy MIDI Player 1.1</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: 120px;}
        .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">🎹 Grand Piano</option>
            <option value="guitar">🎸 Acoustic Guitar</option>
            <option value="fm">✨ FM Electric</option>
            <option value="strings">🎻 Strings (Synth)</option>
            <option value="synth" selected>🎹 Basic Synth</option>
            <option value="beep">👾 8-bit Beep</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; 
    const FALL_SPEED = 200; // 落下速度 (px/sec)

    // --- グローバル変数 ---
    let canvas = document.getElementById('gameCanvas');
    let ctx = canvas.getContext('2d');
    
    // ゲームステート
    let notes = [];
    let isPlaying = false;
    let startTime = 0;
    let songDuration = 0;
    
    // UI関連
    let targetY = CANVAS_HEIGHT - 100;
    let areaFlash = 0.0;
    const startBtn = document.getElementById('startBtn');
    const statusDiv = document.getElementById('status');
    const modeSelect = document.getElementById('soundMode');

    // --- 音源 (Instruments) ---
    const instruments = {
        // サンプラー系(要ロード)
        piano: null,
        guitar: null,
        // シンセ系(即時使用可)
        synth: null,
        beep: null,
        fm: null,
        strings: null
    };
    
    let loadedSamples = { piano: false, guitar: false };

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

        // 1. シンセサイザーのセットアップ
        // 基本シンセ
        instruments.synth = new Tone.PolySynth(Tone.Synth, {
            volume: -8, oscillator: { type: "triangle" },
            envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 1 }
        }).toDestination();

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

        // FMエレピ風
        instruments.fm = new Tone.PolySynth(Tone.FMSynth, {
            volume: -8, harmonicity: 3, modulationIndex: 10,
            envelope: { attack: 0.01, decay: 0.2 },
            modulation: { type: "square" },
            modulationEnvelope: { attack: 0.5, decay: 0.01 }
        }).toDestination();

        // ストリングス風(アタックを遅く)
        instruments.strings = new Tone.PolySynth(Tone.Synth, {
            volume: -8, oscillator: { type: "sawtooth" },
            envelope: { attack: 0.2, decay: 3, sustain: 0.5, release: 2 }
        }).toDestination();

        // 2. サンプラーのセットアップ(非同期ロード)
        // ピアノ
        instruments.piano = 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: () => { loadedSamples.piano = true; updateStatus(); }
        }).toDestination();

        // アコースティックギター
        instruments.guitar = new Tone.Sampler({
            urls: { "C4": "C4.mp3", "E4": "E4.mp3", "G4": "G4.mp3" },
            release: 1, baseUrl: "https://tonejs.github.io/audio/guitar-acoustic/",
            onload: () => { loadedSamples.guitar = true; updateStatus(); }
        }).toDestination();

        requestAnimationFrame(gameLoop);
    };

    function updateStatus() {
        // 現在選択中の楽器がロード済みかチェック
        const mode = modeSelect.value;
        if ((mode === 'piano' && !loadedSamples.piano) || 
            (mode === 'guitar' && !loadedSamples.guitar)) {
            statusDiv.innerText = "音源ロード中...";
            statusDiv.style.color = "#FF0";
        } else {
            if(notes.length > 0) {
                statusDiv.innerText = isPlaying ? "再生中" : "準備完了";
                statusDiv.style.color = "#0FF";
            } else {
                statusDiv.innerText = "ファイル待機";
            }
        }
    }

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

        // 1. 判定エリア描画
        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;
        
        // 曲が終わったら自動停止
        if (isPlaying && now > songDuration + 2.0) {
            stopGame();
        }

        notes.forEach(note => {
            // ゲーム中は時間経過で座標変化、停止中は画面外へ
            if (isPlaying) {
                // 現在時刻(now)とノート発生時間(note.time)の差分だけ移動
                // targetYに到達するのは note.time == now の瞬間
                const timeDiff = note.time - now;
                note.y = targetY - (timeDiff * FALL_SPEED);
            } else {
                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.strokeStyle = "rgba(0,0,0,0.5)";
                ctx.lineWidth = 1;
                ctx.strokeRect(note.x, note.y, note.w, note.h);
            }
        });

        requestAnimationFrame(gameLoop);
    }

    // --- 入力制御 ---

    // マウス移動でエリア移動
    canvas.addEventListener('mousemove', (e) => {
        const rect = canvas.getBoundingClientRect();
        targetY = (e.clientY - rect.top) - (TARGET_HEIGHT / 2);
        // 上下の制限
        if(targetY < 60) targetY = 60;
        if(targetY > CANVAS_HEIGHT - TARGET_HEIGHT - 10) targetY = CANVAS_HEIGHT - TARGET_HEIGHT - 10;
    });

    // クリック処理
    canvas.addEventListener('mousedown', async (e) => {
        await initAudioContext();
        
        const rect = canvas.getBoundingClientRect();
        const my = e.clientY - rect.top;
        const mx = e.clientX - rect.left;

        // 左クリック: エリア判定
        if (e.button === 0) {
            // エリア内クリックか?
            if (my >= targetY && my <= targetY + TARGET_HEIGHT) {
                areaFlash = 1.0;
                
                // エリアに重なっているノートを探す
                notes.forEach(note => {
                    const nTop = note.y;
                    const nBot = note.y + note.h;
                    const aTop = targetY;
                    const aBot = targetY + TARGET_HEIGHT;

                    // 重なり判定 (AABB Collision)
                    if (nBot >= aTop && nTop <= aBot) {
                        playTone(note.name, "8n");
                        note.flash = 1.0;
                    }
                });
            }
        }
        // 右クリック: 単音試聴
        else if (e.button === 2) {
            let hit = false;
            notes.forEach(note => {
                if (mx >= note.x && mx <= note.x + note.w &&
                    my >= note.y && my <= note.y + note.h) {
                    playTone(note.name, "8n");
                    note.flash = 1.0;
                    hit = true;
                }
            });
        }
    });

    // 右クリックメニュー無効化
    canvas.addEventListener('contextmenu', e => e.preventDefault());

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

    // --- ゲーム進行 ---

    // スタート(リプレイ)処理
    startBtn.addEventListener('click', async () => {
        await initAudioContext();
        if(notes.length === 0) return;

        // リプレイのためのリセット処理
        startTime = Tone.now() + 1.0; // 1秒後スタート
        isPlaying = true;
        
        // ボタンの表示変更
        startBtn.innerText = "最初から再生"; 
        // 誤操作防止のために一時的に無効化してもよいが、リトライしやすいように有効のままにする
        
        statusDiv.innerText = "再生中...";
    });

    function stopGame() {
        isPlaying = false;
        startBtn.innerText = "最初から再生"; // 文言を戻す
        statusDiv.innerText = "停止中";
        // ノートはgameLoop内で自動的に画面外へ送られる
    }

    // --- 音声処理 ---
    
    async function initAudioContext() {
        if (Tone.context.state !== 'running') await Tone.start();
    }

    function playTone(note, duration) {
        const mode = modeSelect.value;
        const inst = instruments[mode];

        if (inst) {
            // サンプラーの場合、ロード完了してないと鳴らない
            if ((mode === 'piano' && !loadedSamples.piano) || 
                (mode === 'guitar' && !loadedSamples.guitar)) {
                // 代わりにシンセを鳴らす
                instruments.synth.triggerAttackRelease(note, duration);
            } else {
                inst.triggerAttackRelease(note, duration);
            }
        }
    }

    // MIDIファイル読み込み
    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 = [];
                songDuration = midiData.duration; // 曲の長さを取得

                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;
                startBtn.innerText = "再生スタート";
                updateStatus();

            } catch(err) {
                console.error(err);
                statusDiv.innerText = "解析エラー";
            }
        };
        reader.readAsArrayBuffer(file);
    });

    modeSelect.addEventListener('change', () => {
        updateStatus();
        modeSelect.blur(); // フォーカスを外し、ESCキーなどを効きやすくする
    });

</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