<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<title>Easy MIDI Player 1.1 </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: 120px;}
.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" >🎹 Grand Piano</option>
<option value ="guitar" >🎸 Acoustic Guitar</option>
<option value ="fm" >✨ FM Electric</option>
<option value ="strings" >🎻 Strings (Synth)</option>
<option value ="synth" selected>🎹 Basic Synth</option>
<option value ="beep" >👾 8 -bit Beep</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 ;
const FALL_SPEED = 200 ;
let canvas = document.getElementById('gameCanvas' );
let ctx = canvas.getContext('2d' );
let notes = [];
let isPlaying = false ;
let startTime = 0 ;
let songDuration = 0 ;
let targetY = CANVAS_HEIGHT - 100 ;
let areaFlash = 0.0 ;
const startBtn = document.getElementById('startBtn' );
const statusDiv = document.getElementById('status' );
const modeSelect = document.getElementById('soundMode' );
const instruments = {
piano: null ,
guitar: null ,
synth: null ,
beep: null ,
fm: null ,
strings: null
};
let loadedSamples = { piano: false , guitar: false };
window.onload = () => {
canvas.width = CANVAS_WIDTH;
canvas.height = CANVAS_HEIGHT;
instruments.synth = new Tone.PolySynth(Tone.Synth, {
volume: -8 , oscillator: { type: "triangle" },
envelope: { attack: 0.02 , decay: 0.1 , sustain: 0.3 , release: 1 }
}).toDestination();
instruments.beep = new Tone.PolySynth(Tone.Synth, {
volume: -10 , oscillator: { type: "square" },
envelope: { attack: 0.01 , decay: 0.1 , sustain: 0.1 , release: 0.1 }
}).toDestination();
instruments.fm = new Tone.PolySynth(Tone.FMSynth, {
volume: -8 , harmonicity: 3 , modulationIndex: 10 ,
envelope: { attack: 0.01 , decay: 0.2 },
modulation: { type: "square" },
modulationEnvelope: { attack: 0.5 , decay: 0.01 }
}).toDestination();
instruments.strings = new Tone.PolySynth(Tone.Synth, {
volume: -8 , oscillator: { type: "sawtooth" },
envelope: { attack: 0.2 , decay: 3 , sustain: 0.5 , release: 2 }
}).toDestination();
instruments.piano = 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: () => { loadedSamples.piano = true ; updateStatus(); }
}).toDestination();
instruments.guitar = new Tone.Sampler({
urls: { "C4" : "C4.mp3" , "E4" : "E4.mp3" , "G4" : "G4.mp3" },
release: 1 , baseUrl: "https://tonejs.github.io/audio/guitar-acoustic/" ,
onload: () => { loadedSamples.guitar = true ; updateStatus(); }
}).toDestination();
requestAnimationFrame(gameLoop);
};
function updateStatus ( ) {
const mode = modeSelect.value ;
if ((mode === 'piano' && !loadedSamples.piano) ||
(mode === 'guitar' && !loadedSamples.guitar)) {
statusDiv.innerText = "音源ロード中..." ;
statusDiv.style.color = "#FF0" ;
} else {
if (notes.length > 0 ) {
statusDiv.innerText = isPlaying ? "再生中" : "準備完了" ;
statusDiv.style.color = "#0FF" ;
} else {
statusDiv.innerText = "ファイル待機" ;
}
}
}
function gameLoop ( ) {
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 ;
if (isPlaying && now > songDuration + 2.0 ) {
stopGame();
}
notes.forEach(note => {
if (isPlaying) {
const timeDiff = note.time - now;
note.y = targetY - (timeDiff * FALL_SPEED);
} else {
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.strokeStyle = "rgba(0,0,0,0.5)" ;
ctx.lineWidth = 1 ;
ctx.strokeRect(note.x, note.y, note.w, note.h);
}
});
requestAnimationFrame(gameLoop);
}
canvas.addEventListener('mousemove' , (e) => {
const rect = canvas.getBoundingClientRect();
targetY = (e.clientY - rect.top) - (TARGET_HEIGHT / 2 );
if (targetY < 60 ) targetY = 60 ;
if (targetY > CANVAS_HEIGHT - TARGET_HEIGHT - 10 ) targetY = CANVAS_HEIGHT - TARGET_HEIGHT - 10 ;
});
canvas.addEventListener('mousedown' , async (e) => {
await initAudioContext();
const rect = canvas.getBoundingClientRect();
const my = e.clientY - rect.top;
const mx = e.clientX - rect.left;
if (e.button === 0 ) {
if (my >= targetY && my <= targetY + TARGET_HEIGHT) {
areaFlash = 1.0 ;
notes.forEach(note => {
const nTop = note.y;
const nBot = note.y + note.h;
const aTop = targetY;
const aBot = targetY + TARGET_HEIGHT;
if (nBot >= aTop && nTop <= aBot) {
playTone(note.name, "8n" );
note.flash = 1.0 ;
}
});
}
}
else if (e.button === 2 ) {
let hit = false ;
notes.forEach(note => {
if (mx >= note.x && mx <= note.x + note.w &&
my >= note.y && my <= note.y + note.h) {
playTone(note.name, "8n" );
note.flash = 1.0 ;
hit = true ;
}
});
}
});
canvas.addEventListener('contextmenu' , e => e.preventDefault());
window.addEventListener('keydown' , (e) => {
if (e.key === "Escape" ) stopGame();
});
startBtn.addEventListener('click' , async () => {
await initAudioContext();
if (notes.length === 0 ) return ;
startTime = Tone.now() + 1.0 ;
isPlaying = true ;
startBtn.innerText = "最初から再生" ;
statusDiv.innerText = "再生中..." ;
});
function stopGame ( ) {
isPlaying = false ;
startBtn.innerText = "最初から再生" ;
statusDiv.innerText = "停止中" ;
}
async function initAudioContext ( ) {
if (Tone.context.state !== 'running' ) await Tone.start();
}
function playTone (note, duration ) {
const mode = modeSelect.value ;
const inst = instruments[mode];
if (inst) {
if ((mode === 'piano' && !loadedSamples.piano) ||
(mode === 'guitar' && !loadedSamples.guitar)) {
instruments.synth.triggerAttackRelease(note, duration);
} else {
inst.triggerAttackRelease(note, duration);
}
}
}
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 = [];
songDuration = midiData.duration;
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 ;
startBtn.innerText = "再生スタート" ;
updateStatus();
} catch (err) {
console.error(err);
statusDiv.innerText = "解析エラー" ;
}
};
reader.readAsArrayBuffer(file);
});
modeSelect.addEventListener('change' , () => {
updateStatus();
modeSelect.blur();
});
</script>
</body>
</html>
copy
コメント