PageSourceSearch

https://stream00.vercel.app/vendor/cuelume/audio/engine.js

js stream00.vercel.app collected 2026-10-03 07:48:26 UTC 4,998 bytes, 157 lines download raw bytes

1import { RECIPES, isSoundName } from "../sounds/recipes.js";
2
3const SOURCE_STOP_PADDING = 0.05;
4const CLEANUP_MARGIN = 0.05;
5const INAUDIBLE_GAIN = 0.001;
6
7function renderTone(context, destination, layer, startTime) {
8  const oscillator = context.createOscillator();
9  oscillator.type = layer.waveform;
10  oscillator.frequency.setValueAtTime(layer.frequency, startTime);
11  if (layer.detune) oscillator.detune.value = layer.detune;
12  if (layer.glideTo !== undefined) {
13    const glideTime = layer.glideTime ?? layer.attack + layer.decay;
14    oscillator.frequency.exponentialRampToValueAtTime(
15      layer.glideTo,
16      startTime + glideTime,
17    );
18  }
19  const gain = context.createGain();
20  gain.gain.setValueAtTime(0.0001, startTime);
21  gain.gain.exponentialRampToValueAtTime(
22    layer.peak,
23    startTime + layer.attack,
24  );
25  gain.gain.exponentialRampToValueAtTime(
26    0.0001,
27    startTime + layer.attack + layer.decay,
28  );
29  oscillator.connect(gain).connect(destination);
30  oscillator.start(startTime);
31  oscillator.stop(startTime + layer.attack + layer.decay + SOURCE_STOP_PADDING);
32}
33
34function renderNoise(context, destination, layer, startTime) {
35  const duration = layer.attack + layer.decay + SOURCE_STOP_PADDING;
36  const length = Math.max(1, Math.floor(duration * context.sampleRate));
37  const buffer = context.createBuffer(1, length, context.sampleRate);
38  const data = buffer.getChannelData(0);
39  for (let i = 0; i < length; i++) data[i] = 2 * Math.random() - 1;
40  const source = context.createBufferSource();
41  source.buffer = buffer;
42  const filter = context.createBiquadFilter();
43  filter.type = layer.filterType;
44  filter.frequency.value = layer.filterFrequency;
45  if (layer.filterQ !== undefined) filter.Q.value = layer.filterQ;
46  const gain = context.createGain();
47  gain.gain.setValueAtTime(0.0001, startTime);
48  gain.gain.exponentialRampToValueAtTime(
49    layer.peak,
50    startTime + layer.attack,
51  );
52  gain.gain.exponentialRampToValueAtTime(
53    0.0001,
54    startTime + layer.attack + layer.decay,
55  );
56  source.connect(filter).connect(gain).connect(destination);
57  source.start(startTime);
58  source.stop(startTime + duration);
59}
60
61function attachShimmer(context, source, destination, shimmer) {
62  const delay = context.createDelay(1);
63  delay.delayTime.value = shimmer.delay;
64  const feedbackFilter = context.createBiquadFilter();
65  feedbackFilter.type = "lowpass";
66  feedbackFilter.frequency.value = shimmer.lowpass;
67  const feedbackGain = context.createGain();
68  feedbackGain.gain.value = shimmer.feedback;
69  const wetGain = context.createGain();
70  wetGain.gain.value = shimmer.wet;
71  source.connect(delay);
72  delay.connect(feedbackFilter);
73  feedbackFilter.connect(feedbackGain);
74  feedbackGain.connect(delay);
75  feedbackFilter.connect(wetGain);
76  wetGain.connect(destination);
77  return [delay, feedbackFilter, feedbackGain, wetGain];
78}
79
80function sourceEnd(recipe) {
81  return Math.max(
82    ...recipe.layers.map(
83      (layer) =>
84        (layer.offset ?? 0) + layer.attack + layer.decay + SOURCE_STOP_PADDING,
85    ),
86  );
87}
88
89function shimmerTail(shimmer) {
90  if (!shimmer || shimmer.feedback <= 0) return 0;
91  if (shimmer.feedback >= 1) return shimmer.delay;
92  return (
93    shimmer.delay *
94    (1 + Math.ceil(Math.log(INAUDIBLE_GAIN) / Math.log(shimmer.feedback)))
95  );
96}
97
98function renderRecipe(context, recipe) {
99  const now = context.currentTime;
100  const master = context.createGain();
101  master.gain.value = recipe.masterGain;
102  master.connect(context.destination);
103  const shimmerNodes = recipe.shimmer
104    ? attachShimmer(context, master, context.destination, recipe.shimmer)
105    : [];
106  for (const layer of recipe.layers) {
107    const startTime = now + (layer.offset ?? 0);
108    if (layer.kind === "tone") renderTone(context, master, layer, startTime);
109    else renderNoise(context, master, layer, startTime);
110  }
111  const cleanupAfterMs =
112    (sourceEnd(recipe) + shimmerTail(recipe.shimmer) + CLEANUP_MARGIN) * 1000;
113  setTimeout(() => {
114    master.disconnect();
115    for (const node of shimmerNodes) node.disconnect();
116  }, cleanupAfterMs);
117}
118
119let sharedContext = null;
120let enabled = true;
121
122export function setEnabled(value) {
123  if (typeof value === "boolean") enabled = value;
124}
125
126function getAudioContext() {
127  if (sharedContext) return sharedContext;
128  if (typeof window === "undefined") return null;
129  const Ctor = window.AudioContext ?? window.webkitAudioContext;
130  if (!Ctor) return null;
131  try {
132    sharedContext = new Ctor();
133  } catch {
134    return null;
135  }
136  return sharedContext;
137}
138
139export function play(sound) {
140  if (!enabled || !isSoundName(sound)) return;
141  const context = getAudioContext();
142  if (!context) return;
143  const recipe = RECIPES[sound];
144  if (context.state === "running") {
145    renderRecipe(context, recipe);
146  } else {
147    try {
148      void context.resume().then(
149        () => {
150          if (enabled && context.state === "running")
151            renderRecipe(context, recipe);
152        },
153        () => {},
154      );
155    } catch {}
156  }
157}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.