mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-22 13:13:10 -04:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 753b08edf0 | |||
| ccb1256aa2 | |||
| 657d89cb98 | |||
| 5797c42753 | |||
| afa55535ba | |||
| 1c94074ed6 | |||
| d2b6b257c0 | |||
| c6e1f758be |
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
);
|
||||
@@ -101,6 +126,7 @@ export function SettingsTab({ started }) {
|
||||
panelPosition,
|
||||
audioDeviceName,
|
||||
audioEngineTarget,
|
||||
audioVolume,
|
||||
} = useSettings();
|
||||
const shouldAlwaysSync = isUdels();
|
||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -173,6 +174,9 @@ export function useReplContext() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// set stored audio volume
|
||||
useEffect(() => setGlobalAudioVolume(_settings.audioVolume), [_settings.audioVolume]);
|
||||
|
||||
//
|
||||
// UI Actions
|
||||
//
|
||||
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
|
||||
@@ -34,6 +34,7 @@ export const defaultSettings = {
|
||||
userPatterns: '{}',
|
||||
audioDeviceName: defaultAudioDeviceName,
|
||||
audioEngineTarget: audioEngineTargets.webaudio,
|
||||
audioVolume: 50,
|
||||
};
|
||||
|
||||
let search = null;
|
||||
|
||||
Reference in New Issue
Block a user