From c6e1f758be97ee65994e798e811ec62f54764a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Sun, 18 Aug 2024 02:14:03 -0300 Subject: [PATCH 1/7] Add Audio Volume setting to REPL Allows for adjusting the global volume/gain of the REPL. Set default global value to 50%. Acts similarily to `gain()`, but without modifying the code, so passerbys who don't know how to use the tool can adjust the volume too). The volume slider uses a logarithmic scale, so it adjusts better to human sound perception. --- packages/superdough/superdough.mjs | 8 ++++++++ .../src/repl/components/panel/SettingsTab.jsx | 17 +++++++++++++++-- website/src/repl/useReplContext.jsx | 11 ++++++++--- website/src/repl/util.mjs | 9 ++++++++- website/src/settings.mjs | 1 + 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/superdough/superdough.mjs b/packages/superdough/superdough.mjs index 01569788b..dda7072eb 100644 --- a/packages/superdough/superdough.mjs +++ b/packages/superdough/superdough.mjs @@ -143,6 +143,14 @@ export function initializeAudioOutput() { destinationGain.connect(audioContext.destination); } +export function setGlobalGain(gain) { + if (destinationGain == null) { + initializeAudioOutput(); + } + + destinationGain.gain.value = gain; +} + // input: AudioNode, channels: ?Array export const connectToDestination = (input, channels = [0, 1]) => { const ctx = getAudioContext(); diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index e1d047eaf..e2385dad6 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,6 +1,6 @@ import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; -import { isUdels } from '../../util.mjs'; +import { isUdels, setGlobalAudioVolume } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; @@ -42,7 +42,7 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) { /> onChange(Number(e.target.value))} @@ -96,6 +96,7 @@ export function SettingsTab({ started }) { fontFamily, panelPosition, audioDeviceName, + audioVolume, } = useSettings(); const shouldAlwaysSync = isUdels(); return ( @@ -109,6 +110,18 @@ export function SettingsTab({ started }) { /> )} + + { + settingsMap.setKey('audioVolume', audioVolume); + setGlobalAudioVolume(audioVolume); + }} + min={0} + max={100} + step={.1} + /> + settingsMap.setKey('theme', theme)} /> diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index d61699b6d..ef684aa3f 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -15,7 +15,7 @@ import { initAudioOnFirstClick, } from '@strudel/webaudio'; import { defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; +import { getAudioDevices, setAudioDevice, setGlobalAudioVolume, setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; import { useCallback, useEffect, useRef, useState } from 'react'; @@ -154,9 +154,11 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_settings]); - // on first load, set stored audio device if possible + // on first load... useEffect(() => { - const { audioDeviceName } = _settings; + const { audioDeviceName, audioVolume } = _settings; + + // set stored audio device if possible if (audioDeviceName !== defaultAudioDeviceName) { getAudioDevices().then((devices) => { const deviceID = devices.get(audioDeviceName); @@ -166,6 +168,9 @@ export function useReplContext() { setAudioDevice(deviceID); }); } + + // set stored audio volume + setGlobalAudioVolume(audioVolume); }, []); // diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 4aa61fb03..9371adc68 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,6 @@ import { evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setVersionDefaults } from '@strudel/webaudio'; +import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setGlobalGain, setVersionDefaults } from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; @@ -180,6 +180,13 @@ export const setAudioDevice = async (id) => { initializeAudioOutput(); }; +export const setGlobalAudioVolume = (volume) => { + // Adjust user visible volume to a gain value in the range [0, 1] + // Pow is used to also adjust the volume to a logarithmic scale (as perceived by us humans) + const gain = Math.pow(volume / 100, 2); + setGlobalGain(gain); +} + export function setVersionDefaultsFrom(code) { try { const metadata = getMetadata(code); diff --git a/website/src/settings.mjs b/website/src/settings.mjs index f9b9e2810..9b73d38b6 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -28,6 +28,7 @@ export const defaultSettings = { panelPosition: 'right', userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, + audioVolume: 50 }; let search = null; From 1c94074ed6d93bd2f3ad6ed6b2f5540b5ae0eb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 15:23:20 -0300 Subject: [PATCH 2/7] Run codeformat --- website/src/repl/components/panel/SettingsTab.jsx | 2 +- website/src/repl/util.mjs | 10 ++++++++-- website/src/settings.mjs | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index bbaffbb27..dfe984f53 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -145,7 +145,7 @@ export function SettingsTab({ started }) { }} min={0} max={100} - step={.1} + step={0.1} /> diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index 86b9cd83b..c4aece189 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -1,6 +1,12 @@ import { evalScope, hash2code, logger } from '@strudel/core'; import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs'; -import { getAudioContext, initializeAudioOutput, setDefaultAudioContext, setGlobalGain, setVersionDefaults } from '@strudel/webaudio'; +import { + getAudioContext, + initializeAudioOutput, + setDefaultAudioContext, + setGlobalGain, + setVersionDefaults, +} from '@strudel/webaudio'; import { getMetadata } from '../metadata_parser'; import { isTauri } from '../tauri.mjs'; import './Repl.css'; @@ -193,7 +199,7 @@ export const setGlobalAudioVolume = (volume) => { // Pow is used to also adjust the volume to a logarithmic scale (as perceived by us humans) const gain = Math.pow(volume / 100, 2); setGlobalGain(gain); -} +}; export function setVersionDefaultsFrom(code) { try { diff --git a/website/src/settings.mjs b/website/src/settings.mjs index bbfa45afd..54add2196 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -34,7 +34,7 @@ export const defaultSettings = { userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, audioEngineTarget: audioEngineTargets.webaudio, - audioVolume: 50 + audioVolume: 50, }; let search = null; From afa55535ba784ab9d811d7141e7c5c6ad4178896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 15:42:41 -0300 Subject: [PATCH 3/7] Disable Audio Volume slider when Audio Engine Target is OSC --- website/src/repl/components/panel/SettingsTab.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index dfe984f53..da62f6142 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,4 +1,4 @@ -import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; +import { audioEngineTargets, defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; import { isUdels, setGlobalAudioVolume } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; @@ -137,6 +137,9 @@ export function SettingsTab({ started }) { /> + {audioEngineTarget === audioEngineTargets.osc && ( + Has no effect when Audio Engine Target is OSC + )} { @@ -146,6 +149,7 @@ export function SettingsTab({ started }) { min={0} max={100} step={0.1} + disabled={audioEngineTarget === audioEngineTargets.osc} /> From 5797c4275344f89f7db2197cf880c9c0ce210eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 17:58:52 -0300 Subject: [PATCH 4/7] Calculate global gain with a better approximation of human perceived loudness Taken from https://www.dr-lex.be/info-stuff/volumecontrols.html --- website/src/repl/util.mjs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/website/src/repl/util.mjs b/website/src/repl/util.mjs index c4aece189..2b1b4d9d3 100644 --- a/website/src/repl/util.mjs +++ b/website/src/repl/util.mjs @@ -194,10 +194,16 @@ export const setAudioDevice = async (id) => { initializeAudioOutput(); }; +const NATURAL_LOG_10 = Math.log(1000); +const LOW_VOLUME_CONSTANT = Math.exp(0.1 * NATURAL_LOG_10); export const setGlobalAudioVolume = (volume) => { - // Adjust user visible volume to a gain value in the range [0, 1] - // Pow is used to also adjust the volume to a logarithmic scale (as perceived by us humans) - const gain = Math.pow(volume / 100, 2); + // Gain is calculated to adjust the volume to a logarithmic scale to match how us humans perceive loudness. + // Formula is taken from https://www.dr-lex.be/info-stuff/volumecontrols.html + volume /= 100; // [0, 1] + + let gain = volume >= 0.1 ? 0.001 * Math.exp(NATURAL_LOG_10 * volume) : volume * 10 * 0.001 * LOW_VOLUME_CONSTANT; + gain = Math.max(0, Math.min(gain, 1)); // just in case + setGlobalGain(gain); }; From 657d89cb98d5bb16a489e815bd168fa841ca823b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 18:12:42 -0300 Subject: [PATCH 5/7] Fix settings number slider browser up arrow not increasing the value correctly when step < 1 If we keep flooring the value, but have a step of, e.g., 0.1, then we are never going to reach the next whole number value. This applies specifically to the Audio Volume slider. --- website/src/repl/components/panel/SettingsTab.jsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index da62f6142..e326a7b3e 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { audioEngineTargets, defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { themes } from '@strudel/codemirror'; import { isUdels, setGlobalAudioVolume } from '../../util.mjs'; @@ -32,6 +33,16 @@ function SelectInput({ value, options, onChange }) { } function NumberSlider({ value, onChange, step = 1, ...rest }) { + const fractionalDigits = useMemo(() => { + const stepStr = step.toString(); + const decimalPointIdx = stepStr.indexOf('.'); + if (decimalPointIdx < 0) { + return 0; + } + + return stepStr.slice(decimalPointIdx + 1).length; + }, [step]); + return (
onChange(Number(e.target.value))} From ccb1256aa286be95fff28adf32dd06e71c8bf243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 18:16:51 -0300 Subject: [PATCH 6/7] Adjust settings number slider input width according to max contents This is a bit overkill, but this way we can somewhat adjust the width of the input so no symbols are cut off (either by the input being too small, or the up/down arrows from the browser). I am not happy about having to use `calc()` with some magic numbers either, but this is what looked nicest to me and had the least effort. --- .../src/repl/components/panel/SettingsTab.jsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index e326a7b3e..e78903e56 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -32,7 +32,7 @@ function SelectInput({ value, options, onChange }) { ); } -function NumberSlider({ value, onChange, step = 1, ...rest }) { +function NumberSlider({ value, onChange, min, max, step = 1, ...rest }) { const fractionalDigits = useMemo(() => { const stepStr = step.toString(); const decimalPointIdx = stepStr.indexOf('.'); @@ -43,22 +43,36 @@ function NumberSlider({ value, onChange, step = 1, ...rest }) { return stepStr.slice(decimalPointIdx + 1).length; }, [step]); + const textInputCharWidth = useMemo(() => { + const maxValueWholePartLength = Math.floor(max).toString().length; + return maxValueWholePartLength + '.'.length + fractionalDigits; + }, [max, fractionalDigits]); + return (
onChange(Number(e.target.value))} {...rest} /> onChange(Number(e.target.value))} + {...rest} />
); From 753b08edf0a853d209297ed9234a16bba7564200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Tue, 3 Sep 2024 18:24:48 -0300 Subject: [PATCH 7/7] Update global audio volume exclusively through useReplContext effect This ensures the audio volume is synced across tabs, since changing the volume in one tab would retrigger the effect in the other tabs. It also means we can remove the explicit call to `setGlobalAudioVolume()` from --- website/src/repl/components/panel/SettingsTab.jsx | 5 +---- website/src/repl/useReplContext.jsx | 11 +++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index e78903e56..baa5506ba 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -167,10 +167,7 @@ export function SettingsTab({ started }) { )} { - settingsMap.setKey('audioVolume', audioVolume); - setGlobalAudioVolume(audioVolume); - }} + onChange={(audioVolume) => settingsMap.setKey('audioVolume', audioVolume)} min={0} max={100} step={0.1} diff --git a/website/src/repl/useReplContext.jsx b/website/src/repl/useReplContext.jsx index 19a95acd2..04d300412 100644 --- a/website/src/repl/useReplContext.jsx +++ b/website/src/repl/useReplContext.jsx @@ -159,11 +159,10 @@ export function useReplContext() { editorRef.current?.updateSettings(editorSettings); }, [_settings]); - // on first load... + // on first load, set stored audio device if possible useEffect(() => { - const { audioDeviceName, audioVolume } = _settings; + const { audioDeviceName } = _settings; - // set stored audio device if possible if (audioDeviceName !== defaultAudioDeviceName) { getAudioDevices().then((devices) => { const deviceID = devices.get(audioDeviceName); @@ -173,11 +172,11 @@ export function useReplContext() { setAudioDevice(deviceID); }); } - - // set stored audio volume - setGlobalAudioVolume(audioVolume); }, []); + // set stored audio volume + useEffect(() => setGlobalAudioVolume(_settings.audioVolume), [_settings.audioVolume]); + // // UI Actions //