SREAL's picture
Update index.html
54a93f6 verified
<!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() {
// Initialize Tone.js components
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'; // default key
this.scale = 'major'; // default scale
this.complexity = 5; // default complexity
this.baseOctave = 4; // default base octave
this.direction = 'mixed'; // default direction
this.tempo = 120; // default tempo
this.timeSignature = [4, 4]; // default time signature
this.swing = 0; // default swing
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);
}
}
// Set initial displayed values for sliders and selectors
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);
};
// Add event listeners for other controls
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; // -1, 0, or 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');
// Update sequence based on UI state
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>