still-free.openapps.xyz

Source

This app’s code and revision history are public. Download a copy or explore the files below.

Public
The app’s code and revision history are public.
Owned app · rev 0fa0e84d537b
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone a8c677472ab5691741798a3d
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
← All files
src/lib/noise.ts
// Soundscapes made on the device from noise and filters: no audio files. Each sound is a small
// Web Audio graph over a few generated buffers (made once per visit, then reused).
import type { Voice } from "./audio";

export type SoundId = "rain" | "heavy-rain" | "ocean" | "wind" | "stream" | "fire" | "fan" | "white" | "pink" | "brown";

export const SOUNDS: { id: SoundId; name: string; hint: string }[] = [
  { id: "rain", name: "Rain", hint: "Steady rain on leaves" },
  { id: "heavy-rain", name: "Heavy rain", hint: "A downpour on the roof" },
  { id: "ocean", name: "Ocean waves", hint: "Slow waves on a beach" },
  { id: "wind", name: "Wind", hint: "Wind moving through trees" },
  { id: "stream", name: "Stream", hint: "Water over stones" },
  { id: "fire", name: "Fireplace", hint: "A crackling wood fire" },
  { id: "fan", name: "Fan", hint: "A soft, steady fan" },
  { id: "white", name: "White noise", hint: "Even hiss, masks everything" },
  { id: "pink", name: "Pink noise", hint: "Softer, like steady rain" },
  { id: "brown", name: "Brown noise", hint: "Deep, low rumble" },
];

export const soundName = (id: SoundId) => SOUNDS.find((s) => s.id === id)?.name ?? id;

const cache = new Map<string, AudioBuffer>();

/**
 * A looping stereo buffer. `fill` writes `n` samples per channel plus a little extra, and the extra
 * is crossfaded over the start so the loop has no seam. Levels are evened out to one loudness.
 */
function looped(c: BaseAudioContext, key: string, seconds: number, rate: number, fill: (d: Float32Array, rate: number, ch: number) => void): AudioBuffer {
  const hit = cache.get(key);
  if (hit) return hit;
  const n = Math.floor(seconds * rate);
  const x = Math.floor(rate * 0.5);
  const buf = c.createBuffer(2, n, rate);
  let sum = 0;
  const chans: Float32Array[] = [];
  for (let ch = 0; ch < 2; ch++) {
    const d = new Float32Array(n + x);
    fill(d, rate, ch);
    for (let i = 0; i < x; i++) {
      const k = i / x;
      d[i] = d[i] * Math.sqrt(k) + d[n + i] * Math.sqrt(1 - k);
    }
    for (let i = 0; i < n; i++) sum += d[i] * d[i];
    chans.push(d);
  }
  const scale = 0.2 / Math.sqrt(sum / (2 * n) || 1);
  chans.forEach((d, ch) => {
    const out = buf.getChannelData(ch);
    for (let i = 0; i < n; i++) out[i] = Math.max(-1, Math.min(1, d[i] * scale));
  });
  cache.set(key, buf);
  return buf;
}

const rnd = () => Math.random() * 2 - 1;

function white(d: Float32Array) {
  for (let i = 0; i < d.length; i++) d[i] = rnd();
}

function pink(d: Float32Array) {
  let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
  for (let i = 0; i < d.length; i++) {
    const w = rnd();
    b0 = 0.99886 * b0 + w * 0.0555179;
    b1 = 0.99332 * b1 + w * 0.0750759;
    b2 = 0.969 * b2 + w * 0.153852;
    b3 = 0.8665 * b3 + w * 0.3104856;
    b4 = 0.55 * b4 + w * 0.5329522;
    b5 = -0.7616 * b5 - w * 0.016898;
    d[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362;
    b6 = w * 0.115926;
  }
}

function brown(d: Float32Array) {
  let last = 0;
  for (let i = 0; i < d.length; i++) {
    last = (last + 0.02 * rnd()) / 1.02;
    d[i] = last;
  }
}

/** Short ticks of water at random times and places: `perSecond` drops, mostly small. */
function drops(perSecond: number) {
  return (d: Float32Array, rate: number) => {
    let t = 0;
    while (t < d.length) {
      t += Math.floor((-Math.log(1 - Math.random()) / perSecond) * rate);
      const amp = 0.08 + Math.pow(Math.random(), 3) * 0.92;
      const len = Math.floor(rate * (0.0015 + Math.random() * 0.006));
      for (let i = 0; i < len && t + i < d.length; i++) d[t + i] += rnd() * Math.exp(-i / (len / 4)) * amp;
    }
  };
}

/** Waves: a periodic swell and break, filtered noise that opens up as each wave crashes. */
function waves(d: Float32Array, rate: number, ch: number) {
  const step = rate / 100;
  const loop = Math.floor((d.length - rate * 0.5) / step);
  const env = new Float32Array(loop).fill(0.06);
  let t = ch * 30;
  while (t < loop + ch * 30) {
    const period = (7 + Math.random() * 5) * 100;
    const peak = 0.55 + Math.random() * 0.45;
    for (let k = 0; k < period * 1.7; k++) {
      const x = k / period;
      const v = x < 0.6 ? Math.pow(Math.sin((x / 0.6) * Math.PI / 2), 2) : Math.exp(-(x - 0.6) * 4.5);
      const idx = Math.floor(t + k) % loop;
      env[idx] = Math.max(env[idx], 0.06 + v * peak);
    }
    t += period;
  }
  let y = 0;
  let deep = 0;
  for (let i = 0; i < d.length; i++) {
    const p = (i / step) % loop;
    const a = env[Math.floor(p)];
    const b = env[(Math.floor(p) + 1) % loop];
    const e = a + (b - a) * (p - Math.floor(p));
    const w = rnd();
    const fc = 180 + 2600 * Math.pow(e, 1.6);
    y += (1 - Math.exp((-2 * Math.PI * fc) / rate)) * (w - y);
    deep = (deep + 0.02 * w) / 1.02;
    d[i] = y * (0.1 + e) + w * 0.05 * Math.pow(e, 3) + deep;
  }
}

/** A brook: narrow resonances that hop between pitches many times a second, over a soft bed. */
function babble(d: Float32Array, rate: number) {
  for (let layer = 0; layer < 3; layer++) {
    let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
    let fc = 800, target = 800, q = 6, amp = 0.6, next = 0;
    let b0 = 0, a1 = 0, a2 = 0, a0 = 1;
    for (let i = 0; i < d.length; i++) {
      if (i >= next) {
        target = 350 * Math.pow(7, Math.random());
        q = 4 + Math.random() * 7;
        amp = 0.25 + Math.random() * 0.75;
        next = i + Math.floor(rate * (0.015 + Math.random() * 0.05));
      }
      if (i % 32 === 0) {
        fc += (target - fc) * 0.25;
        const w0 = (2 * Math.PI * fc) / rate;
        const alpha = Math.sin(w0) / (2 * q);
        a0 = 1 + alpha;
        b0 = alpha;
        a1 = -2 * Math.cos(w0);
        a2 = 1 - alpha;
      }
      const x0 = rnd();
      const y0 = (b0 * x0 - b0 * x2 - a1 * y1 - a2 * y2) / a0;
      x2 = x1; x1 = x0; y2 = y1; y1 = y0;
      d[i] += y0 * amp * 2.5;
    }
  }
  let hp = 0, prev = 0;
  for (let i = 0; i < d.length; i++) {
    const w = rnd() * 0.04;
    hp = 0.97 * (hp + w - prev);
    prev = w;
    d[i] += hp;
  }
}

/** Fire: tiny sharp crackles, often in little bursts, and now and then a deeper pop. */
function crackle(d: Float32Array, rate: number) {
  const click = (at: number, amp: number, ms: number, soft: boolean) => {
    const len = Math.floor((rate * ms) / 1000);
    let prev = 0, lp = 0;
    for (let i = 0; i < len && at + i < d.length; i++) {
      const w = rnd() * Math.exp(-i / (len / 5));
      lp += 0.25 * (w - lp);
      d[at + i] += (soft ? lp * 2.5 : w - prev) * amp;
      prev = w;
    }
  };
  let t = 0;
  while (t < d.length) {
    t += Math.floor((-Math.log(1 - Math.random()) / 6) * rate);
    const amp = 0.1 + Math.pow(Math.random(), 2.5) * 0.9;
    click(t, amp, 0.6 + Math.random() * 2.5, false);
    if (Math.random() < 0.3) {
      let u = t;
      for (let k = 2 + Math.floor(Math.random() * 4); k > 0; k--) {
        u += Math.floor(rate * (0.004 + Math.random() * 0.035));
        click(u, amp * (0.3 + Math.random() * 0.6), 0.5 + Math.random() * 1.5, false);
      }
    }
    if (Math.random() < 0.07) click(t + Math.floor(rate * 0.1), 0.6 + Math.random() * 0.4, 8 + Math.random() * 14, true);
  }
}

/** A slow random wander between -1 and 1, used to make wind gust and rain swell without a pattern. */
function wander(c: BaseAudioContext, key: string): AudioBuffer {
  const hit = cache.get(key);
  if (hit) return hit;
  const rate = 3000;
  const seconds = 90;
  const buf = c.createBuffer(1, rate * seconds, rate);
  const d = buf.getChannelData(0);
  const points: [number, number][] = [];
  for (let t = 0; t < seconds; t += 2 + Math.random() * 5) points.push([t, rnd()]);
  points.push([seconds, points[0][1]]);
  for (let p = 0; p < points.length - 1; p++) {
    const [t0, v0] = points[p];
    const [t1, v1] = points[p + 1];
    for (let i = Math.floor(t0 * rate); i < Math.floor(t1 * rate); i++) {
      const k = (i / rate - t0) / (t1 - t0);
      d[i] = v0 + (v1 - v0) * (0.5 - Math.cos(k * Math.PI) / 2);
    }
  }
  cache.set(key, buf);
  return buf;
}

type Graph = { c: BaseAudioContext; out: AudioNode; nodes: AudioScheduledSourceNode[] };

function loop(g: Graph, buf: AudioBuffer, ...chain: AudioNode[]): AudioBufferSourceNode {
  const s = g.c.createBufferSource();
  s.buffer = buf;
  s.loop = true;
  let node: AudioNode = s;
  for (const n of chain) node = node.connect(n);
  node.connect(g.out);
  g.nodes.push(s);
  return s;
}

function filter(c: BaseAudioContext, type: BiquadFilterType, frequency: number, Q = 0.7, gain = 0): BiquadFilterNode {
  const f = c.createBiquadFilter();
  f.type = type;
  f.frequency.value = frequency;
  f.Q.value = Q;
  f.gain.value = gain;
  return f;
}

function amp(c: BaseAudioContext, value: number): GainNode {
  const g = c.createGain();
  g.gain.value = value;
  return g;
}

/** Moves an audio setting slowly and unpredictably around its value, by up to `depth`. */
function sway(g: Graph, param: AudioParam, depth: number, key: string) {
  const s = g.c.createBufferSource();
  s.buffer = wander(g.c, key);
  s.loop = true;
  const d = amp(g.c, depth);
  s.connect(d).connect(param);
  g.nodes.push(s);
}

/** Evens out how loud each sound feels (measured, then nudged for bright and deep sounds). */
const TRIM: Record<SoundId, number> = { rain: 1.3, "heavy-rain": 1.3, ocean: 0.95, wind: 2, stream: 1, fire: 0.9, fan: 0.8, white: 1.1, pink: 0.9, brown: 0.8 };

/** Builds one soundscape into `out`. Every sound comes out at about the same loudness. */
export function startSound(c: BaseAudioContext, id: SoundId, out: AudioNode): Voice {
  const trim = amp(c, TRIM[id]);
  trim.connect(out);
  const g: Graph = { c, out: trim, nodes: [] };
  const full = c.sampleRate;
  const half = Math.min(full, 24000);
  const W = () => looped(c, "white", 9, full, white);
  const P = () => looped(c, "pink", 11, full, pink);
  const B = () => looped(c, "brown", 13, half, brown);
  switch (id) {
    case "white":
      loop(g, W(), filter(c, "lowpass", 13000), amp(c, 0.5));
      break;
    case "pink":
      loop(g, P(), amp(c, 0.75));
      break;
    case "brown":
      loop(g, B(), amp(c, 1.1));
      break;
    case "fan": {
      const body = amp(c, 0.8);
      sway(g, body.gain, 0.04, "w-fan");
      loop(g, P(), filter(c, "lowpass", 1100), filter(c, "peaking", 160, 1, 5), body);
      loop(g, B(), amp(c, 0.3));
      const blade = c.createOscillator();
      blade.frequency.value = 7.3;
      const depth = amp(c, 0.035);
      blade.connect(depth).connect(body.gain);
      g.nodes.push(blade);
      break;
    }
    case "rain": {
      const bed = amp(c, 0.42);
      sway(g, bed.gain, 0.08, "w-rain");
      loop(g, P(), filter(c, "highpass", 400), filter(c, "lowpass", 5000), bed);
      loop(g, looped(c, "drops-light", 10, full, drops(55)), filter(c, "highpass", 1000), filter(c, "lowpass", 6000), filter(c, "peaking", 3000, 1, 3), amp(c, 0.55));
      loop(g, B(), filter(c, "lowpass", 400), amp(c, 0.18));
      break;
    }
    case "heavy-rain": {
      const bed = amp(c, 0.6);
      sway(g, bed.gain, 0.2, "w-heavy");
      loop(g, W(), filter(c, "lowpass", 3600), filter(c, "highpass", 180), bed);
      loop(g, looped(c, "drops-heavy", 7, full, drops(170)), filter(c, "highpass", 900), filter(c, "lowpass", 6000), amp(c, 0.45));
      loop(g, B(), filter(c, "lowpass", 240), amp(c, 0.3));
      break;
    }
    case "ocean":
      loop(g, looped(c, "ocean", 47, half, waves), amp(c, 0.9));
      break;
    case "wind": {
      const band = filter(c, "bandpass", 480, 0.9);
      sway(g, band.frequency, 260, "w-wind-f");
      const gust = amp(c, 0.85);
      sway(g, gust.gain, 0.45, "w-wind-g");
      loop(g, P(), band, gust);
      const whistle = filter(c, "bandpass", 900, 7);
      sway(g, whistle.frequency, 320, "w-wind-w");
      const wg = amp(c, 0.12);
      sway(g, wg.gain, 0.1, "w-wind-g");
      loop(g, P(), whistle, wg);
      break;
    }
    case "stream":
      loop(g, looped(c, "stream", 17, full, babble), filter(c, "highpass", 250), filter(c, "lowpass", 6000), amp(c, 0.7));
      break;
    case "fire": {
      const roar = amp(c, 0.55);
      sway(g, roar.gain, 0.2, "w-fire");
      loop(g, B(), filter(c, "lowpass", 320), roar);
      loop(g, P(), filter(c, "bandpass", 2400, 0.5), amp(c, 0.06));
      loop(g, looped(c, "crackle", 19, full, crackle), filter(c, "highpass", 600), amp(c, 0.8));
      break;
    }
  }
  const t = c.currentTime;
  // Start each layer at a random point, so two sounds sharing a buffer never line up.
  for (const n of g.nodes) {
    if (n instanceof AudioBufferSourceNode && n.buffer) n.start(t, Math.random() * n.buffer.duration);
    else n.start(t);
  }
  return {
    stop() {
      const now = c.currentTime;
      for (const n of g.nodes) { try { n.stop(now + 0.05); } catch { /* already stopped */ } }
      window.setTimeout(() => trim.disconnect(), 200);
    },
  };
}

Change history

Link to OpenApps pages and add a contribution strip
0fa0e84d53 · 19 hours ago
Bind the agent address in the policy
6f3baa1c72 · 1 day ago
Add favicon.ico so browsers that ask for it stop getting a 404
1a004fe0e3 · 1 day ago
Bounded source sync
afe642fe50 · 2 days ago
Bounded source sync
2c581f55ea · 2 days ago
Bounded source sync
375934c28b · 2 days ago
Bounded source sync
74ff37d51a · 2 days ago