mirror of
https://github.com/wgreenberg/musicbox.git
synced 2024-11-22 19:40:25 +01:00
Independent sequencers allow for mismatched measure lengths
This commit is contained in:
parent
34d07d363e
commit
13c87ce547
2 changed files with 60 additions and 78 deletions
|
@ -8,6 +8,7 @@ textarea {
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
<span id='status'>loading samples...</span><br>
|
||||||
<textarea id='beats'></textarea>
|
<textarea id='beats'></textarea>
|
||||||
<textarea id='piano'></textarea>
|
<textarea id='piano'></textarea>
|
||||||
</html>
|
</html>
|
||||||
|
|
137
musicbox.js
137
musicbox.js
|
@ -12,19 +12,26 @@ function createBufferSource(ctx, buffer) {
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|
||||||
class PianoSequencer {
|
class Piano {
|
||||||
constructor(notes) {
|
constructor(notes) {
|
||||||
// load 3 octaves of the given notes
|
// load 3 octaves of the given notes
|
||||||
this.notes = crossProduct(notes, '345');
|
this.notes = crossProduct(notes, '345');
|
||||||
this.input = '';
|
|
||||||
this.lowOctave = 'qwertyu';
|
this.lowOctave = 'qwertyu';
|
||||||
this.midOctave = 'asdfghj';
|
this.midOctave = 'asdfghj';
|
||||||
this.highOctave = 'zxcvbnm';
|
this.highOctave = 'zxcvbnm';
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(ctx) {
|
async init(ctx) {
|
||||||
this.ff = await loadNotes(ctx, 'ff', this.notes);
|
this.ff = await this.loadNotes(ctx, 'ff', this.notes);
|
||||||
this.mf = await loadNotes(ctx, 'mf', this.notes);
|
this.mf = await this.loadNotes(ctx, 'mf', this.notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNotes(ctx, volume, notes) {
|
||||||
|
return await Promise.all(notes.map(async (note) => {
|
||||||
|
const path = `samples/piano/${volume}/${note}.mp3`;
|
||||||
|
const buffer = await loadBuffer(ctx, path);
|
||||||
|
return createBufferSource(ctx, buffer);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
parseLine(line) {
|
parseLine(line) {
|
||||||
|
@ -51,9 +58,8 @@ class PianoSequencer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class BeatSequencer {
|
class Beats {
|
||||||
cosntructor() {
|
cosntructor() {
|
||||||
this.input = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(ctx) {
|
async init(ctx) {
|
||||||
|
@ -72,37 +78,20 @@ class BeatSequencer {
|
||||||
}
|
}
|
||||||
|
|
||||||
class Sequencer {
|
class Sequencer {
|
||||||
constructor(piano, beats) {
|
constructor(instrument) {
|
||||||
this.piano = piano;
|
this.instrument = instrument;
|
||||||
this.beats = beats;
|
this.sequences = [];
|
||||||
this.pianoSequences = [];
|
|
||||||
this.beatsSequences = [];
|
|
||||||
this.idx = 0;
|
this.idx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
toBase64() {
|
|
||||||
const json = JSON.stringify({
|
|
||||||
piano: this.piano.input,
|
|
||||||
beats: this.beats.input,
|
|
||||||
});
|
|
||||||
return btoa(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
buildSequence(instrument, input) {
|
|
||||||
instrument.input = input;
|
|
||||||
return input.split('\n').map(line => instrument.parseLine(line));
|
|
||||||
}
|
|
||||||
|
|
||||||
length() {
|
length() {
|
||||||
return this.pianoSequences.map(a => a.length)
|
return this.sequences.map(a => a.length)
|
||||||
.concat(this.beatsSequences.map(a => a.length))
|
|
||||||
.reduce((a, b) => Math.max(a, b), 0);
|
.reduce((a, b) => Math.max(a, b), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
play() {
|
step() {
|
||||||
const length = this.length();
|
const length = this.length();
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
console.log('empty');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,29 +99,16 @@ class Sequencer {
|
||||||
this.idx = 0;
|
this.idx = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pianoSequences.map(a => a[this.idx])
|
this.sequences.map(a => a[this.idx])
|
||||||
.concat(this.beatsSequences.map(a => a[this.idx]))
|
|
||||||
.filter(maybeBuf => !!maybeBuf)
|
.filter(maybeBuf => !!maybeBuf)
|
||||||
.forEach(buf => cloneBuf(buf).start());
|
.forEach(buf => createBufferSource(buf.context, buf.buffer).start());
|
||||||
this.idx++;
|
this.idx++;
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePiano(input) {
|
update(input) {
|
||||||
this.pianoSequences = this.buildSequence(this.piano, input);
|
this.sequences = input.split('\n')
|
||||||
console.log(this.pianoSequences);
|
.map(line => this.instrument.parseLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBeats(input) {
|
|
||||||
this.beatsSequences = this.buildSequence(this.beats, input);
|
|
||||||
console.log(this.beatsSequences);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadNotes(ctx, volume, notes) {
|
|
||||||
return await Promise.all(notes.map(async (note) => {
|
|
||||||
const buffer = await loadBuffer(ctx, `samples/piano/${volume}/${note}.mp3`);
|
|
||||||
return createBufferSource(ctx, buffer);
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadBeats(ctx, letters) {
|
async function loadBeats(ctx, letters) {
|
||||||
|
@ -143,35 +119,36 @@ async function loadBeats(ctx, letters) {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSequencer(piano, beats) {
|
function setupSequencer(instrument, name) {
|
||||||
const sequencer = new Sequencer(piano, beats);
|
const sequencer = new Sequencer(instrument);
|
||||||
const pianoTextbox = document.getElementById('piano');
|
const textbox = document.getElementById(name);
|
||||||
const beatsTextbox = document.getElementById('beats');
|
sequencer.update(textbox.value);
|
||||||
if (window.location.hash.length > 0) {
|
textbox.addEventListener('input', e => {
|
||||||
const base64 = window.location.hash.slice(1);
|
sequencer.update(e.target.value);
|
||||||
try {
|
setHash();
|
||||||
const json = JSON.parse(atob(base64));
|
|
||||||
pianoTextbox.value = json.piano;
|
|
||||||
beatsTextbox.value = json.beats;
|
|
||||||
} catch(e) {
|
|
||||||
console.log(`invalid sequence ${base64}: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sequencer.updatePiano(pianoTextbox.value);
|
|
||||||
sequencer.updateBeats(beatsTextbox.value);
|
|
||||||
pianoTextbox.addEventListener('input', e => {
|
|
||||||
sequencer.updatePiano(e.target.value);
|
|
||||||
window.location.hash = sequencer.toBase64();
|
|
||||||
});
|
|
||||||
beatsTextbox.addEventListener('input', e => {
|
|
||||||
sequencer.updateBeats(e.target.value);
|
|
||||||
window.location.hash = sequencer.toBase64();
|
|
||||||
});
|
});
|
||||||
return sequencer;
|
return sequencer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneBuf(note) {
|
function setHash() {
|
||||||
return createBufferSource(note.context, note.buffer);
|
const state = JSON.stringify({
|
||||||
|
piano: document.getElementById('piano').value,
|
||||||
|
beats: document.getElementById('beats').value,
|
||||||
|
});
|
||||||
|
window.location.hash = btoa(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHash() {
|
||||||
|
if (window.location.hash.length > 0) {
|
||||||
|
const base64 = window.location.hash.slice(1);
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(atob(base64));
|
||||||
|
document.getElementById('piano').value = json.piano;
|
||||||
|
document.getElementById('beats').value = json.beats;
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`invalid sequence ${base64}: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function crossProduct(a, b) {
|
function crossProduct(a, b) {
|
||||||
|
@ -179,16 +156,20 @@ function crossProduct(a, b) {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('load', async () => {
|
window.addEventListener('load', async () => {
|
||||||
|
loadHash();
|
||||||
const ctx = new AudioContext();
|
const ctx = new AudioContext();
|
||||||
const piano = new PianoSequencer('CDEFGAB');
|
const piano = new Piano('CDEFGAB');
|
||||||
const beats = new BeatSequencer();
|
const beats = new Beats();
|
||||||
console.log('loading...');
|
|
||||||
await piano.init(ctx);
|
await piano.init(ctx);
|
||||||
await beats.init(ctx);
|
await beats.init(ctx);
|
||||||
console.log('done');
|
document.getElementById('status').remove();
|
||||||
const sequencer = setupSequencer(piano, beats);
|
const pianoSequencer = setupSequencer(piano, 'piano');
|
||||||
|
const beatsSequencer = setupSequencer(beats, 'beats');
|
||||||
|
|
||||||
const bpm = 120;
|
const bpm = 240;
|
||||||
const msPerBeat = (1 / bpm) * 60 * 1000;
|
const msPerBeat = (1 / bpm) * 60 * 1000;
|
||||||
setInterval(() => sequencer.play(), msPerBeat);
|
setInterval(() => {
|
||||||
|
pianoSequencer.step();
|
||||||
|
beatsSequencer.step();
|
||||||
|
}, msPerBeat);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue