glocken_spiel_und_so.php
Quell Code
<html>
<head>
<style>
html,
body,
#container {
height: 100%;
margin: 0;
padding: 0;
background: linear-gradient(0.25turn, #752525, #050505, #252575);
color: #f3f3f3;
}
#container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
}
#vis {
width: 95vmin;
height: 95vmin;
overflow: visible;
}
.halo {
opacity: 0;
}
.note {
stroke-width: 0.5;
stroke: #151515;
fill: rgba(255, 255, 255, 1);
opacity: 0.4;
}
.hover .note {
opacity: 0.7;
}
.on .note {
fill: #e91e63;
opacity: 1;
}
.pointer-area {
stroke: none;
opacity: 0;
}
.controls {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
}
.control-group {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.controls button {
width: 100%;
min-width: 7vw;
height: 4vh;
margin: 2px;
background: none;
color: white;
border: 1px solid white;
}
.controls button:hover {
background: rgba(255, 255, 255, 0.5);
}
.controls button.active {
background: white;
color: black;
}
#generating,
#loading {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.js"></script>
<script src="https://teropa.info/libs/musicvae-1.1.3-with-tf.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tone@0.12.80/build/Tone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/web-animations-js@2.3.1/web-animations.min.js"></script>
<script src="https://cdn.rawgit.com/tambien/StartAudioContext/8da8637e/StartAudioContext.js"></script>
</head>
<body>
<div id="container">
<div class="controls">
<div class="control-group">
<button class="tonic-left active" data-tonic="0">C</button>
<button class="tonic-left" data-tonic="1">C♯ / D♭</button>
<button class="tonic-left" data-tonic="2">D</button>
<button class="tonic-left" data-tonic="3">E♯ / E♭</button>
<button class="tonic-left" data-tonic="4">E</button>
<button class="tonic-left" data-tonic="5">F</button>
<button class="tonic-left" data-tonic="6">F♯ / G♭</button>
<button class="tonic-left" data-tonic="7">G</button>
<button class="tonic-left" data-tonic="8">G♯ / A♭</button>
<button class="tonic-left" data-tonic="9">A</button>
<button class="tonic-left" data-tonic="10">A♯ / B♭</button>
<button class="tonic-left" data-tonic="11">B</button>
</div>
<div class="control-group">
<button class="chord-left active" data-chord="major">Major</button>
<button class="chord-left" data-chord="minor">Minor</button>
<button class="chord-left" data-chord="major7th">Major 7th</button>
<button class="chord-left" data-chord="minor7th">Minor 7th</button>
<button class="chord-left" data-chord="dominant7th">Dominant 7th</button>
<button class="chord-left" data-chord="sus2">Sus2</button>
<button class="chord-left" data-chord="sus4">Sus4</button>
</div>
</div>
<svg id="vis" viewBox="0 0 1000 1000">
<defs>
<radialGradient id="halo">
<stop offset="0%" stop-color="rgba(255, 255, 255, 0.5)" />
<stop offset="95%" stop-color="rgba(255, 255, 255, 0.5)" />
<stop offset="100%" stop-color="rgba(255, 255, 255, 0)" />
</radialGradient>
</defs>
<g id="vis-halos"></g>
<g id="vis-elements"></g>
</svg>
<div class="controls">
<div class="control-group">
<button class="tonic-right active" data-tonic="0">C</button>
<button class="tonic-right" data-tonic="1">C♯ / D♭</button>
<button class="tonic-right" data-tonic="2">D</button>
<button class="tonic-right" data-tonic="3">E♯ / E♭</button>
<button class="tonic-right" data-tonic="4">E</button>
<button class="tonic-right" data-tonic="5">F</button>
<button class="tonic-right" data-tonic="6">F♯ / G♭</button>
<button class="tonic-right" data-tonic="7">G</button>
<button class="tonic-right" data-tonic="8">G♯ / A♭</button>
<button class="tonic-right" data-tonic="9">A</button>
<button class="tonic-right" data-tonic="10">A♯ / B♭</button>
<button class="tonic-right" data-tonic="11">B</button>
</div>
<div class="control-group">
<button class="chord-right active" data-chord="major">Major</button>
<button class="chord-right" data-chord="minor">Minor</button>
<button class="chord-right" data-chord="major7th">Major 7th</button>
<button class="chord-right" data-chord="minor7th">Minor 7th</button>
<button class="chord-right" data-chord="dominant7th">Dominant 7th</button>
<button class="chord-right" data-chord="sus2">Sus2</button>
<button class="chord-right" data-chord="sus4">Sus4</button>
</div>
</div>
</div>
<div id="loading">Loading models…</div>
<div id="generating" style="display: none">Generating…</div>
<script>
const MIN_NOTE = 48;
const MAX_NOTE = 84;
const SEQ_LENGTH = 32;
const HUMANIZE_TIMING = 0.0085;
const N_INTERPOLATIONS = 10;
const CHORD_INTERVALS = {
major: [0, 4, 7],
minor: [0, 3, 7],
major7th: [0, 4, 7, 11],
minor7th: [0, 3, 7, 10],
dominant7th: [0, 4, 7, 10],
sus2: [0, 2, 7],
sus4: [0, 5, 7]
};
const SAMPLE_SCALE = [
'C3',
'D#3',
'F#3',
'A3',
'C4',
'D#4',
'F#4',
'A4',
'C5',
'D#5',
'F#5',
'A5'
];
Tone.Transport.bpm.value = 90;
let mvae = new musicvae.MusicVAE('https://storage.googleapis.com/download.magenta.tensorflow.org/models/music_vae/dljs/mel_small');
let tf = musicvae.tf;
// Using the Improv RNN pretrained model from https://github.com/tensorflow/magenta/tree/master/magenta/models/improv_rnn
let lstmLoader = new musicvae.CheckpointLoader('https://teropa.info/improv_rnn_pretrained_checkpoint/');
let lstm;
let rnnLoadPromise = lstmLoader.getAllVariables().then(vars => {
lstm = {
kernel1: vars['RNN/MultiRNNCell/Cell0/BasicLSTMCell/Linear/Matrix'],
bias1: vars['RNN/MultiRNNCell/Cell0/BasicLSTMCell/Linear/Bias'],
kernel2: vars['RNN/MultiRNNCell/Cell1/BasicLSTMCell/Linear/Matrix'],
bias2: vars['RNN/MultiRNNCell/Cell1/BasicLSTMCell/Linear/Bias'],
kernel3: vars['RNN/MultiRNNCell/Cell2/BasicLSTMCell/Linear/Matrix'],
bias3: vars['RNN/MultiRNNCell/Cell2/BasicLSTMCell/Linear/Bias'],
fullyConnectedBiases: vars['fully_connected/biases'],
fullyConnectedWeights: vars['fully_connected/weights']
};
});
let reverb = new Tone.Convolver('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/hm2_000_ortf_48k.mp3').toMaster();
reverb.wet.value = 0.15;
let samplers = [
{
high: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-').connect(
new Tone.Panner(-0.4).connect(reverb)
),
mid: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-mid-').connect(
new Tone.Panner(-0.4).connect(reverb)
),
low: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/marimba-classic-low-').connect(
new Tone.Panner(-0.4).connect(reverb)
)
},
{
high: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-').connect(
new Tone.Panner(0.4).connect(reverb)
),
mid: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-mid-').connect(
new Tone.Panner(0.4).connect(reverb)
),
low: buildSampler('https://s3-us-west-2.amazonaws.com/s.cdpn.io/969699/xylophone-dark-low-').connect(
new Tone.Panner(0.4).connect(reverb)
)
}
];
let sixteenth = Tone.Time('16n').toSeconds();
let quarter = Tone.Time('4n').toSeconds();
let temperature = tf.variable(tf.scalar(1.1));
let loadingIndicator = document.querySelector('#loading');
let generatingIndicator = document.querySelector('#generating');
let container = document.querySelector('#vis-elements');
let haloContainer = document.querySelector('#vis-halos');
let tonicLeftButtons = document.querySelectorAll('.tonic-left');
let tonicRightButtons = document.querySelectorAll('.tonic-right');
let chordLeftButtons = document.querySelectorAll('.chord-left');
let chordRightButtons = document.querySelectorAll('.chord-right');
let sequences = [];
let mouseDown = false;
let chordLeft = CHORD_INTERVALS['major'],
chordRight = CHORD_INTERVALS['major'];
let tonicLeft = 0,
tonicRight = 0;
function buildSampler(urlPrefix) {
return new Tone.Sampler(
_.fromPairs(
SAMPLE_SCALE.map(n => [
n,
new Tone.Buffer(`${urlPrefix}${n.toLowerCase().replace('#', 's')}.mp3`)
])
)
);
}
function encodePitchChord(chord) {
let oneHot = _.times(3 * 12 + 1, () => 0);
if (chord === null) {
oneHot[0] = 1;
return oneHot;
}
let chordPcs = chord.map(n => n % 12);
// Root pc
oneHot[1 + chordPcs[0]] = 1;
// Pitches
for (let pc of chordPcs) {
oneHot[1 + 12 + pc] = 1;
}
// Bass pc (=root)
oneHot[1 + 12 + 12 + chordPcs[0]] = 1;
return oneHot;
}
// Melodies encoded in a one-hot vector, where 0 = no event, 1 = note off, the rest are note ons.
function encodeMelodyIndex(note) {
return note < 0 ? note + 2 : note - MIN_NOTE + 2;
}
function decodeMelodyIndex(index) {
if (index - 2 < 0) {
return index - 2;
} else {
return index - 2 + MIN_NOTE;
}
}
function generateSeq(chord, startNotes, conditionSeq = []) {
let seq = tf.tidy(() => {
let forgetBias = tf.scalar(1.0);
let state = [
tf.zeros([1, lstm.bias1.shape[0] / 4]),
tf.zeros([1, lstm.bias2.shape[0] / 4]),
tf.zeros([1, lstm.bias3.shape[0] / 4])
];
let output = [
tf.zeros([1, lstm.bias1.shape[0] / 4]),
tf.zeros([1, lstm.bias2.shape[0] / 4]),
tf.zeros([1, lstm.bias3.shape[0] / 4])
];
let lstm1 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel1, lstm.bias1);
let lstm2 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel2, lstm.bias2);
let lstm3 = tf.basicLSTMCell.bind(tf, forgetBias, lstm.kernel3, lstm.bias3);
let seq = [tf.tensor1d([encodeMelodyIndex(startNotes[0])])];
while (seq.length < SEQ_LENGTH) {
let condChord;
if (seq.length < conditionSeq.length && conditionSeq[seq.length] > 0) {
condChord = chord.concat([conditionSeq[seq.length]]);
} else {
condChord = chord;
}
let pitchChord = tf.tensor1d(encodePitchChord(condChord));
let input = pitchChord.concat(
tf.oneHot(_.last(seq), MAX_NOTE - MIN_NOTE + 2).flatten()
);
let nextOutput = tf.multiRNNCell(
[lstm1, lstm2, lstm3],
input.as2D(1, -1),
state,
output
);
state = nextOutput[0];
output = nextOutput[1];
let outputH = output[2];
let weightedResult = tf.matMul(outputH, lstm.fullyConnectedWeights);
let logits = tf.add(weightedResult, lstm.fullyConnectedBiases);
let softmax = tf.softmax(tf.div(logits.as1D(), temperature));
if (seq.length >= startNotes.length) {
seq.push(tf.multinomial(softmax, 1, null, true));
} else {
seq.push(tf.tensor1d([encodeMelodyIndex(startNotes[seq.length])]));
}
}
return seq.map(s => s.asScalar());
});
return Promise.all(seq.map(s => s.data())).then(s =>
s.map(decodeMelodyIndex)
);
}
function toNoteSequence(seq) {
let notes = [];
for (let i = 0; i < seq.length; i++) {
if (seq[i] === -1 && notes.length) {
_.last(notes).quantizedEndStep = i;
} else if (seq[i] > 0) {
if (notes.length && !_.last(notes).quantizedEndStep) {
_.last(notes).quantizedEndStep = i;
}
notes.push({ pitch: seq[i], quantizedStartStep: i });
}
}
if (notes.length && !_.last(notes).quantizedEndStep) {
_.last(notes).quantizedEndStep = SEQ_LENGTH;
}
return { notes };
}
function isValidNote(note, forgive = 0) {
return note <= MAX_NOTE + forgive && note >= MIN_NOTE - forgive;
}
function octaveShift(chord) {
let root = _.min(chord);
let shift = MAX_NOTE - root > root - MIN_NOTE ? 12 : -12;
let delta = 0;
while (isValidNote(root + delta + shift)) {
delta += shift;
}
return chord.map(n => n + delta).map(transposeIntoRange);
}
function transposeIntoRange(note) {
while (note > MAX_NOTE) {
note -= 12;
}
while (note < MIN_NOTE) {
note += 12;
}
return note;
}
function mountChord(tonic, chord) {
return chord.map(d => MIN_NOTE + tonic + d);
}
function restPad(note) {
if (Math.random() < 0.6) {
return [note, -2];
} else if (Math.random() < 0.8) {
return [note];
} else {
return [note, -2, -2];
}
}
function playStep(time, step) {
let notesToPlay = distributeNotesToPlay(collectNotesToPlay(step));
for (let { delay, notes } of notesToPlay) {
let voice = 0;
let stepSamplers = _.shuffle(samplers);
for (let { pitch, path, halo } of notes) {
let freq = Tone.Frequency(pitch, 'midi');
let playTime = time + delay + HUMANIZE_TIMING * Math.random();
let velocity;
if (delay === 0) velocity = 'high';
else if (delay === sixteenth / 2) velocity = 'mid';
else velocity = 'low';
stepSamplers[voice++ % stepSamplers.length][velocity].triggerAttack(
freq,
playTime
);
Tone.Draw.schedule(() => animatePlay(path, halo), playTime);
}
}
}
function collectNotesToPlay(step) {
let notesToPlay = [];
for (let seq of sequences) {
if (!seq.on) continue;
if (seq.notes.has(step)) {
notesToPlay.push(seq.notes.get(step));
}
}
return _.shuffle(notesToPlay);
}
function distributeNotesToPlay(notes) {
let subdivisions = [
{ delay: 0, notes: [] },
{ delay: sixteenth / 2, notes: [] },
{ delay: sixteenth, notes: [] },
{ delay: sixteenth * 3 / 2, notes: [] }
];
if (notes.length) {
subdivisions[0].notes.push(notes.pop());
}
if (notes.length) {
subdivisions[2].notes.push(notes.pop());
}
while (notes.length && Math.random() < Math.min(notes.length, 6) / 10) {
let rnd = Math.random();
let subdivision;
if (rnd < 0.4) {
subdivision = 0;
} else if (rnd < 0.6) {
subdivision = 1;
} else if (rnd < 0.8) {
subdivision = 2;
} else {
subdivision = 3;
}
subdivisions[subdivision].notes.push(notes.pop());
}
return subdivisions;
}
function animatePlay(pathEl, haloEl) {
pathEl.animate([{ fill: 'white' }, { fill: '#e91e63' }], {
duration: quarter * 1000,
easing: 'ease-out'
});
haloEl.animate([{ opacity: 1 }, { opacity: 0 }], {
duration: quarter * 1000,
easing: 'ease-out'
});
}
function toggleSeq(seqObj) {
if (seqObj.on) {
seqObj.on = false;
seqObj.group.setAttribute('class', '');
} else {
seqObj.on = true;
seqObj.group.setAttribute('class', 'on');
}
}
function toggleHover(seqObj, on) {
let cls = seqObj.group.getAttribute('class') || '';
if (on && cls.indexOf('hover') < 0) {
seqObj.group.setAttribute('class', cls + ' hover');
} else if (!on && cls.indexOf('hover') >= 0) {
seqObj.group.setAttribute('class', cls.replace('hover', ''));
}
}
function buildSlice(centerX, centerY, startAngle, endAngle, radius) {
let startX = centerX + Math.cos(startAngle) * radius;
let startY = centerY + Math.sin(startAngle) * radius;
let endX = centerX + Math.cos(endAngle) * radius;
let endY = centerY + Math.sin(endAngle) * radius;
let pathString = `M ${centerX} ${centerY} L ${startX} ${startY} A ${radius} ${radius} 0 0 1 ${endX} ${endY} Z`;
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathString);
path.setAttribute('style', `transform-origin: ${centerX}px ${centerY}px`);
return path;
}
function generateSpace() {
let previouslyOn = _.fromPairs(sequences.map((s, idx) => [idx, s.on]));
let chords = [
octaveShift(mountChord(tonicLeft, chordLeft)),
mountChord(tonicLeft, chordLeft),
octaveShift(mountChord(tonicRight, chordRight)),
mountChord(tonicRight, chordRight)
];
let seed1 = _.flatMap(_.shuffle(chords[0]), restPad);
let seed2 = _.flatMap(_.shuffle(chords[1]), restPad);
let seed3 = _.flatMap(_.shuffle(chords[2]), restPad);
let seed4 = _.flatMap(_.shuffle(chords[3]), restPad);
return Promise.all([
generateSeq(chords[0], seed1),
generateSeq(chords[1], seed2),
generateSeq(chords[2], seed3),
generateSeq(chords[3], seed4)
])
.then(seqs => seqs.map(toNoteSequence))
.then(noteSeqs => mvae.interpolate(noteSeqs, N_INTERPOLATIONS))
.then(res => {
while (container.firstChild) {
container.firstChild.remove();
}
while (haloContainer.firstChild) {
haloContainer.firstChild.remove();
}
let cellSize = 1000 / N_INTERPOLATIONS;
let margin = cellSize / 30;
sequences = res.map((noteSeq, idx) => {
let row = Math.floor(idx / N_INTERPOLATIONS);
let col = idx - row * N_INTERPOLATIONS;
let centerX = (col + 0.5) * cellSize + margin;
let centerY = (row + 0.5) * cellSize + margin;
let maxInterval = MAX_NOTE;
let maxRadius = cellSize / 2 - 2 * margin;
let group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
if (previouslyOn[idx]) {
group.setAttribute('class', 'on');
}
group.style.transformOrigin = `${centerX}px ${centerY}px`;
group.style.transform = 'scale(0)';
group.animate([{ transform: 'scale(0)' }, { transform: 'scale(1)' }], {
duration: 200,
delay:
(N_INTERPOLATIONS / 2 - Math.abs(row - N_INTERPOLATIONS / 2)) * 25 +
(N_INTERPOLATIONS / 2 - Math.abs(col - N_INTERPOLATIONS / 2)) * 25,
fill: 'forwards'
});
container.appendChild(group);
let halo = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
halo.setAttribute('class', 'halo');
halo.setAttribute('fill', 'url(#halo');
halo.setAttribute('cx', centerX);
halo.setAttribute('cy', centerY);
halo.setAttribute('r', maxRadius + 2);
haloContainer.appendChild(halo);
let notes = new Map();
for ({ pitch, quantizedStartStep, quantizedEndStep } of noteSeq.notes) {
if (!isValidNote(pitch, 4)) {
continue;
}
let relPitch = (maxInterval - (pitch - MIN_NOTE)) / maxInterval;
let radius = relPitch * maxRadius;
let startAngle = quantizedStartStep / SEQ_LENGTH * Math.PI * 2;
let endAngle = quantizedEndStep / SEQ_LENGTH * Math.PI * 2;
let path = buildSlice(centerX, centerY, startAngle, endAngle, radius);
path.setAttribute('class', 'note');
group.appendChild(path);
notes.set(quantizedStartStep, {
pitch,
path,
halo
});
}
let pointerArea = document.createElementNS(
'http://www.w3.org/2000/svg',
'rect'
);
pointerArea.setAttribute('x', col * cellSize);
pointerArea.setAttribute('y', row * cellSize);
pointerArea.setAttribute('width', cellSize);
pointerArea.setAttribute('height', cellSize);
pointerArea.setAttribute('class', 'pointer-area');
group.appendChild(pointerArea);
let seqObj = { notes, group, on: previouslyOn[idx] };
pointerArea.addEventListener('mousedown', () => toggleSeq(seqObj));
pointerArea.addEventListener('mouseover', () => {
toggleHover(seqObj, true);
mouseDown && toggleSeq(seqObj);
});
pointerArea.addEventListener('mouseout', () =>
toggleHover(seqObj, false)
);
return seqObj;
});
});
}
function regenerateSpace() {
// Pause Tone timeline while regenerating so events don't pile up if it's laggy.
Tone.Transport.pause();
generatingIndicator.style.display = 'flex';
setTimeout(() => {
generateSpace().then(() => {
generatingIndicator.style.display = 'none';
setTimeout(() => Tone.Transport.start(), 0);
});
}, 0);
}
Promise.all([
rnnLoadPromise,
mvae.initialize(),
new Promise(res => Tone.Buffer.on('load', res))
])
.then(generateSpace)
.then(() => (loadingIndicator.style.display = 'none'))
.then(() => {
new Tone.Sequence(playStep, _.range(SEQ_LENGTH), '16n').start();
Tone.Transport.start();
});
document.documentElement.addEventListener(
'mousedown',
() => (mouseDown = true)
);
document.documentElement.addEventListener('mouseup', () => (mouseDown = false));
tonicLeftButtons.forEach(el =>
el.addEventListener('click', evt => {
tonicLeft = +evt.target.dataset.tonic;
tonicLeftButtons.forEach(b =>
b.classList.toggle('active', b === evt.target)
);
regenerateSpace();
})
);
tonicRightButtons.forEach(el =>
el.addEventListener('click', evt => {
tonicRight = +evt.target.dataset.tonic;
tonicRightButtons.forEach(b =>
b.classList.toggle('active', b === evt.target)
);
regenerateSpace();
})
);
chordLeftButtons.forEach(el =>
el.addEventListener('click', evt => {
chordLeft = CHORD_INTERVALS[evt.target.dataset.chord];
chordLeftButtons.forEach(b =>
b.classList.toggle('active', b === evt.target)
);
regenerateSpace();
})
);
chordRightButtons.forEach(el =>
el.addEventListener('click', evt => {
chordRight = CHORD_INTERVALS[evt.target.dataset.chord];
chordRightButtons.forEach(b =>
b.classList.toggle('active', b === evt.target)
);
regenerateSpace();
})
);
StartAudioContext(Tone.context, container);
</script>
</body>
</html>