MIDI-Melody-Generator / index.html
kolaslab's picture
Update index.html
4e9cf78 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MIDI Melody Generator</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js"></script>
<style>
:root {
--primary: #2c3e50;
--secondary: #3498db;
--accent: #e74c3c;
--background: #f5f6fa;
--surface: #ffffff;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
body {
background: var(--background);
color: var(--primary);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: var(--surface);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
padding: 2rem;
}
.workspace {
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
}
.panel {
background: #f8f9fa;
border-radius: 12px;
padding: 1.5rem;
}
.section {
margin-bottom: 2rem;
}
h2, h3 {
margin-bottom: 1rem;
color: var(--primary);
}
.control {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.9rem;
color: #666;
}
select, input[type="range"], input[type="number"] {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
}
input[type="range"] {
-webkit-appearance: none;
height: 8px;
background: #ddd;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
background: var(--secondary);
border-radius: 50%;
cursor: pointer;
}
.value-display {
font-size: 0.8rem;
color: var(--secondary);
text-align: right;
}
.editor {
display: flex;
flex-direction: column;
gap: 1rem;
}
.piano-roll {
background: #1a1a1a;
border-radius: 12px;
height: 500px;
position: relative;
overflow: hidden;
transform: scaleY(-1);
}
.grid {
position: absolute;
inset: 0;
display: grid;
grid-template-columns: repeat(32, 1fr);
grid-template-rows: repeat(88, 1fr);
gap: 1px;
padding: 1px;
background: #2a2a2a;
}
.cell {
background: #333;
cursor: pointer;
transition: all 0.1s ease;
}
.cell:hover {
background: #444;
}
.cell.active {
background: var(--secondary);
}
.transport {
display: flex;
gap: 1rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 12px;
}
button {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
color: white;
}
.btn-primary { background: var(--primary); }
.btn-secondary { background: var(--secondary); }
.btn-accent { background: var(--accent); }
button:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.synth-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.wave-selector {
display: flex;
gap: 0.5rem;
}
.wave-btn {
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 20px;
color: #666;
cursor: pointer;
}
.wave-btn.active {
background: var(--secondary);
color: white;
border-color: var(--secondary);
}
</style>
</head>
<body>
<div class="container">
<div class="workspace">
<div class="panel">
<div class="section">
<h3>Sound</h3>
<div class="wave-selector">
<div class="wave-btn active" data-wave="sine">Sine</div>
<div class="wave-btn" data-wave="square">Square</div>
<div class="wave-btn" data-wave="sawtooth">Saw</div>
</div>
</div>
<div class="section">
<h3>Key & Scale</h3>
<div class="control">
<label>Key</label>
<select id="key">
<option value="C">C</option>
<option value="C#">C#/Db</option>
<option value="D">D</option>
<option value="D#">D#/Eb</option>
<option value="E">E</option>
<option value="F">F</option>
<option value="F#">F#/Gb</option>
<option value="G">G</option>
<option value="G#">G#/Ab</option>
<option value="A">A</option>
<option value="A#">A#/Bb</option>
<option value="B">B</option>
</select>
</div>
<div class="control">
<label>Scale</label>
<select id="scale">
<option value="major">Major</option>
<option value="minor">Natural Minor</option>
<option value="harmonicMinor">Harmonic Minor</option>
<option value="dorian">Dorian</option>
<option value="phrygian">Phrygian</option>
<option value="lydian">Lydian</option>
<option value="mixolydian">Mixolydian</option>
</select>
</div>
</div>
<div class="section">
<h3>Rhythm</h3>
<div class="control">
<label>Tempo: <span id="tempo-value">120</span> BPM</label>
<input type="range" id="tempo" min="60" max="200" value="120">
</div>
<div class="control">
<label>Note Length</label>
<select id="noteLength">
<option value="1">Whole</option>
<option value="2">Half</option>
<option value="4" selected>Quarter</option>
<option value="8">Eighth</option>
<option value="16">Sixteenth</option>
</select>
</div>
</div>
<div class="section">
<h3>Melody</h3>
<div class="control">
<label>Complexity: <span id="complexity-value">5</span></label>
<input type="range" id="complexity" min="1" max="10" value="5">
</div>
<div class="control">
<label>Base Octave: <span id="octave-value">4</span></label>
<input type="range" id="octave" min="2" max="6" value="4">
</div>
</div>
</div>
<div class="editor">
<div class="piano-roll">
<div class="grid" id="grid"></div>
</div>
<div class="transport">
<button class="btn-primary" id="generate">Generate</button>
<button class="btn-secondary" id="play">Play</button>
<button class="btn-secondary" id="stop">Stop</button>
<button class="btn-accent" id="download">Download MIDI</button>
</div>
</div>
</div>
</div>
<script>
class MelodyGenerator {
constructor() {
this.synth = new Tone.PolySynth(Tone.Synth).toDestination();
this.sequence = [];
this.isPlaying = false;
this.currentWaveform = 'sine';
this.currentPart = null; // Store the current Tone.Part
this.initUI();
this.setupEventListeners();
}
initUI() {
// Initialize grid
const grid = document.getElementById('grid');
for (let i = 0; i < 88; i++) {
for (let j = 0; j < 32; j++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.note = i;
cell.dataset.time = j;
cell.onclick = () => this.toggleCell(cell);
grid.appendChild(cell);
}
}
// Initialize value displays
document.querySelectorAll('input[type="range"]').forEach(input => {
const display = document.getElementById(`${input.id}-value`);
if (display) display.textContent = input.value;
});
}
setupEventListeners() {
document.getElementById('generate').onclick = () => this.generateMelody();
document.getElementById('play').onclick = () => this.togglePlay();
document.getElementById('stop').onclick = () => this.stop();
document.getElementById('download').onclick = () => this.downloadMIDI();
// Waveform selection
document.querySelectorAll('.wave-btn').forEach(btn => {
btn.onclick = (e) => {
document.querySelectorAll('.wave-btn').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
this.currentWaveform = e.target.dataset.wave;
this.updateSynthSettings();
};
});
// Range input updates
document.querySelectorAll('input[type="range"]').forEach(input => {
input.oninput = (e) => {
const display = document.getElementById(`${input.id}-value`);
if (display) {
display.textContent = input.id === 'tempo' ?
`${e.target.value} BPM` : e.target.value;
}
};
});
}
updateSynthSettings() {
this.synth.set({
oscillator: { type: this.currentWaveform }
});
}
generateMelody() {
this.stop(); // Stop any existing playback
this.clearGrid();
const key = document.getElementById('key').value;
const scale = document.getElementById('scale').value;
const complexity = parseInt(document.getElementById('complexity').value);
const baseOctave = parseInt(document.getElementById('octave').value);
this.sequence = this.createMelodySequence(key, scale, complexity, baseOctave);
this.visualizeSequence();
}
createMelodySequence(key, scale, complexity, baseOctave) {
const noteCount = Math.floor(complexity * 4);
const sequence = [];
const scaleNotes = this.getScaleNotes(key, scale);
for (let i = 0; i < noteCount; i++) {
const time = Math.floor(Math.random() * 32);
const note = scaleNotes[Math.floor(Math.random() * scaleNotes.length)];
sequence.push({
note: `${note}${baseOctave}`,
time: time,
duration: 0.25
});
}
return sequence;
}
getScaleNotes(key, scale) {
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
const scales = {
major: [0, 2, 4, 5, 7, 9, 11],
minor: [0, 2, 3, 5, 7, 8, 10],
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
dorian: [0, 2, 3, 5, 7, 9, 10],
phrygian: [0, 1, 3, 5, 7, 8, 10],
lydian: [0, 2, 4, 6, 7, 9, 11],
mixolydian: [0, 2, 4, 5, 7, 9, 10]
};
const keyIndex = notes.indexOf(key);
const scalePattern = scales[scale];
return scalePattern.map(interval => notes[(keyIndex + interval) % 12]);
}
visualizeSequence() {
this.sequence.forEach(note => {
const noteIndex = this.getNoteIndex(note.note);
const cell = document.querySelector(
`.cell[data-note="${noteIndex}"][data-time="${note.time}"]`
);
if (cell) cell.classList.add('active');
});
}
getNoteIndex(note) {
const noteName = note.slice(0, -1);
const octave = parseInt(note.slice(-1));
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
return (octave * 12) + notes.indexOf(noteName);
}
async togglePlay() {
if (this.isPlaying) {
this.stop();
} else {
await Tone.start();
this.play();
}
}
play() {
if (this.currentPart) {
this.currentPart.stop();
this.currentPart.dispose();
}
this.isPlaying = true;
const tempo = document.getElementById('tempo').value;
Tone.Transport.bpm.value = tempo;
this.currentPart = new Tone.Part(((time, note) => {
this.synth.triggerAttackRelease(note.note, note.duration, time);
}), this.sequence.map(note => ({
time: note.time * 0.25,
note: note.note,
duration: note.duration
}))).start(0);
Tone.Transport.start();
}
stop() {
this.isPlaying = false;
if (this.currentPart) {
this.currentPart.stop();
this.currentPart.dispose();
}
Tone.Transport.stop();
Tone.Transport.position = 0;
}
toggleCell(cell) {
cell.classList.toggle('active');
}
clearGrid() {
document.querySelectorAll('.cell').forEach(cell => {
cell.classList.remove('active');
});
}
downloadMIDI() {
// Basic MIDI file structure
const midiData = [
0x4D, 0x54, 0x68, 0x64, // MThd
0x00, 0x00, 0x00, 0x06, // Header size
0x00, 0x01, // Format
0x00, 0x01, // Tracks
0x01, 0x80 // Division
];
const blob = new Blob([new Uint8Array(midiData)], { type: 'audio/midi' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'melody.mid';
a.click();
window.URL.revokeObjectURL(url);
}
}
const generator = new MelodyGenerator();
</script>
</body>
</html>