wambam/index.html
2024-05-09 17:55:23 -04:00

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>