diff --git a/fedac/native/pieces/notepat.mjs b/fedac/native/pieces/notepat.mjs index f422ebae2..ba9cf6037 100644 --- a/fedac/native/pieces/notepat.mjs +++ b/fedac/native/pieces/notepat.mjs @@ -5,8 +5,6 @@ import { PERCUSSION_LABELS, PERCUSSION_COLORS, PERCUSSION_NOTATION, - NOTATION_WAVE_RGB, - HAT_FREQS, playPercussion as sharedPlayPercussion, } from "/lib/percussion.mjs"; @@ -1181,255 +1179,42 @@ const ZOO_COLORS = { "g#": [170, 230, 130], // snake — lime-olive "a#": [100, 180, 230], // whale — ocean blue }; -const ZOO_NOTATION = { - c: [["w",180,0.8],["n",2500,0.3]], // dog - d: [["s",500,0.6],["s",350,0.4]], // cat - e: [["t",90,1.2],["s",45,0.8]], // cow - f: [["t",380,0.5]], // sheep - g: [["s",2500,0.5],["s",3500,0.4]], // bird - a: [["w",140,0.7],["w",180,0.5]], // pig - b: [["n",400,1.0],["w",70,1.2]], // lion - "c#": [["s",320,0.6]], // owl - "d#": [["w",110,0.6]], // frog - "f#": [["q",450,0.5],["s",500,0.3]], // horse - "g#": [["n",2000,0.8]], // snake - "a#": [["s",100,1.2]], // whale -}; // Which zoo sounds sustain while held (release kills the voice). const ZOO_SUSTAIN = { "g#": true, // snake — long hiss }; -const ZOO_DURATION = { - c: 0.12, d: 0.45, e: 0.60, f: 0.35, - g: 0.16, a: 0.25, b: 0.90, - "c#": 0.55, "d#": 0.18, - "f#": 0.55, "a#": 2.0, -}; const ZOO_RELEASE = { "g#": 0.25, }; -// Fire a zoo-kit animal sound. Classic subtractive recipes, one-shot or -// sustained. `pitchFactor` scales all tonal voices so the kit bends -// with octave/pitch like perc does. +// Fire a zoo-kit animal sound. Sample-based — plays a recording from +// the named one-shot bank (audio.c: load_oneshot_bank). `pitchFactor` +// is forwarded to the C engine so octave shifts speed up / slow down +// the playback the same way they do for the perc kit. Sustained sounds +// (e.g. snake hiss) push their handle into `holdVoices` so the matching +// "up" phase can kill them via sound.zoo.kill(). function playZoo(sound, letter, volume = 1.0, pan = 0, pitchFactor = 1.0, phase = "both", holdVoices = null) { - if (!sound?.synth) return; - if (phase === "up") return; - const v = Math.max(0.1, Math.min(2.2, volume)); - const pf = Math.max(0.25, Math.min(4, pitchFactor)); - const isLive = phase === "down" && Array.isArray(holdVoices); - const rj = (c, f) => c * (1 + (Math.random() - 0.5) * 2 * f); - const fire = (params, dur, sustainLen) => { - if (isLive && ZOO_SUSTAIN[letter]) { - const h = sound.synth({ ...params, duration: Infinity }); - if (h) holdVoices.push({ handle: h, releaseFade: ZOO_RELEASE[letter] ?? 0.1 }); - } else { - sound.synth({ ...params, duration: dur ?? sustainLen ?? 0.2 }); - } - }; - switch (letter) { - case "c": { - // DOG BARK — Bruce's Pure Data recipe + Farnell Ch. 52 formant - // frame. Primary energy 400-700 Hz (Indian wolf harmonic data, - // Sadhukhan PLOS One). Saw source sweeping 700 → 400 Hz over ~15 ms - // (approximated with 3 staggered voices) + narrow-band noise - // burst at ~200 Hz (simulating BPF Q≈1 throat resonance) + - // high-frequency noise crack on attack. Percussive envelope - // (<5 ms attack, 80-150 ms decay, zero sustain). - const p1 = 700 * pf, p2 = 500 * pf, p3 = 400 * pf; - sound.synth({ type: "sawtooth", tone: p1, duration: 0.012, volume: rj(0.80, 0.08) * v, attack: 0.001, decay: 0.011, pan }); - sound.synth({ type: "sawtooth", tone: p2, duration: 0.040, volume: rj(0.72, 0.08) * v, attack: 0.008, decay: 0.032, pan }); - sound.synth({ type: "sawtooth", tone: p3, duration: 0.100, volume: rj(0.68, 0.08) * v, attack: 0.022, decay: 0.080, pan }); - // Throat resonance (approx. BPF-noise at ~200 Hz) - sound.synth({ type: "triangle", tone: 210 * pf, duration: 0.12, volume: rj(0.35, 0.08) * v, attack: 0.003, decay: 0.11, pan }); - // High-freq click on attack - sound.synth({ type: "noise", tone: 2800 * pf, duration: 0.006, volume: 0.40 * v, attack: 0.0003, decay: 0.005, pan }); - break; - } - case "d": { - // CAT MEOW — MeowSynth / Farnell vowel-interpolation recipe. - // Arched F0 rises from 500 → 750 Hz, then falls to 400 Hz — - // food-context meow, per Schötz & Eklund. Four staggered - // saw-ish stages approximate the pitch arch. Vowel transition - // from /e/ (F1=400, F2=1900) → /o/ (F1=500, F2=900) sketched - // with two parallel formant-tracker sine voices that also - // sweep. ADSR ~30 ms A, 400 ms R, total 500-700 ms. - const atkBase = 0.025; - sound.synth({ type: "sawtooth", tone: 500 * pf, duration: 0.18, volume: rj(0.45, 0.08) * v, attack: atkBase, decay: 0.14, pan }); - sound.synth({ type: "sawtooth", tone: 700 * pf, duration: 0.18, volume: rj(0.50, 0.08) * v, attack: atkBase + 0.08, decay: 0.14, pan }); - sound.synth({ type: "sawtooth", tone: 550 * pf, duration: 0.18, volume: rj(0.48, 0.08) * v, attack: atkBase + 0.22, decay: 0.16, pan }); - sound.synth({ type: "sawtooth", tone: 400 * pf, duration: 0.24, volume: rj(0.40, 0.08) * v, attack: atkBase + 0.36, decay: 0.22, pan }); - // F1 / F2 formant singers — sine voices sweeping from /e/ to /o/ - sound.synth({ type: "sine", tone: 1900 * pf, duration: 0.22, volume: rj(0.15, 0.05) * v, attack: atkBase, decay: 0.18, pan }); - sound.synth({ type: "sine", tone: 900 * pf, duration: 0.28, volume: rj(0.18, 0.05) * v, attack: atkBase + 0.20, decay: 0.22, pan }); - break; - } - case "e": { - // COW MOO — Green et al. formant measurements: F0 80-150 Hz, - // F1 ~790 Hz, F2 ~1942 Hz. Low F0 drifts slightly downward, - // F1 opens ~600 → ~900 Hz over the tail ("mmm → ooo"). Saw - // source for harmonic richness + slow sine LFO (via a detuned - // voice) for vocal-fold ripple. - const base = 110 * pf; - sound.synth({ type: "sawtooth", tone: base, duration: 1.4, volume: rj(0.70, 0.06) * v, attack: 0.08, decay: 1.0, pan }); - sound.synth({ type: "sawtooth", tone: base * 0.93, duration: 1.4, volume: rj(0.45, 0.06) * v, attack: 0.10, decay: 1.0, pan }); - // F1 — mouth-closed to mouth-open - sound.synth({ type: "sine", tone: 600 * pf, duration: 0.7, volume: rj(0.35, 0.05) * v, attack: 0.10, decay: 0.5, pan }); - sound.synth({ type: "sine", tone: 900 * pf, duration: 0.9, volume: rj(0.30, 0.05) * v, attack: 0.50, decay: 0.7, pan }); - // F2 always-on - sound.synth({ type: "sine", tone: 1942 * pf, duration: 1.3, volume: rj(0.10, 0.03) * v, attack: 0.12, decay: 1.0, pan }); - break; - } - case "f": { - // SHEEP BAA — Briefer et al. *Current Biology* 2017: sheep - // vocalizations evolved a characteristic 5-8 Hz vibrato (±15%). - // Approximated with 6 staggered saw voices alternating between - // F0 and F0×1.15, ~50 ms each = 7 Hz vibrato. Base F0 ~280 Hz. - const base = 280 * pf; - const hi = base * 1.15; - const vibDur = 0.05; - for (let i = 0; i < 7; i++) { - const tone = (i & 1) ? hi : base; - sound.synth({ type: "sawtooth", tone, duration: vibDur + 0.03, - volume: rj(0.50, 0.08) * v, - attack: i * vibDur + 0.002, decay: vibDur + 0.02, pan }); - } - // Lamb/nasal harmonic - sound.synth({ type: "triangle", tone: base * 2, duration: 0.5, volume: rj(0.18, 0.05) * v, attack: 0.01, decay: 0.45, pan }); - break; - } - case "g": { - // BIRD CHIRP — Van Hunter Adams' Pico recipe: "swoop + chirp". - // Swoop = sine sinusoidal F0 contour 1740 → 2000 → 1740 Hz over - // 130 ms. Chirp = sine sweeping 2 → 7 kHz in 40-80 ms. Silence - // ~50 ms between primitives. - // Swoop approximation (3 stages around the peak) - sound.synth({ type: "sine", tone: 1740 * pf, duration: 0.045, volume: rj(0.52, 0.1) * v, attack: 0.002, decay: 0.04, pan }); - sound.synth({ type: "sine", tone: 2000 * pf, duration: 0.045, volume: rj(0.58, 0.1) * v, attack: 0.045, decay: 0.04, pan }); - sound.synth({ type: "sine", tone: 1740 * pf, duration: 0.045, volume: rj(0.52, 0.1) * v, attack: 0.090, decay: 0.04, pan }); - // Chirp — 2 → 7 kHz exponential sweep in 60 ms (5 stages) - for (let i = 0; i < 5; i++) { - const t = i / 4; - const tone = 2000 * Math.pow(3.5, t) * pf; // 2k → 7k - sound.synth({ type: "sine", tone, duration: 0.012 + 0.015, - volume: rj(0.50 - t * 0.08, 0.1) * v, - attack: 0.20 + i * 0.012 + 0.0005, - decay: 0.012, pan }); - } - break; - } - case "a": { - // PIG OINK — Garcia et al. ventricular-fold study: F0 bounces - // between grunt ~93 Hz (folds engaged) and squeal ~409 Hz - // (folds disengaged). Classic oink = two staccato bursts, 100 ms - // each, 80 ms gap. Random F0 toggle mimics chaos. - const grunt = 93 * pf, squeal = 300 * pf; - // Burst 1 - sound.synth({ type: "sawtooth", tone: grunt, duration: 0.09, volume: rj(0.72, 0.08) * v, attack: 0.005, decay: 0.08, pan }); - sound.synth({ type: "sawtooth", tone: squeal, duration: 0.04, volume: rj(0.50, 0.08) * v, attack: 0.03, decay: 0.03, pan }); - // Gap + burst 2 - sound.synth({ type: "sawtooth", tone: grunt * 1.05, duration: 0.09, volume: rj(0.68, 0.08) * v, attack: 0.18, decay: 0.08, pan }); - sound.synth({ type: "sawtooth", tone: squeal * 0.92, duration: 0.04, volume: rj(0.48, 0.08) * v, attack: 0.21, decay: 0.03, pan }); - // Breath noise - sound.synth({ type: "noise", tone: 800 * pf, duration: 0.25, volume: rj(0.15, 0.05) * v, attack: 0.01, decay: 0.23, pan }); - break; - } - case "b": { - // LION ROAR — Klemuk et al. PLOS ONE + Anikin et al. RSB 2025. - // Two coupled oscillators + forced subharmonic at F0/2 produce - // the characteristic chaos. F0 ~130 Hz sliding down to ~90 Hz - // over the roar; subharmonic layer kicks in at 40% through. - // Filtered noise 600-1200 Hz for breathy growl. 2-4 s total. - const f0a = 130 * pf, f0b = 90 * pf; - sound.synth({ type: "sawtooth", tone: f0a, duration: 2.0, volume: rj(0.75, 0.08) * v, attack: 0.20, decay: 1.5, pan }); - sound.synth({ type: "sawtooth", tone: f0b, duration: 2.0, volume: rj(0.65, 0.08) * v, attack: 0.40, decay: 1.4, pan }); - // Forced subharmonic — emerges later, gives the growl ending - sound.synth({ type: "sawtooth", tone: f0a * 0.5, duration: 1.4, volume: rj(0.40, 0.08) * v, attack: 0.80, decay: 1.2, pan }); - // Filtered-noise breath band (600-1200 Hz approximated by - // band-limited noise at 900 Hz) - sound.synth({ type: "noise", tone: 900 * pf, duration: 1.8, volume: rj(0.28, 0.06) * v, attack: 0.30, decay: 1.4, pan }); - break; - } - case "c#": { - // OWL HOOT — Cornell Lab Great Horned Owl data: near-pure sine, - // F0 250-350 Hz, slight 5% droop at tail. Call pattern often - // 4-5 hoots in 5 s; we do single hoot here (grid key = 1 hoot). - sound.synth({ type: "sine", tone: 300 * pf, duration: 0.5, volume: rj(0.72, 0.06) * v, attack: 0.10, decay: 0.35, pan }); - sound.synth({ type: "sine", tone: 285 * pf, duration: 0.2, volume: rj(0.45, 0.06) * v, attack: 0.40, decay: 0.15, pan }); - // Low body harmonic (great horned owls have faint F2 ~600 Hz) - sound.synth({ type: "sine", tone: 600 * pf, duration: 0.55, volume: rj(0.10, 0.04) * v, attack: 0.12, decay: 0.40, pan }); - break; - } - case "d#": { - // FROG RIBBIT — Yamaguchi *Xenopus* pulse-train research + - // Dinacon Túngara recipe. Fast trill = 60 Hz click rate, energy - // 100-500 Hz. Approximated with 8 quick sawtooth bursts at - // 60 Hz (~17 ms spacing) + a narrow-band resonance peak at 1 kHz. - const base = 180 * pf; - for (let i = 0; i < 8; i++) { - sound.synth({ type: "sawtooth", tone: base, duration: 0.012, - volume: rj(0.55, 0.08) * v * (1 - i * 0.05), - attack: 0.001, decay: 0.010, pan }); - // Stagger via accumulated attack — next voice starts ~17 ms later - const nextBase = base + (Math.random() - 0.5) * 20; - sound.synth({ type: "sawtooth", tone: nextBase, duration: 0.012, - volume: rj(0.50, 0.08) * v * (1 - i * 0.05), - attack: 0.016 * (i + 1), decay: 0.010, pan }); + if (!sound?.zoo?.play) return; + if (phase === "up") { + // Release any sustained zoo voices (e.g. snake hiss) registered + // by an earlier "down" phase. + if (Array.isArray(holdVoices) && holdVoices.length > 0) { + for (const h of holdVoices) { + if (h?.handle && sound?.zoo?.kill) { + sound.zoo.kill(h.handle, h.releaseFade ?? 0.1); + } } - // Species-specific 1 kHz resonance (vocal sac Helmholtz) - sound.synth({ type: "sine", tone: 1000 * pf, duration: 0.25, volume: rj(0.20, 0.05) * v, attack: 0.002, decay: 0.22, pan }); - break; - } - case "f#": { - // HORSE WHINNY — Stoeger et al. *Current Biology* 2026: whinnies - // are BIPHONIC — two independent non-harmonic fundamentals. - // Voice 1: F0 ~400 Hz, saw with 7 Hz vibrato ±15 %, descending. - // Voice 2: G0 ~1500 Hz, pure sine aerodynamic whistle (horse - // literally whistles through voice box), parallel descent. - // Overlap ~80%, stagger attacks by ~150 ms. - const f0 = 400 * pf, g0 = 1500 * pf; - // Voice 1 — biphonic low with vibrato (3 vibrato cycles) - for (let i = 0; i < 5; i++) { - const vibMul = (i & 1) ? 1.15 : 0.85; - sound.synth({ type: "sawtooth", tone: f0 * vibMul * (1 - i * 0.04), duration: 0.18, - volume: rj(0.55, 0.06) * v * (1 - i * 0.06), - attack: 0.05 + i * 0.14, decay: 0.15, pan }); - } - // Voice 2 — high aerodynamic whistle (joins 150 ms later, descends parallel) - for (let i = 0; i < 4; i++) { - sound.synth({ type: "sine", tone: g0 * (1 - i * 0.08), duration: 0.22, - volume: rj(0.35, 0.06) * v * (1 - i * 0.10), - attack: 0.15 + i * 0.18, decay: 0.20, pan }); - } - break; - } - case "g#": { - // SNAKE HISS — Aubret & Mangin: vipers hiss low (200-400 Hz - // BPF), grass/colubrid snakes hiss high (5-10 kHz). "Cartoon" - // cobra hiss combines both. Slow 0.5 Hz amplitude wobble for - // breath rhythm. Sustained while held. - const low = { type: "noise", tone: 300 * pf, volume: rj(0.40, 0.06) * v, attack: 0.08, decay: 0.2, pan }; - const high = { type: "noise", tone: 6000 * pf, volume: rj(0.22, 0.05) * v, attack: 0.12, decay: 0.3, pan }; - fire(low, 1.2, 1.2); - fire(high, 1.2, 1.2); - break; - } - case "a#": { - // WHALE SONG — Cazau additive-resynthesis approach: multiple - // sines with slow FM (1-3 Hz vibrato) + long glide 600 → 1000 - // → 400 Hz over 4 s. Long reverb tail would help but not in - // this kit. Approximate with 3 sine stages tracing the glide - // + a lower octave drone. - const p1 = 600 * pf, p2 = 1000 * pf, p3 = 400 * pf; - sound.synth({ type: "sine", tone: p1, duration: 1.4, volume: rj(0.60, 0.06) * v, attack: 0.4, decay: 0.9, pan }); - sound.synth({ type: "sine", tone: p2, duration: 1.4, volume: rj(0.55, 0.06) * v, attack: 1.3, decay: 0.9, pan }); - sound.synth({ type: "sine", tone: p3, duration: 1.4, volume: rj(0.50, 0.06) * v, attack: 2.4, decay: 1.1, pan }); - // Lower-octave drone for sub presence - sound.synth({ type: "sine", tone: p1 * 0.5, duration: 3.0, volume: rj(0.25, 0.05) * v, attack: 0.6, decay: 2.2, pan }); - // Subtle 2 Hz tremolo approximated by mid-call amplitude voice - sound.synth({ type: "sine", tone: p1 * 1.01, duration: 2.5, volume: rj(0.20, 0.05) * v, attack: 1.0, decay: 1.5, pan }); - break; + holdVoices.length = 0; } + return; + } + const name = ZOO_NAMES[letter]; + if (!name) return; + const v = Math.max(0.1, Math.min(2.2, volume)); + const pf = Math.max(0.25, Math.min(4, pitchFactor)); + const handle = sound.zoo.play(name, { volume: v, pan, pitch: pf }); + if (handle && Array.isArray(holdVoices) && ZOO_SUSTAIN[letter]) { + holdVoices.push({ handle, releaseFade: ZOO_RELEASE[letter] ?? 0.1 }); } } @@ -1707,14 +1492,6 @@ function kitColorsFor(kit) { if (kit === "lasers") return LASER_COLORS; return null; } -function kitNotationFor(kit) { - if (isWarKit(kit)) return WAR_NOTATION; - if (kit === "perc") return PERCUSSION_NOTATION; - if (kit === "zoo") return ZOO_NOTATION; - if (kit === "lasers") return LASER_NOTATION; - return null; -} - // Cycle the kit state for a side: off → perc → warA → warB → zoo → lasers → off. // warA = preset-default model per weapon (mostly classic); warB forces // physical/DWG synthesis for direct A/B comparison; zoo + lasers are @@ -5619,7 +5396,6 @@ function paint({ wipe, ink, box, line, write, screen, sound, system, trackpad, p const kitNames = kitNamesFor(kit); const kitLabels = kitLabelsFor(kit); const kitColors = kitColorsFor(kit); - const kitNotation = kitNotationFor(kit); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const noteName = grid[r][c]; @@ -5710,90 +5486,18 @@ function paint({ wipe, ink, box, line, write, screen, sound, system, trackpad, p : ""; write(label, { x: x + 2, y: y + 2, size: 1, font: "font_1" }); - // Stochastic graphic notation inside drum pads (percussion mode only). - // Each drum's voices are plotted as horizontal lines (tonal) or noise - // dots (noise-based) at log-frequency Y positions. Every frame, the - // positions jitter by ±1 px and noise dots are re-scattered, so the - // viewer sees the randomness mechanism animating even when idle. - if (drumActive) { - const sig = kitNotation && kitNotation[letter]; - if (sig) { - // Usable plot area inside the pad (leave room for label at top - // and bottom-label area). - const pxMin = x + 3; - const pxMax = x + btnW - 4; - const pyMin = y + 11; // below top label - const pyMax = y + btnH - 13; // above bottom label - if (pxMax > pxMin && pyMax > pyMin + 4) { - const plotW = pxMax - pxMin; - const plotH = pyMax - pyMin; - const logLo = Math.log(30); - const logHi = Math.log(10000); - // Per-pad PRNG. Previously seeded with `frame` so the - // scatter shimmered every frame ("shake constantly") - // even when nothing was being played — visually noisy - // and confusing about what the pad represents. Now the - // seed is purely per-key, so each pad's scatter is a - // STATIC fingerprint of that drum's voice signature. - // Hit feedback comes from per-pad highlighting (see - // padHitUntil[letter]) and the screen-wide drumFlashes - // bg pulse, not from notation animation. - const seed = ((key ? key.charCodeAt(0) : 0) * 374761393) >>> 0; - const prng = (n) => { - let s = (seed ^ (n * 2246822519)) >>> 0; - s ^= s << 13; s >>>= 0; - s ^= s >>> 17; - s ^= s << 5; s >>>= 0; - return (s >>> 0) / 4294967296; - }; - for (let i = 0; i < sig.length; i++) { - const [t, f, vol] = sig[i]; - const logF = Math.log(Math.max(30, Math.min(10000, f))); - const yNorm = 1 - (logF - logLo) / (logHi - logLo); - const py = pyMin + Math.round(yNorm * plotH); - const rgb = NOTATION_WAVE_RGB[t] || [180, 180, 200]; - const alpha = isActive ? 220 : 130; - if (t === "n") { - // Scatter ~vol*8 random dots around py - const count = Math.max(2, Math.round(vol * 8)); - for (let d = 0; d < count; d++) { - const dx = pxMin + Math.floor(prng(i * 31 + d) * plotW); - const dy = py + Math.floor((prng(i * 31 + d + 17) - 0.5) * 5); - if (dx >= pxMin && dx <= pxMax && dy >= pyMin && dy <= pyMax) { - ink(rgb[0], rgb[1], rgb[2], alpha); - box(dx, dy, 1, 1, true); - } - } - } else { - // Horizontal line (tonal voice); thickness ∝ volume - const jitterY = Math.floor((prng(i) - 0.5) * 2); - const lineY = Math.max(pyMin, Math.min(pyMax - 1, py + jitterY)); - const thickness = Math.max(1, Math.min(2, Math.round(vol * 0.9))); - ink(rgb[0], rgb[1], rgb[2], alpha); - box(pxMin, lineY, plotW, thickness, true); - } - } - } - } - } else if (btnH > 12) { + if (!drumActive && btnH > 12) { if (isActive) ink(255, 255, 255, 180); else { const sl = dark ? (sharp ? 80 : 120) : (sharp ? 160 : 110); ink(sl, sl, sl); } write(letter + noteOctave, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); } - // In drum mode the bottom label is the drum name, not the note. - // Prefer the full name ("kick", "snare", "splash") when the pad - // has space; fall back to the 3-char abbreviation when tight. - // font_1 is 6px per char, so "kick"=26px, "snare"=32px, "splash"=38px, - // "cowbell"=44px. Pick full name when btnW >= label width + padding. + // In drum mode the bottom label is the drum's full name. if (drumActive && btnH > 12) { if (isActive) ink(255, 255, 255, 210); else { const sl = dark ? (sharp ? 100 : 150) : (sharp ? 120 : 80); ink(sl, sl, sl); } - const fullName = (kitNames && kitNames[letter]) || ""; - const shortLabel = (kitLabels && kitLabels[letter]) || (letter + noteOctave); - const fullPx = fullName.length * 6 + 4; - const bottomLabel = (fullName && btnW >= fullPx) ? fullName : shortLabel; - write(bottomLabel, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); + const fullName = (kitNames && kitNames[letter]) || (kitLabels && kitLabels[letter]) || (letter + noteOctave); + write(fullName, { x: x + 2, y: y + btnH - 12, size: 1, font: "font_1" }); } // Pressure bar — fills from bottom of pad proportional to analog pressure diff --git a/fedac/native/samples/zoo/bird.raw b/fedac/native/samples/zoo/bird.raw new file mode 100644 index 0000000000000000000000000000000000000000..0c40e7cb2f99b44bf3002239e949879d5c627f09 GIT binary patch literal 204800 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