jevstrudel.git / website / src / jev / audioControls.mjs
1// The listener's volume and pause, for whatever is playing: the page's own
2// audio and the sandbox's (a listener's song plays in a frame with its own
3// AudioContext; sandbox.mjs passes both on).
4//
5// Volume is a gain after everything that is measured (superdough's
6// `volume`, behind `speakers`): turning the sound down changes what is
7// heard, not what the meters, the mixer and Jev read. It is kept per browser.
8//
9// Pause suspends the AudioContext rather than the scheduler. The scheduler
10// runs on the audio clock, so everything waits in place (the song, held
11// notes and reverb tails, Jev's sections, the highlights, the lyrics) and
12// continues from the same sample, and nothing that watches "stopped" (the
13// performance recorder, autoplay, a party) sees a stop. Playing, stopping
14// or running the code ends a pause, so the play button never plays into a
15// suspended context.
16import { getAudioContext, setMasterVolume } from '@strudel/webaudio';
17import { sandboxPaused, sandboxVolume } from './sandbox.mjs';
18
19const VOLUME = 'jevstrudel.volume';
20// the slider is 0 to 100; the gain is its square, so the middle sounds like half
21export const gainOf = (percent) => Math.pow(Math.min(100, Math.max(0, percent)) / 100, 2);
22
23const stored = () => {
24  try {
25    const v = Number(localStorage.getItem(VOLUME));
26    return localStorage.getItem(VOLUME) !== null && v >= 0 && v <= 100 ? v : 100;
27  } catch {
28    return 100;
29  }
30};
31
32let state = { volume: typeof localStorage === 'undefined' ? 100 : stored(), paused: false };
33const listeners = new Set();
34function set(patch) {
35  state = { ...state, ...patch };
36  for (const l of listeners) l();
37}
38export const audioControls = {
39  subscribe(l) {
40    listeners.add(l);
41    return () => listeners.delete(l);
42  },
43  get: () => state,
44};
45
46function apply() {
47  const gain = gainOf(state.volume);
48  setMasterVolume(gain);
49  sandboxVolume(gain);
50}
51
52export function setVolume(percent) {
53  const volume = Math.round(Math.min(100, Math.max(0, Number(percent) || 0)));
54  try {
55    localStorage.setItem(VOLUME, String(volume));
56  } catch {}
57  set({ volume });
58  apply();
59}
60
61export async function pause() {
62  if (state.paused) return;
63  set({ paused: true });
64  sandboxPaused(true);
65  await getAudioContext().suspend();
66}
67
68// Also what play, stop and run call (useReplContext): they end a pause.
69export async function unpause() {
70  if (!state.paused) return;
71  set({ paused: false });
72  sandboxPaused(false);
73  await getAudioContext().resume();
74}
75
76// useReplContext, once: the stored volume reaches the first sound.
77export function initAudioControls() {
78  apply();
79}