Compare commits

..

No commits in common. "c0bf81c3fca16198d9cee5527f2021743f6301f8" and "678482a0f24b3d8ce3ea447ab157884f5a4c74ad" have entirely different histories.

10 changed files with 46 additions and 74 deletions

View file

@ -1 +1 @@
0.0.41 0.0.40

1
assets/samples.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Assemble the deployed single-file pages from source + shared partials + assets/. # Assemble the deployed single-file pages from source + shared partials + assets/.
# #
# Every page (the Concepts landing, the editor app, and the device/form-factor # Every page (the landing index.html, the editor.html app, the device mockups and
# pages) is a source that shares code via markers: # the info-*.html pages) is a source that shares code via markers:
# /*@BUILD:include:src/<file>@*/ inlines a shared partial (engine, seed lists, base CSS, header/footer/chrome) # /*@BUILD:include:src/<file>@*/ inlines a shared partial (engine, seed lists, base CSS)
# @BUILD:favicon@ / @BUILD:logo-*@ inline base64 assets (voices are all synthesized — no samples) # @BUILD:favicon@ / @BUILD:logo-*@ / /*@BUILD:samples@*/{} inline base64 assets
# This resolves them so each built page in dist/ is one self-contained file # This resolves them so each built page in dist/ is one self-contained file
# (zero deps, works fully offline). deploy.sh runs this first. dist/ is generated — # (zero deps, works fully offline). deploy.sh runs this first. dist/ is generated —
# don't edit or commit it. # don't edit or commit it.
@ -12,18 +12,20 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")" cd "$(dirname "${BASH_SOURCE[0]}")"
mkdir -p dist mkdir -p dist
python3 - <<'PY' python3 - <<'PY'
import os, pathlib, re import json, os, pathlib, re
A = pathlib.Path("assets") A = pathlib.Path("assets")
SAMPLES = json.dumps(json.load(open(A / "samples.json")))
def build(name): def build(name):
src = pathlib.Path(name).read_text() src = pathlib.Path(name).read_text()
# 1) inline shared partials (function-replacement: no backslash/group interpretation) # 1) inline shared partials (function-replacement: no backslash/group interpretation)
src = re.sub(r"/\*@BUILD:include:([^@]+)@\*/", src = re.sub(r"/\*@BUILD:include:([^@]+)@\*/",
lambda m: pathlib.Path(m.group(1)).read_text().rstrip("\n"), src) lambda m: pathlib.Path(m.group(1)).read_text().rstrip("\n"), src)
# 2) inline base64 assets (voices are all synthesized now — no samples) # 2) inline base64 assets
src = src.replace("@BUILD:favicon@", (A / "favicon.b64").read_text().strip()) src = src.replace("@BUILD:favicon@", (A / "favicon.b64").read_text().strip())
src = src.replace("@BUILD:logo-dark@", (A / "logo-dark.b64").read_text().strip()) src = src.replace("@BUILD:logo-dark@", (A / "logo-dark.b64").read_text().strip())
src = src.replace("@BUILD:logo-light@", (A / "logo-light.b64").read_text().strip()) src = src.replace("@BUILD:logo-light@", (A / "logo-light.b64").read_text().strip())
src = src.replace("/*@BUILD:samples@*/{}", SAMPLES)
assert "@BUILD:" not in src, f"unresolved build marker(s) remain in {name}" assert "@BUILD:" not in src, f"unresolved build marker(s) remain in {name}"
out = pathlib.Path("dist") / name out = pathlib.Path("dist") / name
out.write_text(src) out.write_text(src)

View file

@ -434,7 +434,7 @@ let meterSeq = 0; // id counter
/* ---- shared engine: audio voices, scheduler primitives, share-language codec. /* ---- shared engine: audio voices, scheduler primitives, share-language codec.
Inlined from src/engine.js by build.sh; identical in player.html. ---- */ Inlined from src/engine.js by build.sh; identical in player.html. ---- */
const SAMPLES = {}; // all voices are synthesized (samples removed; 808/909 renders are the kit) const SAMPLES = /*@BUILD:samples@*/{}; // CC0 acoustic one-shots (inlined at build); empty ⇒ pure synth
/*@BUILD:include:src/engine.js@*/ /*@BUILD:include:src/engine.js@*/
/* ========================================================================= /* =========================================================================
@ -552,12 +552,12 @@ function setBpm(v) {
========================================================================= */ ========================================================================= */
function laneColor(id) { return `hsl(${(id * 67) % 360} 70% 62%)`; } function laneColor(id) { return `hsl(${(id * 67) % 360} 70% 62%)`; }
function addMeter(groupsStr = "4", stepsPerBeat = 4, sound = "beep", beatsOn = null, poly = false, swing = false, gainDb = 0) { function addMeter(groupsStr = "4", stepsPerBeat = 4, sound = "beep", beatsOn = null, poly = false, swing = false) {
const id = ++meterSeq; const id = ++meterSeq;
const p = parseGroups(groupsStr); const p = parseGroups(groupsStr);
const m = { const m = {
id, groupsStr, groups: p.groups, beatsPerBar: p.beatsPerBar, groupStarts: p.groupStarts, id, groupsStr, groups: p.groups, beatsPerBar: p.beatsPerBar, groupStarts: p.groupStarts,
stepsPerBeat, sound, enabled: true, poly: !!poly, swing: !!swing, gainDb: gainDb || 0, color: laneColor(id), stepsPerBeat, sound, enabled: true, poly: !!poly, swing: !!swing, color: laneColor(id),
beatsOn: beatsOn ? beatsOn.slice() : [], // per-STEP dynamics mask (one entry per pad: 0 mute / 1 normal / 2 accent) beatsOn: beatsOn ? beatsOn.slice() : [], // per-STEP dynamics mask (one entry per pad: 0 mute / 1 normal / 2 accent)
tick: 0, nextTime: 0, vq: [], vqPtr: 0, currentStep: -1, currentBar: 0, tick: 0, nextTime: 0, vq: [], vqPtr: 0, currentStep: -1, currentBar: 0,
el: null, stripEl: null, barEl: null, el: null, stripEl: null, barEl: null,
@ -709,11 +709,11 @@ const LS = { presets: "metronome.presets", setlists: "metronome.setlists", logs:
function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } } function lsGet(k, fb) { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : fb; } catch (e) { return fb; } }
function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } } function lsSet(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); return true; } catch (e) { console.warn("localStorage unavailable", e); return false; } }
function snapshotLanes() { return meters.map((m) => ({ groupsStr: m.groupsStr, stepsPerBeat: m.stepsPerBeat, sound: m.sound, enabled: m.enabled, poly: m.poly, swing: !!m.swing, gainDb: m.gainDb || 0, beatsOn: m.beatsOn.slice() })); } function snapshotLanes() { return meters.map((m) => ({ groupsStr: m.groupsStr, stepsPerBeat: m.stepsPerBeat, sound: m.sound, enabled: m.enabled, poly: m.poly, swing: !!m.swing, beatsOn: m.beatsOn.slice() })); }
function applyLanes(lanes) { function applyLanes(lanes) {
while (meters.length) removeMeter(meters[0].id); while (meters.length) removeMeter(meters[0].id);
for (const c of lanes) { for (const c of lanes) {
addMeter(c.groupsStr, c.stepsPerBeat, c.sound, c.beatsOn, c.poly, c.swing, c.gainDb); addMeter(c.groupsStr, c.stepsPerBeat, c.sound, c.beatsOn, c.poly, c.swing);
const m = meters[meters.length - 1]; const m = meters[meters.length - 1];
setLaneEnabled(m, c.enabled !== undefined ? !!c.enabled : (c.mute === undefined ? true : !c.mute)); // back-compat with old "mute" setLaneEnabled(m, c.enabled !== undefined ? !!c.enabled : (c.mute === undefined ? true : !c.mute)); // back-compat with old "mute"
} }

View file

@ -190,7 +190,7 @@ function scheduler(){
function buildMeters(lanes){ function buildMeters(lanes){
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr); return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts, return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0, stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; }); tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
} }
function startAudio(){ function startAudio(){

View file

@ -316,7 +316,7 @@ function buildMeters(lanes){
return (lanes||[]).map(c=>{ return (lanes||[]).map(c=>{
const p=parseGroups(c.groupsStr); const p=parseGroups(c.groupsStr);
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts, return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0, stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0};
}); });
} }

View file

@ -133,7 +133,7 @@ function scheduler(){
function buildMeters(lanes){ function buildMeters(lanes){
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr); return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts, return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0, stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; }); tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
} }
function startAudio(){ function startAudio(){

View file

@ -1,17 +1,15 @@
/* ========================================================================= /* =========================================================================
SHARED ENGINE inlined into every page by build.sh. SHARED ENGINE inlined into BOTH index.html and player.html by build.sh.
Audio voices (all SYNTHESIZED no samples), the Web Audio look-ahead Audio voices, the Web Audio look-ahead scheduler primitives (PORTS TO
scheduler primitives (PORTS TO FIRMWARE), and the share-language codec. FIRMWARE), and the share-language codec. Each host supplies its own state
Each host supplies its own state globals (state, meters, ramp, trainer, globals (state, meters, ramp, trainer, segBars, masterBeat), its own
segBars, masterBeat), its own setBpm, advanceMaster and scheduler(). setBpm, advanceMaster and scheduler(), and its own SAMPLES object (the
Conventions (kept close to what people already know): GM drum names + editor inlines CC0 one-shots; the player passes {} for pure synth).
note-number aliases, drum-tab step patterns (X accent / x normal / g ghost /
. - _ rest), subdivisions /2 /3 /4, Euclidean (k,n) shorthand, per-lane gain
in dB (@<db>), and ~ = polymeter.
========================================================================= */ ========================================================================= */
const LOOKAHEAD_MS = 25, SCHEDULE_AHEAD = 0.12; const LOOKAHEAD_MS = 25, SCHEDULE_AHEAD = 0.12;
const SWING_RATIO = 2 / 3; // triplet swing: the off-beat lands on the last triplet const SWING_RATIO = 2 / 3; // triplet swing: the off-beat lands on the last triplet
let audioCtx = null, masterGain = null, noiseBuf = null, schedulerTimer = null; let audioCtx = null, masterGain = null, noiseBuf = null, schedulerTimer = null;
const sampleBuffers = {}; // decoded CC0 acoustic one-shots (VCSL, CC0); acoustic voices play these, electronic stay synth
// --- grouping: "2+2+3" → groups / beatsPerBar / group-start indices --- // --- grouping: "2+2+3" → groups / beatsPerBar / group-start indices ---
@ -26,13 +24,18 @@ function parseGroups(str) {
return { groups, beatsPerBar, groupStarts }; return { groups, beatsPerBar, groupStarts };
} }
// --- audio context --- // --- audio context + embedded-sample decode ---
function ensureAudio() { function ensureAudio() {
if (audioCtx) return; if (audioCtx) return;
audioCtx = new (window.AudioContext || window.webkitAudioContext)(); audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain(); masterGain = audioCtx.createGain();
masterGain.gain.value = state.volume; masterGain.gain.value = state.volume;
masterGain.connect(audioCtx.destination); masterGain.connect(audioCtx.destination);
for (const k in SAMPLES) { // decode embedded one-shots once
const bin = atob(SAMPLES[k]); const u8 = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
audioCtx.decodeAudioData(u8.buffer, (b) => { sampleBuffers[k] = b; }, () => {});
}
} }
// --- shared noise buffer --- // --- shared noise buffer ---
@ -115,30 +118,12 @@ const VOICES = [
["kick808", "808 kick"], ["snare808", "808 snare"], ["clap808", "808 clap"], ["hat808", "808 hat"], ["openHat808", "808 open hat"], ["cowbell808", "808 cowbell"], ["tom808", "808 tom"], ["kick808", "808 kick"], ["snare808", "808 snare"], ["clap808", "808 clap"], ["hat808", "808 hat"], ["openHat808", "808 open hat"], ["cowbell808", "808 cowbell"], ["tom808", "808 tom"],
["kick909", "909 kick"], ["snare909", "909 snare"], ["clap909", "909 clap"], ["hat909", "909 hat"], ["ride909", "909 ride"], ["crash909", "909 crash"], ["kick909", "909 kick"], ["snare909", "909 snare"], ["clap909", "909 clap"], ["hat909", "909 hat"], ["ride909", "909 ride"], ["crash909", "909 crash"],
]; ];
// Default kit points the friendly GM names at the punchier 808/909 renders const SAMPLE_ALIAS = { tomMid: "tomLow" }; // VCSL has only 2 toms — mid = low tom pitched up
// (samples removed — those synth voices sound better). Pick "kick808"/"kick909" const SAMPLE_RATE = { tomMid: 1.19 }; // ~3 semitones
// etc. explicitly for a specific machine flavour.
const KIT_ALIAS = {
kick: "kick909", snare: "snare909", clap: "clap909",
hatClosed: "hat909", hatOpen: "openHat808", ride: "ride909", crash: "crash909",
cowbell: "cowbell808",
};
// General-MIDI percussion note numbers → our voice names (so MIDI/DAW users can type numbers)
const GM_NUM = {
35: "kick", 36: "kick", 37: "rim", 38: "snare", 39: "clap", 40: "snare",
41: "tomLow", 42: "hatClosed", 43: "tomLow", 44: "hatClosed", 45: "tomMid",
46: "hatOpen", 47: "tomMid", 48: "tomHigh", 49: "crash", 50: "tomHigh",
51: "ride", 53: "ride", 54: "tambourine", 56: "cowbell", 75: "claves",
76: "woodblock", 77: "woodblock",
};
// Euclidean / Bjorklund-class even distribution: k hits over n steps, rotated by rot
function euclid(k, n, rot) {
n = Math.max(1, n | 0); k = Math.max(0, Math.min(n, k | 0)); rot = (((rot | 0) % n) + n) % n;
const r = []; for (let i = 0; i < n; i++) { const j = (i + rot) % n; r.push(((j * k) % n) < k ? 1 : 0); }
return r;
}
function playInstrument(type, time, level) { function playInstrument(type, time, level) {
(DRUMS[KIT_ALIAS[type] || type] || DRUMS[type] || DRUMS.beep)(time, level); const buf = sampleBuffers[type] || sampleBuffers[SAMPLE_ALIAS[type]];
if (buf) { const s = audioCtx.createBufferSource(); s.buffer = buf; s.playbackRate.value = SAMPLE_RATE[type] || 1; const g = audioCtx.createGain(); g.gain.value = level; s.connect(g); g.connect(masterGain); s.start(time); return; }
(DRUMS[type] || DRUMS.beep)(time, level);
} }
// --- scheduler primitives (PORTS TO FIRMWARE) --- // --- scheduler primitives (PORTS TO FIRMWARE) ---
@ -155,8 +140,7 @@ function scheduleMeterTick(m, time) {
if (!m.enabled || isMutedAt(time)) return; if (!m.enabled || isMutedAt(time)) return;
const lvl = m.beatsOn[tickInBar] | 0; // dynamics: 0 mute · 1 normal · 2 accent · 3 ghost const lvl = m.beatsOn[tickInBar] | 0; // dynamics: 0 mute · 1 normal · 2 accent · 3 ghost
if (!lvl) return; if (!lvl) return;
const lin = m.gainDb ? Math.pow(10, m.gainDb / 20) : 1; // per-lane dB gain → linear, applied at schedule time (no stutter) playInstrument(m.sound, time, lvl === 2 ? 1.0 : lvl === 3 ? 0.25 : 0.6);
playInstrument(m.sound, time, (lvl === 2 ? 1.0 : lvl === 3 ? 0.25 : 0.6) * lin);
} }
function refBarDur() { return (meters.length ? meters[0].beatsPerBar : 4) * (60 / state.bpm); } function refBarDur() { return (meters.length ? meters[0].beatsPerBar : 4) * (60 / state.bpm); }
@ -177,42 +161,27 @@ function laneCfgToStr(c) {
let s = c.sound + ":" + c.groupsStr; let s = c.sound + ":" + c.groupsStr;
const spb = c.stepsPerBeat || 1; const spb = c.stepsPerBeat || 1;
if (spb !== 1 || c.swing) s += "/" + spb + (c.swing ? "s" : ""); // "/2s" = swung eighths if (spb !== 1 || c.swing) s += "/" + spb + (c.swing ? "s" : ""); // "/2s" = swung eighths
const on = c.beatsOn || []; // per-step dynamics: one char per pad (X accent / x normal / g ghost / . mute) const on = c.beatsOn || []; // per-step dynamics: one char per pad (X accent / x normal / . mute)
const isDefault = on.length && on.every((v, i) => (v | 0) === ((i % spb) === 0 ? 2 : 1)); const isDefault = on.length && on.every((v, i) => (v | 0) === ((i % spb) === 0 ? 2 : 1));
if (on.length && !isDefault) s += "=" + on.map((v) => (v === 3 ? "g" : v >= 2 ? "X" : v >= 1 ? "x" : ".")).join(""); if (on.length && !isDefault) s += "=" + on.map((v) => (v === 3 ? "g" : v >= 2 ? "X" : v >= 1 ? "x" : ".")).join("");
if (c.gainDb) s += "@" + c.gainDb; // per-lane gain in dB (e.g. @-3, @2)
if (c.poly) s += "~"; if (c.poly) s += "~";
if (c.enabled === false) s += "!"; // "!" = silenced / disabled if (c.enabled === false) s += "!"; // "!" = silenced / disabled
return s; return s;
} }
function laneStrToCfg(tok) { function laneStrToCfg(tok) {
let poly = false, disabled = false, gainDb = 0; let poly = false, disabled = false;
while (/[~!]$/.test(tok)) { if (tok.endsWith("!")) disabled = true; else poly = true; tok = tok.slice(0, -1); } while (/[~!]$/.test(tok)) { if (tok.endsWith("!")) disabled = true; else poly = true; tok = tok.slice(0, -1); }
const at = tok.indexOf("@"); if (at >= 0) { gainDb = parseFloat(tok.slice(at + 1)) || 0; tok = tok.slice(0, at); } // @<db> gain
const ci = tok.indexOf(":"); if (ci < 0) return null; const ci = tok.indexOf(":"); if (ci < 0) return null;
let sound = tok.slice(0, ci), rest = tok.slice(ci + 1), pattern = null; let sound = tok.slice(0, ci), rest = tok.slice(ci + 1), pattern = null;
if (/^\d+$/.test(sound) && GM_NUM[sound]) sound = GM_NUM[sound]; // GM note number → name
// Euclidean shorthand (k,n[,rot]) — replaces an explicit =pattern
let eucK = null, eucN = null, eucRot = 0;
const em = rest.match(/\((\d+)(?:,(\d+))?(?:,(\d+))?\)/);
if (em) { eucK = +em[1]; eucN = em[2] != null ? +em[2] : null; eucRot = em[3] != null ? +em[3] : 0; rest = rest.slice(0, em.index) + rest.slice(em.index + em[0].length); }
const eq = rest.indexOf("="); if (eq >= 0) { pattern = rest.slice(eq + 1); rest = rest.slice(0, eq); } const eq = rest.indexOf("="); if (eq >= 0) { pattern = rest.slice(eq + 1); rest = rest.slice(0, eq); }
let groupsStr = rest, sub = 1, swing = false; const sl = rest.indexOf("/"); let groupsStr = rest, sub = 1, swing = false; const sl = rest.indexOf("/");
if (sl >= 0) { groupsStr = rest.slice(0, sl); const sp = rest.slice(sl + 1); swing = /s$/i.test(sp); sub = parseInt(sp, 10) || 1; } if (sl >= 0) { groupsStr = rest.slice(0, sl); const sp = rest.slice(sl + 1); swing = /s$/i.test(sp); sub = parseInt(sp, 10) || 1; }
let bpb = parseGroups(groupsStr).beatsPerBar; const bpb = parseGroups(groupsStr).beatsPerBar;
let beatsOn; // pattern levels: X=accent(2), x/1=normal(1), . / anything else = mute(0); no pattern → default (first of each beat accented)
if (eucK != null) { // k hits spread evenly; first hit accented const beatsOn = pattern ? pattern.split("").map((ch) => ch === "X" ? 2 : ch === "g" ? 3 : (ch === "x" || ch === "1") ? 1 : 0)
let n = eucN || (bpb * sub);
if (eucN) { if (n % bpb === 0) sub = n / bpb; else { bpb = n; sub = 1; groupsStr = String(n); } }
let first = true;
beatsOn = euclid(eucK, n, eucRot).map((h) => h ? (first ? (first = false, 2) : 1) : 0);
} else {
// pattern levels: X=accent(2), g=ghost(3), x/1=normal(1), . - _ / anything else = mute(0); no pattern → first of each beat accented
beatsOn = pattern ? pattern.split("").map((ch) => ch === "X" ? 2 : ch === "g" ? 3 : (ch === "x" || ch === "1") ? 1 : 0)
: Array.from({ length: bpb * sub }, (_, i) => ((i % sub) === 0 ? 2 : 1)); : Array.from({ length: bpb * sub }, (_, i) => ((i % sub) === 0 ? 2 : 1));
}
if (!DRUMS[sound]) sound = "beep"; if (!DRUMS[sound]) sound = "beep";
return { groupsStr, stepsPerBeat: sub, sound, beatsOn, poly, swing, enabled: !disabled, gainDb }; return { groupsStr, stepsPerBeat: sub, sound, beatsOn, poly, swing, enabled: !disabled };
} }
// --- share-language codec: patch ⇄ setup --- // --- share-language codec: patch ⇄ setup ---

View file

@ -173,7 +173,7 @@ function scheduler(){
function buildMeters(lanes){ function buildMeters(lanes){
return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr); return (lanes||[]).map(c=>{ const p=parseGroups(c.groupsStr);
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts, return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0, stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; }); tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; });
} }
function startAudio(){ function startAudio(){

View file

@ -369,7 +369,7 @@ function buildMeters(lanes){
return (lanes||[]).map(c=>{ return (lanes||[]).map(c=>{
const p=parseGroups(c.groupsStr); const p=parseGroups(c.groupsStr);
return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts, return {groupsStr:c.groupsStr,groups:p.groups,beatsPerBar:p.beatsPerBar,groupStarts:p.groupStarts,
stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,gainDb:c.gainDb||0, stepsPerBeat:c.stepsPerBeat||1,sound:c.sound,beatsOn:(c.beatsOn||[]).slice(),poly:!!c.poly,swing:!!c.swing,enabled:c.enabled!==false,
tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0}; tick:0,nextTime:0,vq:[],vqPtr:0,currentStep:-1,currentBar:0};
}); });
} }