Files
corescope-voices/voices/audio-v6-dub.js
T
richandClaude Opus 5.5 6dc4592ed2 Initial voice pack: seven live-map voices for CoreScope
Voices taken from CoreScope feat/audio-voices (ac257569), plus a compat
shim for stock audio.js, an idempotent install/uninstall script, a
derived-image Dockerfile, and tests that run against a real public/ dir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:39:51 -04:00

141 lines
6.6 KiB
JavaScript

// Voice v6: "Dub" — reggae/dub, A natural minor
// One-drop drums (kick + rimshot on beat 3), offbeat chord skanks and rims thrown
// into a long feedback echo, a deep bass line, and a dub siren on traces.
// ADVERT → two bars: skanks + bass + one-drop
// GRP_TXT → one bar of skanks + rimshot (light, it's the most frequent type)
// TXT_MSG → one bar: bass line + skanks, no drums
// TRACE → a dub siren sweep + rimshots into the echo
// More hops send more into the echo, so long routes trail away; observers
// widen the skanks' stereo spread. Requires audio-synthkit.js.
(function () {
'use strict';
const { sampleBytes, midiToFreq } = MeshAudio.helpers;
const kit = MeshAudio.synthkit;
const SCALE = [0, 2, 3, 5, 7, 8, 10]; // A natural minor
const ROOT = 45; // A2
const CHORDS = [0, 3, 6, 4]; // i, iv, VII, v
const noteAt = (base, d) => base + 12 * Math.floor(d / 7) + SCALE[d % 7];
const triad = (base, d) => [noteAt(base, d), noteAt(base, d + 2), noteAt(base, d + 4)];
const chordOf = (b) => CHORDS[Math.min(CHORDS.length - 1, Math.floor((b / 256) * CHORDS.length))];
// Pure: packet → one-bar-per-chord arrangement on a 16th grid. No audio here.
// { steps, skanks: [{step,midis,side}], bass: [{step,len,midi}],
// drums: [{step,kind:'kick'|'rim'}], sirens: [{step,len,midi}] }
function arrange(parsed) {
const { payloadBytes, typeName } = parsed;
const kind = ['GRP_TXT', 'TXT_MSG', 'TRACE'].includes(typeName) ? typeName : 'ADVERT';
const out = { steps: 0, skanks: [], bass: [], drums: [], sirens: [] };
if (kind === 'TRACE') {
const bytes = sampleBytes(payloadBytes, 2, 3);
bytes.forEach((b, i) => {
out.sirens.push({ step: i * 8, len: 8, midi: noteAt(69, b % 5) }); // from A4..E5
out.drums.push({ step: i * 8 + 6, kind: 'rim' });
});
out.steps = bytes.length * 8;
return out;
}
const bars = kind === 'ADVERT' ? sampleBytes(payloadBytes, 2, 2) : sampleBytes(payloadBytes, 1, 1);
bars.forEach((b, bar) => {
const d = chordOf(b);
const s0 = bar * 16;
// Skanks on the offbeat 8ths ("and" of each beat), alternating sides
[2, 6, 10, 14].forEach((s, k) => out.skanks.push({ step: s0 + s, midis: triad(ROOT + 12, d), side: k % 2 ? 1 : -1 }));
if (kind !== 'GRP_TXT') {
// Bass: root and fifth on the beats; the byte picks a passing tone before beat 3 and the last note's octave
const root = noteAt(ROOT, d);
out.bass.push({ step: s0, len: 3, midi: root });
out.bass.push({ step: s0 + 4, len: 2, midi: root });
out.bass.push({ step: s0 + 7, len: 1, midi: noteAt(ROOT, d + 1 + (b & 1)) });
out.bass.push({ step: s0 + 8, len: 3, midi: noteAt(ROOT, d + 4) });
out.bass.push({ step: s0 + 12, len: 2, midi: root + (b & 2 ? 12 : 0) });
}
if (kind !== 'TXT_MSG') {
// One drop: kick and rimshot together on beat 3
if (kind === 'ADVERT') out.drums.push({ step: s0 + 8, kind: 'kick' });
out.drums.push({ step: s0 + 8, kind: 'rim' });
}
});
out.steps = bars.length * 16;
return out;
}
const SKANK = { type: 'square', detunes: [-5, 5], cutoff: [2600, 2200, 1400], q: 1, attack: 0.003, sustain: 0.15, release: 0.05, level: 0.6 };
const BASS = { type: 'triangle', detunes: [0], cutoff: [700, 700, 500], q: 0.5, attack: 0.01, sustain: 0.9, release: 0.08, level: 0.5 };
const RIM = { level: 0.35, decay: 0.07, body: 420 };
const KICK = { from: 110, to: 45, sweep: 0.1, decay: 0.4, level: 0.9 };
// Dub siren: a sine sweeping up and back down, straight into the echo
function playSiren(audioCtx, dest, echoIn, midi, start, dur) {
const osc = audioCtx.createOscillator();
const env = audioCtx.createGain();
const send = audioCtx.createGain();
const f = midiToFreq(midi);
osc.type = 'sine';
osc.frequency.setValueAtTime(f, start);
osc.frequency.exponentialRampToValueAtTime(f * 2, start + dur * 0.5);
osc.frequency.exponentialRampToValueAtTime(f, start + dur);
env.gain.setValueAtTime(0.0001, start);
env.gain.exponentialRampToValueAtTime(0.12, start + 0.05);
env.gain.setValueAtTime(0.12, start + dur * 0.8);
env.gain.exponentialRampToValueAtTime(0.0001, start + dur);
send.gain.value = 0.8;
osc.connect(env);
env.connect(dest);
env.connect(send);
send.connect(echoIn);
osc.start(start);
osc.stop(start + dur + 0.05);
osc.onended = () => { osc.disconnect(); env.disconnect(); send.disconnect(); };
return start + dur + 0.05;
}
function play(audioCtx, masterGain, parsed, opts) {
const { hopCount, obsCount } = parsed;
const sixteenth = 0.125 * opts.tempoMultiplier;
const noise = kit.bus(audioCtx, masterGain, 'noise').buffer;
const hall = kit.bus(audioCtx, masterGain, 'hall').input;
const echo = kit.bus(audioCtx, masterGain, 'dubecho');
echo.delays[0].delayTime.value = 3 * sixteenth; // dotted 8th
const arr = arrange(parsed);
const distance = Math.min(1, (hopCount - 1) / 9);
const throwAmount = 0.25 + 0.55 * distance; // more hops → more echo
const width = Math.min(0.8, 0.2 + 0.1 * (obsCount - 1));
const { mix, nodes } = kit.createMix(audioCtx, masterGain, Math.min(0.7, 0.5 + (obsCount - 1) * 0.015));
const sides = [-1, 1].map((side) => {
const p = audioCtx.createStereoPanner();
p.pan.value = side * width;
p.connect(mix);
return p;
});
const t0 = kit.gridStart(audioCtx, sixteenth);
const at = (step) => t0 + step * sixteenth;
let lastEnd = t0;
const track = (t) => { if (t > lastEnd) lastEnd = t; };
arr.skanks.forEach((ev) => track(kit.playSynth(audioCtx, sides[ev.side > 0 ? 1 : 0], SKANK, ev.midis, at(ev.step), sixteenth * 0.9,
{ sends: [[echo.input, throwAmount]] })));
arr.bass.forEach((ev) => track(kit.playSynth(audioCtx, mix, BASS, [ev.midi], at(ev.step), ev.len * sixteenth - 0.02)));
arr.sirens.forEach((ev) => track(playSiren(audioCtx, mix, echo.input, ev.midi, at(ev.step), ev.len * sixteenth)));
arr.drums.forEach((ev) => {
if (ev.kind === 'kick') track(kit.playKick(audioCtx, mix, at(ev.step), KICK));
else track(kit.playSnare(audioCtx, mix, noise, at(ev.step), [[echo.input, throwAmount + 0.2], [hall, 0.15]], RIM));
});
// Echo tails ring in the shared bus, so the voice slot frees when the notes end.
setTimeout(() => {
nodes.concat(sides).forEach((n) => { try { n.disconnect(); } catch (e) {} });
}, (lastEnd - audioCtx.currentTime + 1) * 1000);
return lastEnd - audioCtx.currentTime;
}
MeshAudio.registerVoice('dub', { name: 'dub', play, arrange });
})();