|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Advanced 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: 350px 1fr; |
|
gap: 2rem; |
|
} |
|
|
|
.panel { |
|
background: #f8f9fa; |
|
border-radius: 12px; |
|
padding: 1.5rem; |
|
height: fit-content; |
|
} |
|
|
|
.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; |
|
} |
|
|
|
.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.white-key { |
|
background: #444; |
|
} |
|
|
|
.cell.black-key { |
|
background: #222; |
|
} |
|
|
|
.cell:hover { |
|
background: #555; |
|
} |
|
|
|
.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: grid; |
|
grid-template-columns: repeat(2, 1fr); |
|
gap: 1rem; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.wave-selector { |
|
display: flex; |
|
gap: 0.5rem; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.wave-btn { |
|
padding: 0.5rem 1rem; |
|
background: white; |
|
border: 1px solid #ddd; |
|
border-radius: 20px; |
|
color: #666; |
|
cursor: pointer; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.wave-btn.active { |
|
background: var(--secondary); |
|
color: white; |
|
border-color: var(--secondary); |
|
} |
|
|
|
.chord-progression { |
|
display: grid; |
|
grid-template-columns: repeat(4, 1fr); |
|
gap: 0.5rem; |
|
margin-top: 1rem; |
|
} |
|
|
|
.chord-slot { |
|
padding: 0.5rem; |
|
text-align: center; |
|
background: white; |
|
border: 1px solid #ddd; |
|
border-radius: 6px; |
|
font-size: 0.9rem; |
|
} |
|
|
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<div class="workspace"> |
|
<div class="panel"> |
|
<div class="section"> |
|
<h3>Sound Design</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 class="wave-btn" data-wave="triangle">Triangle</div> |
|
</div> |
|
|
|
<div class="control"> |
|
<label>Filter Cutoff</label> |
|
<input type="range" id="filterCutoff" min="20" max="20000" value="2000"> |
|
</div> |
|
|
|
<div class="control"> |
|
<label>Resonance</label> |
|
<input type="range" id="filterResonance" min="0" max="20" value="1"> |
|
</div> |
|
</div> |
|
|
|
<div class="section"> |
|
<h3>Music Theory</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>Mode/Scale</label> |
|
<select id="scale"> |
|
<option value="major">Major</option> |
|
<option value="minor">Natural Minor</option> |
|
<option value="harmonicMinor">Harmonic Minor</option> |
|
<option value="melodicMinor">Melodic Minor</option> |
|
<option value="dorian">Dorian</option> |
|
<option value="phrygian">Phrygian</option> |
|
<option value="lydian">Lydian</option> |
|
<option value="mixolydian">Mixolydian</option> |
|
<option value="locrian">Locrian</option> |
|
</select> |
|
</div> |
|
|
|
<div class="control"> |
|
<label>Chord Progression</label> |
|
<select id="chordProgression"> |
|
<option value="I-IV-V-I">I-IV-V-I (Pop)</option> |
|
<option value="ii-V-I">ii-V-I (Jazz)</option> |
|
<option value="I-vi-IV-V">I-vi-IV-V (50s)</option> |
|
<option value="I-V-vi-IV">I-V-vi-IV (Pop)</option> |
|
<option value="vi-IV-I-V">vi-IV-I-V (Pop)</option> |
|
</select> |
|
<div class="chord-progression" id="progressionDisplay"> |
|
<div class="chord-slot">I</div> |
|
<div class="chord-slot">IV</div> |
|
<div class="chord-slot">V</div> |
|
<div class="chord-slot">I</div> |
|
</div> |
|
</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>Time Signature</label> |
|
<select id="timeSignature"> |
|
<option value="4/4">4/4</option> |
|
<option value="3/4">3/4</option> |
|
<option value="6/8">6/8</option> |
|
<option value="5/4">5/4</option> |
|
</select> |
|
</div> |
|
|
|
<div class="control"> |
|
<label>Swing</label> |
|
<input type="range" id="swing" min="0" max="100" value="0"> |
|
</div> |
|
</div> |
|
|
|
<div class="section"> |
|
<h3>Generation</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 class="control"> |
|
<label>Melodic Direction</label> |
|
<select id="direction"> |
|
<option value="ascending">Ascending</option> |
|
<option value="descending">Descending</option> |
|
<option value="mixed">Mixed</option> |
|
</select> |
|
</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="clear">Clear</button> |
|
<button class="btn-accent" id="download">Download MIDI</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
class AdvancedMelodyGenerator { |
|
constructor() { |
|
|
|
this.filter = new Tone.Filter({ |
|
type: "lowpass", |
|
frequency: 2000, |
|
Q: 1 |
|
}).toDestination(); |
|
|
|
this.synth = new Tone.PolySynth(Tone.Synth, { |
|
oscillator: { |
|
type: "sine" |
|
}, |
|
envelope: { |
|
attack: 0.05, |
|
decay: 0.2, |
|
sustain: 0.2, |
|
release: 0.5 |
|
} |
|
}).connect(this.filter); |
|
|
|
this.sequence = []; |
|
this.isPlaying = false; |
|
this.currentPart = null; |
|
this.key = 'C'; |
|
this.scale = 'major'; |
|
this.complexity = 5; |
|
this.baseOctave = 4; |
|
this.direction = 'mixed'; |
|
this.tempo = 120; |
|
this.timeSignature = [4, 4]; |
|
this.swing = 0; |
|
this.initUI(); |
|
this.setupEventListeners(); |
|
} |
|
|
|
initUI() { |
|
const grid = document.getElementById('grid'); |
|
const notes = this.getAllNotes(); |
|
|
|
for (let i = 0; i < 88; i++) { |
|
for (let j = 0; j < 32; j++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'cell'; |
|
const noteName = notes[i].replace(/[0-9]/, ''); |
|
cell.classList.add(noteName.includes('#') ? 'black-key' : 'white-key'); |
|
cell.dataset.note = notes[i]; |
|
cell.dataset.time = j; |
|
cell.onclick = () => this.toggleCell(cell); |
|
grid.appendChild(cell); |
|
} |
|
} |
|
|
|
|
|
document.getElementById('tempo').value = this.tempo; |
|
document.getElementById('tempo-value').textContent = this.tempo; |
|
document.getElementById('complexity').value = this.complexity; |
|
document.getElementById('complexity-value').textContent = this.complexity; |
|
document.getElementById('octave').value = this.baseOctave; |
|
document.getElementById('octave-value').textContent = this.baseOctave; |
|
document.getElementById('direction').value = this.direction; |
|
document.getElementById('key').value = this.key; |
|
document.getElementById('scale').value = this.scale; |
|
document.getElementById('timeSignature').value = this.timeSignature.join('/'); |
|
document.getElementById('swing').value = this.swing * 100; |
|
} |
|
|
|
getAllNotes() { |
|
const notes = []; |
|
const noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; |
|
for (let octave = 0; octave < 8; octave++) { |
|
noteNames.forEach(note => { |
|
notes.push(`${note}${octave}`); |
|
}); |
|
} |
|
return notes; |
|
} |
|
|
|
setupEventListeners() { |
|
document.getElementById('generate').onclick = () => this.generateMelody(); |
|
document.getElementById('play').onclick = () => this.togglePlay(); |
|
document.getElementById('stop').onclick = () => this.stop(); |
|
document.getElementById('clear').onclick = () => this.clearGrid(); |
|
document.getElementById('download').onclick = () => this.downloadMIDI(); |
|
|
|
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.updateSynthSettings('oscillator', { type: e.target.dataset.wave }); |
|
}; |
|
}); |
|
|
|
document.getElementById('filterCutoff').oninput = (e) => { |
|
this.filter.frequency.value = e.target.value; |
|
}; |
|
|
|
document.getElementById('filterResonance').oninput = (e) => { |
|
this.filter.Q.value = e.target.value; |
|
}; |
|
|
|
document.getElementById('chordProgression').onchange = (e) => { |
|
this.updateChordDisplay(e.target.value); |
|
}; |
|
|
|
|
|
document.getElementById('tempo').oninput = (e) => { |
|
this.tempo = e.target.value; |
|
Tone.Transport.bpm.value = this.tempo; |
|
document.getElementById('tempo-value').textContent = this.tempo; |
|
}; |
|
|
|
document.getElementById('timeSignature').onchange = (e) => { |
|
this.timeSignature = e.target.value.split('/'); |
|
Tone.Transport.timeSignature = this.timeSignature; |
|
}; |
|
|
|
document.getElementById('swing').oninput = (e) => { |
|
this.swing = e.target.value / 100; |
|
Tone.Transport.swing = this.swing; |
|
}; |
|
|
|
document.getElementById('complexity').oninput = (e) => { |
|
this.complexity = e.target.value; |
|
document.getElementById('complexity-value').textContent = this.complexity; |
|
}; |
|
|
|
document.getElementById('octave').oninput = (e) => { |
|
this.baseOctave = e.target.value; |
|
document.getElementById('octave-value').textContent = this.baseOctave; |
|
}; |
|
|
|
document.getElementById('direction').onchange = (e) => { |
|
this.direction = e.target.value; |
|
}; |
|
|
|
document.getElementById('key').onchange = (e) => { |
|
this.key = e.target.value; |
|
}; |
|
|
|
document.getElementById('scale').onchange = (e) => { |
|
this.scale = e.target.value; |
|
}; |
|
} |
|
|
|
updateChordDisplay(progression) { |
|
const chords = progression.split('-'); |
|
const display = document.getElementById('progressionDisplay'); |
|
display.innerHTML = ''; |
|
chords.forEach(chord => { |
|
const slot = document.createElement('div'); |
|
slot.className = 'chord-slot'; |
|
slot.textContent = chord; |
|
display.appendChild(slot); |
|
}); |
|
} |
|
|
|
updateSynthSettings(param, value) { |
|
this.synth.set({ |
|
[param]: value |
|
}); |
|
} |
|
|
|
getChordProgression(key, type) { |
|
const progressions = { |
|
'I-IV-V-I': ['I', 'IV', 'V', 'I'], |
|
'ii-V-I': ['ii', 'V', 'I'], |
|
'I-vi-IV-V': ['I', 'vi', 'IV', 'V'], |
|
'I-V-vi-IV': ['I', 'V', 'vi', 'IV'], |
|
'vi-IV-I-V': ['vi', 'IV', 'I', 'V'] |
|
}; |
|
|
|
const chordMap = { |
|
'I': this.getChord(key, 1), |
|
'ii': this.getChord(key, 2), |
|
'iii': this.getChord(key, 3), |
|
'IV': this.getChord(key, 4), |
|
'V': this.getChord(key, 5), |
|
'vi': this.getChord(key, 6), |
|
'vii': this.getChord(key, 7) |
|
}; |
|
|
|
return progressions[type].map(chord => chordMap[chord]); |
|
} |
|
|
|
getChord(root, degree) { |
|
const scale = this.getScaleNotes(root, document.getElementById('scale').value); |
|
const chordTones = [0, 2, 4].map(interval => scale[(degree - 1 + interval) % 7]); |
|
return chordTones; |
|
} |
|
|
|
generateMelody() { |
|
this.stop(); |
|
this.clearGrid(); |
|
|
|
const progression = document.getElementById('chordProgression').value; |
|
|
|
const chords = this.getChordProgression(this.key, progression); |
|
this.sequence = this.createMelodySequence(this.key, this.scale, this.complexity, this.baseOctave, chords); |
|
this.visualizeSequence(); |
|
} |
|
|
|
createMelodySequence(key, scale, complexity, baseOctave, chords) { |
|
const sequence = []; |
|
const scaleNotes = this.getScaleNotes(key, scale); |
|
const noteCount = Math.floor(complexity * 4); |
|
const direction = document.getElementById('direction').value; |
|
|
|
let previousNote = null; |
|
for (let i = 0; i < noteCount; i++) { |
|
const time = Math.floor(i * (32 / noteCount)); |
|
let note; |
|
|
|
if (direction === 'ascending') { |
|
note = scaleNotes[i % scaleNotes.length]; |
|
} else if (direction === 'descending') { |
|
note = scaleNotes[scaleNotes.length - 1 - (i % scaleNotes.length)]; |
|
} else { |
|
if (previousNote) { |
|
const currentIndex = scaleNotes.indexOf(previousNote.replace(/[0-9]/, '')); |
|
const step = Math.floor(Math.random() * 3) - 1; |
|
const newIndex = Math.min(Math.max(0, currentIndex + step), scaleNotes.length - 1); |
|
note = scaleNotes[newIndex]; |
|
} else { |
|
note = scaleNotes[Math.floor(Math.random() * scaleNotes.length)]; |
|
} |
|
} |
|
|
|
const octaveOffset = Math.floor(Math.random() * 2); |
|
const fullNote = `${note}${baseOctave + octaveOffset}`; |
|
previousNote = fullNote; |
|
|
|
sequence.push({ |
|
note: fullNote, |
|
time: time, |
|
duration: 0.25 |
|
}); |
|
} |
|
|
|
return sequence; |
|
} |
|
|
|
visualizeSequence() { |
|
this.sequence.forEach(note => { |
|
const noteIndex = this.getNoteIndex(note.note); |
|
const cell = document.querySelector( |
|
`.cell[data-note="${note.note}"][data-time="${note.time}"]` |
|
); |
|
if (cell) cell.classList.add('active'); |
|
}); |
|
} |
|
|
|
getNoteIndex(note) { |
|
const notes = this.getAllNotes(); |
|
return notes.indexOf(note); |
|
} |
|
|
|
togglePlay() { |
|
if (this.isPlaying) { |
|
this.stop(); |
|
} else { |
|
Tone.start().then(() => this.play()); |
|
} |
|
} |
|
|
|
play() { |
|
this.isPlaying = true; |
|
Tone.Transport.bpm.value = this.tempo; |
|
|
|
if (this.currentPart) { |
|
this.currentPart.dispose(); |
|
} |
|
|
|
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.dispose(); |
|
} |
|
Tone.Transport.stop(); |
|
} |
|
|
|
toggleCell(cell) { |
|
cell.classList.toggle('active'); |
|
|
|
const activeNotes = Array.from(document.querySelectorAll('.cell.active')).map(cell => ({ |
|
note: cell.dataset.note, |
|
time: parseInt(cell.dataset.time), |
|
duration: 0.25 |
|
})); |
|
this.sequence = activeNotes; |
|
} |
|
|
|
clearGrid() { |
|
document.querySelectorAll('.cell').forEach(cell => { |
|
cell.classList.remove('active'); |
|
}); |
|
this.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], |
|
melodicMinor: [0, 2, 3, 5, 7, 9, 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], |
|
locrian: [0, 1, 3, 5, 6, 8, 10] |
|
}; |
|
|
|
const keyIndex = notes.indexOf(key); |
|
return scales[scale].map(interval => notes[(keyIndex + interval) % 12]); |
|
} |
|
|
|
downloadMIDI() { |
|
const midiData = [ |
|
0x4D, 0x54, 0x68, 0x64, |
|
0x00, 0x00, 0x00, 0x06, |
|
0x00, 0x01, |
|
0x00, 0x01, |
|
0x01, 0x80 |
|
]; |
|
|
|
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 AdvancedMelodyGenerator(); |
|
</script> |
|
</body> |
|
</html> |