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);
|
destinationGain.connect(audioContext.destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setGlobalGain(gain) {
|
||||||
|
if (destinationGain == null) {
|
||||||
|
initializeAudioOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
destinationGain.gain.value = gain;
|
||||||
|
}
|
||||||
|
|
||||||
// input: AudioNode, channels: ?Array<int>
|
// input: AudioNode, channels: ?Array<int>
|
||||||
export const connectToDestination = (input, channels = [0, 1]) => {
|
export const connectToDestination = (input, channels = [0, 1]) => {
|
||||||
const ctx = getAudioContext();
|
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 { themes } from '@strudel/codemirror';
|
||||||
import { isUdels } from '../../util.mjs';
|
import { isUdels, setGlobalAudioVolume } from '../../util.mjs';
|
||||||
import { ButtonGroup } from './Forms.jsx';
|
import { ButtonGroup } from './Forms.jsx';
|
||||||
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
|
||||||
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.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 (
|
return (
|
||||||
<div className="flex space-x-2 gap-1">
|
<div className="flex space-x-2 gap-1">
|
||||||
<input
|
<input
|
||||||
className="p-2 grow"
|
className="p-2 grow"
|
||||||
type="range"
|
type="range"
|
||||||
value={value}
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
step={step}
|
step={step}
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
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}
|
step={step}
|
||||||
className="w-16 bg-background rounded-md"
|
className="bg-background rounded-md"
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -101,6 +126,7 @@ export function SettingsTab({ started }) {
|
|||||||
panelPosition,
|
panelPosition,
|
||||||
audioDeviceName,
|
audioDeviceName,
|
||||||
audioEngineTarget,
|
audioEngineTarget,
|
||||||
|
audioVolume,
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
const shouldAlwaysSync = isUdels();
|
const shouldAlwaysSync = isUdels();
|
||||||
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
|
||||||
@@ -135,6 +161,19 @@ export function SettingsTab({ started }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</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">
|
<FormItem label="Theme">
|
||||||
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
<SelectInput options={themeOptions} value={theme} onChange={(theme) => settingsMap.setKey('theme', theme)} />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
resetLoadedSounds,
|
resetLoadedSounds,
|
||||||
initAudioOnFirstClick,
|
initAudioOnFirstClick,
|
||||||
} from '@strudel/webaudio';
|
} 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 { StrudelMirror, defaultSettings } from '@strudel/codemirror';
|
||||||
import { clearHydra } from '@strudel/hydra';
|
import { clearHydra } from '@strudel/hydra';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
@@ -162,6 +162,7 @@ export function useReplContext() {
|
|||||||
// on first load, set stored audio device if possible
|
// on first load, set stored audio device if possible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { audioDeviceName } = _settings;
|
const { audioDeviceName } = _settings;
|
||||||
|
|
||||||
if (audioDeviceName !== defaultAudioDeviceName) {
|
if (audioDeviceName !== defaultAudioDeviceName) {
|
||||||
getAudioDevices().then((devices) => {
|
getAudioDevices().then((devices) => {
|
||||||
const deviceID = devices.get(audioDeviceName);
|
const deviceID = devices.get(audioDeviceName);
|
||||||
@@ -173,6 +174,9 @@ export function useReplContext() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// set stored audio volume
|
||||||
|
useEffect(() => setGlobalAudioVolume(_settings.audioVolume), [_settings.audioVolume]);
|
||||||
|
|
||||||
//
|
//
|
||||||
// UI Actions
|
// UI Actions
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { evalScope, hash2code, logger } from '@strudel/core';
|
import { evalScope, hash2code, logger } from '@strudel/core';
|
||||||
import { settingPatterns, defaultAudioDeviceName } from '../settings.mjs';
|
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 { getMetadata } from '../metadata_parser';
|
||||||
import { isTauri } from '../tauri.mjs';
|
import { isTauri } from '../tauri.mjs';
|
||||||
import './Repl.css';
|
import './Repl.css';
|
||||||
@@ -188,6 +194,19 @@ export const setAudioDevice = async (id) => {
|
|||||||
initializeAudioOutput();
|
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) {
|
export function setVersionDefaultsFrom(code) {
|
||||||
try {
|
try {
|
||||||
const metadata = getMetadata(code);
|
const metadata = getMetadata(code);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export const defaultSettings = {
|
|||||||
userPatterns: '{}',
|
userPatterns: '{}',
|
||||||
audioDeviceName: defaultAudioDeviceName,
|
audioDeviceName: defaultAudioDeviceName,
|
||||||
audioEngineTarget: audioEngineTargets.webaudio,
|
audioEngineTarget: audioEngineTargets.webaudio,
|
||||||
|
audioVolume: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
let search = null;
|
let search = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user