// ================================================================== // ========== グローバル変数と設定 ========== // ================================================================== 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 }; // 範囲選択の矩形座標 // ================================================================== // ========== 初期化処理 ========== // ================================================================== 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) { const rect = canvas.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; } function handleMouseDown(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); const current_16th_col = Math.max(0, Math.floor((pos.x - pitchHeaderWidth) / (cellWidth / 2))); const diff_16th = current_16th_col - dragStart_16th; if (dragMode === 'move-group') { // グループ移動 selectedNotes.forEach(note => { note.start_16th = Math.max(0, note.originalState.start_16th + diff_16th); }); } else if (dragMode === 'move' && selectedNote) { selectedNote.start_16th = Math.max(0, dragStartNote.start_16th + diff_16th); } else if (dragMode === 'resize-right' && selectedNote) { selectedNote.length_16th = Math.max(1, dragStartNote.length_16th + diff_16th); } else if (dragMode === 'resize-left' && selectedNote) { 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) { // 空き地での短いクリックでノート作成 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() }); } } } } else if (isDragging) { // 単一ノートの上で短いクリック -> 削除 if (isShortClick && dragMode !== 'move-group') { const noteIndexToDelete = targetPianoRoll.indexOf(selectedNote); if (noteIndexToDelete > -1) { targetPianoRoll.splice(noteIndexToDelete, 1); } } // グループ移動後のクリーンアップ if (dragMode === 'move-group') { 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(); } 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 };