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}