Skip to content
This repository was archived by the owner on Jun 19, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/superdough/superdough.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ export function initializeAudioOutput() {
destinationGain.connect(audioContext.destination);
}

export function setGlobalGain(gain) {
if (destinationGain == null) {
initializeAudioOutput();
}

destinationGain.gain.value = gain;
}

// input: AudioNode, channels: ?Array<int>
export const connectToDestination = (input, channels = [0, 1]) => {
const ctx = getAudioContext();
Expand Down
49 changes: 44 additions & 5 deletions website/src/repl/components/panel/SettingsTab.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs';
import { useMemo } from 'react';
import { audioEngineTargets, 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';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
Expand Down Expand Up @@ -31,23 +32,47 @@ 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('.');
if (decimalPointIdx < 0) {
return 0;
}

return stepStr.slice(decimalPointIdx + 1).length;
}, [step]);

const textInputCharWidth = useMemo(() => {
const maxValueWholePartLength = Math.floor(max).toString().length;
return maxValueWholePartLength + '.'.length + fractionalDigits;
}, [max, fractionalDigits]);

return (
<div className="flex space-x-2 gap-1">
<input
className="p-2 grow"
type="range"
value={value}
min={min}
max={max}
step={step}
onChange={(e) => onChange(Number(e.target.value))}
{...rest}
/>
<input
type="number"
value={value}
style={{
// approximate text size + some leeway for the default padding + some space between the browser's up/down arrows and the input's value
width: `calc(${textInputCharWidth}ch + 2 * 0.75rem + 1rem)`,
}}
value={Number(value).toFixed(fractionalDigits)}
min={min}
max={max}
step={step}
className="w-16 bg-background rounded-md"
className="bg-background rounded-md"
onChange={(e) => onChange(Number(e.target.value))}
{...rest}
/>
</div>
);
Expand Down Expand Up @@ -101,6 +126,7 @@ export function SettingsTab({ started }) {
panelPosition,
audioDeviceName,
audioEngineTarget,
audioVolume,
} = useSettings();
const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
Expand Down Expand Up @@ -135,6 +161,19 @@ export function SettingsTab({ started }) {
}}
/>
</FormItem>
<FormItem label="Audio Volume">
{audioEngineTarget === audioEngineTargets.osc && (
<span class="text-sm italic">Has no effect when Audio Engine Target is OSC</span>
)}
<NumberSlider
value={audioVolume}
onChange={(audioVolume) => settingsMap.setKey('audioVolume', audioVolume)}
min={0}
max={100}
step={0.1}
disabled={audioEngineTarget === audioEngineTargets.osc}
/>
</FormItem>
<FormItem label="Theme">
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
</FormItem>
Expand Down
6 changes: 5 additions & 1 deletion website/src/repl/useReplContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
resetLoadedSounds,
initAudioOnFirstClick,
} from '@strudel/webaudio';
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';
Expand Down Expand Up @@ -162,6 +162,7 @@ export function useReplContext() {
// on first load, set stored audio device if possible
useEffect(() => {
const { audioDeviceName } = _settings;

if (audioDeviceName !== defaultAudioDeviceName) {
getAudioDevices().then((devices) => {
const deviceID = devices.get(audioDeviceName);
Expand All @@ -173,6 +174,9 @@ export function useReplContext() {
}
}, []);

// set stored audio volume
useEffect(() => setGlobalAudioVolume(_settings.audioVolume), [_settings.audioVolume]);

//
// UI Actions
//
Expand Down
21 changes: 20 additions & 1 deletion website/src/repl/util.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
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';
import './Repl.css';
Expand Down Expand Up @@ -188,6 +194,19 @@ 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) => {
// 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);
};

export function setVersionDefaultsFrom(code) {
try {
const metadata = getMetadata(code);
Expand Down
1 change: 1 addition & 0 deletions website/src/settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const defaultSettings = {
userPatterns: '{}',
audioDeviceName: defaultAudioDeviceName,
audioEngineTarget: audioEngineTargets.webaudio,
audioVolume: 50,
};

let search = null;
Expand Down