PageSourceSearch

https://cybermobbing.web.app/js/audio.js?v=1784223914

js cybermobbing.web.app collected 2026-10-03 12:20:39 UTC 10,204 bytes, 306 lines download raw bytes

1/**
2 * @file audio.js
3 * @description Web Audio API sound engine and pausable timeout system.
4 *   Provides synthesized notification sounds for each platform (WhatsApp, Instagram,
5 *   TikTok, iMessage), a camera shutter effect, typing indicator audio, and background
6 *   music control. Also implements a custom pausable timer system ({@link simTimeout})
7 *   that replaces native setTimeout so the entire simulation can be paused/resumed
8 *   without losing timer state.
9 * @requires i18n.js - t() for UI label translation (pause/resume button text)
10 */
11
12// ========== AUDIO ENGINE ==========
13
14/** @type {AudioContext} Shared Web Audio context used by all sound functions */
15var ax;
16
17/**
18 * Initializes the shared AudioContext. Must be called from a user gesture (click)
19 * to satisfy browser autoplay policies.
20 */
21function initAudio() {
22  ax = new (window.AudioContext || window.webkitAudioContext)();
23  ax.resume();
24}
25
26/**
27 * Core oscillator builder. Creates a single tone with an attack-decay envelope.
28 * Silently returns if the AudioContext is missing or the simulation is paused.
29 * @param {number} freq    - Frequency in Hz
30 * @param {number} start   - AudioContext time to begin playback
31 * @param {number} dur     - Duration in seconds
32 * @param {number} [vol=0.1]  - Peak gain (0-1)
33 * @param {string} [type='sine'] - OscillatorNode waveform type
34 */
35function tone(freq, start, dur, vol, type) {
36  if (!ax || simPaused) return;
37  var o = ax.createOscillator();
38  var g = ax.createGain();
39  o.connect(g);
40  g.connect(ax.destination);
41  o.frequency.setValueAtTime(freq, start);
42  o.type = type || 'sine';
43  // Quick 5ms attack ramp to avoid click artifacts, then exponential decay
44  g.gain.setValueAtTime(0, start);
45  g.gain.linearRampToValueAtTime(vol || 0.1, start + 0.005);
46  g.gain.exponentialRampToValueAtTime(0.001, start + dur);
47  o.start(start);
48  o.stop(start + dur);
49}
50
51/**
52 * WhatsApp-style notification: two-note rising chime (A5 -> D6).
53 */
54function sndWa() {
55  if (!ax || simPaused) return;
56  var t = ax.currentTime;
57  tone(880, t, 0.12, 0.1);
58  tone(1175, t + 0.12, 0.15, 0.1);
59}
60
61/**
62 * Instagram-style notification: three-note ascending arpeggio (C6 -> E6 -> G6).
63 */
64function sndIg() {
65  if (!ax || simPaused) return;
66  var t = ax.currentTime;
67  tone(1047, t, 0.06, 0.06);
68  tone(1319, t + 0.07, 0.06, 0.06);
69  tone(1568, t + 0.14, 0.08, 0.05);
70}
71
72/**
73 * TikTok-style notification: descending pitch sweep (1200 Hz -> 800 Hz)
74 * giving a short "bloop" effect.
75 */
76function sndTk() {
77  if (!ax || simPaused) return;
78  var t = ax.currentTime;
79  var o = ax.createOscillator();
80  var g = ax.createGain();
81  o.connect(g);
82  g.connect(ax.destination);
83  // Rapid frequency sweep downward creates the characteristic bloop
84  o.frequency.setValueAtTime(1200, t);
85  o.frequency.exponentialRampToValueAtTime(800, t + 0.08);
86  o.type = 'sine';
87  g.gain.setValueAtTime(0.08, t);
88  g.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
89  o.start(t);
90  o.stop(t + 0.1);
91}
92
93/**
94 * iMessage-style notification: three-note ascending chime (C6 -> E6 -> G6),
95 * slightly longer and louder than the Instagram sound.
96 */
97function sndIm() {
98  if (!ax || simPaused) return;
99  var t = ax.currentTime;
100  tone(1047, t, 0.1, 0.08);
101  tone(1319, t + 0.12, 0.1, 0.08);
102  tone(1568, t + 0.24, 0.15, 0.07);
103}
104
105/**
106 * Camera shutter sound: synthesized from a short burst of white noise
107 * shaped with an exponential decay envelope and high-pass filtered at 2 kHz
108 * to simulate the mechanical "click" of a camera.
109 */
110function sndShutter() {
111  if (!ax || simPaused) return;
112  var t = ax.currentTime;
113  // Generate 80ms of white noise with exponential decay
114  var b = ax.createBuffer(1, ax.sampleRate * 0.08, ax.sampleRate);
115  var d = b.getChannelData(0);
116  for (var i = 0; i < d.length; i++) {
117    var env = Math.exp(-i / (d.length * 0.08));
118    d[i] = (Math.random() * 2 - 1) * env * 0.4;
119  }
120  var s = ax.createBufferSource();
121  var g = ax.createGain();
122  var f = ax.createBiquadFilter();
123  s.buffer = b;
124  // High-pass filter removes rumble, keeping only the sharp click
125  f.type = 'highpass';
126  f.frequency.value = 2000;
127  s.connect(f);
128  f.connect(g);
129  g.connect(ax.destination);
130  g.gain.setValueAtTime(0.2, t);
131  s.start(t);
132}
133
134/**
135 * Phone vibration buzz: low-frequency sawtooth wave (150 Hz) that mimics
136 * the haptic buzz of a phone notification.
137 */
138function sndBuzz() {
139  if (!ax || simPaused) return;
140  var t = ax.currentTime;
141  var o = ax.createOscillator();
142  var g = ax.createGain();
143  o.connect(g);
144  g.connect(ax.destination);
145  o.frequency.setValueAtTime(150, t);
146  o.type = 'sawtooth';
147  g.gain.setValueAtTime(0.04, t);
148  g.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
149  o.start(t);
150  o.stop(t + 0.15);
151}
152
153/**
154 * Starts a rapid stream of faint, randomized high-pitched ticks (2500-3300 Hz)
155 * to simulate the sound of someone typing on a phone keyboard.
156 */
157var typActive = false;
158function typStart() {
159  if (!ax) return;
160  typActive = true;
161  function typTick() {
162    if (!typActive) return;
163    tone(2500 + Math.random() * 800, ax.currentTime, 0.015, 0.01, 'sine');
164    simTimeout(typTick, 60 + Math.random() * 40);
165  }
166  simTimeout(typTick, 60);
167}
168
169/**
170 * Stops the typing indicator sound loop.
171 */
172function typStop() {
173  typActive = false;
174}
175
176// ========== BACKGROUND MUSIC ==========
177
178/** @type {HTMLAudioElement|null} Reference to the <audio id="bgm"> element */
179var bgMusic;
180
181/**
182 * Starts the background music track at 40% volume.
183 * Silently catches play() rejections (e.g. if autoplay is blocked).
184 */
185function startMusic() {
186  bgMusic = document.getElementById('bgm');
187  if (bgMusic) {
188    bgMusic.volume = 0.4;
189    bgMusic.play().catch(function () {});
190  }
191}
192
193// ========== PAUSABLE TIMEOUT SYSTEM ==========
194//
195// Architecture: Every scene uses simTimeout() instead of native setTimeout().
196// Each timer stores its remaining time. On pause, all native timeouts are cleared
197// and elapsed time is subtracted from `remaining`. On resume, each timer is
198// rescheduled with its remaining duration. This lets the entire simulation
199// freeze and resume seamlessly without drifting or losing callbacks.
200
201/** @type {boolean} Global pause flag checked by all sounds and intervals */
202var simPaused = false;
203
204/**
205 * @type {number} Test-only time-lapse factor, set via URL parameter
206 * `?testspeed=N` (integer 1-60). All simTimeout delays and the progress-bar
207 * tick interval are divided by it, so N=10 runs the 120s simulation in ~12s.
208 * Defaults to 1 (real time); invalid or out-of-range values fall back to 1,
209 * so production behavior is unchanged unless the parameter is given explicitly.
210 * Used by the E2E tests (scripts/run-e2e.js), see docs/adr/ADR-0006.
211 */
212var SIM_SPEED = (function () {
213  var m = /[?&]testspeed=(\d+)/.exec(window.location.search);
214  var v = m ? parseInt(m[1], 10) : 1;
215  return v >= 1 && v <= 60 ? v : 1;
216})();
217
218/**
219 * @type {Array<{id: number, fn: Function, remaining: number, startedAt: number, nativeId: number, schedule: Function}>}
220 * Active pausable timers. Each entry tracks the callback, how many ms remain,
221 * when it was last (re-)started, and a schedule() function to arm it.
222 */
223var simTimers = [];
224
225/** @type {number} Monotonically increasing counter for timer IDs */
226var simTimerIdCounter = 0;
227
228/**
229 * Pausable replacement for setTimeout. Registers a callback that survives
230 * pause/resume cycles by tracking remaining time.
231 *
232 * Flow: schedule() arms a native setTimeout for `remaining` ms. When it fires,
233 * the timer removes itself from the list and calls fn(). If togglePause() is
234 * called before it fires, the native timeout is cleared, elapsed time is
235 * subtracted from `remaining`, and schedule() is called again on resume.
236 *
237 * @param {Function} fn    - Callback to execute after delay
238 * @param {number}   delay - Delay in milliseconds
239 * @returns {number} Unique timer ID (not the native setTimeout ID)
240 */
241function simTimeout(fn, delay) {
242  var id = ++simTimerIdCounter;
243  var timer = { id: id, fn: fn, remaining: delay / SIM_SPEED, startedAt: Date.now() };
244
245  function schedule() {
246    timer.startedAt = Date.now();
247    timer.nativeId = setTimeout(function () {
248      // Timer has fired -- remove from active list, then execute callback
249      simTimers = simTimers.filter(function (t) {
250        return t.id !== id;
251      });
252      fn();
253    }, timer.remaining);
254  }
255
256  timer.schedule = schedule;
257  simTimers.push(timer);
258  schedule();
259  return id;
260}
261
262/**
263 * Toggles the simulation between paused and running states.
264 *
265 * Pause coordination (all five systems are frozen/resumed together):
266 *   1. simTimers  -- clear native timeouts, snapshot remaining time
267 *   2. bgMusic    -- pause/resume the <audio> element
268 *   3. clockInt   -- stop/restart the phone clock display interval
269 *   4. tmr        -- stop/restart the progress bar tick interval
270 *   5. UI         -- swap button label, show/hide pause overlay
271 *
272 * On resume, clockStart is recalculated so the phone clock picks up where
273 * it left off without jumping forward.
274 */
275function togglePause() {
276  simPaused = !simPaused;
277  var btn = document.getElementById('pauseBtn');
278  var overlay = document.getElementById('pauseOverlay');
279
280  if (simPaused) {
281    // -- PAUSE: freeze everything --
282    simTimers.forEach(function (t) {
283      clearTimeout(t.nativeId);
284      // Calculate how much time has already elapsed and subtract it
285      t.remaining -= Date.now() - t.startedAt;
286      if (t.remaining < 0) t.remaining = 0;
287    });
288    if (bgMusic) bgMusic.pause();
289    if (typeof clockInt !== 'undefined') clearInterval(clockInt);
290    if (typeof tmr !== 'undefined') clearInterval(tmr);
291    btn.textContent = t('ui.resume');
292    overlay.classList.remove('hidden');
293  } else {
294    // -- RESUME: restart everything with corrected offsets --
295    simTimers.forEach(function (t) {
296      t.schedule();
297    });
298    if (bgMusic) bgMusic.play().catch(function () {});
299    // Recompute clockStart so elapsed time stays consistent after the pause gap
300    clockStart = Date.now() - sec * 1000;
301    if (typeof startClock === 'function') startClock();
302    tmr = setInterval(tick, 100 / SIM_SPEED);
303    btn.textContent = t('ui.pause');
304    overlay.classList.add('hidden');
305  }
306}

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.