Newer
Older
System-Takizawa-kenban / kenban_test / kenban_test.js
// ==================================================================
// ========== グローバル変数と設定 ==========
// ==================================================================
const noteScales = ["B6","A#6","A6","G#6","G6","F#6","F6","E6","D#6","D6","C#6","C6","B5","A#5","A5","G#5","G5","F#5","F5","E5","D#5","D5","C#5","C5","B4","A#4","A4","G#4","G4","F#4","F4","E4","D#4","D4","C#4","C4","B3","A#3","A3","G#3","G3","F#3","F3","E3","D#3","D3","C#3","C3","B2","A#2","A2","G#2","G2","F#2","F2","E2","D#2","D2","C#2","C2","B1","A#1","A1","G#1","G1","F#1","F1","E1","D#1","D1","C#1","C1"];
let numColumns = 16;
let pianoRoll1 = []; let pianoRoll2 = [];
let pianoRoll3 = []; let pianoRoll4 = [];
const instruments = {
    "synth": (polyphony = 8) => new Tone.PolySynth(Tone.Synth, { maxPolyphony: polyphony }).toDestination(),
    "piano": () => new Tone.Sampler({ urls: { "C3":"C3.mp3", "C4":"C4.mp3", "C5":"C5.mp3", "C6":"C6.mp3" }, baseUrl: "https://nbrosowsky.github.io/tonejs-instruments/samples/piano/" }).toDestination(),
    "violin": () => new Tone.Sampler({ urls: { "C3":"C3.mp3", "C4":"C4.mp3", "C5":"C5.mp3", "C6":"C6.mp3" }, baseUrl: "https://nbrosowsky.github.io/tonejs-instruments/samples/violin/" }).toDestination(),
    "trumpet": () => new Tone.Sampler({ urls: { "C4":"C4.mp3", "D5":"D5.mp3", "G3":"G3.mp3" }, baseUrl: "https://nbrosowsky.github.io/tonejs-instruments/samples/trumpet/" }).toDestination()
};
let instrument1 = instruments.synth(); let instrument2 = instruments.synth();
let instrument3 = instruments.piano(); let instrument4 = instruments.piano();
let part1, part2, part3, part4;
let currentOctave = 4; let activeKeyboardNote = null;
let currentTrack = "track1";
let canvas, ctx, timelineCanvas, timelineCtx;
const baseCellWidth = 50;
const baseCellHeight = 17;
let cellWidth = baseCellWidth;
let cellHeight = baseCellHeight;
const pitchHeaderWidth = 50;
let animationFrameId = null; let playbackCursor_16th = 0;
let isAuditionMode = false; let isDottedMode = false;
let currentNoteLengthBase = "8n";
 
// ▼▼▼ 範囲選択とノート操作のための新しい変数 ▼▼▼
let selectedNotes = [];   // 選択されたノートオブジェクトを格納
let isDragging = false;
let isSelecting = false;  // 範囲選択中かどうかのフラグ
let dragMode = null;
let selectedNote = null;
let dragStart_16th = 0;
let metronomeSound;
let metronomeEventId = null;
let dragStartNote = null;
let selectionRect = { startX: 0, startY: 0, currentX: 0, currentY: 0 }; // 範囲選択の矩形座標
let dragStartEvent = null;
let autoScrollInterval = null;
let isResizing = false;

const KEY_TO_NOTE = {
    'a': 60, 'w': 61, 's': 62, 'e': 63, 'd': 64, 'f': 65, 't': 66,
    'g': 67, 'y': 68, 'h': 69, 'u': 70, 'j': 71, 'k': 72
};
let pressedKeys = {}; // 現在押されているキーを管理

let noteBuffer = []; // 入力ノートを一時的にためる配列
let bufferTimeout = null; // タイマーID
// ==================================================================
// ========== 初期化処理 ==========
// ==================================================================
document.addEventListener("DOMContentLoaded", () => {
    canvas = document.getElementById("pianoRollCanvas");
    ctx = canvas.getContext("2d");
    timelineCanvas = document.getElementById("timelineCanvas");
    timelineCtx = timelineCanvas.getContext("2d");
    metronomeSound = new Tone.MembraneSynth({ pitchDecay: 0.01, octaves: 6, oscillator: { type: "square" }, envelope: { attack: 0.001, decay: 0.2, sustain: 0, release: 0.1 }, volume: -10 }).toDestination();
    console.log("Metronome synth created.");
    setupMainControls();
    setupNoteTools();
    setupEventListeners();
    drawMiniKeyboard();
    document.getElementById("setMeasuresButton").addEventListener("click", setNumMeasures);
    document.getElementById("showDataButton").addEventListener("click", toggleDataDisplay);
    
    initializeMIDI(); // MIDI初期化
    setInterval(pollMIDIDevices, 1000); // 1秒ごとにMIDI接続をチェック
    
    resizeCanvas();
    redrawAll();
});
// ==================================================================
// ========== UIセットアップ ==========
// ==================================================================
 
function setupMainControls() {
    document.getElementById("playButton").addEventListener("click", playMusic);
    document.getElementById("pauseButton").addEventListener("click", pauseMusic);
    document.getElementById("stopButton").addEventListener("click", stopMusic);
    document.getElementById("exportMidiButton").addEventListener("click", exportMidi);
    
    const importMidiInput = document.getElementById("importMidiInput");
    importMidiInput.addEventListener("change", (event) => {
        importMidi(event.target.files[0]);
    });
    
        // ▼▼▼ ヘルプモーダルのロジックを修正 ▼▼▼
     const helpModal = document.getElementById("help-modal");
    const accordionContainer = document.getElementById("accordion-container");
    let isHelpContentLoaded = false; // コンテンツが読み込まれたかどうかのフラグ

    // --- ヘルプコンテンツを非同期で読み込み、アコーディオンを生成する関数 ---
    async function loadHelpContent() {
        if (isHelpContentLoaded) return; // 既に読み込み済みなら何もしない

        try {
            const response = await fetch('./help-contents.json'); // JSONファイルを読み込む
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const data = await response.json();

            // JSONデータからHTMLを生成
            data.sections.forEach(section => {
                const item = document.createElement('div');
                item.className = 'accordion-item';

                const header = document.createElement('button');
                header.className = 'accordion-header';
                header.textContent = section.title;

                const content = document.createElement('div');
                content.className = 'accordion-content';
                
                // コンテンツの各行を<p>タグに変換して追加
                section.content.forEach(line => {
                    const p = document.createElement('p');
                    p.innerHTML = line; // innerHTMLで <strong> などを解釈させる
                    content.appendChild(p);
                });

                item.appendChild(header);
                item.appendChild(content);
                accordionContainer.appendChild(item);

                // アコーディオンのクリックイベントを設定
                header.addEventListener('click', () => {
                    header.classList.toggle('active');
                    if (content.style.maxHeight) {
                        content.style.maxHeight = null;
                        content.style.padding = "0 18px";
                    } else {
                        content.style.padding = "18px";
                        content.style.maxHeight = content.scrollHeight + "px";
                    }
                });
            });
            isHelpContentLoaded = true; // 読み込み完了フラグを立てる
        } catch (error) {
            console.error("Could not load help content:", error);
            accordionContainer.innerHTML = "<p>ヘルプコンテンツの読み込みに失敗しました。</p>";
        }
    }

    // --- 各ボタンのイベントリスナー ---
    document.getElementById("helpButton").addEventListener("click", () => {
        loadHelpContent(); // 開くときにコンテンツを読み込む
        helpModal.classList.remove("hidden");
    });

    document.getElementById("close-modal-button").addEventListener("click", () => {
        helpModal.classList.add("hidden");
    });

    helpModal.addEventListener("click", (event) => {
        if (event.target === helpModal) {
            helpModal.classList.add("hidden");
        }
    });
    
    const trackSelector = document.getElementById("track-selector");
    trackSelector.querySelectorAll('.tool-item').forEach(item => {
        if (item.dataset.track === currentTrack) item.classList.add('selected');
        item.addEventListener('click', () => {
            trackSelector.querySelector('.selected').classList.remove('selected');
            item.classList.add('selected');
            currentTrack = item.dataset.track;
            redrawAll();
            updatePianoRollTextBox();
        });
    });
 
    const inst1Select = document.getElementById("instrumentSelect1");
    const inst2Select = document.getElementById("instrumentSelect2");
    const inst3Select = document.getElementById("instrumentSelect3");
    const inst4Select = document.getElementById("instrumentSelect4");
 
    inst1Select.addEventListener('change', () => instrument1 = instruments[inst1Select.value]());
    inst2Select.addEventListener('change', () => instrument2 = instruments[inst2Select.value]());
    inst3Select.addEventListener('change', () => instrument3 = instruments[inst3Select.value]());
    inst4Select.addEventListener('change', () => instrument4 = instruments[inst4Select.value]());
    
    const metronomeSelector = document.getElementById("metronome-selector");
    const metroItems = metronomeSelector.querySelectorAll('.tool-item');
 
    // 初期状態を 'off' に設定
    metronomeSelector.querySelector('[data-state="off"]').classList.add('selected');
 
    metroItems.forEach(item => {
        item.addEventListener('click', () => {
            metroItems.forEach(i => i.classList.remove('selected'));
            item.classList.add('selected');
            const state = item.dataset.state;
            toggleMetronome(state);
        });
    });
}
 
function setupNoteTools() {
    const noteLengthSelector = document.getElementById("note-length-selector");
    
    noteLengthSelector.querySelectorAll('.tool-item[data-length]').forEach(tool => {
        if (tool.dataset.length === currentNoteLengthBase) {
            tool.classList.add('selected');
        }
        tool.addEventListener('click', () => {
            noteLengthSelector.querySelector('.selected')?.classList.remove('selected');
            tool.classList.add('selected');
            currentNoteLengthBase = tool.dataset.length;
        });
    });
 
    const dotButton = document.getElementById("dotButton");
    dotButton.addEventListener('click', () => {
        isDottedMode = !isDottedMode;
        dotButton.classList.toggle('active', isDottedMode);
    });
 
    const auditionButton = document.getElementById("auditionButton");
    auditionButton.addEventListener('click', () => {
        isAuditionMode = !isAuditionMode;
        auditionButton.classList.toggle('active', isAuditionMode);
    });
    
    const zoomXInput = document.getElementById('zoom-x');
    const zoomYInput = document.getElementById('zoom-y');

    const applyZoom = () => {
        const zoomX = parseInt(zoomXInput.value, 10) / 100;
        const zoomY = parseInt(zoomYInput.value, 10) / 100;

        // 新しいセルサイズを計算
        cellWidth = baseCellWidth * zoomX;
        cellHeight = baseCellHeight * zoomY;

        // Canvasのサイズを再計算して、全体を再描画
        resizeCanvas();
        redrawAll();
    };

    zoomXInput.addEventListener('change', applyZoom);
    zoomYInput.addEventListener('change', applyZoom);
}
 
 /**
 * UIにミニキーボードを描画する
 */
function drawMiniKeyboard() {
    const container = document.getElementById("mini-keyboard-container");
    container.innerHTML = '';

    const keyLayout = [
        { key: 'a', midiNote: 60, type: 'white', blackKey: { key: 'w', midiNote: 61 } },
        { key: 's', midiNote: 62, type: 'white', blackKey: { key: 'e', midiNote: 63 } },
        { key: 'd', midiNote: 64, type: 'white' },
        { key: 'f', midiNote: 65, type: 'white', blackKey: { key: 't', midiNote: 66 } },
        { key: 'g', midiNote: 67, type: 'white', blackKey: { key: 'y', midiNote: 68 } },
        { key: 'h', midiNote: 69, type: 'white', blackKey: { key: 'u', midiNote: 70 } },
        { key: 'j', midiNote: 71, type: 'white' },
        { key: 'k', midiNote: 72, type: 'white' },
    ];
    
    /**
     * キーが操作されたときに音を鳴らし、ノートを記録する共通関数
     * @param {number} midiNote - 再生・記録するMIDIノート番号
     * @param {HTMLElement} keyDiv - 操作されたキーのDOM要素
     */
    const onKeyInteraction = (midiNote, keyDiv) => {
        const noteName = getNoteName(midiNote + (currentOctave - 4) * 12);
        const instrument = eval(`instrument${currentTrack.slice(-1)}`);
        const noteDuration = getCurrentNoteLength();
        
        // 音を鳴らす
        instrument.triggerAttack(noteName, Tone.now());
        
        // ノートを記録
        recordNoteData({ scale: noteName, length: noteDuration });

        // ▼▼▼ ハイライト処理 ▼▼▼
        // 1. すぐに 'pressed' クラスを追加
        keyDiv.classList.add('pressed');

        // 2. 音の長さ分だけ待ってから、音を止め、クラスを削除
        setTimeout(() => {
            instrument.triggerRelease(noteName);
            keyDiv.classList.remove('pressed');
        }, Tone.Time(noteDuration).toMilliseconds());
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
    };

    keyLayout.forEach(keyInfo => {
        if (keyInfo.type === 'white') {
            const whiteKeyDiv = document.createElement('div');
            whiteKeyDiv.className = 'mini-key white';
            whiteKeyDiv.dataset.key = keyInfo.key;
            whiteKeyDiv.textContent = keyInfo.key.toUpperCase();
            
            whiteKeyDiv.addEventListener('mousedown', (e) => {
                e.stopPropagation();
                onKeyInteraction(keyInfo.midiNote, whiteKeyDiv); // ★ keyDivを渡す
            });

            if (keyInfo.blackKey) {
                const blackKeyDiv = document.createElement('div');
                blackKeyDiv.className = 'mini-key black';
                blackKeyDiv.dataset.key = keyInfo.blackKey.key;
                blackKeyDiv.textContent = keyInfo.blackKey.key.toUpperCase();
                
                blackKeyDiv.addEventListener('mousedown', (e) => {
                    e.stopPropagation();
                    onKeyInteraction(keyInfo.blackKey.midiNote, blackKeyDiv); // ★ keyDivを渡す
                });

                whiteKeyDiv.appendChild(blackKeyDiv);
            }
            container.appendChild(whiteKeyDiv);
        }
    });
}
/**
 * 現在選択されている音符の長さを計算して返す(付点を考慮)
 * @returns {string} Tone.jsが解釈できる音長表記 (例: "4n", "8n.")
 */
function getCurrentNoteLength() {
    // 付点モードがオンの場合
    if (isDottedMode) {
        // 選択されている基本の長さに "." を付けて返す
        return `${currentNoteLengthBase}.`;
    }
    
    // 付点モードがオフの場合は、そのまま基本の長さを返す
    return currentNoteLengthBase;
}
 
 
// ==================================================================
// ========== Canvas描画関連 ==========
// ==================================================================
 
function resizeCanvas() {
    const newWidth = pitchHeaderWidth + (cellWidth * numColumns);
    
    // ▼▼▼ Canvasの高さは、常に全音域を描画できる高さに固定する ▼▼▼
    const newHeight = cellHeight * noteScales.length;
    // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
 
    canvas.width = newWidth;
    canvas.height = newHeight;
    
    // タイムラインの幅は追従させ、高さは固定
    timelineCanvas.width = newWidth;
    timelineCanvas.height = 30;
}
 
function redrawAll() {
    drawPianoRoll();
    drawTimeline();
}
 
function drawPianoRoll() {
    if (!ctx) return;
    ctx.fillStyle = "#333";
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.font = "12px Arial";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    noteScales.forEach((scale, i) => {
        const y = i * cellHeight;
        if (scale.includes("#")) { ctx.fillStyle = "#383838"; } else { ctx.fillStyle = "#484848"; }
        ctx.fillRect(0, y, canvas.width, cellHeight);
        ctx.strokeStyle = "#555";
        ctx.beginPath();
        ctx.moveTo(pitchHeaderWidth, y);
        ctx.lineTo(canvas.width, y);
        ctx.stroke();
        ctx.fillStyle = "white";
        ctx.fillText(scale, pitchHeaderWidth / 2, y + cellHeight / 2);
    });
    ctx.strokeStyle = "#999";
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(pitchHeaderWidth, 0);
    ctx.lineTo(pitchHeaderWidth, canvas.height);
    ctx.stroke();
    ctx.strokeStyle = "#4a4a4a";
    ctx.lineWidth = 1;
    for (let i = 0; i < numColumns * 2; i++) {
        if (i % 2 !== 0) {
            const x = pitchHeaderWidth + (i * (cellWidth / 2));
            ctx.beginPath();
            ctx.moveTo(x, 0);
            ctx.lineTo(x, canvas.height);
            ctx.stroke();
        }
    }
    for (let i = 0; i <= numColumns; i++) {
        const x = pitchHeaderWidth + i * cellWidth;
        if (i % 8 === 0) { ctx.strokeStyle = "#999"; ctx.lineWidth = 2; } 
        else if (i % 4 === 0) { ctx.strokeStyle = "#666"; ctx.lineWidth = 1; } 
        else { ctx.strokeStyle = "#555"; ctx.lineWidth = 1; }
        ctx.beginPath();
        ctx.moveTo(x, 0);
        ctx.lineTo(x, canvas.height);
        ctx.stroke();
    }
    ctx.lineWidth = 1;
    
    const inactiveColor = (hex) => `${hex}80`;
    drawNotes(pianoRoll1, currentTrack === 'track1' ? "#4CAF50" : inactiveColor("#4CAF50"));
    drawNotes(pianoRoll2, currentTrack === 'track2' ? "#3498DB" : inactiveColor("#3498DB"));
    drawNotes(pianoRoll3, currentTrack === 'track3' ? "#FFC107" : inactiveColor("#FFC107"));
    drawNotes(pianoRoll4, currentTrack === 'track4' ? "#E91E63" : inactiveColor("#E91E63"));
    
     if (isSelecting) {
        ctx.fillStyle = "rgba(52, 152, 219, 0.3)"; // 半透明の青
        ctx.strokeStyle = "rgba(52, 152, 219, 0.8)";
        ctx.lineWidth = 1;
        const rect = getNormalizedSelectionRect();
        ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
        ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
    }
}
 
function drawNotes(pianoRollData, color) {
    pianoRollData.forEach(note => {
        if (note.scale === "rest") return;
        const startCol_8th = note.start_16th / 2;
        const durationInCols_8th = note.length_16th / 2;
        const rowIndex = noteScales.indexOf(note.scale);
        if (rowIndex !== -1) {
            const x = pitchHeaderWidth + startCol_8th * cellWidth;
            const y = rowIndex * cellHeight;
            const noteWidth = durationInCols_8th * cellWidth - 1;
            
            ctx.fillStyle = color;
            ctx.fillRect(x + 1, y + 1, noteWidth, cellHeight - 2);
 
            // ▼▼▼ 選択されているノートにハイライトを追加 ▼▼▼
            if (selectedNotes.includes(note)) {
                ctx.strokeStyle = "white";
                ctx.lineWidth = 2;
                ctx.strokeRect(x + 1, y + 1, noteWidth, cellHeight - 2);
            }
        }
    });
}
 
function drawTimeline() {
    if (!timelineCtx) return;
    const t_ctx = timelineCtx;
    t_ctx.fillStyle = "#2c2c2c";
    t_ctx.fillRect(0, 0, timelineCanvas.width, timelineCanvas.height);
    t_ctx.font = "12px Arial";
    t_ctx.textAlign = "left";
    t_ctx.textBaseline = "middle";
    t_ctx.fillStyle = "white";
    const numMeasures = numColumns / 8;
    for (let i = 0; i < numMeasures; i++) {
        const measureX = pitchHeaderWidth + (i * 8) * cellWidth;
        t_ctx.fillText(i + 1, measureX + 5, 15);
        for (let j = 0; j < 8; j++) {
            const beatX = measureX + j * cellWidth;
            if (j === 0) { t_ctx.strokeStyle = "#999"; t_ctx.beginPath(); t_ctx.moveTo(beatX, 0); t_ctx.lineTo(beatX, 30); t_ctx.stroke(); } 
            else if (j % 2 === 0) { t_ctx.strokeStyle = "#666"; t_ctx.beginPath(); t_ctx.moveTo(beatX, 10); t_ctx.lineTo(beatX, 30); t_ctx.stroke(); } 
            else { t_ctx.strokeStyle = "#555"; t_ctx.beginPath(); t_ctx.moveTo(beatX, 20); t_ctx.lineTo(beatX, 30); t_ctx.stroke(); }
        }
    }
    const endX = pitchHeaderWidth + numColumns * cellWidth;
    t_ctx.strokeStyle = "#999";
    t_ctx.beginPath();
    t_ctx.moveTo(endX, 0);
    t_ctx.lineTo(endX, 30);
    t_ctx.stroke();
    if (playbackCursor_16th >= 0) {
        const cursorX = pitchHeaderWidth + (playbackCursor_16th / 2) * cellWidth;
        timelineCtx.strokeStyle = "rgba(255, 0, 0, 0.8)";
        timelineCtx.lineWidth = 2;
        timelineCtx.beginPath();
        timelineCtx.moveTo(cursorX, 0);
        timelineCtx.lineTo(cursorX, 30);
        timelineCtx.stroke();
        timelineCtx.lineWidth = 1;
    }
}
 
 // ==================================================================
// ========== イベントハンドラ (オートスクロール最終修正版) ==========
// ==================================================================

function setupEventListeners() {
    // マウスダウンイベントのみを、常に監視する
    canvas.addEventListener('mousedown', handleMouseDown);
    timelineCanvas.addEventListener('mousedown', handleTimelineClick);
    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    
    // リサイズハンドルのイベント
    const resizeHandle = document.getElementById("resize-handle");
    resizeHandle.addEventListener('mousedown', (event) => {
        isResizing = true;
        window.addEventListener('mousemove', handleResizeMove);
        window.addEventListener('mouseup', handleResizeUp);
    });
}

/**
 * マウスイベントから座標を取得する。
 * @param {MouseEvent} event - マウスイベントオブジェクト
 * @param {boolean} relativeToContainer - trueの場合、コンテナの表示領域基準の座標を返す
 * @returns {{x: number, y: number}} 座標オブジェクト
 */
function getMousePos(event, relativeToContainer = false) {
    const container = document.getElementById("pianoRollContainer");
    const rect = container.getBoundingClientRect();
    
    let x = event.clientX - rect.left;
    let y = event.clientY - rect.top;

    if (!relativeToContainer) {
        // Canvas内の絶対座標(スクロール量を加味)を返す
        x += container.scrollLeft;
        y += container.scrollTop;
    }
    // relativeToContainer = true なら、ビューポート(表示領域)基準の座標を返す
    return { x, y };
}

/**
 * ピアノロールでマウスボタンが押されたときの処理
 * @param {MouseEvent} event 
 */
function handleMouseDown(event) {
    dragStartEvent = event;
    const pos = getMousePos(event);
    if (pos.x < pitchHeaderWidth) return;

    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    const clicked_16th_col = Math.floor((pos.x - pitchHeaderWidth) / (cellWidth / 2));
    const scale = noteScales[Math.floor((pos.y - timelineCanvas.height) / cellHeight)];
    
    const clickedNote = targetPianoRoll.find(note => 
        note.scale === scale && 
        note.start_16th <= clicked_16th_col && 
        clicked_16th_col < note.start_16th + note.length_16th
    );
    
    dragStart_16th = clicked_16th_col;
    isDragging = true;

    if (clickedNote) {
        selectedNote = clickedNote;
        dragStartNote = { ...selectedNote }; 
        
        const handleWidth = 8; // ハンドルの判定幅を少し広げる
        const xInNote = pos.x - (pitchHeaderWidth + (selectedNote.start_16th / 2) * cellWidth);
        const isRightEdge = xInNote > (selectedNote.length_16th / 2 * cellWidth) - handleWidth;

        // ▼▼▼ ここからロジックを修正 ▼▼▼
        if (selectedNotes.includes(clickedNote)) {
            // --- 選択済みのノートの上でクリック ---
            if (isRightEdge) {
                // 右端なら -> グループリサイズモード
                dragMode = 'resize-group-right';
            } else {
                // それ以外なら -> グループ移動モード
                dragMode = 'move-group';
            }
            // 全選択ノートの元の状態を保存
            selectedNotes.forEach(note => note.originalState = { ...note });

        } else {
            // --- 選択されていないノートの上でクリック ---
            if (isRightEdge) {
                dragMode = 'resize-right';
            } else if (xInNote < handleWidth) {
                dragMode = 'resize-left';
            } else {
                dragMode = 'move';
            }
        }
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

    } else {
        // --- 空の場所でクリック ---
        isSelecting = true;
        selectionRect.startX = pos.x;
        selectionRect.startY = pos.y;
        selectionRect.currentX = pos.x;
        selectionRect.currentY = pos.y;
        selectedNotes = [];
    }
    
    window.addEventListener('mousemove', handleMouseMove);
    window.addEventListener('mouseup', handleMouseUp);
}

/**
 * マウスが動いているときの処理
 * @param {MouseEvent} event 
 */
function handleMouseMove(event) {
    const pos = getMousePos(event);
    
    if (isSelecting) {
        handleAutoScroll(event);
        selectionRect.currentX = pos.x;
        selectionRect.currentY = pos.y;
        updateSelection();
    } else if (isDragging && selectedNote) {
        const current_16th_col = Math.max(0, Math.floor((pos.x - pitchHeaderWidth) / (cellWidth / 2)));
        const currentRow = Math.floor((pos.y - timelineCanvas.height) / cellHeight);
        const diff_16th = current_16th_col - dragStart_16th;
	if (dragMode === 'resize-group-right') {
        // グループリサイズ
            selectedNotes.forEach(note => {
                const newLength = note.originalState.length_16th + diff_16th;
                note.length_16th = Math.max(1, newLength); // 最小長は1
            });
        }if (dragMode === 'move-group') {
            const startPos = getMousePos(dragStartEvent);
            const startRow = Math.floor((startPos.y - timelineCanvas.height) / cellHeight);
            const diff_row = currentRow - startRow;
            selectedNotes.forEach(note => {
                note.start_16th = Math.max(0, note.originalState.start_16th + diff_16th);
                const originalRow = noteScales.indexOf(note.originalState.scale);
                const newRow = originalRow + diff_row;
                if (newRow >= 0 && newRow < noteScales.length) {
                    note.scale = noteScales[newRow];
                }
            });
        } else if (dragMode === 'move') {
            selectedNote.start_16th = Math.max(0, dragStartNote.start_16th + diff_16th);
            if (currentRow >= 0 && currentRow < noteScales.length) {
                selectedNote.scale = noteScales[currentRow];
            }
        } else if (dragMode === 'resize-right') {
            selectedNote.length_16th = Math.max(1, dragStartNote.length_16th + diff_16th);
        } else if (dragMode === 'resize-left') {
            const newStart = dragStartNote.start_16th + diff_16th;
            if (newStart < dragStartNote.start_16th + dragStartNote.length_16th) {
                const finalNewStart = Math.max(0, newStart);
                const newLength = dragStartNote.length_16th - (finalNewStart - dragStartNote.start_16th);
                if (newLength >= 1) {
                    selectedNote.start_16th = finalNewStart;
                    selectedNote.length_16th = newLength;
                }
            }
        }
    }
    
    redrawAll();
}

/**
 * マウスボタンが離されたときの処理
 * @param {MouseEvent} event 
 */
function handleMouseUp(event) {
    window.removeEventListener('mousemove', handleMouseMove);
    window.removeEventListener('mouseup', handleMouseUp);
    
    if (autoScrollInterval) {
        clearInterval(autoScrollInterval);
        autoScrollInterval = null;
    }

    if (!isDragging && !isSelecting) return;
    
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    const pos = getMousePos(event);
    const up_16th_col = Math.floor((pos.x - pitchHeaderWidth) / (cellWidth / 2));
    
    if (isSelecting) {
        updateSelection();
        if (selectionRect.startX === selectionRect.currentX && selectionRect.startY === selectionRect.currentY) {
            const scale = noteScales[Math.floor((pos.y - timelineCanvas.height) / cellHeight)];
            if(scale) {
                const currentLength = getCurrentNoteLength();
                if(currentLength !== 'rest') {
                    targetPianoRoll.push({
                        start_16th: up_16th_col,
                        scale: scale,
                        length_16th: Tone.Time(currentLength).toTicks() / Tone.Time("16n").toTicks()
                    });
                }
            }
            selectedNotes = [];
        }
    } else if (isDragging && selectedNote) {
        const isShortClick = dragStart_16th === up_16th_col && dragStartNote.scale === noteScales[Math.floor((pos.y - timelineCanvas.height)/cellHeight)];
        if (isShortClick && dragMode !== 'move-group') {
            const noteIndexToDelete = targetPianoRoll.indexOf(selectedNote);
            if (noteIndexToDelete > -1) {
                targetPianoRoll.splice(noteIndexToDelete, 1);
            }
        } else {
            if (dragMode === 'move-group') {
                selectedNotes.forEach(note => delete note.originalState);
            }
        }if (dragMode === 'move-group' || dragMode === 'resize-group-right') {
            selectedNotes.forEach(note => delete note.originalState);
        }
    }

    targetPianoRoll.sort((a, b) => a.start_16th - b.start_16th);
    isDragging = false;
    isSelecting = false;
    dragMode = null;
    selectedNote = null;
    dragStartNote = null;
    redrawAll();
    updatePianoRollTextBox();
}

/**
 * 範囲選択中のオートスクロールを処理する
 * @param {MouseEvent} event 
 */
function handleAutoScroll(event) {
    const container = document.getElementById("pianoRollContainer");
    const pos = getMousePos(event, true); // ビューポート基準の座標
    const scrollMargin = 40;
    const scrollSpeed = 10;
    let dx = 0, dy = 0;

    if (pos.x < scrollMargin) dx = -scrollSpeed;
    else if (pos.x > container.clientWidth - scrollMargin) dx = scrollSpeed;
    if (pos.y < scrollMargin) dy = -scrollSpeed;
    else if (pos.y > container.clientHeight - scrollMargin) dy = scrollSpeed;

    if (dx !== 0 || dy !== 0) {
        if (!autoScrollInterval) {
            autoScrollInterval = setInterval(() => {
                container.scrollBy(dx, dy);
                selectionRect.currentX += dx;
                selectionRect.currentY += dy;
                updateSelection();
                redrawAll();
            }, 16);
        }
    } else {
        if (autoScrollInterval) {
            clearInterval(autoScrollInterval);
            autoScrollInterval = null;
        }
    }
}

/**
 * タイムラインがクリックされたときの処理
 * @param {MouseEvent} event 
 */
function handleTimelineClick(event) {
    if (Tone.Transport.state === 'started') return;
    const rect = timelineCanvas.getBoundingClientRect();
    const x = event.clientX - rect.left;
    let clicked_16th_col;
    if (x < pitchHeaderWidth) {
        clicked_16th_col = 0;
    } else {
        clicked_16th_col = Math.round((x - pitchHeaderWidth) / (cellWidth / 2));
    }
    const max_16th_cols = numColumns * 2;
    playbackCursor_16th = Math.min(clicked_16th_col, max_16th_cols);
    Tone.Transport.seconds = playbackCursor_16th * Tone.Time("16n").toSeconds();
    redrawAll();
}

/**
 * 小節数設定ボタンが押されたときの処理
 */
function setNumMeasures() {
    const measures = parseInt(document.getElementById("numMeasuresInput").value, 10);
    if (!isNaN(measures) && measures > 0) {
        numColumns = measures * 8;
        resizeCanvas();
        redrawAll();
    } else {
        alert("小節数は1以上の正の整数を入力してください。");
    }
}

/**
 * キーボードショートカットを処理する
 * @param {KeyboardEvent} event 
 */
function handleKeyDown(event) {
    if (event.target.tagName === 'TEXTAREA' || event.target.tagName === 'INPUT') {
        return;
    }
    const key = event.key.toLowerCase();

    if (event.ctrlKey || event.metaKey) {
        switch(key) {
            case "c": event.preventDefault(); copySelectedNotes(); return;
            case "v": event.preventDefault(); pasteNotes(); return;
        }
    }
    
    if (KEY_TO_NOTE.hasOwnProperty(key)) {
        event.preventDefault();
        if (!pressedKeys[key]) {
            pressedKeys[key] = true;
            document.querySelector(`.mini-key[data-key="${key}"]`)?.classList.add('pressed');
            const midiNote = KEY_TO_NOTE[key] + (currentOctave - 4) * 12;
            const noteName = getNoteName(midiNote);
            const instrument = eval(`instrument${currentTrack.slice(-1)}`);
            instrument.triggerAttack(noteName, Tone.now());
            recordNoteData({ scale: noteName, length: getCurrentNoteLength() });
        }
        return;
    }

    let needsRedraw = true;
    switch(event.key) {
        case "l": decreaseOctave(); break;
        case ";": case "+": increaseOctave(); break;
        case "ArrowUp": case "ArrowDown": case "ArrowLeft": case "ArrowRight":
            if (selectedNotes.length === 0) { needsRedraw = false; break; }
            event.preventDefault();
            moveSelectedNotes(event.key);
            break;
        case "Delete":
        case "Backspace":
            if (selectedNotes.length === 0) { needsRedraw = false; break; }
            event.preventDefault();
            deleteSelectedNotes(); // ★ ヘルパー関数を呼び出す
            break;
        default:
            needsRedraw = false;
    }
    if (needsRedraw) {
        redrawAll();
        updatePianoRollTextBox();
    }
}

/**
 * 選択されたノートを矢印キーで移動させるヘルパー関数
 * @param {string} direction 
 */
function moveSelectedNotes(direction) {
    switch(direction) {
        case "ArrowUp":
            selectedNotes.forEach(note => {
                const currentIndex = noteScales.indexOf(note.scale);
                if (currentIndex > 0) note.scale = noteScales[currentIndex - 1];
            });
            break;
        case "ArrowDown":
            selectedNotes.forEach(note => {
                const currentIndex = noteScales.indexOf(note.scale);
                if (currentIndex < noteScales.length - 1) note.scale = noteScales[currentIndex + 1];
            });
            break;
        case "ArrowLeft":
            if (selectedNotes.every(note => note.start_16th > 0)) {
                selectedNotes.forEach(note => note.start_16th -= 1);
            }
            break;
        case "ArrowRight":
            const maxCols = numColumns * 2;
            if (selectedNotes.every(note => (note.start_16th + note.length_16th) < maxCols)){
                selectedNotes.forEach(note => note.start_16th += 1);
            }
            break;
    }
}

/**
 * 選択されたノートを削除するヘルパー関数
 */
function deleteSelectedNotes() {
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    selectedNotes.forEach(note => {
        const index = targetPianoRoll.indexOf(note);
        if (index > -1) {
            targetPianoRoll.splice(index, 1);
        }
    });
    selectedNotes = []; // 削除後は選択を解除
}

/**
 * キーが離されたときの処理
 * @param {KeyboardEvent} event 
 */
function handleKeyUp(event) {
    const key = event.key.toLowerCase();
    if (KEY_TO_NOTE[key] && pressedKeys[key]) {
        event.preventDefault();
        pressedKeys[key] = false;
        document.querySelector(`.mini-key[data-key="${key}"]`)?.classList.remove('pressed');
        const midiNote = KEY_TO_NOTE[key] + (currentOctave - 4) * 12;
        const noteName = getNoteName(midiNote);
        instrument1.triggerRelease(noteName);
        instrument2.triggerRelease(noteName);
        instrument3.triggerRelease(noteName);
        instrument4.triggerRelease(noteName);
    }
}

/**
 * ピアノロールのリサイズハンドルがドラッグされたときの処理
 * @param {MouseEvent} event 
 */
function handleResizeMove(event) {
    if (!isResizing) return;
    const pianoRollContainer = document.getElementById("pianoRollContainer");
    const containerTop = pianoRollContainer.getBoundingClientRect().top;
    const newHeight = event.clientY - containerTop;
    if (newHeight > 100 && newHeight < window.innerHeight * 0.9) {
        pianoRollContainer.style.height = `${newHeight}px`;
    }
}

/**
 * ピアノロールのリサイズが終了したときの処理
 */
function handleResizeUp() {
    if (!isResizing) return;
    isResizing = false;
    window.removeEventListener('mousemove', handleResizeMove);
    window.removeEventListener('mouseup', handleResizeUp);
    redrawAll();
}
// ==================================================================
// ========== MIDI & スクリーンキーボード入力 ==========
// ==================================================================
let midiAccess = null; 

/**
 * MIDIデバイスへのアクセスを要求する初期化関数
 */
function initializeMIDI() {
    if (navigator.requestMIDIAccess) {
        navigator.requestMIDIAccess({ sysex: false })
            .then(
                (access) => {
                    console.log("MIDI Access successful. Starting connection polling...");
                    midiAccess = access; // 取得したアクセスオブジェクトをグローバル変数に保存
                },
                (error) => {
                    console.error("Failed to get MIDI access.", error);
                    document.getElementById("midistatus").innerText = "MIDI: 許可されていません";
                }
            );
    } else {
        document.getElementById("midistatus").innerText = "MIDI: 非対応ブラウザ";
    }
}

/**
 * MIDIデバイスの接続状態を定期的にチェックし、UIとイベントハンドラを更新する
 */
function pollMIDIDevices() {
    if (!midiAccess) return;

    let physicalDeviceFound = false;
    
    midiAccess.inputs.forEach(input => {
        const deviceName = input.name.toLowerCase();
        if (!deviceName.includes("through") && !deviceName.includes("microsoft")) {
            physicalDeviceFound = true;
        }
    });

    const statusElement = document.getElementById("midistatus");
    const isCurrentlyDisplayedAsConnected = statusElement.innerText === "MIDI: 接続済み";

    if (physicalDeviceFound && !isCurrentlyDisplayedAsConnected) {
        console.log("MIDI device connected. Updating status and attaching listeners.");
        statusElement.innerText = "MIDI: 接続済み";
        midiAccess.inputs.forEach(input => {
            console.log(`Attaching MIDI listener to: ${input.name}`);
            input.onmidimessage = handleMIDIMessage;
        });
    } else if (!physicalDeviceFound && isCurrentlyDisplayedAsConnected) {
        console.log("MIDI device disconnected. Updating status.");
        statusElement.innerText = "MIDI: 未接続";
    }
}

/**
 * MIDIメッセージを受信したときの処理
 * @param {MIDIMessageEvent} event 
 */
function handleMIDIMessage(event) {
    const [status, note, velocity] = event.data;
    const noteName = getNoteName(note);
    const messageType = status & 0xf0;
 
    if (messageType === 144 && velocity > 0) { // ノートオン
        const instrument = eval(`instrument${currentTrack.slice(-1)}`);
        instrument.triggerAttack(noteName, Tone.now(), velocity / 127);
        recordNoteData({ scale: noteName, length: getCurrentNoteLength() });
    } else if (messageType === 128 || (messageType === 144 && velocity === 0)) { // ノートオフ
        instrument1.triggerRelease(noteName);
        instrument2.triggerRelease(noteName);
        instrument3.triggerRelease(noteName);
        instrument4.triggerRelease(noteName);
    }
}

function getNoteName(noteNumber) {
    const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
    const octave = Math.floor(noteNumber / 12) - 1;
    return `${notes[noteNumber % 12]}${octave}`;
}

/**
 * MIDIやスクリーンキーボードからのノート入力を記録する
 * 短時間に連続して入力されたノートは「和音」とみなし、同じ開始時間で記録する
 * @param {object} noteObject - {scale: string, length: string}
 */
function recordNoteData(noteObject) {
    // 試奏モードがオンの場合は、ピアノロールに記録せずに関数を終了する
    if (isAuditionMode) {
        return;
    }
 
    // 1. 既存のタイマーがあればクリアする
    //    (前のノート入力から50ms以内に次のノートが来たら、タイマーをリセットし、和音の構成音とみなす)
    clearTimeout(bufferTimeout);
 
    // 2. 新しいノートを一時的なバッファ配列に追加
    noteBuffer.push(noteObject);
 
    // 3. 50ミリ秒後に、バッファにたまったノートをまとめて記録するタイマーを設定
    bufferTimeout = setTimeout(() => {
        // タイマーが発火した時点でバッファが空なら何もしない
        if (noteBuffer.length === 0) return;
 
        // 現在の編集トラックを取得
        const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
        
        // バッファ内の全ノートの開始位置は、現在の再生カーソル位置に統一
        const start_16th = playbackCursor_16th;
        
        // カーソルを進めるために、和音の中で最も長い音符の長さを記録する変数
        let maxNoteLength_16th = 0;
 
        // バッファにたまったすべてのノートを処理
        noteBuffer.forEach(note => {
            // 音符の長さを「16分音符何個分か」に変換
            const noteLength_16th = Tone.Time(note.length).toTicks() / Tone.Time("16n").toTicks();
            
            // 和音の中で一番長いノートの長さを更新
            if (noteLength_16th > maxNoteLength_16th) {
                maxNoteLength_16th = noteLength_16th;
            }
 
            // すべてのノートを同じ開始時間でピアノロール配列に追加
            targetPianoRoll.push({
                start_16th: start_16th,
                scale: note.scale,
                length_16th: noteLength_16th
            });
        });
 
        // カーソルを、和音の中で一番長いノートの分だけ進める
        playbackCursor_16th += maxNoteLength_16th;
        // Tone.Transportの再生時間も同期させる
        Tone.Transport.seconds = playbackCursor_16th * Tone.Time("16n").toSeconds();
 
        // 最後に、次の入力に備えてバッファをクリア
        noteBuffer = [];
 
        // ピアノロール配列を時間順にソートし、画面を再描画
        targetPianoRoll.sort((a, b) => a.start_16th - b.start_16th);
        
        focusOnCursor();
        
        redrawAll();
        updatePianoRollTextBox();
 
    }, 50); // 50ミリ秒の猶予時間。この値で和音と判定する時間間隔を調整できる
}
 
const adjustOctave = (scale) => {
    const note = scale.slice(0, -1);
    const octave = parseInt(scale.slice(-1));
    return `${note}${octave + currentOctave - 4}`;
};
 
const stop = () => {
    if (activeKeyboardNote) {
        const instrument = eval(`instrument${currentTrack.slice(-1)}`);
        instrument.triggerRelease(activeKeyboardNote);
        activeKeyboardNote = null;
    }
};
 
// ==================================================================
// ========== 再生エンジン ==========
// ==================================================================
/**
 * メトロノームのオン/オフと間隔を制御する
 * @param {string} state - "off", "4n", "8n"
 */
function toggleMetronome(state) {
    if (metronomeEventId !== null) {
        Tone.Transport.clear(metronomeEventId);
        metronomeEventId = null;
    }

    if (state === "off") {
        if (Tone.Transport.state === 'started') {
            const currentTime = Tone.Transport.seconds;
            stopMusic();
            playMusic();
            Tone.Transport.seconds = currentTime;
        }
        return;
    }

    metronomeEventId = Tone.Transport.scheduleRepeat((time) => {
        // 現在の再生位置を拍数で取得
        const ticks = Tone.Transport.ticks;
        const ppq = Tone.Transport.PPQ; // 4分音符あたりのTick数
        const measureTicks = ppq * 4; // 1小節あたりのTick数

        // ▼▼▼ 判定ロジックを修正 ▼▼▼
        
        // Tickのズレを許容する許容範囲
        const tolerance = 10; 
        
        // 小節の1拍目か?
        const isFirstBeat = (ticks % measureTicks) < tolerance;
        // 4分音符のタイミングか?
        const isDownBeat = (ticks % ppq) < tolerance;

        if (isFirstBeat) {
            // 小節の頭: 高い音 & 大きい音量
            metronomeSound.triggerAttackRelease("G4", "16n", time, 1.0);
        } else if (isDownBeat) {
            // 4分音符の拍: 中くらいの音 & 通常の音量
            metronomeSound.triggerAttackRelease("C4", "16n", time, 0.7);
        } else {
            // 8分音符の裏拍 (8nモードの時のみ呼ばれる)
            metronomeSound.triggerAttackRelease("C4", "16n", time, 0.4);
        }
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

    }, state, 0);

    if (Tone.Transport.state === 'started') {
        const currentTime = Tone.Transport.seconds;
        stopMusic();
        playMusic();
        Tone.Transport.seconds = currentTime;
    }
}
/**
 * 音楽を再生する(メトロノーム単独再生対応)
 */
// ==================================================================
// ========== 再生エンジン (ライブアップデート対応版) ==========
// ==================================================================

async function playMusic() {
    await Tone.start();
    Tone.Transport.bpm.value = document.getElementById("tempo").value;
    
    // 一時停止からの再開
    if (Tone.Transport.state === 'paused') {
        Tone.Transport.start();
        startDrawingLoop();
        return;
    }

    // 停止状態から新規に再生する場合
    if (Tone.Transport.state !== 'started') {
        await Tone.loaded();
        
        // ▼▼▼ ループ開始イベントのスケジュールをここに追加 ▼▼▼
        Tone.Transport.scheduleRepeat(time => {
            // ループの開始地点で呼ばれる
            // ここで再生スケジュールを再構築する
            Tone.Draw.schedule(() => {
                console.log("Loop start: Rebuilding playback schedule.");
                setupPlayback(); // 最新のノート情報でパートを再作成
            }, time);
        }, "1m", 0); // 1小節(1m)ごとにチェック(曲の長さに応じて調整可能)
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

        const totalDuration = numColumns * Tone.Time("8n").toSeconds();
        Tone.Transport.loop = true;
        Tone.Transport.loopStart = 0;
        Tone.Transport.loopEnd = totalDuration > 0 ? totalDuration : Tone.Time("1m").toSeconds();
        
        // 初回再生の準備
        setupPlayback();
        
        Tone.Transport.start();
        startDrawingLoop();
    }
}

function pauseMusic() {
    Tone.Transport.pause();
    if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
    }
}

function stopMusic() {
    Tone.Transport.stop();
    playbackCursor_16th = 0;
    if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
    }
    // ▼▼▼ 停止時にスケジュールを全クリア ▼▼▼
    Tone.Transport.cancel(0);
    // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
    redrawAll();
}

function setupPlayback() {
    // 既存のパートを破棄するだけで、Transportのスケジュールはクリアしない
    if (part1) part1.dispose(); if (part2) part2.dispose();
    if (part3) part3.dispose(); if (part4) part4.dispose();
    
    const createPart = (pianoRoll, instrument) => {
        const partData = convertToPartData(pianoRoll);
        if (partData.length === 0) return null;
        // パートを作成し、即座に開始させる
        return new Tone.Part((time, value) => {
            instrument.triggerAttackRelease(value.note, value.duration, time);
        }, partData).start(0);
    };

    part1 = createPart(pianoRoll1, instrument1);
    part2 = createPart(pianoRoll2, instrument2);
    part3 = createPart(pianoRoll3, instrument3);
    part4 = createPart(pianoRoll4, instrument4);
}
 
function startDrawingLoop() {
    if (animationFrameId) {
        cancelAnimationFrame(animationFrameId);
    }
    function loop() {
        if (Tone.Transport.state !== 'started') {
            animationFrameId = null;
            return;
        }
        const sixteenthSecs = Tone.Time("16n").toSeconds();
        playbackCursor_16th = Tone.Transport.seconds / sixteenthSecs;
        redrawAll();
        animationFrameId = requestAnimationFrame(loop);
    }
    animationFrameId = requestAnimationFrame(loop);
}
 
function resetMusic() {
    stopMusic();
    pianoRoll1 = []; pianoRoll2 = [];
    pianoRoll3 = []; pianoRoll4 = [];
    redrawAll();
    updatePianoRollTextBox();
    document.getElementById("pianoRoll").value = "";
    document.getElementById("data-display-container").classList.add("hidden");
}
 
 
// ==================================================================
// ========== ユーティリティ & 残りのUI操作 ==========
// ==================================================================
/**
 * 範囲選択矩形の正規化された座標(x, y, w, h)を返す
 */
function getNormalizedSelectionRect() {
    const x = Math.min(selectionRect.startX, selectionRect.currentX);
    const y = Math.min(selectionRect.startY, selectionRect.currentY);
    const w = Math.abs(selectionRect.startX - selectionRect.currentX);
    const h = Math.abs(selectionRect.startY - selectionRect.currentY);
    return { x, y, w, h };
}
 
/**
 * 範囲選択矩形内のノートを計算し、selectedNotes配列を更新する
 */
function updateSelection() {
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    const selRect = getNormalizedSelectionRect();
    selectedNotes = [];
 
    targetPianoRoll.forEach(note => {
        if (note.scale === 'rest') return;
        const noteX = pitchHeaderWidth + (note.start_16th / 2) * cellWidth;
        const noteY = noteScales.indexOf(note.scale) * cellHeight;
        const noteW = (note.length_16th / 2) * cellWidth;
        const noteH = cellHeight;
        
        // AABB交差判定(矩形が重なっているか)
        if (noteX < selRect.x + selRect.w && noteX + noteW > selRect.x &&
            noteY < selRect.y + selRect.h && noteY + noteH > selRect.y) {
            selectedNotes.push(note);
        }
    });
}
 
/**
 * 現在選択されているノートをクリップボードにコピーする
 */
function copySelectedNotes() {
    if (selectedNotes.length === 0) return;
 
    const firstNoteTime = Math.min(...selectedNotes.map(n => n.start_16th));
    
    clipboard = selectedNotes.map(note => {
        return {
            start_16th: note.start_16th - firstNoteTime,
            scale: note.scale,
            length_16th: note.length_16th
        };
    });
    
    console.log("Notes copied to clipboard:", clipboard);
}
 
function convertToPartData(pianoRoll) {
    const sixteenthSecs = Tone.Time("16n").toSeconds();
    return pianoRoll.map(note => {
        if (note.scale === 'rest') return null;
        return {
            time: note.start_16th * sixteenthSecs,
            note: note.scale,
            duration: note.length_16th * sixteenthSecs
        };
    }).filter(Boolean);
}
 
function updatePianoRollTextBox() {
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    document.getElementById("pianoRoll").value = targetPianoRoll.map(n =>
        `start:${n.start_16th} scale:${n.scale} len:${n.length_16th}`
    ).join('\n');
}
 
function editNote() {
    const textData = document.getElementById("pianoRoll").value.trim();
    const newPianoRoll = textData.split("\n").map(line => {
        try {
            const startMatch = line.match(/start:(\d+)/);
            const scaleMatch = line.match(/scale:(\S+)/);
            const lenMatch = line.match(/len:(\d+)/);
            if (startMatch && scaleMatch && lenMatch) {
                return {
                    start_16th: parseInt(startMatch[1]),
                    scale: scaleMatch[1],
                    length_16th: parseInt(lenMatch[1])
                };
            } return null;
        } catch (e) { return null; }
    }).filter(Boolean);
    if (textData === "") {
        eval(`pianoRoll${currentTrack.slice(-1)} = []`);
    } else {
        eval(`pianoRoll${currentTrack.slice(-1)} = newPianoRoll`);
    }
    redrawAll();
}
 
function toggleDataDisplay() {
    const container = document.getElementById("data-display-container");
    container.classList.toggle("hidden");
    if (!container.classList.contains("hidden")) {
        updatePianoRollTextBox();
    }
}
 
const toggleKeyboard = () => document.getElementById("keyboard").classList.toggle("hidden");

const increaseOctave = () => {
    // ▼▼▼ 最大値を 7 -> 6 に変更 ▼▼▼
    if (currentOctave < 6) {
        document.getElementById("currentOctave").textContent = ++currentOctave;
    }
};

const decreaseOctave = () => { if (currentOctave > 1) document.getElementById("currentOctave").textContent = --currentOctave };
 
/**
 * 現在選択されているノートをクリップボードにコピーする
 */
function copySelectedNotes() {
    if (selectedNotes.length === 0) return;
 
    // 選択範囲の最初のノートの開始時間を基準点(0)とする
    const firstNoteTime = Math.min(...selectedNotes.map(n => n.start_16th));
    
    clipboard = selectedNotes.map(note => {
        return {
            // 基準点からの相対的な開始時間で保存
            start_16th: note.start_16th - firstNoteTime,
            scale: note.scale,
            length_16th: note.length_16th
        };
    });
    
    console.log("Notes copied to clipboard:", clipboard); // デバッグ用
}
 
/**
 * クリップボードのノートを現在の再生カーソル位置に貼り付ける
 */
 function pasteNotes() {
    if (clipboard.length === 0) {
        console.log("Clipboard is empty."); // デバッグ用
        return;
    }
 
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    const pasteStartTime = playbackCursor_16th;
 
    const newNotes = clipboard.map(note => {
        return {
            start_16th: pasteStartTime + note.start_16th,
            scale: note.scale,
            length_16th: note.length_16th
        };
    });
 
    // ▼▼▼ 重なりチェックを削除 ▼▼▼
    /*
    const isOverlapping = newNotes.some(newNote => ... );
    if (isOverlapping) {
        return;
    }
    */
    // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
 
    targetPianoRoll.push(...newNotes);
    targetPianoRoll.sort((a, b) => a.start_16th - b.start_16th);
    
    // 貼り付け後、新しく貼り付けたノートを選択状態にする
    selectedNotes = newNotes;
 
    // カーソルを貼り付けたノート群の末尾に移動
    const lastNoteEndTime = Math.max(...newNotes.map(n => n.start_16th + n.length_16th));
    playbackCursor_16th = lastNoteEndTime;
    Tone.Transport.seconds = playbackCursor_16th * Tone.Time("16n").toSeconds();
 
    redrawAll();
    updatePianoRollTextBox();
    console.log("Notes pasted.");
}
 
 /**
 * 現在の再生カーソル位置が画面の中心に来るように、ピアノロールを自動スクロールする
 */
function focusOnCursor() {
    const container = document.getElementById("pianoRollContainer");

    // --- 横方向のスクロール ---
    // カーソルのX座標(ピクセル単位)を計算
    const cursorX = pitchHeaderWidth + (playbackCursor_16th / 2) * cellWidth;
    // 画面中央より少し左(4分の1の位置)に来るように目標を設定
    const targetScrollLeft = cursorX - (container.clientWidth / 4);

    // `scrollLeft` は、コンテナの現在のスクロール位置
    const currentScrollLeft = container.scrollLeft;
    const viewWidth = container.clientWidth;

    // カーソルが画面外に出た場合のみスクロールを実行
    if (cursorX < currentScrollLeft || cursorX > currentScrollLeft + viewWidth) {
        container.scrollLeft = targetScrollLeft;
    }

    // --- 縦方向のスクロール ---
    // 最後に入力されたノートを探す(カーソル位置の直前にあるノート)
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    // playbackCursor_16thはノート入力後に進んでいるため、その前の位置のノートを探す
    const lastNote = targetPianoRoll.filter(n => n.start_16th < playbackCursor_16th).pop();

    if (lastNote && lastNote.scale) {
        const noteRow = noteScales.indexOf(lastNote.scale);
        if (noteRow !== -1) {
            // ノートのY座標(ピクセル単位)を計算
            const noteY = noteRow * cellHeight;
            // 画面の中央に来るように目標のスクロール位置を計算
            const targetScrollTop = noteY - (container.clientHeight / 2);

            const currentScrollTop = container.scrollTop;
            const viewHeight = container.clientHeight;

            // ノートが画面外に出た場合のみスクロールを実行
            if (noteY < currentScrollTop || noteY > currentScrollTop + viewHeight - cellHeight) {
                container.scrollTop = targetScrollTop;
            }
        }
    }
}
 
// ==================================================================
// ========== MIDIファイル入出力 ==========
// ==================================================================
 
/**
 * 現在のプロジェクトをMIDIファイルとして書き出す
 */
async function exportMidi() {
    // ▼▼▼ ここを修正 ▼▼▼
    // 1. 新しいMidiオブジェクトを作成
    const midi = new Midi(); // "Midi.Midi" から "Midi" に変更
    // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
 
    // 2. ヘッダー情報を設定
    midi.header.setTempo(parseInt(document.getElementById("tempo").value, 10));
    midi.header.timeSignatures.push({ ticks: 0, timeSignature: [4, 4] });
 
    // 3. 各トラックのノートをMIDIトラックに追加
    const allTracks = [
        { data: pianoRoll1, name: "Track 1", instrument: instrument1 },
        { data: pianoRoll2, name: "Track 2", instrument: instrument2 },
        { data: pianoRoll3, name: "Track 3", instrument: instrument3 },
        { data: pianoRoll4, name: "Track 4", instrument: instrument4 }
    ];
 
    allTracks.forEach((trackInfo, index) => {
        if (trackInfo.data.length === 0) return; // 空のトラックはスキップ
 
        const track = midi.addTrack();
        track.name = trackInfo.name;
        // 楽器の種類を設定 (簡略版)
        if(trackInfo.instrument.name === "Sampler") track.instrument.number = 0; // 0: Acoustic Grand Piano
 
        const sixteenthSecs = Tone.Time("16n").toSeconds();
 
        trackInfo.data.forEach(note => {
            if (note.scale !== 'rest') {
                track.addNote({
                    name: note.scale,
                    time: note.start_16th * sixteenthSecs,
                    duration: note.length_16th * sixteenthSecs,
                    velocity: 0.8 // ベロシティは固定
                });
            }
        });
    });
 
    // 4. MIDIデータをBlobとして生成
    const blob = new Blob([midi.toArray()], { type: "audio/midi" });
    
    // 5. ダウンロード用のリンクを作成してクリック
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "my_song.mid";
    a.click();
    URL.revokeObjectURL(a.href);
}
 
/**
 * ユーザーが選択したMIDIファイルを読み込む
 * @param {File} file - 読み込むファイルオブジェクト
 */
async function importMidi(file) {
    if (!file) return;
 
    const reader = new FileReader();
    reader.onload = async (e) => {
        try {
            // ▼▼▼ ここを修正 ▼▼▼
            const midi = new Midi(e.target.result); // "Midi.Midi" から "Midi" に変更
            // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
 
            resetMusic();
 
            if (midi.header.tempos.length > 0) {
                document.getElementById("tempo").value = midi.header.tempos[0].bpm;
                Tone.Transport.bpm.value = midi.header.tempos[0].bpm;
            }
 
            const allPianoRolls = [pianoRoll1, pianoRoll2, pianoRoll3, pianoRoll4];
            midi.tracks.forEach((track, index) => {
                if (index >= allPianoRolls.length) return; // 4トラックまで対応
 
                const targetPianoRoll = allPianoRolls[index];
                track.notes.forEach(note => {
                    // MIDIデータを16分音符単位に変換
                    const sixteenthTicks = Tone.Time("16n").toTicks();
                    
                    targetPianoRoll.push({
                        start_16th: Math.round(Tone.Time(note.time).toTicks() / sixteenthTicks),
                        scale: note.name,
                        length_16th: Math.round(Tone.Time(note.duration).toTicks() / sixteenthTicks)
                    });
                });
            });
 
            console.log("MIDI file imported successfully.");
            redrawAll();
            updatePianoRollTextBox();
 
        } catch (error) {
            console.error("Error parsing MIDI file:", error);
            alert("MIDIファイルの読み込みに失敗しました。");
        }
    };
    reader.readAsArrayBuffer(file);
}