Skip to content

Instantly share code, notes, and snippets.

@qls0ulp
Last active December 18, 2020 13:36
Show Gist options
  • Save qls0ulp/4dbcd70b1760b654fe87f5a9b2858299 to your computer and use it in GitHub Desktop.
Save qls0ulp/4dbcd70b1760b654fe87f5a9b2858299 to your computer and use it in GitHub Desktop.
Seeded Procedural Music Generator
<header id="intro">
<section>
<h1>Procedurally Generated Music</h1>
<p>
This will infinitely generate a random song based on the <em>seed</em> that you provide it with.
Procedural generation ensures that the song will always be the exact
same song for the seed data you provide, but will be unique to any other seed.
</p>
<p>
Type anything into the input below and hit "Load" to generate and hear your song.
</p>
<input id="seed" type="text" placeholder="Seed" />
<button id="load">Load</button><br>
<small>
You can download a MIDI file of whatever you have generated in the browser when you hit "Pause".
To import into Ableton, you may need to rename the extension to ".mid".
The conversion is handled by ABCjs, and the midi accuracy varies with different software.
</small>
</section>
</header>
<main>
<button id="play">Play</button><br>
<p id="midi"></p>
<section id="staff-container">
<div id="staff"></div>
</section>
</main>
// console.clear();
var APP = {};
document.documentElement.addEventListener('mousedown', () => {
if (Tone.context.state !== 'running') Tone.context.resume();
});
// There's a bug that happens occasionally. Intercept it.
// a seed that triggers it: d12d21ddd
window.ABCJS.parse.each = function(a, d, c) {
var e = (a === undefined) ? 0 : a.length;
// for (var b = 0, e = a.length; b < e; b++) {
// ^^ this is the code that gets around it
for (var b = 0; b < e; b++) {
d.apply(c, [a[b], b])
}
}
document.getElementById("load").addEventListener("click", function(e) {
var seed = document.getElementById("seed").value;
if(seed) {
// if(seed.match(/^[A-Za-z0-9]+$/g)) {
document.getElementById("intro").style.display = "none";
APP.MusicGenerator.init(seed);
document.getElementById("play").focus();
document.getElementById("play").addEventListener("click", function(e) {
var $el = e.target;
if($el.getAttribute("data-playing") === "true") {
APP.MusicGenerator.pause();
$el.setAttribute("data-playing", "false");
$el.innerHTML = "Play";
APP.MusicGenerator.report();
} else {
APP.MusicGenerator.play();
$el.innerHTML = "Pause";
$el.setAttribute("data-playing", "true");
}
});
// } else {
// alert("Letters and numbers only");
// }
} else {
alert("Please enter a seed into the input");
}
});
(function() {
APP.MusicGenerator = {};
var _v_key = "WeAddaBabyEetsABoy"; // change this to refresh the seed
var _roots = "C Db D Eb E F F# G Ab A Bb".split(" ");
var _modes = "maj min".split(" ");
var _hold = "HOLD";
var _container = document.getElementById("staff-container");
var _staff = document.getElementById("staff");
var _midi = document.getElementById("midi");
var _min_oct = 2;
var _max_oct = 5;
var _playing = false;
var _res = 8;
var _curr_meas = 0;
var _curr_note = 0;
var _scale = [];
var _measures = [];
var _abc = "";
var _total_abc = "";
var _treb_abc = "";
var _bass_abc = "";
var _gen_meas = 0;
var _meas_gen = 4;
var _first_one = true;
var _bpm = null;
var _generator = null;
var _len_seqs = null;
var _mode = null;
var _output = null;
var _root = null;
var _seed = null;
var _synth = null;
APP.MusicGenerator.init = function(seed) {
_staff.innerHTML = "";
_seed = seed + _v_key;
_setGenerator(_seed);
_setKey();
if(!_synth) _setSynth();
_setTransport();
_genStaff();
_renderStaff();
_genLengthSequences();
_genScale();
_addMeasure(_meas_gen);
};
APP.MusicGenerator.play = playPlayer;
APP.MusicGenerator.pause = pausePlayer;
APP.MusicGenerator.report = reportData;
//////////////
// PUBLIC METHODS
//////////////
// play the player
function playPlayer() {
_midi.innerHTML = "";
Tone.Transport.start();
}
// pause the player
function pausePlayer() {
Tone.Transport.stop();
}
// reporting the data
function reportData() {
_output = {
root: _root, mode: _mode, resolution: _res,
scale: _scale, measures: _measures, abc: _total_abc
};
ABCJS.renderMidi(_midi, _total_abc , {}, {}, {});
console.log(_total_abc);
console.log(_output);
}
//////////////
// PRIVATE METHODS
//////////////
// get a unique random number generator based on the seed
function _setGenerator(seed) {
_generator = new Math.seedrandom(seed);
}
// get randomly generated key and mode
function _setKey() {
_root = _roots[Math.floor(_generator.quick() * _roots.length)];
var mode_index = Math.floor(_generator.quick() * _modes.length);
_mode = _modes[mode_index];
}
// play the bass and treble notes
function _playSong() {
var measure = _measures[_curr_meas];
if(measure) {
var treb = measure.treb[_curr_note];
var bass = measure.bass[_curr_note];
if(treb.chord) {
_synth.triggerAttackRelease(treb.chord, treb.len + "n");
}
if(bass.chord) {
_synth.triggerAttackRelease(bass.chord, bass.len + "n");
}
if(_curr_note + _curr_meas === 0) {
var hidden = document.querySelector(".hide");
if(hidden) hidden.className = "";
}
_curr_note++;
if(_curr_note >= _res) {
_curr_note = 0;
_curr_meas++;
if(_gen_meas % _meas_gen === 0) {
_addMeasure(_meas_gen);
} else if (_gen_meas % _meas_gen === 3) {
// scroll to the bottom of container
var hidden = document.querySelector(".hide");
if(hidden) hidden.className = "";
_utilScrollTo(_container, _container.scrollHeight, 500);
}
_gen_meas++;
}
} else {
pausePlayer();
}
}
// add a measure to the sequence
function _addMeasure(how_many, loaded) {
loaded = loaded || 0;
var get_measure = _getMeasure();
get_measure.then(function(measure) {
loaded++;
// console.log("_getMeasure", measure);
_measures.push(measure);
_staffABC(measure);
if(loaded < how_many) {
_addMeasure(how_many, loaded);
} else {
_renderStaff();
}
});
}
// generate a measure's worth of notes
function _getMeasure() {
return new Promise(function(resMeasure, rejMeasure) {
var data = {
scale: _scale.length / 2,
bass: { max_notes: 1, offset: 0 },
treb: { max_notes: 3, offset: Math.floor(_scale.length / 2) }
};
var measure = {
bass: [], treb: []
};
// for each clef
var get_treb = _getClef('treb', data);
get_treb.then(function(t) {
// console.log("_getClef: t", t);
measure.treb = measure.treb.concat(t);
var get_bass = _getClef('bass', data);
get_bass.then(function(b) {
// console.log("_getClef: b", b);
measure.bass = measure.bass.concat(b);
resMeasure(measure);
}, function(b) {
rejMeasure(b);
});
}, function(t) {
rejMeasure(t);
});
});
}
// generate notes for a clef
function _getClef(clef_name, data) {
return new Promise(function(resClef, rejClef) {
var _clef_generator = new Math.seedrandom(_generator.int32() + "");
// find a sequence
var seq = _len_seqs[Math.floor(_clef_generator.quick() * _len_seqs.length)];
// shuffle the sequence order
_utilShuffleArray(seq);
// set the data
var max_notes = data[clef_name].max_notes;
var note_offset = data[clef_name].offset;
var chord_promises = [];
// for each length in sequence
for(var s = 0; s < seq.length; s++) {
// generate x notes for a random bass count. no rests.
var note_count = Math.ceil(_clef_generator.quick() * max_notes);
// generate random note length
var note_length = seq[s];
chord_promises.push(_getChord(_clef_generator, data.scale, note_offset, note_count, note_length));
}
Promise.all(chord_promises).then(function(chords) {
var clef = [];
// console.log("_getChord: promises", chords);
for(var i = 0; i < chords.length; i++) {
var chord = chords[i];
clef.push(chord);
if(chord.len) {
var chord_length_i = _res / chord.len;
for(var e = 0; e < chord_length_i - 1; e++) {
clef.push(_hold);
}
}
}
resClef(clef);
}, function(chords) {
rejClef(chords);
});
});
}
// generate the chord notes
function _getChord(_clef_generator, scale, offset, note_count, note_length) {
return new Promise(function(resNotes, rejNotes) {
// used note indexes
var used_note_indexes = [];
var note_promises = [];
// generate the notes
for(var i = 0; i < note_count; i++) {
note_promises.push(_getNote(_clef_generator, scale, offset, used_note_indexes));
}
Promise.all(note_promises).then(function(chord) {
// console.log("_getNote: promises", chord);
var abc = _getABC(chord, note_length);
// add notes to measure
resNotes({ abc: abc, chord: chord, len: note_length });
}, function(chord) {
rejNotes(chord);
});
});
}
// generate a note
function _getNote(_clef_generator, scale, offset, used_note_indexes) {
return new Promise(function(resNote, rejNote) {
// add the note to the chord
resNote(_note());
function _note() {
// get a random index
var index = Math.floor(_clef_generator.quick() * scale) + offset;
// for measuring note is a "good" one.
var good_note = true;
// ensure we havent selected this index
if(used_note_indexes.indexOf(index) !== -1) good_note = false;
// or one below it
if(index > 0 && used_note_indexes.indexOf(index - 1) !== -1) good_note = false;
// or one above it
if(used_note_indexes.indexOf(index + 1) !== -1) good_note = false;
// if we found a good note
if(good_note) {
// add the index to the used array
used_note_indexes.push(index);
// grab the note
var note = _scale[index];
// build the tone version of the note
var n = note.note + note.octave;
return n;
} else {
return _note();
}
}
});
}
//////////////
// ABC MUSICAL NOTATION
//////////////
// get abc notation for notes at a certain length
function _getABC(notes, len) {
if(!len) return "";
var str = notes.length > 1 ? "[" : "";
for(var n = 0; n < notes.length; n++) {
str += _ABCNotation(notes[n], len);
}
str += notes.length > 1 ? "]" : "";
return str;
}
// render the current abc to staff
function _renderStaff() {
var $par = document.createElement("div");
if(_first_one) {
_first_one = false;
} else {
$par.className = "hide";
}
var $el = document.createElement("div");
// beats per second times four beats * measures per line * 1 second
$par.appendChild($el);
_staff.appendChild($par);
_abc += _treb_abc;
_abc += "\n" + _bass_abc;
var cleaned = _abc;
_total_abc += cleaned;
_abc = _genStaffHeader();
_treb_abc = "[V: 1] ";
_bass_abc = "[V: 2] ";
_renderABC($el, cleaned);
}
function _renderABC(el, abc) {
console.log(abc);
ABCJS.renderAbc(el, abc, {}, {
staffwidth: 800,
paddingright: 0,
paddingleft: 0,
scale: 1,
add_classes: true
}, {});
}
// take a measure and generate the abc output
function _staffABC(measure) {
var abc = "";
// TODO: every two measures new line with _gen_meas, _treb_abc, _bass_abc
for(var i = 0; i < measure.treb.length; i++) {
if(measure.treb[i].abc) {
_treb_abc += measure.treb[i].abc + " ";
}
}
for(var i = 0; i < measure.bass.length; i++) {
if(measure.bass[i].abc) {
_bass_abc += measure.bass[i].abc + " ";
}
}
_treb_abc = _treb_abc.replace(/ $/g, "|");
_bass_abc = _bass_abc.replace(/ $/g, "|");
// abc += [treb, bass].join("\n");
// return abc;
}
// set the initial staff
function _genStaff() {
_abc = [
"M:/4/4",
"O:I",
"R:R",
"Q:1/4=" + _bpm,
"",
"X:2",
"T:" + _seed.replace(_v_key, ""),
"C:" + _root.replace(/b$/g, "♭") + " " + _mode + ", " + _bpm + " bpm",
].join("\n");
}
// generate the staff header
function _genStaffHeader() {
return [
"K:" + _root + _mode,
"L:1/" + _res,
"%%staves {1 2}",
"V: 1 clef=treble",
"V: 2 clef=bass",
""
].join("\n");
}
// abc notation
function _ABCNotation(note, length) {
// note = note.replace(/([A-G])b/,"_$1");
// TODO: know if this is needed based on mode.
note = note.replace(/([A-G])b/,"$1");
note = note
.replace(/(.)1/, "$1,,,")
.replace(/(.)2/, "$1,,")
.replace(/(.)3/, "$1,")
.replace(/(.)4/, "$1");
if(note.match(/(.)5|6/)) {
note = note
.replace(/(.)5/, "$1")
.replace(/(.)6/, "$1'")
.toLowerCase();
}
note += _res / length;
return note;
}
//////////////
// DATA
//////////////
// generate the scale
function _genScale() {
// generate the scale
var _steps_lib = _genStepsLib();
var _notes_lib = _genNotesLib();
var steps = _steps_lib;
var start = _notes_lib.indexOf(_root + _min_oct);
var last_note = (_max_oct - _min_oct) * 8;
for(var o = 0; o < last_note; o++) {
var index = start + steps[o % steps.length] + (Math.floor(o / steps.length) * 12);
var note = _notes_lib[index];
if(note) {
_scale.push({ note: note.match(/[^\d]+/)[0], octave: note.match(/\d/)[0] });
}
}
if(!_scale.length) console.error("Too high up. Lower the octave or note count.");
}
// potential combinations of note lengths
// this could be done programmatically.
function _genLengthSequences() {
_len_seqs = [
[1],
[2,2],
[2,4,4],
[2,4,8,8],
[4,4,4,4],
[2,8,8,8,8],
[4,4,4,8,8],
[8,8,8,8,8,8,8,8],
// [2,4,8,16,16],
// [2,4,16,16,16,16],
// [2,8,8,8,16,16],
// [2,8,8,16,16,16,16],
// [2,8,16,16,16,16,16,16],
// [2,16,16,16,16,16,16,16,16],
// [4,4,4,8,16,16],
// [4,4,4,16,16,16,16],
// [4,4,8,8,16,16,16,16],
// [4,4,8,16,16,16,16,16,16],
// [4,4,16,16,16,16,16,16,16,16],
// [4,8,8,16,16,16,16,16,16,16,16],
// [4,8,16,16,16,16,16,16,16,16,16,16],
// [4,16,16,16,16,16,16,16,16,16,16,16,16],
// [8,8,8,8,8,8,8,16,16],
// [8,8,8,8,8,8,16,16,16,16],
// [8,8,8,8,8,16,16,16,16,16,16],
// [8,8,8,8,16,16,16,16,16,16,16,16],
// [8,8,8,16,16,16,16,16,16,16,16,16,16],
// [8,8,16,16,16,16,16,16,16,16,16,16,16,16],
// [8,16,16,16,16,16,16,16,16,16,16,16,16,16,16],
// [16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16],
];
}
// set the notes for the key
function _genStepsLib() {
// generate major and minor integer step arrays from whole/half notation
// whole/half note steps for minor and major
var mode = {
// ionian: "W W H W W W H",
// dorian: "W H W W W H W",
// phrygian: "H W W W H W W",
// lydian: "W W W H W W H",
// mixolydian: "W W H W W H W",
// aeolian: "W H W W H W W",
// locrian: "H W W H W W W",
maj: "W W H W W W H",
min: "W H W W H W W",
}[_mode].split(" ");
// start each with root step
var steps = [0];
// loop through
var step = 0;
for(var s = 0; s < mode.length; s++) {
var key = mode[s];
if(key === "W") {
steps.push(steps[s] + 2);
} else if(key == "WH") {
steps.push(steps[s] + 3);
} else {
steps.push(steps[s] + 1);
}
step++;
}
return steps;
}
// generate every possible note/octave pairing in order
function _genNotesLib() {
var notes = [];
for(var i = 0; i < 9; i++) {
var n = "C Db D Eb E F Gb G Ab A Bb B".split(" ");
for(var x = 0; x < n.length; x++) notes.push(n[x] + i);
}
return notes;
}
//////////////
// TONE.js
//////////////
// set the tone js transport
function _setTransport() {
_bpm = Math.round(_generator.quick() * 60) + 60;
Tone.Transport.bpm.value = _bpm;
Tone.Transport.scheduleRepeat(function(time) {
_playSong();
}, _res + "n");
}
// set the synthesizer
function _setSynth() {
// master compression
var multiband = new Tone.MultibandCompressor({
lowFrequency: 200,
highFrequency: 1300,
low: { threshold: -12 }
});
var reverb = new Tone.Freeverb();
reverb.roomSize.value = 0.6;
reverb.wet.value = 0.2;
Tone.Master.chain(reverb, multiband);
// _synth = new Tone.PolySynth(6, Tone.SimpleAM).toMaster();
_synth = new Tone.PolySynth(6, Tone.SimpleSynth).toMaster();
_synth.set({
oscillator: {
type: "triangle"
},
envelope: {
attack: 0.05,
decay: 0.1,
sustain: 0.3,
release: 1
}
});
// _synth.set({
// envelope: {
// attack: 0.5,
// release: 0.5
// }
// });
// _synth.set("carrier", {
// volume: -8,
// oscillator: {
// type: "triangle8",
// partials: [1, 0.2, 0.01]
// },
// envelope: {
// attack: 0.05,
// decay: 0.02,
// sustain: 0.6,
// release: 0.8
// }
// });
// _synth.set("modulator", {
// volume: -16,
// oscillator: {
// type: "triangle8",
// detune: 0,
// phase: 90,
// partials: [1, 0.2, 0.01]
// },
// envelope: {
// attack: 0.05,
// decay: 0.01,
// sustain: 1,
// release: 1
// }
// });
}
//////////////
// UTILS
//////////////
// shuffle an array
// http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
function _utilShuffleArray(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(_generator() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
function _utilScrollTo(element, to, duration) {
if (duration <= 0) return;
var difference = to - element.scrollTop;
var perTick = difference / duration * 10;
setTimeout(function() {
element.scrollTop = element.scrollTop + perTick;
if (element.scrollTop === to) return;
_utilScrollTo(element, to, duration - 10);
}, 10);
}
}());
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/2.4.0/seedrandom.min.js"></script>
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/111863/Tone.min.js"></script>
<script src="https://cdn.rawgit.com/paulrosen/abcjs/master/bin/abcjs_basic_2.0-min.js"></script>

Seeded Procedural Music Generator

Procedural Generation allows you to create unique infiniteness that can be consistently reproduced. Enter seed data (your name is a great place to start) and hear a unique, infinite, and reproducible song.

Everything is notated in flats instead of sharps for simplicity sake.

Still has the occasional bug. If it doesnt play anything, refresh and try a new seed.

abcjs | tone js | seedrandom

A Pen by samxunitmailxru on CodePen.

License.

$staff-w: 800px;
$staff-h: 220px;
header {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
z-index: 9;
background: rgba(0,0,0,0.6);
section {
position: absolute;
border-radius: 2px;
width: 90%;
max-width: 600px;
top: 50%; left: 50%;
background: white;
color: #222;
padding: 2rem;
transform: translate(-50%, -50%);
text-align: center;
}
h1 { margin-top: 0; }
p { text-align: left; }
small {
margin-top: 1rem;
color: #CCC;
display: inline-block;
}
}
#staff-container {
width: $staff-w;
overflow: auto;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 0 4rem;
background-color: #FFF;
border-radius: 4px;
box-shadow: 0px 8px 16px rgba(0,0,0,0.1);
}
input, button {
border-radius: 2px;
line-height: 1.4;
margin: 0;
padding: 0.25em 0.5em;
}
input {
border: 1px solid darken(#FFF, 10%);
width: 280px;
}
button {
background: #444;
color: white;
border: 1px solid darken(#444, 10%);
}
#play, #midi {
position: absolute;
top: 1rem;
z-index: 8;
}
#play {
left: 50%;
transform: translateX(-50%);
}
#midi {
right: 1rem;
text-align: right;
margin: 0;
// ABCJS gives us an embed player that requires quicktime plugin. gross.
embed { position: absolute; top: -99999px; opacity: 0; pointer-events: none; }
a {
color: #444;
text-transform: uppercase;
font-size: 0.8em;
letter-spacing: 0.0125em;
}
}
#staff {
height: $staff-h * 2;
> div {
box-sizing: border-box;
height: $staff-h;
display: flex;
opacity: 1;
transition: opacity 250ms ease-out;
&:not(:nth-child(2)) path.symbol.l0 { opacity: 0; }
&:not(:nth-child(2)) { transition-delay: 250ms; }
&.hide { opacity: 0; }
&:first-child {
border-bottom: 1px solid #F0F0F0;
}
&:first-child > div { align-self: flex-end; }
}
}
@for $i from 3 through 6 {
@media (min-height: #{$i * $staff-h * 1.2}) {
#staff {
height: $staff-h * $i;
}
}
}
html, body {
height: 100%;
}
body {
background-color: #F0F0F0;
color: #222;
line-height: 1.7;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment