Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

notepat: harp loudness + sustain + Shift-pluck; whistle low-octave fix

Audio (audio.c):
- Harp: stretch 0.996 → 0.9985 (T60 ~15s at A4 — true pedal-harp
sustain). Output gain 1.0 → 2.5× to match perceived loudness of
sine/triangle. Karplus-Strong's circulating amplitude is heavily
attenuated by the LPF + stretch each cycle, so raw output is much
quieter than oscillators at the same `volume`.
- Harp short-pluck variant: when caller passes decay in (0, 0.2),
stretch drops to 0.990 → ~0.4s damp. Lets notepat trigger a tight
staccato when Shift is held (no Karplus-Strong code branching, just
picks a smaller stretch coefficient).
- Whistle: clamp the freq floor 110Hz → 30Hz so the bottom notepat
octave (C1≈33Hz, C2≈65Hz, A2=110Hz) actually plays at pitch instead
of all bass notes silently snapping to A2.

Notepat (pieces/notepat.mjs):
- Shift+letter on harp: pass duration=0.4 + decay=0.1 to trigger the
short-pluck path. Other waves keep their existing infinite-hold.
- Wave-row label switches "harp" → "pluck" while Shift is held — first
pass at surfacing the alternate-mode UI the user asked for.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+42 -12
+18 -5
fedac/native/pieces/notepat.mjs
··· 2883 2883 compositeVoices: [a, b, c, d, e2], 2884 2884 }, system, 1); 2885 2885 } else { 2886 + // Shift+letter on harp = short staccato pluck. Pass decay=0.1 as 2887 + // a signal to the C generate_harp_sample (which switches to a 2888 + // tighter stretch factor and damps in ~0.4s). Finite duration 2889 + // so the voice naturally ends without needing kill(). 2890 + const isShortPluck = wave === "harp" && shiftHeld; 2886 2891 synth = sound.synth({ 2887 - type: wave, tone: playFreq, duration: Infinity, 2888 - volume: 0.5, attack: currentAttack(), decay: currentDecay(), pan, 2892 + type: wave, tone: playFreq, 2893 + duration: isShortPluck ? 0.4 : Infinity, 2894 + volume: 0.5, 2895 + attack: currentAttack(), 2896 + decay: isShortPluck ? 0.1 : currentDecay(), 2897 + pan, 2889 2898 }); 2890 2899 rememberSound(hitNote.key, { synth, note: hitNote.letter, octave: hitNote.octave, baseFreq: freq }, system, 1); 2891 2900 } ··· 4713 4722 ink(dark ? 40 : 190, dark ? 40 : 190, dark ? 45 : 195); 4714 4723 box(bx, waveRowY, 1, waveRowH, true); 4715 4724 } 4716 - // Label 4717 - const lx = bx + Math.floor((btnW2 - waveLabels[i].length * 6) / 2); 4718 - write(waveLabels[i], { x: lx, y: waveRowY + 3, size: 1, font: "font_1" }); 4725 + // Label — switch "harp" → "pluck" while Shift is held to surface 4726 + // the alternate short-pluck mode. The active button also gets a 4727 + // subtle bracket marker so it's obvious from the keyboard. 4728 + let label = waveLabels[i]; 4729 + if (wavetypes[i] === "harp" && shiftHeld) label = "pluck"; 4730 + const lx = bx + Math.floor((btnW2 - label.length * 6) / 2); 4731 + write(label, { x: lx, y: waveRowY + 3, size: 1, font: "font_1" }); 4719 4732 } 4720 4733 4721 4734 // Octave / REC button (right side)
+24 -7
fedac/native/src/audio.c
··· 190 190 // Bore and jet delay lengths — bore = SR/freq (one wavelength), 191 191 // jet = 0.32 × bore (Cook's flute ratio; 0.45 for pennywhistle, 192 192 // 0.5 for ocarina). Clamp to the delay buffer sizes. 193 - double freq = clampd(v->frequency, 110.0, sample_rate * 0.20); 193 + // Allow the full notepat pitch range — C1 ≈ 33Hz, so clamp at 30Hz 194 + // so we don't lose the bottom octave. On sample rates where SR/freq 195 + // exceeds the bore buffer (BORE_N=2048, fine at 48kHz; caps around 196 + // 94Hz at 192kHz), the bore_delay clampd below pins the delay to the 197 + // buffer size — the worst case is that very low notes play slightly 198 + // sharper than requested on 192kHz hardware. Still better than the 199 + // previous 110Hz hard-clamp which silenced every low octave. 200 + double freq = clampd(v->frequency, 30.0, sample_rate * 0.20); 194 201 double bore_delay = sample_rate / freq; 195 202 double jet_delay = bore_delay * 0.32; 196 203 // Cap to buffer sizes with safety margin ··· 303 310 // Stretch factor S (Jaffe-Smith EKS). S < 1 makes the circulating 304 311 // pattern decay exponentially. With the two-point LPF ≈ unity at DC, 305 312 // the per-cycle loss is dominated by S: amplitude ≈ S^(f·t) per second. 306 - // S = 0.996 gives T60 ≈ 4s at 440Hz — longer for bass strings, shorter 307 - // for high strings, matching real string behavior. 308 - double decayed = filtered * 0.996; 313 + // S = 0.9985 gives T60 ≈ 15s at 440Hz — long sustain like a real 314 + // pedal harp letting the string ring out. Higher pitches still fade 315 + // faster than bass strings (matches physics of real strings). 316 + // 317 + // "Short pluck" variant: when the caller passes decay > 0 (notepat 318 + // does this for Shift+letter), shorten the stretch factor so the 319 + // string dies in ~0.4s — feels like a damped/staccato pluck. 320 + double stretch = (v->decay > 0.0 && v->decay < 0.2) ? 0.990 : 0.9985; 321 + double decayed = filtered * stretch; 309 322 310 323 // Write back to close the delay loop. 311 324 v->whistle_bore_buf[v->whistle_bore_w] = (float)decayed; 312 325 v->whistle_bore_w = (v->whistle_bore_w + 1) % STRING_N; 313 326 314 - // Envelope applies mainly to key-up fade. Attack is instantaneous — 315 - // the pluck is the initial noise burst already in the delay line. 316 - return decayed * env; 327 + // Output gain — Karplus-Strong's circulating amplitude is heavily 328 + // attenuated by the LPF + stretch each cycle, so raw output is much 329 + // quieter than sine/square at the same `volume`. Boost by 2.5× so a 330 + // plucked note feels comparable in loudness to other wave types. 331 + // Envelope still controls key-up fade; attack is instantaneous (the 332 + // initial noise burst IS the pluck). 333 + return 2.5 * decayed * env; 317 334 } 318 335 319 336 // ============================================================