mirror of
https://github.com/wgreenberg/musicbox.git
synced 2024-11-25 12:50:25 +01:00
allow rows to desync
This commit is contained in:
parent
22fe91432d
commit
ad975b67d8
2 changed files with 113 additions and 29 deletions
70
index.html
70
index.html
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
|
|
66
musicbox.js
66
musicbox.js
|
@ -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 => {
|
||||
let idx;
|
||||
if (syncSequences) {
|
||||
idx = this.idx % longestLength;
|
||||
} else {
|
||||
idx = this.idx % a.length;
|
||||
}
|
||||
|
||||
this.sequences.map(a => a[this.idx])
|
||||
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);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue