| <!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; |
| this.initUI(); |
| this.setupEventListeners(); |
| } |
| |
| initUI() { |
| |
| 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); |
| } |
| } |
| |
| |
| 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(); |
| |
| |
| 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(); |
| }; |
| }); |
| |
| |
| 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(); |
| 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() { |
| |
| 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 MelodyGenerator(); |
| </script> |
| </body> |
| </html> |