Spaces:
Running
Running
<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> |