From d80c06cd553e79328fef4a5c0860318125472e99 Mon Sep 17 00:00:00 2001 From: "Jade (Rose) Rowland" Date: Sun, 11 Aug 2024 00:35:20 -0400 Subject: [PATCH] fix confirm dialog --- packages/desktopbridge/oscbridge.mjs | 103 ++++++++++-------- packages/webaudio/webaudio.mjs | 2 + website/src/repl/Repl.jsx | 8 +- .../panel/AudioEngineTargetSelector.jsx | 16 +++ .../src/repl/components/panel/SettingsTab.jsx | 47 +++++++- website/src/settings.mjs | 6 + 6 files changed, 128 insertions(+), 54 deletions(-) create mode 100644 website/src/repl/components/panel/AudioEngineTargetSelector.jsx diff --git a/packages/desktopbridge/oscbridge.mjs b/packages/desktopbridge/oscbridge.mjs index 42a9a6d4..a789cca8 100644 --- a/packages/desktopbridge/oscbridge.mjs +++ b/packages/desktopbridge/oscbridge.mjs @@ -1,64 +1,75 @@ import { parseNumeral, Pattern, averageArray } from '@strudel/core'; + + import { Invoke } from './utils.mjs'; +import { getAudioContext } from '../superdough/superdough.mjs'; let offsetTime; let timeAtPrevOffsetSample; let prevOffsetTimes = []; -Pattern.prototype.osc = function () { - return this.onTrigger(async (time, hap, currentTime, cps = 1, targetTime) => { - hap.ensureObjectValue(); - const cycle = hap.wholeOrPart().begin.valueOf(); - const delta = hap.duration.valueOf(); - const controls = Object.assign({}, { cps, cycle, delta }, hap.value); - // make sure n and note are numbers - controls.n && (controls.n = parseNumeral(controls.n)); - controls.note && (controls.note = parseNumeral(controls.note)); - const params = []; +export const superdirtOutput = (hap, deadline, hapDuration, cps, targetTime) => { + const ctx = getAudioContext(); + const currentTime = ctx.currentTime; + return oscTrigger(null, hap, currentTime, cps, targetTime) +} - const unixTimeSecs = Date.now() / 1000; - const newOffsetTime = unixTimeSecs - currentTime; - if (offsetTime == null) { - offsetTime = newOffsetTime; - } - prevOffsetTimes.push(newOffsetTime); - if (prevOffsetTimes.length > 8) { - prevOffsetTimes.shift(); - } - // every two seconds, the average of the previous 8 offset times is calculated and used as a stable reference - // for calculating the timestamp that will be sent to the backend - if (timeAtPrevOffsetSample == null || unixTimeSecs - timeAtPrevOffsetSample > 2) { - timeAtPrevOffsetSample = unixTimeSecs; - const rollingOffsetTime = averageArray(prevOffsetTimes); - //account for the js clock freezing or resets set the new offset - if (Math.abs(rollingOffsetTime - offsetTime) > 0.01) { - offsetTime = rollingOffsetTime; - } +async function oscTrigger(t_deprecate, hap, currentTime, cps = 1, targetTime) { + hap.ensureObjectValue(); + const cycle = hap.wholeOrPart().begin.valueOf(); + const delta = hap.duration.valueOf(); + const controls = Object.assign({}, { cps, cycle, delta }, hap.value); + // make sure n and note are numbers + controls.n && (controls.n = parseNumeral(controls.n)); + controls.note && (controls.note = parseNumeral(controls.note)); + + const params = []; + + const unixTimeSecs = Date.now() / 1000; + const newOffsetTime = unixTimeSecs - currentTime; + if (offsetTime == null) { + offsetTime = newOffsetTime; + } + prevOffsetTimes.push(newOffsetTime); + if (prevOffsetTimes.length > 8) { + prevOffsetTimes.shift(); + } + // every two seconds, the average of the previous 8 offset times is calculated and used as a stable reference + // for calculating the timestamp that will be sent to the backend + if (timeAtPrevOffsetSample == null || unixTimeSecs - timeAtPrevOffsetSample > 2) { + timeAtPrevOffsetSample = unixTimeSecs; + const rollingOffsetTime = averageArray(prevOffsetTimes); + //account for the js clock freezing or resets set the new offset + if (Math.abs(rollingOffsetTime - offsetTime) > 0.01) { + offsetTime = rollingOffsetTime; } + } - const timestamp = offsetTime + targetTime; + const timestamp = offsetTime + targetTime; - Object.keys(controls).forEach((key) => { - const val = controls[key]; - const value = typeof val === 'number' ? val.toString() : val; + Object.keys(controls).forEach((key) => { + const val = controls[key]; + const value = typeof val === 'number' ? val.toString() : val; - if (value == null) { - return; - } - params.push({ - name: key, - value, - valueisnumber: typeof val === 'number', - }); - }); - - if (params.length === 0) { + if (value == null) { return; } - const message = { target: '/dirt/play', timestamp, params }; - setTimeout(() => { - Invoke('sendosc', { messagesfromjs: [message] }); + params.push({ + name: key, + value, + valueisnumber: typeof val === 'number', }); }); + + if (params.length === 0) { + return; + } + const message = { target: '/dirt/play', timestamp, params }; + setTimeout(() => { + Invoke('sendosc', { messagesfromjs: [message] }); + }); +} +Pattern.prototype.osc = function () { + return this.onTrigger(oscTrigger); }; diff --git a/packages/webaudio/webaudio.mjs b/packages/webaudio/webaudio.mjs index 44a68348..1dfaca5a 100644 --- a/packages/webaudio/webaudio.mjs +++ b/packages/webaudio/webaudio.mjs @@ -20,6 +20,8 @@ export const webaudioOutputTrigger = (t, hap, ct, cps) => superdough(hap2value(h export const webaudioOutput = (hap, deadline, hapDuration, cps, t) => superdough(hap2value(hap), t ? `=${t}` : deadline, hapDuration); + + Pattern.prototype.webaudio = function () { return this.onTrigger(webaudioOutputTrigger); }; diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index 3b536c2f..b01f1d79 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -14,7 +14,8 @@ import { resetLoadedSounds, initAudioOnFirstClick, } from '@strudel/webaudio'; -import { defaultAudioDeviceName } from '../settings.mjs'; +import { superdirtOutput } from '@strudel/desktopbridge/oscbridge.mjs'; +import { audioEngineTargets, defaultAudioDeviceName } from '../settings.mjs'; import { getAudioDevices, setAudioDevice, setVersionDefaultsFrom } from './util.mjs'; import { StrudelMirror, defaultSettings } from '@strudel/codemirror'; import { clearHydra } from '@strudel/hydra'; @@ -61,13 +62,14 @@ async function getModule(name) { export function Repl({ embedded = false }) { const isEmbedded = embedded || isIframe; - const { panelPosition, isZen, isSyncEnabled } = useSettings(); + const { panelPosition, isZen, isSyncEnabled, audioEngineTarget } = useSettings(); + const defaultOutput = audioEngineTarget === audioEngineTargets.superdirt ? superdirtOutput : webaudioOutput; const init = useCallback(() => { const drawTime = [-2, 2]; const drawContext = getDrawContext(); const editor = new StrudelMirror({ sync: isSyncEnabled, - defaultOutput: webaudioOutput, + defaultOutput, getTime: () => getAudioContext().currentTime, setInterval, clearInterval, diff --git a/website/src/repl/components/panel/AudioEngineTargetSelector.jsx b/website/src/repl/components/panel/AudioEngineTargetSelector.jsx new file mode 100644 index 00000000..8d70e502 --- /dev/null +++ b/website/src/repl/components/panel/AudioEngineTargetSelector.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { audioEngineTargets } from '../../../settings.mjs'; +import { SelectInput } from './SelectInput'; + +// Allows the user to select an audio interface for Strudel to play through +export function AudioEngineTargetSelector({ target, onChange, isDisabled }) { + const onTargetChange = (target) => { + onChange(target); + }; + const options = new Map(); + Array.from(Object.keys(audioEngineTargets)).map((key) => { + options.set(key, key); + }); + + return ; +} diff --git a/website/src/repl/components/panel/SettingsTab.jsx b/website/src/repl/components/panel/SettingsTab.jsx index e1d047ea..69553da8 100644 --- a/website/src/repl/components/panel/SettingsTab.jsx +++ b/website/src/repl/components/panel/SettingsTab.jsx @@ -3,6 +3,8 @@ import { themes } from '@strudel/codemirror'; import { isUdels } from '../../util.mjs'; import { ButtonGroup } from './Forms.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; +import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; +import { isTauri } from '@src/tauri.mjs'; function Checkbox({ label, value, onChange, disabled = false }) { return ( @@ -78,6 +80,20 @@ const fontFamilyOptions = { galactico: 'galactico', }; +const RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; + +function confirmDialog(msg) { + // confirm dialog is a promise in Tauri and possibly other browsers... normalize it to be a promise everywhere + return new Promise(function (resolve, reject) { + let confirmed = confirm(msg); + if (confirmed instanceof Promise) { + confirmed.then((r) => (r ? resolve(true) : reject(false))); + } else { + return confirmed ? resolve(true) : reject(false); + } + }); +} + export function SettingsTab({ started }) { const { theme, @@ -96,11 +112,14 @@ export function SettingsTab({ started }) { fontFamily, panelPosition, audioDeviceName, + audioEngineTarget, } = useSettings(); const shouldAlwaysSync = isUdels(); + const inDesktopApp = isTauri(); + const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; return (
- {AudioContext.prototype.setSinkId != null && ( + {canChangeAudioDevice && ( )} + {inDesktopApp && ( + + { + confirmDialog(RELOAD_MSG).then((r) => { + if (r == true) { + settingsMap.setKey('audioEngineTarget', target); + return window.location.reload(); + } + }); + }} + /> + + )} settingsMap.setKey('theme', theme)} /> @@ -193,10 +227,13 @@ export function SettingsTab({ started }) { { - if (confirm('Changing this setting requires the window to reload itself. OK?')) { - settingsMap.setKey('isSyncEnabled', cbEvent.target.checked); - window.location.reload(); - } + const newVal = cbEvent.target.checked; + confirmDialog(RELOAD_MSG).then((r) => { + if (r) { + settingsMap.setKey('isSyncEnabled', newVal); + window.location.reload(); + } + }); }} disabled={shouldAlwaysSync} value={isSyncEnabled} diff --git a/website/src/settings.mjs b/website/src/settings.mjs index f9b9e281..def6754b 100644 --- a/website/src/settings.mjs +++ b/website/src/settings.mjs @@ -5,6 +5,11 @@ import { isUdels } from './repl/util.mjs'; export const defaultAudioDeviceName = 'System Standard'; +export const audioEngineTargets = { + webaudio: 'webaudio', + superdirt: 'superdirt', +}; + export const defaultSettings = { activeFooter: 'intro', keybindings: 'codemirror', @@ -28,6 +33,7 @@ export const defaultSettings = { panelPosition: 'right', userPatterns: '{}', audioDeviceName: defaultAudioDeviceName, + audioEngineTarget: audioEngineTargets.webaudio //webaudio | superdirt }; let search = null;