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

【更新履歴】

 2026/1/30 ・バージョン1.0公開。
 2026/1/30 ・バージョン1.1公開。(再生エリアの移動を追加)
 2026/1/30 ・バージョン1.2公開。(音伸ばしと再生速度調整を追加)
 2026/2/1
   ・バージョン1.3公開。(ビブラート、強弱調節などを追加)


画像
ゲーム画面

《ゲームの概要》

・いわゆる「音ゲー」のワンクリック版みたいなものです。

《操作説明》

・まず、「ファイルを選択」ボタンを押して、
 演奏するMIDIファイルを選びます。

・「最初から再生」ボタンを押すと、演奏開始です。

・マウスを移動させると、再生エリアが上下に動きますから、
 上から落ちてくるノート(発音ブロック)を合わせて、
 左クリックをすると、
 再生エリアと重なっているノートの音だけが鳴ります。

・ノートにマウスポインタを合わせて右クリックすると、
 そのノートの音だけが鳴ります。

・マウスボタンは、押しっぱなしにすると、
 その間だけスクロールが止まって、
 音が鳴りっぱなしになります。


・ダウンロードされる方はこちら。↓

・著作権フリーのMIDIファイルは
 こちらのwebサイトからダウンロードできます。↓


・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Easy MIDI Player 1.3</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; user-select: none; }
        
        canvas { 
            display: block; 
            margin: 0 auto; 
            background: #222; 
            border-left: 1px solid #444; 
            border-right: 1px solid #444;
            cursor: crosshair;
        }
        canvas:active { cursor: grabbing; }

        #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; flex-wrap: wrap;
        }

        .control-group { display: flex; align-items: center; gap: 5px; background: #333; padding: 2px 8px; border-radius: 4px; }
        label { font-size: 12px; color: #ccc; }
        
        button, select, input[type=number] { 
            background: #444; color: white; border: 1px solid #777; 
            padding: 4px 8px; cursor: pointer; font-size: 14px; border-radius: 4px;
        }
        button:hover, select:hover { background: #666; }
        input[type=number] { width: 50px; text-align: center; }
        input[type=range] { width: 100px; cursor: pointer; }

        #status { font-size: 12px; color: #0ff; margin-left: 5px; min-width: 100px;}
        .instruction { 
            font-size: 12px; color: #aaa; position: absolute; bottom: 10px; right: 20px; text-align: right; 
            line-height: 1.6; background: rgba(0,0,0,0.7); padding: 10px; border-radius: 8px; pointer-events: none;
        }
        .highlight { color: #ffeb3b; font-weight: bold; }
    </style>
</head>
<body>

<div id="ui">
    <div class="control-group">
        <label>音色</label>
        <select id="soundMode">
            <option value="piano">🎹 Grand Piano</option>
            <option value="guitar">🎸 Guitar</option>
            <option value="fm">✨ FM Electric</option>
            <option value="strings">🎻 Strings</option>
            <option value="synth" selected>🎹 Synth</option>
        </select>
    </div>

    <div class="control-group">
        <label>速度 <span id="speedVal">1.0</span>x</label>
        <input type="range" id="speedRange" min="0.1" max="2.0" step="0.1" value="1.0">
    </div>

    <div class="control-group">
        <label>高さ</label>
        <input type="number" id="areaHeightInput" min="1" max="10" value="3">
    </div>

    <input type="file" id="fileInput" accept=".mid,.midi" style="width: 180px;">
    <button id="startBtn" disabled>再生スタート</button>
    <div id="status">ファイル未選択</div>
</div>

<div class="instruction">
    <span class="highlight">◆ 操作ガイド Ver 12.0</span><br>
    [マウス追従] 再生バー移動<br>
    [左右の端でドラッグ] <span class="highlight">スクロール</span> (時間移動)<br>
    [中央でドラッグ] <span class="highlight">演奏</span> (空白は無視)<br>
    [左右に揺らす] ビブラート<br>
    [ドロップ/枠外] 全音即停止
</div>

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

<script>
    // --- 設定 ---
    const CANVAS_WIDTH = 600;
    const CANVAS_HEIGHT = window.innerHeight; 
    const NOTE_HEIGHT = 20; 
    const BASE_FALL_SPEED = 200; 
    const MIN_BAR_Y = 60; 
    const SCROLL_THRESHOLD = 2.0; 
    const SCROLL_AREA_WIDTH = 80; // ★左右のスクロール用エリアの幅

    // --- グローバル変数 ---
    let canvas = document.getElementById('gameCanvas');
    let ctx = canvas.getContext('2d');
    
    // ゲーム時間
    let notes = [];
    let isPlaying = false;
    let gameTime = 0;
    let lastFrameTime = 0;
    let speedRate = 1.0;

    // 操作状態
    let isHolding = false;
    let interactionMode = null; // 'play', 'scroll', or null
    let targetY = CANVAS_HEIGHT - 150; 
    let targetHeight = NOTE_HEIGHT * 3;
    let areaFlash = 0.0;
    
    // 譜面の描画基準位置 (固定)
    const ANCHOR_Y = CANVAS_HEIGHT - 150;

    // スクロール・表現用
    let lastMouseY = 0;
    let holdStartX = 0; 
    let vibratoAmount = 0;

    // 音響関連
    let limiter;
    let vibratoEffect;
    const instruments = {};
    let loadedSamples = { piano: false, guitar: false };

    // UI Reference
    const startBtn = document.getElementById('startBtn');
    const statusDiv = document.getElementById('status');
    const modeSelect = document.getElementById('soundMode');
    const speedRange = document.getElementById('speedRange');
    const speedVal = document.getElementById('speedVal');
    const areaHeightInput = document.getElementById('areaHeightInput');

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

        limiter = new Tone.Limiter(-1).toDestination();
        
        vibratoEffect = new Tone.Vibrato({
            frequency: 6, depth: 0, wet: 1
        }).connect(limiter);

        const connectToChain = (inst) => inst.connect(vibratoEffect);

        // 楽器セットアップ
        instruments.synth = new Tone.PolySynth(Tone.Synth, {
            volume: -10, oscillator: { type: "triangle" },
            envelope: { attack: 0.02, decay: 0.1, sustain: 0.3, release: 2 }
        });
        connectToChain(instruments.synth);

        instruments.fm = new Tone.PolySynth(Tone.FMSynth, {
            volume: -10, harmonicity: 3, modulationIndex: 10,
            envelope: { attack: 0.01, decay: 0.2, sustain: 0.5, release: 2 }
        });
        connectToChain(instruments.fm);

        instruments.strings = new Tone.PolySynth(Tone.Synth, {
            volume: -10, oscillator: { type: "sawtooth" },
            envelope: { attack: 0.4, decay: 0.5, sustain: 0.8, release: 3 }
        });
        connectToChain(instruments.strings);

        instruments.piano = new Tone.Sampler({
            urls: { "C4": "C4.mp3", "D#4": "Ds4.mp3", "F#4": "Fs4.mp3", "A4": "A4.mp3" },
            release: 1.5, volume: -5,
            baseUrl: "https://tonejs.github.io/audio/salamander/",
            onload: () => { loadedSamples.piano = true; updateStatus(); }
        });
        connectToChain(instruments.piano);

        instruments.guitar = new Tone.Sampler({
            urls: { "C4": "C4.mp3", "E4": "E4.mp3", "G4": "G4.mp3" },
            release: 2.0, volume: -5,
            baseUrl: "https://tonejs.github.io/audio/guitar-acoustic/",
            onload: () => { loadedSamples.guitar = true; updateStatus(); }
        });
        connectToChain(instruments.guitar);

        requestAnimationFrame(gameLoop);
    };

    function updateStatus() {
        const mode = modeSelect.value;
        if ((mode === 'piano' && !loadedSamples.piano) || 
            (mode === 'guitar' && !loadedSamples.guitar)) {
            statusDiv.innerText = "音源ロード中...";
            statusDiv.style.color = "#FF0";
        } else {
            statusDiv.innerText = notes.length > 0 ? (isPlaying ? "再生中" : "準備完了") : "ファイル待機";
            statusDiv.style.color = "#0FF";
        }
    }

    // --- メインループ ---
    function gameLoop(timestamp) {
        if (!lastFrameTime) lastFrameTime = timestamp;
        const deltaTime = (timestamp - lastFrameTime) / 1000;
        lastFrameTime = timestamp;

        ctx.fillStyle = "#111";
        ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);

        // --- サイドエリア(スクロール領域)の可視化 ---
        // ユーザーにここが「外」だとわかるように少し色を変える
        ctx.fillStyle = "#181818";
        ctx.fillRect(0, 0, SCROLL_AREA_WIDTH, CANVAS_HEIGHT);
        ctx.fillRect(CANVAS_WIDTH - SCROLL_AREA_WIDTH, 0, SCROLL_AREA_WIDTH, CANVAS_HEIGHT);
        ctx.strokeStyle = "#333";
        ctx.beginPath();
        ctx.moveTo(SCROLL_AREA_WIDTH, 0); ctx.lineTo(SCROLL_AREA_WIDTH, CANVAS_HEIGHT);
        ctx.moveTo(CANVAS_WIDTH - SCROLL_AREA_WIDTH, 0); ctx.lineTo(CANVAS_WIDTH - SCROLL_AREA_WIDTH, CANVAS_HEIGHT);
        ctx.stroke();

        // 時間進行
        if (isPlaying && !isHolding) {
            gameTime += deltaTime * speedRate;
        }

        // --- 描画 ---
        if (areaFlash > 0) areaFlash -= 0.05;
        if (areaFlash < 0) areaFlash = 0;

        // 再生エリア
        let r = 0, g = 255, b = 200; 
        if (isHolding) {
            if (interactionMode === 'play') { r = 255; g = 50; b = 150; } 
            else if (interactionMode === 'scroll') { r = 255; g = 165; b = 0; }
        }

        ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${isHolding ? (0.3 + areaFlash * 0.4) : 0.2})`;
        ctx.fillRect(0, targetY, CANVAS_WIDTH, targetHeight);
        
        ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${isHolding ? 0.8 : 0.5})`;
        ctx.lineWidth = 2 + (vibratoAmount * 8); 
        ctx.strokeRect(0, targetY, CANVAS_WIDTH, targetHeight);

        // ノート描画
        const lastNote = notes[notes.length - 1];
        if (isPlaying && lastNote && gameTime > lastNote.time + 3.0) {
            stopGame();
        }

        notes.forEach(note => {
            if (isPlaying) {
                const timeDiff = note.time - gameTime;
                note.y = ANCHOR_Y - (timeDiff * BASE_FALL_SPEED);
            } else {
                note.y = -100;
            }

            if (note.flash > 0) note.flash -= 0.05;
            if (note.flash < 0) note.flash = 0;

            if (note.y > -50 && note.y < CANVAS_HEIGHT) {
                ctx.fillStyle = note.flash > 0 ? "#FFF" : note.color;
                
                const wobble = (interactionMode === 'play' && isHolding && note.flash > 0) 
                               ? Math.sin(timestamp / 30) * vibratoAmount * 3 : 0;
                
                ctx.fillRect(note.x + wobble, note.y, note.w, note.h);
                ctx.lineWidth = 1; 
                ctx.strokeStyle = "rgba(0,0,0,0.5)";
                ctx.strokeRect(note.x + wobble, note.y, note.w, note.h);

                // ID表示: サイドエリア内に表示
                if (note.shouldDrawId) {
                    ctx.fillStyle = "rgba(255, 255, 255, 0.5)"; // 少し控えめに
                    ctx.font = "20px sans-serif"; 
                    ctx.textAlign = "center";
                    ctx.textBaseline = "middle";

                    // 左端のサイドエリア中央
                    ctx.fillText(note.id, SCROLL_AREA_WIDTH / 2, note.y + note.h/2);
                    // 右端のサイドエリア中央
                    ctx.fillText(note.id, CANVAS_WIDTH - (SCROLL_AREA_WIDTH / 2), note.y + note.h/2);
                }
            }
        });

        requestAnimationFrame(gameLoop);
    }

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

    areaHeightInput.addEventListener('change', (e) => {
        let val = parseInt(e.target.value);
        if(val < 1) val = 1; if(val > 10) val = 10;
        targetHeight = val * NOTE_HEIGHT;
    });

    speedRange.addEventListener('input', (e) => {
        speedRate = parseFloat(e.target.value);
        speedVal.innerText = speedRate.toFixed(1);
    });

    // マウス移動
    canvas.addEventListener('mousemove', (e) => {
        const rect = canvas.getBoundingClientRect();
        const mx = e.clientX - rect.left;
        const my = e.clientY - rect.top;
        
        // バーの追従
        targetY = my - (targetHeight / 2);
        if(targetY < MIN_BAR_Y) targetY = MIN_BAR_Y;
        if(targetY > CANVAS_HEIGHT - targetHeight - 10) targetY = CANVAS_HEIGHT - targetHeight - 10;

        // ドラッグ中処理
        if (isHolding) {
            // ★サイドエリアを掴んでいる時だけスクロール
            if (interactionMode === 'scroll') {
                const deltaY = my - lastMouseY;
                if (Math.abs(deltaY) > SCROLL_THRESHOLD) {
                    gameTime += deltaY * 0.01; 
                    if(gameTime < 0) gameTime = 0;
                }
            } 
            // ★演奏モードの時だけビブラート
            else if (interactionMode === 'play') {
                const dist = Math.abs(mx - holdStartX);
                let depth = (dist - 10) / 200;
                if(depth < 0) depth = 0; if(depth > 1) depth = 1;
                vibratoEffect.depth.rampTo(depth, 0.1);
                vibratoAmount = depth;
            }
            // interactionMode === null (中央の空白を掴んだ) の場合は何もしない
        }
        
        lastMouseY = my;
    });

    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) { // 左クリック
            isHolding = true;
            
            targetY = my - (targetHeight / 2);
            if(targetY < MIN_BAR_Y) targetY = MIN_BAR_Y;
            if(targetY > CANVAS_HEIGHT - targetHeight - 10) targetY = CANVAS_HEIGHT - targetHeight - 10;

            lastMouseY = my;
            holdStartX = mx;
            areaFlash = 1.0;

            // ★エリア判定: 左右の端(スクロールエリア)か?
            if (mx < SCROLL_AREA_WIDTH || mx > CANVAS_WIDTH - SCROLL_AREA_WIDTH) {
                // タイムラインの外 -> スクロールモード
                interactionMode = 'scroll';
            } else {
                // 中央エリア -> 演奏モード判定
                const notesToPlay = [];
                notes.forEach(note => {
                    const nBot = note.y + note.h;
                    const nTop = note.y;
                    const aTop = targetY; 
                    const aBot = targetY + targetHeight;
                    if (nBot >= aTop && nTop <= aBot) {
                        notesToPlay.push(note);
                    }
                });

                if (notesToPlay.length > 0) {
                    interactionMode = 'play';
                    // ベロシティ計算
                    // 中央エリア内での相対位置を使う (SCROLL_AREA_WIDTH ~ CANVAS_WIDTH - SCROLL_AREA_WIDTH)
                    let playWidth = CANVAS_WIDTH - (SCROLL_AREA_WIDTH * 2);
                    let localX = mx - SCROLL_AREA_WIDTH;
                    let velocity = 0.1 + (localX / playWidth) * 0.9;
                    velocity = Math.max(0.1, Math.min(1.0, velocity));
                    
                    notesToPlay.forEach(n => n.flash = 1.0);
                    triggerSoundAttack(notesToPlay, velocity);
                } else {
                    // 中央かつノート無し -> 何もしない (誤操作防止のためスクロールもしない)
                    interactionMode = null;
                }
            }
        } 
        else if (e.button === 2) {
            notes.forEach(note => {
                if (mx >= note.x && mx <= note.x + note.w &&
                    my >= note.y && my <= note.y + note.h) {
                    const inst = getCurrentInstrument();
                    if(inst) inst.triggerAttackRelease(note.name, "8n", Tone.now(), 0.8);
                    note.flash = 1.0;
                }
            });
        }
    });

    function releaseAllSounds() {
        if (!isHolding) return;
        
        isHolding = false;
        interactionMode = null;
        vibratoEffect.depth.rampTo(0, 0.05);
        vibratoAmount = 0;
        
        Object.values(instruments).forEach(inst => {
            if(inst) inst.releaseAll();
        });
    }

    window.addEventListener('mouseup', releaseAllSounds);
    canvas.addEventListener('mouseleave', releaseAllSounds);
    window.addEventListener('keydown', (e) => { if (e.key === "Escape") stopGame(); });
    canvas.addEventListener('contextmenu', e => e.preventDefault());

    // --- 音声制御 ---

    function getCurrentInstrument() {
        const mode = modeSelect.value;
        const inst = instruments[mode];
        if ((mode === 'piano' && !loadedSamples.piano) || 
            (mode === 'guitar' && !loadedSamples.guitar)) {
            return instruments.synth;
        }
        return inst;
    }

    function triggerSoundAttack(targetNotes, velocity) {
        const uniqueNotes = Array.from(new Set(targetNotes.map(n => n.name)));
        const inst = getCurrentInstrument();
        if(inst) {
            const now = Tone.now();
            uniqueNotes.forEach((noteName, index) => {
                const strumble = index * 0.02; 
                inst.triggerAttack(noteName, now + strumble, velocity);
            });
        }
    }

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

    // --- ファイル・再生制御 ---
    startBtn.addEventListener('click', async () => {
        await initAudioContext();
        if(notes.length === 0) return;
        
        gameTime = 0; 
        isHolding = false;
        Object.values(instruments).forEach(inst => inst.releaseAll());

        let hVal = parseInt(areaHeightInput.value);
        targetHeight = hVal * NOTE_HEIGHT;

        isPlaying = true;
        startBtn.innerText = "最初から再生";
        statusDiv.innerText = "再生中...";
    });

    function stopGame() {
        isPlaying = false;
        releaseAllSounds();
        startBtn.innerText = "最初から再生";
        statusDiv.innerText = "停止中";
        Object.values(instruments).forEach(inst => inst.releaseAll());
    }

    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({
                            id: 0,
                            name: n.name, time: n.time, 
                            x: SCROLL_AREA_WIDTH + (n.midi % 12) * 35, // 位置調整
                            y: -100, 
                            w: 30, h: NOTE_HEIGHT,
                            color: colors[i % colors.length], flash: 0,
                            shouldDrawId: false
                        });
                    });
                });
                
                notes.sort((a,b) => a.time - b.time);

                // ID重複対策
                let currentId = 0;
                let lastTime = -999;
                notes.forEach(n => {
                    if (Math.abs(n.time - lastTime) > 0.1) {
                        currentId++;
                        lastTime = n.time;
                        n.shouldDrawId = true;
                    } else {
                        n.shouldDrawId = false;
                    }
                    n.id = currentId;
                });

                startBtn.disabled = false;
                startBtn.innerText = "再生スタート";
                updateStatus();
            } catch(err) {
                console.error(err);
                statusDiv.innerText = "解析エラー";
            }
        };
        reader.readAsArrayBuffer(file);
    });
    modeSelect.addEventListener('change', () => { updateStatus(); 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