mirror of
https://github.com/wgreenberg/musicbox.git
synced 2024-11-10 05:40:26 +01:00
basic functionality w/ static bpm
This commit is contained in:
parent
2b1a105858
commit
34d07d363e
3 changed files with 215 additions and 0 deletions
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
musicbox
|
||||
========
|
||||
|
||||
attribution
|
||||
-----------
|
||||
|
||||
* piano samples come from the [University of Iowa's EMS repository](http://theremin.music.uiowa.edu/MISpiano.html).
|
||||
* beat samples come from [typedrummer](http://typedrummer.com)
|
13
index.html
Normal file
13
index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<html>
|
||||
<head>
|
||||
<script src='musicbox.js'></script>
|
||||
<style>
|
||||
textarea {
|
||||
width: 200px;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<textarea id='beats'></textarea>
|
||||
<textarea id='piano'></textarea>
|
||||
</html>
|
194
musicbox.js
Normal file
194
musicbox.js
Normal file
|
@ -0,0 +1,194 @@
|
|||
async function loadBuffer(ctx, path) {
|
||||
const response = await fetch(path);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
|
||||
return audioBuffer;
|
||||
}
|
||||
|
||||
function createBufferSource(ctx, buffer) {
|
||||
let source = ctx.createBufferSource();
|
||||
source.buffer = buffer;
|
||||
source.connect(ctx.destination);
|
||||
return source;
|
||||
}
|
||||
|
||||
class PianoSequencer {
|
||||
constructor(notes) {
|
||||
// load 3 octaves of the given notes
|
||||
this.notes = crossProduct(notes, '345');
|
||||
this.input = '';
|
||||
this.lowOctave = 'qwertyu';
|
||||
this.midOctave = 'asdfghj';
|
||||
this.highOctave = 'zxcvbnm';
|
||||
}
|
||||
|
||||
async init(ctx) {
|
||||
this.ff = await loadNotes(ctx, 'ff', this.notes);
|
||||
this.mf = await loadNotes(ctx, 'mf', this.notes);
|
||||
}
|
||||
|
||||
parseLine(line) {
|
||||
return Array.from(line).map(letter => this.getNote(letter));
|
||||
}
|
||||
|
||||
getNote(letter) {
|
||||
const isCaps = (letter === letter.toUpperCase());
|
||||
const notes = isCaps ? this.ff : this.mf;
|
||||
const idx = this.letterToIndex(letter);
|
||||
return idx === null ? null : notes[idx];
|
||||
}
|
||||
|
||||
letterToIndex(letter) {
|
||||
letter = letter.toLowerCase();
|
||||
if (this.lowOctave.includes(letter)) {
|
||||
return 0 + this.lowOctave.indexOf(letter);
|
||||
} else if (this.midOctave.includes(letter)) {
|
||||
return 7 + this.midOctave.indexOf(letter);
|
||||
} else if (this.highOctave.includes(letter)) {
|
||||
return 14 + this.highOctave.indexOf(letter);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class BeatSequencer {
|
||||
cosntructor() {
|
||||
this.input = '';
|
||||
}
|
||||
|
||||
async init(ctx) {
|
||||
this.alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
this.beats = await loadBeats(ctx, this.alphabet);
|
||||
}
|
||||
|
||||
parseLine(line) {
|
||||
return Array.from(line).map(letter => this.getBeat(letter));
|
||||
}
|
||||
|
||||
getBeat(letter) {
|
||||
const idx = this.alphabet.indexOf(letter.toLowerCase());
|
||||
return idx >= 0 ? this.beats[idx] : null;
|
||||
}
|
||||
}
|
||||
|
||||
class Sequencer {
|
||||
constructor(piano, beats) {
|
||||
this.piano = piano;
|
||||
this.beats = beats;
|
||||
this.pianoSequences = [];
|
||||
this.beatsSequences = [];
|
||||
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() {
|
||||
return this.pianoSequences.map(a => a.length)
|
||||
.concat(this.beatsSequences.map(a => a.length))
|
||||
.reduce((a, b) => Math.max(a, b), 0);
|
||||
}
|
||||
|
||||
play() {
|
||||
const length = this.length();
|
||||
if (length === 0) {
|
||||
console.log('empty');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.idx >= length) {
|
||||
this.idx = 0;
|
||||
}
|
||||
|
||||
this.pianoSequences.map(a => a[this.idx])
|
||||
.concat(this.beatsSequences.map(a => a[this.idx]))
|
||||
.filter(maybeBuf => !!maybeBuf)
|
||||
.forEach(buf => cloneBuf(buf).start());
|
||||
this.idx++;
|
||||
}
|
||||
|
||||
updatePiano(input) {
|
||||
this.pianoSequences = this.buildSequence(this.piano, input);
|
||||
console.log(this.pianoSequences);
|
||||
}
|
||||
|
||||
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) {
|
||||
const beats = Array.from(letters);
|
||||
return await Promise.all(beats.map(async (beat) => {
|
||||
const buffer = await loadBuffer(ctx, `samples/beats/${beat}.mp3`);
|
||||
return createBufferSource(ctx, buffer);
|
||||
}));
|
||||
}
|
||||
|
||||
function setupSequencer(piano, beats) {
|
||||
const sequencer = new Sequencer(piano, beats);
|
||||
const pianoTextbox = document.getElementById('piano');
|
||||
const beatsTextbox = document.getElementById('beats');
|
||||
if (window.location.hash.length > 0) {
|
||||
const base64 = window.location.hash.slice(1);
|
||||
try {
|
||||
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;
|
||||
}
|
||||
|
||||
function cloneBuf(note) {
|
||||
return createBufferSource(note.context, note.buffer);
|
||||
}
|
||||
|
||||
function crossProduct(a, b) {
|
||||
return Array.from(b).flatMap(a_i => Array.from(a).map(b_i => b_i + a_i));
|
||||
}
|
||||
|
||||
window.addEventListener('load', async () => {
|
||||
const ctx = new AudioContext();
|
||||
const piano = new PianoSequencer('CDEFGAB');
|
||||
const beats = new BeatSequencer();
|
||||
console.log('loading...');
|
||||
await piano.init(ctx);
|
||||
await beats.init(ctx);
|
||||
console.log('done');
|
||||
const sequencer = setupSequencer(piano, beats);
|
||||
|
||||
const bpm = 120;
|
||||
const msPerBeat = (1 / bpm) * 60 * 1000;
|
||||
setInterval(() => sequencer.play(), msPerBeat);
|
||||
});
|
Loading…
Reference in a new issue