allow rows to desync

This commit is contained in:
Will Greenberg 2022-11-08 16:10:48 -08:00
parent 22fe91432d
commit ad975b67d8
2 changed files with 113 additions and 29 deletions

View file

@ -5,23 +5,6 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.css">
<script src='musicbox.js'></script>
<style>
textarea {
height: 300px;
font-family: monospace;
}
.sequencers {
display: flex;
flex-direction: row;
}
.sequencer {
display: flex;
flex-direction: column;
width: 200px;
margin: 10px;
}
.body {
display: flex;
flex-direction: column;
@ -34,6 +17,52 @@ button {
p {
font-size: small;
margin-top: 0px;
}
textarea {
height: 150px;
flex: 1;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}
.sequencers {
display: flex;
flex-direction: column;
}
.sequencer-main {
display: flex;
flex-direction: row;
font-family: monospace;
}
.sequencer {
display: flex;
flex-direction: column;
width: 400px;
}
.sequencer-preview {
flex: 1;
background-color: #d0cfcf;
border-radius: 6px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
border: none;
margin-bottom: 6px;
padding: 10px;
white-space: pre;
}
.sequencer-preview > p {
margin: 0px;
}
.active {
background-color: #9b9b9b;
text-decoration: underline;
}
</style>
</head>
@ -41,18 +70,25 @@ p {
<div class='sequencers'>
<div class='sequencer'>
<span>beats</span>
<textarea id='beats'></textarea>
<div class="sequencer-main">
<textarea id='beats'></textarea>
<div class="sequencer-preview" id="beats-preview"></div>
</div>
<p>Each letter corresponds to a different sample. Non-letters are
silent.</p>
</div>
<div class='sequencer'>
<span>piano</span>
<textarea id='piano'></textarea>
<div class="sequencer-main">
<textarea id='piano'></textarea>
<div class="sequencer-preview" id="piano-preview"></div>
</div>
<p>On a QWERTY keyboard, the first 7 letters on each row represent
notes C through B, with lower rows representing higher octaves. Capital
letters are louder, non-letters are silent.</p>
</div>
</div>
<button id="play">loading...</button>
<span><input type="checkbox" id="sync-sequences">sync rows</span>
</div>
</html>

View file

@ -81,6 +81,7 @@ class Sequencer {
constructor(instrument) {
this.instrument = instrument;
this.sequences = [];
this.input = "";
this.idx = 0;
}
@ -89,26 +90,54 @@ class Sequencer {
.reduce((a, b) => Math.max(a, b), 0);
}
step() {
const length = this.length();
if (length === 0) {
step(syncSequences) {
const longestLength = this.length();
if (longestLength === 0) {
return;
}
if (this.idx >= length) {
this.idx = 0;
}
this.sequences.map(a => a[this.idx])
this.sequences.map(a => {
let idx;
if (syncSequences) {
idx = this.idx % longestLength;
} else {
idx = this.idx % a.length;
}
return a[idx];
})
.filter(maybeBuf => !!maybeBuf)
.forEach(buf => createBufferSource(buf.context, buf.buffer).start());
this.idx++;
}
update(input) {
this.input = input;
this.sequences = input.split('\n')
.map(line => this.instrument.parseLine(line));
}
render(syncSequences) {
const longestLength = this.length();
return this.input.split('\n')
.map(line => Array.from(line).map((letter, i) => {
let idx;
if (syncSequences) {
idx = this.idx % longestLength;
} else {
idx = this.idx % line.length;
}
let classes = [];
if (i === idx) {
classes.push('active');
}
if (letter === ' ') {
letter = '·';
}
return `<span class=${classes.join('')}>${letter}</span>`
}).join(''))
.map(line => `<p class="line">${line}</p>`)
.join('');
}
}
async function loadBeats(ctx, letters) {
@ -134,6 +163,7 @@ function setHash() {
const state = JSON.stringify({
piano: document.getElementById('piano').value,
beats: document.getElementById('beats').value,
syncSequences: document.getElementById('sync-sequences').checked,
});
window.location.hash = btoa(state);
}
@ -145,6 +175,7 @@ function loadHash() {
const json = JSON.parse(atob(base64));
document.getElementById('piano').value = json.piano;
document.getElementById('beats').value = json.beats;
document.getElementById('sync-sequences').checked = json.syncSequences;
} catch(e) {
console.log(`invalid sequence ${base64}: ${e}`);
}
@ -155,6 +186,14 @@ function crossProduct(a, b) {
return Array.from(b).flatMap(a_i => Array.from(a).map(b_i => b_i + a_i));
}
function updatePreview(name, sequencer, syncSequences) {
let preview = document.getElementById(name);
let rendered = sequencer.render(syncSequences);
if (preview.innerHTML !== rendered) {
preview.innerHTML = rendered;
}
}
window.addEventListener('load', async () => {
loadHash();
const ctx = new AudioContext();
@ -174,19 +213,28 @@ window.addEventListener('load', async () => {
document.getElementById('play').innerText = 'play/pause';
const pianoSequencer = setupSequencer(piano, 'piano');
const beatsSequencer = setupSequencer(beats, 'beats');
updatePreview('piano-preview', pianoSequencer);
let syncSequences = document.getElementById('sync-sequences').checked;
updatePreview('beats-preview', beatsSequencer, syncSequences);
let stopped = true;
document.getElementById('play').addEventListener('click', () => {
stopped = !stopped;
});
document.getElementById('sync-sequences').addEventListener('click', (e) => {
syncSequences = e.target.checked;
setHash();
});
const bpm = 240;
const msPerBeat = (1 / bpm) * 60 * 1000;
setInterval(() => {
if (!stopped) {
pianoSequencer.step();
beatsSequencer.step();
pianoSequencer.step(syncSequences);
beatsSequencer.step(syncSequences);
updatePreview('piano-preview', pianoSequencer, syncSequences);
updatePreview('beats-preview', beatsSequencer, syncSequences);
}
}, msPerBeat);
});