<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Easy Midi Player 1.0 </title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js" ></script>
<script src="https://unpkg.com/@tonejs/midi" ></script>
<style>
body { margin: 0 ; background: #111; color: white; font-family: sans-serif; overflow: hidden; }
canvas {
display: block;
margin: 0 auto;
background: #222;
border-left: 1 px solid #444;
border-right: 1 px solid #444;
cursor: default ;
}
#ui {
position: absolute;
top: 0 ;
left: 0 ;
width: 100 %;
background: rgba(0 , 0 , 0 , 0.9 );
padding: 8 px;
box-sizing: border-box;
border-bottom: 1 px solid #555;
display: flex;
gap: 15 px;
align-items: center;
justify-content: center;
z-index: 100 ;
}
.control-group { display: flex; align-items: center; gap: 5 px; }
button, select {
background: #444; color: white; border: 1px solid #777;
padding: 5 px 10 px; cursor: pointer; font-size: 14 px; border-radius: 4 px;
}
button:hover, select :hover { background: #666; }
button:disabled { color: #888; cursor: not-allowed; background: #333; }
#status { font-size: 12px; color: #0ff; margin-left: 10px; min-width: 100px;}
.instruction { font-size: 11 px; color: #aaa; position: absolute; bottom: 5px; right: 10px; }
</style>
</head>
<body>
<div id="ui" >
<div class ="control-group" >
<label>音色:</label>
<select id="soundMode" >
<option value ="piano" >🎹 Piano Mode</option>
<option value ="synth" selected>🎹 Synth Mode</option>
<option value ="beep" >👾 Beep Mode</option>
</select >
</div>
<div class ="control-group" >
<input type="file" id="fileInput" accept=".mid,.midi" style="width: 180px;" >
</div>
<button id="startBtn" disabled>再生スタート</button>
<div id="status" >待機中</div>
</div>
<div class ="instruction" >
[マウス移動 ] エリア移動 | [左クリック] エリア内再生 | [右クリック] 単音試聴 | [ESC] 停止
</div>
<canvas id="gameCanvas" ></canvas>
<script>
const CANVAS_WIDTH = 600 ;
const CANVAS_HEIGHT = window.innerHeight;
const NOTE_WIDTH = 40 ;
const NOTE_HEIGHT = 20 ;
const TARGET_HEIGHT = 50 ;
let canvas = document.getElementById('gameCanvas' );
let ctx = canvas.getContext('2d' );
let notes = [];
let isPlaying = false ;
let startTime = 0 ;
let targetY = CANVAS_HEIGHT - 100 ;
let pianoSampler = null ;
let polySynth = null ;
let beepSynth = null ;
let isPianoLoaded = false ;
let areaFlash = 0.0 ;
const startBtn = document.getElementById('startBtn' );
const statusDiv = document.getElementById('status' );
const modeSelect = document.getElementById('soundMode' );
window.onload = () => {
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
polySynth = new Tone.PolySynth(Tone.Synth, {
volume: -8 ,
oscillator: { type: "triangle" },
envelope: { attack: 0.02 , decay: 0.1 , sustain: 0.3 , release: 1 }
}).toDestination();
beepSynth = new Tone.PolySynth(Tone.Synth, {
volume: -10 ,
oscillator: { type: "square" },
envelope: { attack: 0.01 , decay: 0.1 , sustain: 0.1 , release: 0.1 }
}).toDestination();
pianoSampler = new Tone.Sampler({
urls: {
"C4" : "C4.mp3" , "D#4" : "Ds4.mp3" , "F#4" : "Fs4.mp3" , "A4" : "A4.mp3" ,
},
release: 1 ,
baseUrl: "https://tonejs.github.io/audio/salamander/" ,
onload: () => {
isPianoLoaded = true ;
console.log("Piano loaded" );
if (modeSelect.value === 'piano' ) statusDiv.innerText = "ピアノ準備完了" ;
}
}).toDestination();
requestAnimationFrame(loop);
};
function loop ( ) {
ctx.fillStyle = "#111" ;
ctx.fillRect(0 , 0 , CANVAS_WIDTH, CANVAS_HEIGHT);
if (areaFlash > 0 ) areaFlash -= 0.1 ;
if (areaFlash < 0 ) areaFlash = 0 ;
let r = 0 , g = 255 , b = 255 ;
if (areaFlash > 0 ) r = 255 ;
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${0.2 + areaFlash * 0.3 })`;
ctx.fillRect(0 , targetY, CANVAS_WIDTH, TARGET_HEIGHT);
ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 0.8 )`;
ctx.lineWidth = 2 ;
ctx.strokeRect(0 , targetY, CANVAS_WIDTH, TARGET_HEIGHT);
const now = isPlaying ? (Tone.now() - startTime) : 0 ;
notes.forEach(note => {
if (isPlaying) {
const distance = (note.time - now) * 200 ;
note.y = targetY - distance;
} else {
if (startTime === 0 ) note.y = -100 ;
}
if (note.flash > 0 ) {
note.flash -= 0.1 ;
if (note.flash < 0 ) note.flash = 0 ;
}
if (note.y > -50 && note.y < CANVAS_HEIGHT) {
ctx.fillStyle = note.flash > 0 ? "#FFF" : note.color;
ctx.fillRect(note.x, note.y, note.w, note.h);
ctx.strokeRect(note.x, note.y, note.w, note.h);
}
});
requestAnimationFrame(loop);
}
canvas.addEventListener('mousemove' , (e) => {
const rect = canvas.getBoundingClientRect();
const mouseY = e.clientY - rect.top;
targetY = mouseY - (TARGET_HEIGHT / 2 );
if (targetY < 50 ) targetY = 50 ;
if (targetY > CANVAS_HEIGHT - TARGET_HEIGHT) targetY = CANVAS_HEIGHT - TARGET_HEIGHT;
});
canvas.addEventListener('mousedown' , async (e) => {
await initAudioContext();
const rect = canvas.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
if (e.button === 0 ) {
if (mouseY >= targetY && mouseY <= targetY + TARGET_HEIGHT) {
areaFlash = 1.0 ;
notes.forEach(note => {
const noteTop = note.y;
const noteBottom = note.y + note.h;
const areaTop = targetY;
const areaBottom = targetY + TARGET_HEIGHT;
if (noteBottom >= areaTop && noteTop <= areaBottom) {
playTone(note.name, "8n" );
note.flash = 1.0 ;
}
});
}
}
else if (e.button === 2 ) {
let hit = false ;
notes.forEach(note => {
if (mouseX >= note.x && mouseX <= note.x + note.w &&
mouseY >= note.y && mouseY <= note.y + note.h) {
playTone(note.name, "8n" );
note.flash = 1.0 ;
hit = true ;
}
});
}
});
canvas.addEventListener('contextmenu' , (e) => {
e.preventDefault();
return false ;
});
window.addEventListener('keydown' , (e) => {
if (e.key === "Escape" ) {
stopGame();
}
});
function playTone (noteName, duration ) {
const mode = modeSelect.value ;
if (mode === 'piano' ) {
if (isPianoLoaded) {
pianoSampler.triggerAttackRelease(noteName, duration);
} else {
polySynth.triggerAttackRelease(noteName, duration);
}
} else if (mode === 'beep' ) {
beepSynth.triggerAttackRelease(noteName, duration);
} else {
polySynth.triggerAttackRelease(noteName, duration);
}
}
async function initAudioContext ( ) {
if (Tone.context.state !== 'running' ) {
await Tone.start();
}
}
function stopGame ( ) {
isPlaying = false ;
startBtn.disabled = false ;
statusDiv.innerText = "停止中" ;
}
document.getElementById('startBtn' ).addEventListener('click' , async () => {
await initAudioContext();
if (notes.length === 0 ) return ;
startTime = Tone.now() + 1.0 ;
isPlaying = true ;
startBtn.disabled = true ;
statusDiv.innerText = "再生中... [ESC]で停止" ;
});
document.getElementById('fileInput' ).addEventListener('change' , async (e) => {
const file = e.target.files[0 ];
if (!file) return ;
statusDiv.innerText = "解析中..." ;
const reader = new FileReader();
reader.onload = function(ev) {
try {
const midiData = new Midi(ev.target.result);
notes = [];
const colors = ["#ff5252" , "#448aff" , "#69f0ae" , "#ffd740" , "#e040fb" , "#536dfe" ];
midiData.tracks.forEach((track, i) => {
track.notes.forEach(n => {
notes.push({
name: n.name, time: n.time,
x: 50 + (n.midi % 12 ) * 42 , y: -100 , w: 40 , h: 20 ,
color: colors[i % colors.length], flash: 0
});
});
});
notes.sort((a,b) => a.time - b.time);
startBtn.disabled = false ;
statusDiv.innerText = `準備完了 (${notes.length}音)`;
} catch (err) {
console.error(err);
statusDiv.innerText = "エラー" ;
}
};
reader.readAsArrayBuffer(file);
});
modeSelect.addEventListener('change' , () => {
if (modeSelect.value === 'piano' && !isPianoLoaded) {
statusDiv.innerText = "ピアノ音源ロード中..." ;
} else {
statusDiv.innerText = "音色変更完了" ;
}
modeSelect.blur();
});
</script>
</body>
</html>
copy
コメント