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: { "A3":"A3.mp3", "A4":"A4.mp3", "A5":"A5.mp3", "C4":"C4.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 cellWidth = 50; const cellHeight = 17; 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 dragStartNote = null;
let selectionRect = { startX: 0, startY: 0, currentX: 0, currentY: 0 }; // 範囲選択の矩形座標
let dragStartEvent = null;

// ==================================================================
// ========== 初期化処理 ==========
// ==================================================================
document.addEventListener("DOMContentLoaded", () => {
    canvas = document.getElementById("pianoRollCanvas");
    ctx = canvas.getContext("2d");
    timelineCanvas = document.getElementById("timelineCanvas");
    timelineCtx = timelineCanvas.getContext("2d");
    
    setupMainControls();
    setupNoteTools();
    setupEventListeners();
    
    document.getElementById("setMeasuresButton").addEventListener("click", setNumMeasures);
    document.getElementById("showDataButton").addEventListener("click", toggleDataDisplay);
    
    resizeCanvas();
    redrawAll();
});
window.onload = () => drawKeyboard();

// ==================================================================
// ========== UIセットアップ ==========
// ==================================================================

function setupMainControls() {
    document.getElementById("playButton").addEventListener("click", playMusic);
    document.getElementById("pauseButton").addEventListener("click", pauseMusic);
    document.getElementById("stopButton").addEventListener("click", stopMusic);
    
    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]());
}

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);
    });
}

/**
 * 現在選択されている音符の長さを計算して返す(付点を考慮)
 * @returns {string} Tone.jsが解釈できる音長表記 (例: "4n", "8n.")
 */
function getCurrentNoteLength() {
    // 付点モードがオンの場合
    if (isDottedMode) {
        // 選択されている基本の長さに "." を付けて返す
        return `${currentNoteLengthBase}.`;
    }
    
    // 付点モードがオフの場合は、そのまま基本の長さを返す
    return currentNoteLengthBase;
}


// ==================================================================
// ========== Canvas描画関連 ==========
// ==================================================================

function resizeCanvas() {
    const newWidth = pitchHeaderWidth + (cellWidth * numColumns);
    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);
    canvas.addEventListener('mousemove', handleMouseMove);
    canvas.addEventListener('mouseup', handleMouseUp);
    canvas.addEventListener('mouseleave', handleMouseUp);
    timelineCanvas.addEventListener('mousedown', handleTimelineClick);
    window.addEventListener('keydown', handleKeyDown);
}

function getMousePos(event, useInitialEvent = false) {
    const sourceEvent = useInitialEvent ? dragStartEvent : event;
    const rect = canvas.getBoundingClientRect();
    return { 
        x: sourceEvent.clientX - rect.left, 
        y: sourceEvent.clientY - rect.top 
    };
}

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

        // 既に選択されていたノートの上をクリックした場合 -> グループ移動モード
        if (selectedNotes.includes(clickedNote)) {
            dragMode = 'move-group';
            // 全選択ノートの元の状態を保存
            selectedNotes.forEach(note => note.originalState = { ...note });
        } else {
            // 選択されていないノートの上 -> 通常の単一ノート操作モード
            const handleWidth = 4;
            const xInNote = pos.x - (pitchHeaderWidth + selectedNote.start_16th / 2 * cellWidth);
            if (xInNote < handleWidth) { dragMode = 'resize-left'; } 
            else if (xInNote > (selectedNote.length_16th / 2 * cellWidth) - handleWidth) { dragMode = 'resize-right'; } 
            else { dragMode = 'move'; }
        }
    } else {
        // --- 空の場所でクリック ---
        isSelecting = true;
        selectionRect.startX = pos.x;
        selectionRect.startY = pos.y;
        selectionRect.currentX = pos.x;
        selectionRect.currentY = pos.y;
        selectedNotes = [];
    }
}

/**
 * マウスが動いたときの処理(グループの縦横移動に対応)
 */
function handleMouseMove(event) {
    if (!isDragging && !isSelecting) return;

    const pos = getMousePos(event);
    
    // 現在のマウスカーソル位置(16分音符単位と音階単位)を計算
    const current_16th_col = Math.max(0, Math.floor((pos.x - pitchHeaderWidth) / (cellWidth / 2)));
    const currentRow = Math.floor(pos.y / cellHeight);

    if (dragMode === 'move-group') {
        // --- グループ移動 ---
        const diff_16th = current_16th_col - dragStart_16th;
        
        // ▼▼▼ 縦方向の移動量計算を追加 ▼▼▼
        const startRow = Math.floor(getMousePos(event, true).y / cellHeight); // ドラッグ開始時の行
        const dragStartNoteRow = noteScales.indexOf(dragStartNote.scale);
        const diff_row = currentRow - dragStartNoteRow;
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

        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) {
        // --- 単一ノートの移動 ---
        const diff_16th = current_16th_col - dragStart_16th;
        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) {
        const diff_16th = current_16th_col - dragStart_16th;
        selectedNote.length_16th = Math.max(1, dragStartNote.length_16th + diff_16th);
    } 
    else if (dragMode === 'resize-left' && selectedNote) {
        const diff_16th = current_16th_col - dragStart_16th;
        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;
            }
        }
    } 
    else if (isSelecting) {
        // --- 範囲選択 ---
        selectionRect.currentX = pos.x;
        selectionRect.currentY = pos.y;
        updateSelection();
    }
    
    redrawAll();
}

function handleMouseUp(event) {
    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));
    const scale = noteScales[Math.floor(pos.y / cellHeight)];
    
    // 短いクリックだったかどうかを判定(時間軸のみ)
    const isShortClick = dragStart_16th === up_16th_col;

    if (isSelecting) {
        // --- 範囲選択モードの終了 ---
        updateSelection();
        if (selectionRect.startX === selectionRect.currentX && selectionRect.startY === selectionRect.currentY) {
            selectedNotes = []; // 選択解除
        }
    } else if (isDragging && selectedNote) {
        // --- ノート操作モードの終了 ---
        
        // ▼▼▼ ここからロジックを修正 ▼▼▼

        // 1. 単一ノートの短いクリック -> 削除
        if (isShortClick && dragStartNote.scale === scale) {
            const noteIndexToDelete = targetPianoRoll.indexOf(selectedNote);
            if (noteIndexToDelete > -1) {
                targetPianoRoll.splice(noteIndexToDelete, 1);
            }
        } 
        // 2. それ以外のドラッグ操作 -> 変更を確定
        else {
             // (このelseブロックは、単一ノートのドラッグ or グループ移動の確定時に実行される)
             // handleMouseMoveで変更された値がselectedNoteオブジェクトに残っているので、
             // 特にここで何かをする必要はない。
             // ただし、グループ移動が終わった際のクリーンアップは必要。
            if (dragMode === 'move-group') {
                selectedNotes.forEach(note => delete note.originalState);
            }
        }
        // ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲

    } else if (dragMode === 'create' && isShortClick && 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()
            });
        }
    }

    // すべての操作の最後に状態をリセット
    targetPianoRoll.sort((a, b) => a.start_16th - b.start_16th);
    isDragging = false;
    isSelecting = false;
    dragMode = null;
    selectedNote = null;
    dragStartNote = null;

    redrawAll();
    updatePianoRollTextBox();
}

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以上の正の整数を入力してください。");
    }
}

function handleKeyDown(event) {
    if (selectedNotes.length === 0) return;

    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    const isHarmonyTrack = currentTrack === 'track3' || currentTrack === 'track4';
    
    let moved = false;
    switch(event.key) {
        case "ArrowUp":
            event.preventDefault();
            selectedNotes.forEach(note => {
                const currentIndex = noteScales.indexOf(note.scale);
                if (currentIndex > 0) note.scale = noteScales[currentIndex - 1];
            });
            moved = true;
            break;
        case "ArrowDown":
            event.preventDefault();
            selectedNotes.forEach(note => {
                const currentIndex = noteScales.indexOf(note.scale);
                if (currentIndex < noteScales.length - 1) note.scale = noteScales[currentIndex + 1];
            });
            moved = true;
            break;
        case "ArrowLeft":
            event.preventDefault();
            if (selectedNotes.every(note => note.start_16th > 0)) {
                selectedNotes.forEach(note => note.start_16th -= 1);
                moved = true;
            }
            break;
        case "ArrowRight":
            event.preventDefault();
            const maxCols = numColumns * 2;
            if(selectedNotes.every(note => (note.start_16th + note.length_16th) < maxCols)){
                selectedNotes.forEach(note => note.start_16th += 1);
                moved = true;
            }
            break;
        case "Delete":
        case "Backspace":
             event.preventDefault();
             selectedNotes.forEach(note => {
                 const index = targetPianoRoll.indexOf(note);
                 if (index > -1) targetPianoRoll.splice(index, 1);
             });
             selectedNotes = []; // ★ 選択をクリア
             moved = true;
             break;
    }

    if (moved) {
        redrawAll();
        updatePianoRollTextBox();
    }
}
// ==================================================================
// ========== MIDI & スクリーンキーボード入力 ==========
// ==================================================================
if (navigator.requestMIDIAccess) {
    navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
}

function onMIDISuccess(midiAccess) {
    document.getElementById("midistatus").innerText = "MIDI: 接続済み";
    midiAccess.inputs.forEach(input => {
        input.onmidimessage = handleMIDIMessage;
    });
}

function onMIDIFailure() {
    document.getElementById("midistatus").innerText = "MIDI: 接続失敗";
}

function handleMIDIMessage(event) {
    const [status, note, velocity] = event.data;
    const noteName = getNoteName(note);
    if (status === 144 && velocity > 0) {
        const instrument = eval(`instrument${currentTrack.slice(-1)}`);
        instrument.triggerAttack(noteName, Tone.now(), velocity / 127);
        recordNoteData({ scale: noteName, length: getCurrentNoteLength() });
    } else if (status === 128 || (status === 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}`;
}

function recordNoteData(noteObject) {
    if (isAuditionMode) {
        return;
    }
    const targetPianoRoll = eval(`pianoRoll${currentTrack.slice(-1)}`);
    
    // ▼▼▼ Math.round() を削除 ▼▼▼
    const noteLength_16th = Tone.Time(noteObject.length).toTicks() / Tone.Time("16n").toTicks();
    const start_16th = playbackCursor_16th;
    
    targetPianoRoll.push({
        start_16th: start_16th,
        scale: noteObject.scale,
        length_16th: noteLength_16th
    });

    // カーソルを進める際も、整数に丸めずに加算する
    playbackCursor_16th += noteLength_16th;
    Tone.Transport.seconds = playbackCursor_16th * Tone.Time("16n").toSeconds();

    targetPianoRoll.sort((a, b) => a.start_16th - b.start_16th);
    redrawAll();
    updatePianoRollTextBox();
}

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);
        }
    });
}

const drawKeyboard = () => {
    const musicalScaleArray=["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"];const keyboardDiv=document.getElementById("keyboard");let baseKey;for(let i=0;i<25;i++){const key=document.createElement("button");key.id=`key_${musicalScaleArray[i%12]}${Math.floor(i/12)+3}`;key.onmousedown=(e)=>{const scale=e.target.id.split("_")[1];const octaveAdjustedScale=adjustOctave(scale);activeKeyboardNote=octaveAdjustedScale;const instrument=eval(`instrument${currentTrack.slice(-1)}`);instrument.triggerAttack(octaveAdjustedScale);recordNoteData({scale:octaveAdjustedScale,length:getCurrentNoteLength()})};key.onmouseup=stop;key.onmouseleave=stop;if(key.id.includes("#")){key.classList.add("black")}else{key.classList.add("white");baseKey=document.createElement("div")}baseKey.appendChild(key);keyboardDiv.appendChild(baseKey)}};

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;
    }
};


// ==================================================================
// ========== 再生エンジン ==========
// ==================================================================
async function playMusic() {
    await Tone.start();
    Tone.Transport.bpm.value = document.getElementById("tempo").value;
    if (Tone.Transport.state !== 'started') {
        if (Tone.Transport.state !== 'paused') {
            await Tone.loaded();
            const totalDuration = numColumns * Tone.Time("8n").toSeconds();
            if (totalDuration === 0) return;
            Tone.Transport.loop = true;
            Tone.Transport.loopStart = 0;
            Tone.Transport.loopEnd = totalDuration;
            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);
    }
    redrawAll();
}

function setupPlayback() {
    if (part1) part1.dispose(); if (part2) part2.dispose();
    if (part3) part3.dispose(); if (part4) part4.dispose();
    Tone.Transport.cancel(0);
    
    const createPart = (pianoRoll, instrument) => {
        const partData = convertToPartData(pianoRoll);
        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操作 ==========
// ==================================================================
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 = () => { if (currentOctave < 7) document.getElementById("currentOctave").textContent = ++currentOctave };
const decreaseOctave = () => { if (currentOctave > 1) document.getElementById("currentOctave").textContent = --currentOctave };