Updated script that fixes the main problems.
Browse filesHey Kolaslab, Ive fixed some problems in the script. Using this updated html, the audio plays only the pattern displayed in the UI, so no more layering up each time play is pressed.
Also, the midi pattern is being displayed with the correct orientation. (it was upside down)
I'd really appreciate if you could update the space to use this edited code so people can appreciate the tool to its full potential! :)
- index.html +193 -182
index.html
CHANGED
@@ -110,6 +110,7 @@
|
|
110 |
height: 500px;
|
111 |
position: relative;
|
112 |
overflow: hidden;
|
|
|
113 |
}
|
114 |
|
115 |
.grid {
|
@@ -288,196 +289,206 @@
|
|
288 |
|
289 |
<script>
|
290 |
class MelodyGenerator {
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
304 |
-
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
}
|
313 |
-
}
|
314 |
-
|
315 |
-
// Initialize value displays
|
316 |
-
document.querySelectorAll('input[type="range"]').forEach(input => {
|
317 |
-
const display = document.getElementById(`${input.id}-value`);
|
318 |
-
if (display) display.textContent = input.value;
|
319 |
-
});
|
320 |
-
}
|
321 |
-
|
322 |
-
setupEventListeners() {
|
323 |
-
document.getElementById('generate').onclick = () => this.generateMelody();
|
324 |
-
document.getElementById('play').onclick = () => this.togglePlay();
|
325 |
-
document.getElementById('stop').onclick = () => this.stop();
|
326 |
-
document.getElementById('download').onclick = () => this.downloadMIDI();
|
327 |
-
|
328 |
-
// Waveform selection
|
329 |
-
document.querySelectorAll('.wave-btn').forEach(btn => {
|
330 |
-
btn.onclick = (e) => {
|
331 |
-
document.querySelectorAll('.wave-btn').forEach(b => b.classList.remove('active'));
|
332 |
-
e.target.classList.add('active');
|
333 |
-
this.currentWaveform = e.target.dataset.wave;
|
334 |
-
this.updateSynthSettings();
|
335 |
-
};
|
336 |
-
});
|
337 |
-
|
338 |
-
// Range input updates
|
339 |
-
document.querySelectorAll('input[type="range"]').forEach(input => {
|
340 |
-
input.oninput = (e) => {
|
341 |
-
const display = document.getElementById(`${input.id}-value`);
|
342 |
-
if (display) {
|
343 |
-
display.textContent = input.id === 'tempo' ?
|
344 |
-
`${e.target.value} BPM` : e.target.value;
|
345 |
-
}
|
346 |
-
};
|
347 |
-
});
|
348 |
-
}
|
349 |
-
|
350 |
-
updateSynthSettings() {
|
351 |
-
this.synth.set({
|
352 |
-
oscillator: { type: this.currentWaveform }
|
353 |
-
});
|
354 |
-
}
|
355 |
-
|
356 |
-
generateMelody() {
|
357 |
-
this.clearGrid();
|
358 |
-
const key = document.getElementById('key').value;
|
359 |
-
const scale = document.getElementById('scale').value;
|
360 |
-
const complexity = parseInt(document.getElementById('complexity').value);
|
361 |
-
const baseOctave = parseInt(document.getElementById('octave').value);
|
362 |
-
|
363 |
-
this.sequence = this.createMelodySequence(key, scale, complexity, baseOctave);
|
364 |
-
this.visualizeSequence();
|
365 |
-
}
|
366 |
-
|
367 |
-
createMelodySequence(key, scale, complexity, baseOctave) {
|
368 |
-
const noteCount = Math.floor(complexity * 4);
|
369 |
-
const sequence = [];
|
370 |
-
const scaleNotes = this.getScaleNotes(key, scale);
|
371 |
-
|
372 |
-
for (let i = 0; i < noteCount; i++) {
|
373 |
-
const time = Math.floor(Math.random() * 32);
|
374 |
-
const note = scaleNotes[Math.floor(Math.random() * scaleNotes.length)];
|
375 |
-
sequence.push({
|
376 |
-
note: `${note}${baseOctave}`,
|
377 |
-
time: time,
|
378 |
-
duration: 0.25
|
379 |
-
});
|
380 |
-
}
|
381 |
-
|
382 |
-
return sequence;
|
383 |
-
}
|
384 |
-
|
385 |
-
getScaleNotes(key, scale) {
|
386 |
-
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
387 |
-
const scales = {
|
388 |
-
major: [0, 2, 4, 5, 7, 9, 11],
|
389 |
-
minor: [0, 2, 3, 5, 7, 8, 10],
|
390 |
-
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
|
391 |
-
dorian: [0, 2, 3, 5, 7, 9, 10],
|
392 |
-
phrygian: [0, 1, 3, 5, 7, 8, 10],
|
393 |
-
lydian: [0, 2, 4, 6, 7, 9, 11],
|
394 |
-
mixolydian: [0, 2, 4, 5, 7, 9, 10]
|
395 |
-
};
|
396 |
-
|
397 |
-
const keyIndex = notes.indexOf(key);
|
398 |
-
const scalePattern = scales[scale];
|
399 |
-
return scalePattern.map(interval => notes[(keyIndex + interval) % 12]);
|
400 |
-
}
|
401 |
-
|
402 |
-
visualizeSequence() {
|
403 |
-
this.sequence.forEach(note => {
|
404 |
-
const noteIndex = this.getNoteIndex(note.note);
|
405 |
-
const cell = document.querySelector(
|
406 |
-
`.cell[data-note="${noteIndex}"][data-time="${note.time}"]`
|
407 |
-
);
|
408 |
-
if (cell) cell.classList.add('active');
|
409 |
-
});
|
410 |
-
}
|
411 |
-
|
412 |
-
getNoteIndex(note) {
|
413 |
-
const noteName = note.slice(0, -1);
|
414 |
-
const octave = parseInt(note.slice(-1));
|
415 |
-
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
416 |
-
return (octave * 12) + notes.indexOf(noteName);
|
417 |
}
|
|
|
418 |
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
425 |
}
|
426 |
-
}
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
449 |
|
450 |
-
|
451 |
-
|
452 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
453 |
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
458 |
-
|
459 |
|
460 |
-
|
461 |
-
|
462 |
-
|
463 |
-
|
464 |
-
|
465 |
-
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
474 |
-
|
475 |
-
|
476 |
-
|
477 |
-
|
|
|
|
|
478 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
|
480 |
const generator = new MelodyGenerator();
|
481 |
</script>
|
482 |
</body>
|
483 |
-
</html
|
|
|
110 |
height: 500px;
|
111 |
position: relative;
|
112 |
overflow: hidden;
|
113 |
+
transform: scaleY(-1);
|
114 |
}
|
115 |
|
116 |
.grid {
|
|
|
289 |
|
290 |
<script>
|
291 |
class MelodyGenerator {
|
292 |
+
constructor() {
|
293 |
+
this.synth = new Tone.PolySynth(Tone.Synth).toDestination();
|
294 |
+
this.sequence = [];
|
295 |
+
this.isPlaying = false;
|
296 |
+
this.currentWaveform = 'sine';
|
297 |
+
this.currentPart = null; // Store the current Tone.Part
|
298 |
+
this.initUI();
|
299 |
+
this.setupEventListeners();
|
300 |
+
}
|
301 |
+
|
302 |
+
initUI() {
|
303 |
+
// Initialize grid
|
304 |
+
const grid = document.getElementById('grid');
|
305 |
+
for (let i = 0; i < 88; i++) {
|
306 |
+
for (let j = 0; j < 32; j++) {
|
307 |
+
const cell = document.createElement('div');
|
308 |
+
cell.className = 'cell';
|
309 |
+
cell.dataset.note = i;
|
310 |
+
cell.dataset.time = j;
|
311 |
+
cell.onclick = () => this.toggleCell(cell);
|
312 |
+
grid.appendChild(cell);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
313 |
}
|
314 |
+
}
|
315 |
|
316 |
+
// Initialize value displays
|
317 |
+
document.querySelectorAll('input[type="range"]').forEach(input => {
|
318 |
+
const display = document.getElementById(`${input.id}-value`);
|
319 |
+
if (display) display.textContent = input.value;
|
320 |
+
});
|
321 |
+
}
|
322 |
+
|
323 |
+
setupEventListeners() {
|
324 |
+
document.getElementById('generate').onclick = () => this.generateMelody();
|
325 |
+
document.getElementById('play').onclick = () => this.togglePlay();
|
326 |
+
document.getElementById('stop').onclick = () => this.stop();
|
327 |
+
document.getElementById('download').onclick = () => this.downloadMIDI();
|
328 |
+
|
329 |
+
// Waveform selection
|
330 |
+
document.querySelectorAll('.wave-btn').forEach(btn => {
|
331 |
+
btn.onclick = (e) => {
|
332 |
+
document.querySelectorAll('.wave-btn').forEach(b => b.classList.remove('active'));
|
333 |
+
e.target.classList.add('active');
|
334 |
+
this.currentWaveform = e.target.dataset.wave;
|
335 |
+
this.updateSynthSettings();
|
336 |
+
};
|
337 |
+
});
|
338 |
+
|
339 |
+
// Range input updates
|
340 |
+
document.querySelectorAll('input[type="range"]').forEach(input => {
|
341 |
+
input.oninput = (e) => {
|
342 |
+
const display = document.getElementById(`${input.id}-value`);
|
343 |
+
if (display) {
|
344 |
+
display.textContent = input.id === 'tempo' ?
|
345 |
+
`${e.target.value} BPM` : e.target.value;
|
346 |
}
|
347 |
+
};
|
348 |
+
});
|
349 |
+
}
|
350 |
+
|
351 |
+
updateSynthSettings() {
|
352 |
+
this.synth.set({
|
353 |
+
oscillator: { type: this.currentWaveform }
|
354 |
+
});
|
355 |
+
}
|
356 |
+
|
357 |
+
generateMelody() {
|
358 |
+
this.stop(); // Stop any existing playback
|
359 |
+
this.clearGrid();
|
360 |
+
const key = document.getElementById('key').value;
|
361 |
+
const scale = document.getElementById('scale').value;
|
362 |
+
const complexity = parseInt(document.getElementById('complexity').value);
|
363 |
+
const baseOctave = parseInt(document.getElementById('octave').value);
|
364 |
+
|
365 |
+
this.sequence = this.createMelodySequence(key, scale, complexity, baseOctave);
|
366 |
+
this.visualizeSequence();
|
367 |
+
}
|
368 |
+
|
369 |
+
createMelodySequence(key, scale, complexity, baseOctave) {
|
370 |
+
const noteCount = Math.floor(complexity * 4);
|
371 |
+
const sequence = [];
|
372 |
+
const scaleNotes = this.getScaleNotes(key, scale);
|
373 |
+
|
374 |
+
for (let i = 0; i < noteCount; i++) {
|
375 |
+
const time = Math.floor(Math.random() * 32);
|
376 |
+
const note = scaleNotes[Math.floor(Math.random() * scaleNotes.length)];
|
377 |
+
sequence.push({
|
378 |
+
note: `${note}${baseOctave}`,
|
379 |
+
time: time,
|
380 |
+
duration: 0.25
|
381 |
+
});
|
382 |
+
}
|
383 |
|
384 |
+
return sequence;
|
385 |
+
}
|
386 |
+
|
387 |
+
getScaleNotes(key, scale) {
|
388 |
+
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
389 |
+
const scales = {
|
390 |
+
major: [0, 2, 4, 5, 7, 9, 11],
|
391 |
+
minor: [0, 2, 3, 5, 7, 8, 10],
|
392 |
+
harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
|
393 |
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
394 |
+
phrygian: [0, 1, 3, 5, 7, 8, 10],
|
395 |
+
lydian: [0, 2, 4, 6, 7, 9, 11],
|
396 |
+
mixolydian: [0, 2, 4, 5, 7, 9, 10]
|
397 |
+
};
|
398 |
+
|
399 |
+
const keyIndex = notes.indexOf(key);
|
400 |
+
const scalePattern = scales[scale];
|
401 |
+
return scalePattern.map(interval => notes[(keyIndex + interval) % 12]);
|
402 |
+
}
|
403 |
+
|
404 |
+
visualizeSequence() {
|
405 |
+
this.sequence.forEach(note => {
|
406 |
+
const noteIndex = this.getNoteIndex(note.note);
|
407 |
+
const cell = document.querySelector(
|
408 |
+
`.cell[data-note="${noteIndex}"][data-time="${note.time}"]`
|
409 |
+
);
|
410 |
+
if (cell) cell.classList.add('active');
|
411 |
+
});
|
412 |
+
}
|
413 |
+
|
414 |
+
getNoteIndex(note) {
|
415 |
+
const noteName = note.slice(0, -1);
|
416 |
+
const octave = parseInt(note.slice(-1));
|
417 |
+
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
418 |
+
return (octave * 12) + notes.indexOf(noteName);
|
419 |
+
}
|
420 |
+
|
421 |
+
async togglePlay() {
|
422 |
+
if (this.isPlaying) {
|
423 |
+
this.stop();
|
424 |
+
} else {
|
425 |
+
await Tone.start();
|
426 |
+
this.play();
|
427 |
+
}
|
428 |
+
}
|
429 |
|
430 |
+
play() {
|
431 |
+
if (this.currentPart) {
|
432 |
+
this.currentPart.stop();
|
433 |
+
this.currentPart.dispose();
|
434 |
+
}
|
435 |
|
436 |
+
this.isPlaying = true;
|
437 |
+
const tempo = document.getElementById('tempo').value;
|
438 |
+
Tone.Transport.bpm.value = tempo;
|
439 |
+
|
440 |
+
this.currentPart = new Tone.Part(((time, note) => {
|
441 |
+
this.synth.triggerAttackRelease(note.note, note.duration, time);
|
442 |
+
}), this.sequence.map(note => ({
|
443 |
+
time: note.time * 0.25,
|
444 |
+
note: note.note,
|
445 |
+
duration: note.duration
|
446 |
+
}))).start(0);
|
447 |
+
|
448 |
+
Tone.Transport.start();
|
449 |
+
}
|
450 |
+
|
451 |
+
stop() {
|
452 |
+
this.isPlaying = false;
|
453 |
+
if (this.currentPart) {
|
454 |
+
this.currentPart.stop();
|
455 |
+
this.currentPart.dispose();
|
456 |
}
|
457 |
+
Tone.Transport.stop();
|
458 |
+
Tone.Transport.position = 0;
|
459 |
+
}
|
460 |
+
|
461 |
+
toggleCell(cell) {
|
462 |
+
cell.classList.toggle('active');
|
463 |
+
}
|
464 |
+
|
465 |
+
clearGrid() {
|
466 |
+
document.querySelectorAll('.cell').forEach(cell => {
|
467 |
+
cell.classList.remove('active');
|
468 |
+
});
|
469 |
+
}
|
470 |
+
|
471 |
+
downloadMIDI() {
|
472 |
+
// Basic MIDI file structure
|
473 |
+
const midiData = [
|
474 |
+
0x4D, 0x54, 0x68, 0x64, // MThd
|
475 |
+
0x00, 0x00, 0x00, 0x06, // Header size
|
476 |
+
0x00, 0x01, // Format
|
477 |
+
0x00, 0x01, // Tracks
|
478 |
+
0x01, 0x80 // Division
|
479 |
+
];
|
480 |
+
|
481 |
+
const blob = new Blob([new Uint8Array(midiData)], { type: 'audio/midi' });
|
482 |
+
const url = window.URL.createObjectURL(blob);
|
483 |
+
const a = document.createElement('a');
|
484 |
+
a.href = url;
|
485 |
+
a.download = 'melody.mid';
|
486 |
+
a.click();
|
487 |
+
window.URL.revokeObjectURL(url);
|
488 |
+
}
|
489 |
+
}
|
490 |
|
491 |
const generator = new MelodyGenerator();
|
492 |
</script>
|
493 |
</body>
|
494 |
+
</html>
|