221 lines
7.5 KiB
HTML
221 lines
7.5 KiB
HTML
<!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>
|