init spoopy sequencer
This commit is contained in:
commit
37141cd25e
1 changed files with 221 additions and 0 deletions
221
index.html
Normal file
221
index.html
Normal file
|
@ -0,0 +1,221 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en"><head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="ROBOTS" content="NOINDEX, NOFOLLOW">
|
||||
<title>wambam</title>
|
||||
<meta name="description" content="wambam">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
max-width: 50em;
|
||||
margin: 1em auto;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
padding: 0 1em;
|
||||
background: #111111;
|
||||
color: lightgray;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
|
||||
const tracks = [];
|
||||
const lookahead = 25.0; // ms to call scheduler
|
||||
const scheduleAheadTime = 0.1; // sec
|
||||
const beatsPerMeasure = 12;
|
||||
|
||||
let buffers = {};
|
||||
let filters = {};
|
||||
let instruments = {};
|
||||
let notesInQueue = [];
|
||||
let tempo = 60.0;
|
||||
let audioContext;
|
||||
let osc;
|
||||
let timer;
|
||||
|
||||
const startContext = () => {
|
||||
audioContext = new AudioContext();
|
||||
|
||||
initBuffers();
|
||||
defaultPlayer = instruments["💀"]
|
||||
}
|
||||
|
||||
const initBuffers = () => {
|
||||
const skullLength = .1500;
|
||||
const wormLength = .4;
|
||||
buffers.skull = new AudioBuffer({
|
||||
length: audioContext.sampleRate * skullLength,
|
||||
sampleRate: audioContext.sampleRate,
|
||||
});
|
||||
let skullData = buffers.skull.getChannelData(0);
|
||||
skullData.forEach((_, i) => skullData[i] = Math.random() * 2 - 1);
|
||||
buffers.worm = new AudioBuffer({
|
||||
length: audioContext.sampleRate * wormLength,
|
||||
sampleRate: audioContext.sampleRate,
|
||||
});
|
||||
let wormData = buffers.worm.getChannelData(0);
|
||||
wormData.forEach((_, i) => wormData[i] = Math.random() - 1);
|
||||
|
||||
filters.lurk = new BiquadFilterNode(audioContext, {
|
||||
type: "bandpass",
|
||||
frequency: 2000,
|
||||
})
|
||||
|
||||
instruments = {
|
||||
"💀": playBuffer(buffers.skull),
|
||||
"🪱": playBuffer(buffers.worm),
|
||||
"🕷️": playBufferWithFilter(buffers.skull, filters.lurk),
|
||||
"🐞": playBufferWithFilter(buffers.worm, filters.lurk)
|
||||
}
|
||||
}
|
||||
|
||||
const replaceBeginButton = () => {
|
||||
document.getElementById("beginButton").style.display = "none";
|
||||
document.getElementById("playButton").style.display = "block";
|
||||
document.getElementById("pauseButton").style.display = "block";
|
||||
document.getElementById("addTrackButton").style.display = "block";
|
||||
document.getElementById("tempoSlider").style.display = "block";
|
||||
document.getElementById("tempo").addEventListener("input", (event) => tempo = parseInt(event.target.value, 10), false);
|
||||
}
|
||||
|
||||
const play = () => {
|
||||
console.log("playing!");
|
||||
audioContext.resume();
|
||||
scheduler();
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
console.log("paused!");
|
||||
clearTimeout(timer);
|
||||
audioContext.suspend();
|
||||
}
|
||||
|
||||
const playBuffer = (buffer) => (time) => {
|
||||
const player = new AudioBufferSourceNode(audioContext, {buffer});
|
||||
player.connect(audioContext.destination);
|
||||
player.start(time)
|
||||
}
|
||||
|
||||
const playBufferWithFilter = (buffer, filter) => (time) => {
|
||||
const player = new AudioBufferSourceNode(audioContext, { buffer});
|
||||
player.connect(filter).connect(audioContext.destination);
|
||||
player.start(time);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function Track(idx, curTime) {
|
||||
this.play = defaultPlayer;
|
||||
this.hits= Array(beatsPerMeasure).fill(false);
|
||||
this.tempoModifier= idx + 1;
|
||||
this.nextNoteTime= curTime;
|
||||
this.currentNote= 0;
|
||||
this.nextNote= () => {
|
||||
const secondsPerBeat = 60.0 / tempo * this.tempoModifier / beatsPerMeasure;
|
||||
this.nextNoteTime += secondsPerBeat;
|
||||
this.currentNote = (this.currentNote + 1) % beatsPerMeasure;
|
||||
};
|
||||
|
||||
this.changePlayer = (key) => {
|
||||
this.play = instruments[key] || defaultPlayer;
|
||||
};
|
||||
|
||||
this.setHit = (beat, value) => {
|
||||
this.hits[beat] = value || false;
|
||||
};
|
||||
|
||||
this.scheduleNote= (beatNumber, time) => {
|
||||
notesInQueue.push({ track: idx, note: beatNumber});
|
||||
if (this.hits[beatNumber] == true) this.play();
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
const addTrack = () => {
|
||||
idx = tracks.length;
|
||||
curTime = audioContext.currentTime || 0;
|
||||
tracks.push(new Track(idx, curTime));
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
const scheduler = () => {
|
||||
tracks.forEach(track => {
|
||||
while(track.nextNoteTime < audioContext.currentTime + scheduleAheadTime) {
|
||||
track.scheduleNote(track.currentNote, track.nextNoteTime);
|
||||
track.nextNote();
|
||||
}
|
||||
})
|
||||
timer = setTimeout(scheduler, lookahead);
|
||||
}
|
||||
|
||||
const makeTrackTableRow = (idx) => {
|
||||
const table = document.getElementById("sequencer");
|
||||
const row = document.createElement("tr");
|
||||
row.id = `t${idx}`;
|
||||
const rowLabel = document.createElement("td");
|
||||
rowLabel.innerText = `t${idx}`;
|
||||
row.dataset.curBeat = 0;
|
||||
row.append(rowLabel);
|
||||
Object.keys(instruments).forEach((inst, i) => {
|
||||
const radio = document.createElement("input");
|
||||
radio.type = "radio";
|
||||
radio.id = inst;
|
||||
radio.value = inst;
|
||||
radio.name = `t${idx}-inst`;
|
||||
if (i == 0) radio.checked = true;
|
||||
radio.addEventListener("input", (ev) => {
|
||||
tracks[idx].changePlayer(ev.target.value)
|
||||
})
|
||||
const label = document.createElement("label");
|
||||
label.for = inst;
|
||||
label.innerText = inst;
|
||||
const td = document.createElement("td");
|
||||
td.append(radio, label);
|
||||
row.append(td);
|
||||
});
|
||||
|
||||
for (let beatIdx = 0; beatIdx < beatsPerMeasure; beatIdx++) {
|
||||
const td = document.createElement("td");
|
||||
const check = document.createElement("input");
|
||||
check.type = "checkbox";
|
||||
check.id = `t${idx}-${beatIdx}`;
|
||||
check.class = `beat-${beatIdx}`;
|
||||
check.addEventListener("change", (ev) => {
|
||||
tracks[idx].setHit(beatIdx, ev.target.checked);
|
||||
});
|
||||
check.style
|
||||
td.append(check);
|
||||
row.append(td);
|
||||
}
|
||||
|
||||
table.append(row);
|
||||
|
||||
}
|
||||
|
||||
for (let beatIdx = 0; beatIdx < beatsPerMeasure; beatIdx++) {
|
||||
const style = `
|
||||
table[data-currentbeat="${beatIdx}"] input.beat-${beatIdx} {
|
||||
color: purple;
|
||||
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<body>
|
||||
<h1>spoopy sequencer</h1>
|
||||
<p>each new track is faster than the last</p>
|
||||
<table> <tbody id="sequencer">
|
||||
</tbody>
|
||||
</table>
|
||||
<menu id="playControls">
|
||||
<!-- IIFEs! (no fear of parentheses here) -->
|
||||
<li id="beginButton"><button onclick="(() =>{startContext(); replaceBeginButton()})()">Begin</button></li>
|
||||
<li id="playButton" style="display:none;"><button onclick="(() => play())()">Play</button></li>
|
||||
<li id="pauseButton" style="display:none;"><button onclick="(() => pause())()">Pause</button></li>
|
||||
<li id="addTrackButton" style="display:none;"><button onclick="(() => {makeTrackTableRow(addTrack())})()">Add Track</button></li>
|
||||
<li id="tempoSlider" style="display:none;"><label for="tempo">tempo></label><input type="range" id="tempo" name="tempo" min="30" max="240" value="60" /></li>
|
||||
</menu>
|
||||
</body></html>
|
Loading…
Reference in a new issue