Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

web synth: port harp + whistle from native (audio engine parity)

Adds two new wave types to the AudioWorklet Synth class so the web
notepat plays the same instruments as the native AC OS:

- harp: Karplus-Strong plucked string. Pre-fills the delay line with
one wavelength of pre-smoothed white noise on note-on, then
per sample reads the delayed sample, applies the canonical
two-point moving-average filter (Karplus & Strong 1983),
multiplies by stretch S (Jaffe-Smith EKS, 0.9985 sustain or
0.990 short-pluck when caller passes small decay), writes
back. Output ×2.5 to match perceived loudness of oscillators.

- whistle: Cook/STK digital waveguide flute. Bore + jet delay lines,
xorshift32 noise (deterministic, matches the C engine), 5 Hz
vibrato LFO, cubic nonlinearity (limit-cycle generator), 1-
pole DC blocker. Same algorithm + coefficients as the C
generate_whistle_sample, just transcribed.

Aliases supported (mirror of parse_wave_type in js-bindings.c):
pluck/guitar/string → harp
ocarina/flute/skullwhistle/skull-whistle → whistle

Updated web disks/notepat.mjs wavetypes (the user-facing wave selector
ring AND the colon-arg whitelist) to expose both. Slot order now:
sine, triangle, sawtooth, square, harp, whistle, composite, stample, drum.

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

+137 -4
+7 -4
system/public/aesthetic.computer/disks/notepat.mjs
··· 382 382 "triangle", // 1 383 383 "sawtooth", // 2 384 384 "square", // 3 385 - "noise", // 4 - white noise filtered by pitch 386 - "composite", // 5 387 - "stample", // 6 388 - "drum", // 7 - shared 12-drum kit (lib/percussion.mjs), both octaves 385 + "harp", // 4 - Karplus-Strong plucked string (Karplus & Strong 1983) 386 + "whistle", // 5 - digital waveguide flute (Cook/STK) 387 + "composite", // 6 388 + "stample", // 7 389 + "drum", // 8 - shared 12-drum kit (lib/percussion.mjs), both octaves 389 390 ]; 390 391 let waveIndex = 0; // 0; 391 392 const STARTING_WAVE = wavetypes[waveIndex]; //"sine"; ··· 1642 1643 "sawtooth", 1643 1644 "noise-white", 1644 1645 "noise", 1646 + "harp", 1647 + "whistle", 1645 1648 "stample", 1646 1649 "sample", 1647 1650 "drum",
+130
system/public/aesthetic.computer/lib/sound/synth.mjs
··· 68 68 #noiseFilterState3 = 0; 69 69 #noiseFilterState4 = 0; 70 70 71 + // Specific to `harp` — Karplus-Strong plucked string. 72 + // Refs: Karplus & Strong (1983); Jaffe & Smith EKS (1983); 73 + // Smith, "Physical Audio Signal Processing" — CCRMA Stanford. 74 + // Mirrors the C generate_harp_sample in fedac/native/src/audio.c. 75 + #harpBuf = null; // Float32Array — string delay line 76 + #harpW = 0; // write index 77 + #harpLp1 = 0; // 1-pole moving-average LPF state 78 + 79 + // Specific to `whistle` — Cook/STK digital waveguide flute model. 80 + // Mirrors the C generate_whistle_sample in fedac/native/src/audio.c. 81 + #whistleBoreBuf = null; // bore delay line 82 + #whistleBoreW = 0; 83 + #whistleJetBuf = null; // jet delay line 84 + #whistleJetW = 0; 85 + #whistleBreath = 0; // smoothed breath pressure 86 + #whistleVibratoPhase = 0; // 5 Hz LFO phase 87 + #whistleLp1 = 0; // 1-pole loop LPF state 88 + #whistleHpX1 = 0; // 1-pole DC blocker — last input 89 + #whistleHpY1 = 0; // 1-pole DC blocker — last output 90 + #whistleNoiseSeed = 0; // xorshift32 state 91 + 71 92 // Custom waveform generation 72 93 #customGenerator; // Function that generates waveform data 73 94 #customBuffer = []; // Buffer for streaming waveform data ··· 93 114 type === "sawtooth" 94 115 ) { 95 116 this.#frequency = options.tone; 117 + } else if (type === "harp" || type === "pluck" || 118 + type === "guitar" || type === "string") { 119 + // Karplus-Strong: pre-fill the delay line with one wavelength of 120 + // pre-smoothed white noise — the initial "pluck". Loop then decays 121 + // exponentially via the two-point moving-average filter + stretch. 122 + this.#frequency = options.tone; 123 + const N = 2048; 124 + this.#harpBuf = new Float32Array(N); 125 + const stringDelay = clamp(sampleRate / this.#frequency, 2, N - 2); 126 + const n = Math.floor(stringDelay); 127 + let last = 0; 128 + for (let i = 0; i < n; i++) { 129 + const white = random() * 2 - 1; 130 + const filt = 0.5 * (white + last); 131 + last = white; 132 + this.#harpBuf[i] = filt; 133 + } 134 + this.#harpW = n; 135 + this.type = "harp"; // normalize alias 136 + } else if (type === "whistle" || type === "ocarina" || 137 + type === "flute" || type === "skullwhistle" || 138 + type === "skull-whistle") { 139 + // Digital waveguide flute (Cook/STK). Bore delay = SR/freq; jet 140 + // delay = 0.32 × bore. Cubic nonlinearity drives the loop into 141 + // self-oscillation via DC breath pressure. Buffers stay zeroed — 142 + // breath pressure ramps the loop up from rest naturally. 143 + this.#frequency = options.tone; 144 + this.#whistleBoreBuf = new Float32Array(2048); 145 + this.#whistleJetBuf = new Float32Array(512); 146 + this.#whistleNoiseSeed = ((id || 1) * 2654435761) >>> 0; 147 + this.type = "whistle"; // normalize alias 96 148 } else if (type === "sample") { 97 149 this.#frequency = null; // 1; // TODO: This could be a low or high pass 98 150 // option here? ··· 341 393 value = noise; 342 394 } 343 395 // 🚩 TODO: Also add pink and brownian noise. 396 + } else if (this.type === "harp") { 397 + // 🪕 Karplus-Strong plucked string. Read delayed sample → average 398 + // with previous → multiply by stretch S → write back. Output 399 + // boosted ×2.5 because the LPF + stretch leave raw amplitude well 400 + // below the oscillators at the same `volume`. Mirror of C 401 + // generate_harp_sample (fedac/native/src/audio.c). 402 + const N = this.#harpBuf.length; 403 + const stringDelay = clamp(sampleRate / this.#frequency, 2, N - 2); 404 + // Fractional-delay read with linear interpolation. 405 + let rd = this.#harpW - stringDelay; 406 + while (rd < 0) rd += N; 407 + const i0 = floor(rd) | 0; 408 + const i1 = (i0 + 1) % N; 409 + const f = rd - i0; 410 + const delayed = this.#harpBuf[i0] * (1 - f) + this.#harpBuf[i1] * f; 411 + const filtered = 0.5 * (delayed + this.#harpLp1); 412 + this.#harpLp1 = delayed; 413 + // Short-pluck variant when caller passes small decay. 414 + const stretch = (this.#decay > 0 && this.#decay < 0.2) ? 0.990 : 0.9985; 415 + const decayed = filtered * stretch; 416 + this.#harpBuf[this.#harpW] = decayed; 417 + this.#harpW = (this.#harpW + 1) % N; 418 + value = 2.5 * decayed; 419 + } else if (this.type === "whistle") { 420 + // 🎶 Digital waveguide flute (Cook/STK). See C generate_whistle_sample 421 + // for full algorithm notes — same code translated to JS. 422 + const BORE_N = 2048, JET_N = 512; 423 + const env = 1; // attack/release handled by outer envelope below 424 + const breathTarget = 0.18 + 0.82 * Math.sqrt(env); 425 + const breathSlew = env > this.#whistleBreath ? 0.012 : 0.003; 426 + this.#whistleBreath += (breathTarget - this.#whistleBreath) * breathSlew; 427 + this.#whistleVibratoPhase += 5 / sampleRate; 428 + if (this.#whistleVibratoPhase >= 1) this.#whistleVibratoPhase -= 1; 429 + const vibrato = sin(2 * PI * this.#whistleVibratoPhase) * 0.03; 430 + // xorshift32 for deterministic noise (matches C engine). 431 + let s = this.#whistleNoiseSeed; 432 + s ^= s << 13; s >>>= 0; 433 + s ^= s >>> 17; 434 + s ^= s << 5; s >>>= 0; 435 + this.#whistleNoiseSeed = s; 436 + const white = (s / 0xFFFFFFFF) * 2 - 1; 437 + const breath = this.#whistleBreath * (1 + 0.08 * white + vibrato); 438 + const freq = clamp(this.#frequency, 30, sampleRate * 0.20); 439 + let boreDelay = sampleRate / freq; 440 + let jetDelay = boreDelay * 0.32; 441 + if (boreDelay > BORE_N - 2) boreDelay = BORE_N - 2; 442 + if (jetDelay > JET_N - 2) jetDelay = JET_N - 2; 443 + // Frac read — bore. 444 + let rd = this.#whistleBoreW - boreDelay; 445 + while (rd < 0) rd += BORE_N; 446 + let i0 = floor(rd) | 0; 447 + let i1 = (i0 + 1) % BORE_N; 448 + let frac = rd - i0; 449 + const boreOut = this.#whistleBoreBuf[i0] * (1 - frac) + this.#whistleBoreBuf[i1] * frac; 450 + this.#whistleLp1 = 0.35 * (-boreOut) + 0.65 * this.#whistleLp1; 451 + const temp = this.#whistleLp1; 452 + let pd = breath - 0.5 * temp; 453 + this.#whistleJetBuf[this.#whistleJetW] = pd; 454 + this.#whistleJetW = (this.#whistleJetW + 1) % JET_N; 455 + // Frac read — jet. 456 + rd = this.#whistleJetW - jetDelay; 457 + while (rd < 0) rd += JET_N; 458 + i0 = floor(rd) | 0; 459 + i1 = (i0 + 1) % JET_N; 460 + frac = rd - i0; 461 + pd = this.#whistleJetBuf[i0] * (1 - frac) + this.#whistleJetBuf[i1] * frac; 462 + // Cubic nonlinearity — limit-cycle generator. 463 + pd = pd * (pd * pd - 1); 464 + if (pd > 1) pd = 1; 465 + if (pd < -1) pd = -1; 466 + // 1-pole DC blocker. 467 + const y = pd - this.#whistleHpX1 + 0.995 * this.#whistleHpY1; 468 + this.#whistleHpX1 = pd; 469 + this.#whistleHpY1 = y; 470 + const intoBore = y + 0.5 * temp; 471 + this.#whistleBoreBuf[this.#whistleBoreW] = intoBore; 472 + this.#whistleBoreW = (this.#whistleBoreW + 1) % BORE_N; 473 + value = 0.3 * intoBore; 344 474 } else if (this.type === "sample") { 345 475 const bufferData = this.#sampleData.channels[0]; 346 476