From a45214ed3b0537fd88de49dbdd3cbab41a794f2e Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Fri, 8 Nov 2024 18:44:23 -0800 Subject: [PATCH 01/19] improve timer readout --- src/components/Timeline/TimerAndWaveform.tsx | 5 +++-- src/components/Timeline/TimerReadout.tsx | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/Timeline/TimerAndWaveform.tsx b/src/components/Timeline/TimerAndWaveform.tsx index 20aa5870..a7d70d74 100644 --- a/src/components/Timeline/TimerAndWaveform.tsx +++ b/src/components/Timeline/TimerAndWaveform.tsx @@ -8,7 +8,7 @@ import { TimerControls } from "@/src/components/Timeline/TimerControls"; export const TimerAndWaveform = observer(function TimerAndWaveform() { const store = useStore(); - const { uiStore, embeddedViewer } = store; + const { uiStore } = store; const width = uiStore.canTimelineZoom ? uiStore.timeToXPixels(MAX_TIME) @@ -33,10 +33,11 @@ export const TimerAndWaveform = observer(function TimerAndWaveform() { borderColor="black" spacing={0} width="150px" + height="80px" zIndex={18} bgColor="gray.500" > - {!embeddedViewer && } + diff --git a/src/components/Timeline/TimerReadout.tsx b/src/components/Timeline/TimerReadout.tsx index ea5cfef3..c663e3f7 100644 --- a/src/components/Timeline/TimerReadout.tsx +++ b/src/components/Timeline/TimerReadout.tsx @@ -15,7 +15,16 @@ export const TimerReadout = observer(function TimerReadout() { const { audioStore } = useStore(); return ( - + Date: Sun, 10 Nov 2024 01:52:13 -0800 Subject: [PATCH 02/19] basic smoothing implementation --- .../NewVariationButtons.tsx | 2 +- .../AudioVariationControls.tsx | 10 +++++++++ src/types/AudioStore.ts | 14 +++++++++++++ src/types/Block.ts | 2 +- src/types/Variations/AudioVariation.ts | 21 ++++++++++++++++--- src/types/Variations/Variation.ts | 2 +- 6 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/components/ParameterVariations/NewVariationButtons.tsx b/src/components/ParameterVariations/NewVariationButtons.tsx index 73d9b624..eb98b355 100644 --- a/src/components/ParameterVariations/NewVariationButtons.tsx +++ b/src/components/ParameterVariations/NewVariationButtons.tsx @@ -246,7 +246,7 @@ export const NewVariationButtons = memo(function NewVariationButtons({ store.addVariation( block, uniformName, - new AudioVariation(DEFAULT_VARIATION_DURATION, 1, 0, store) + new AudioVariation(DEFAULT_VARIATION_DURATION, 1, 0, 0, store) ); })} /> diff --git a/src/components/VariationControls/AudioVariationControls.tsx b/src/components/VariationControls/AudioVariationControls.tsx index 2c0d18c4..75181e7a 100644 --- a/src/components/VariationControls/AudioVariationControls.tsx +++ b/src/components/VariationControls/AudioVariationControls.tsx @@ -16,6 +16,7 @@ export function AudioVariationControls({ }: AudioVariationControlsProps) { const [factor, setFactor] = useState(variation.factor.toString()); const [offset, setOffset] = useState(variation.offset.toString()); + const [smoothing, setSmoothing] = useState(variation.smoothing.toString()); return ( <> @@ -37,6 +38,15 @@ export function AudioVariationControls({ }} value={offset} /> + { + variation.smoothing = valueNumber; + setSmoothing(valueString); + block.triggerVariationReactions(uniformName); + }} + value={smoothing} + /> ); } diff --git a/src/types/AudioStore.ts b/src/types/AudioStore.ts index a0cf1c56..d0e958f9 100644 --- a/src/types/AudioStore.ts +++ b/src/types/AudioStore.ts @@ -65,6 +65,20 @@ export class AudioStore { return this.peaks[index]; }; + getSmoothedPeakAtTime = (time: number, smoothing: number) => { + if (!this.peaks.length) return 0; + if (smoothing === 0) return this.getPeakAtTime(time); + + const index = Math.floor(time * PEAK_DATA_SAMPLE_RATE); + const start = Math.max(0, index - smoothing); + const end = Math.min(this.peaks.length - 1, index + smoothing); + + let total = 0; + for (let i = start; i <= end; i++) total += this.peaks[i]; + + return total / (end - start + 1); + }; + toggleAudioMuted = () => { this.audioMuted = !this.audioMuted; }; diff --git a/src/types/Block.ts b/src/types/Block.ts index 8110b1c2..19959c93 100644 --- a/src/types/Block.ts +++ b/src/types/Block.ts @@ -30,7 +30,7 @@ export type SerializedBlock = { export type RootStore = { context: Context; audioStore: { - getPeakAtTime: (time: number) => number; + getSmoothedPeakAtTime: (time: number, smoothing: number) => number; }; }; diff --git a/src/types/Variations/AudioVariation.ts b/src/types/Variations/AudioVariation.ts index 57eb265a..f5c677fc 100644 --- a/src/types/Variations/AudioVariation.ts +++ b/src/types/Variations/AudioVariation.ts @@ -4,21 +4,24 @@ export class AudioVariation extends Variation { displayName = "Audio"; factor: number; offset: number; + smoothing: number; constructor( duration: number, factor: number, offset: number, + smoothing: number, readonly store: RootStore ) { super("audio", duration); this.factor = factor; this.offset = offset; + this.smoothing = smoothing ?? 0; } valueAtTime = (time: number, globalTime: number) => - this.factor * this.store.audioStore.getPeakAtTime(globalTime) + this.offset; + this.factor * this.store.audioStore.getSmoothedPeakAtTime(globalTime,this.smoothing) + this.offset; // TODO: computeDomain = () => [0, 1] as [number, number]; @@ -39,7 +42,13 @@ export class AudioVariation extends Variation { }; clone = () => - new AudioVariation(this.duration, this.factor, this.offset, this.store); + new AudioVariation( + this.duration, + this.factor, + this.offset, + this.smoothing, + this.store + ); serialize = () => ({ type: this.type, @@ -49,5 +58,11 @@ export class AudioVariation extends Variation { }); static deserialize = (store: RootStore, data: any) => - new AudioVariation(data.duration, data.factor, data.offset, store); + new AudioVariation( + data.duration, + data.factor, + data.offset, + data.smoothing, + store + ); } diff --git a/src/types/Variations/Variation.ts b/src/types/Variations/Variation.ts index c649aa6e..9b53e7f4 100644 --- a/src/types/Variations/Variation.ts +++ b/src/types/Variations/Variation.ts @@ -2,7 +2,7 @@ import { ParamType } from "@/src/types/PatternParams"; import { generateId } from "@/src/utils/id"; export type RootStore = { - audioStore: { getPeakAtTime: (time: number) => number }; + audioStore: { getSmoothedPeakAtTime: (time: number,smoothing: number) => number }; }; type VariationType = From 77f1597ee8f1d616fc104def3ffa9c3da28cbec0 Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Sun, 10 Nov 2024 03:09:03 -0800 Subject: [PATCH 03/19] latency test work in progress --- public/kick.mp3 | Bin 0 -> 15716 bytes src/components/LatencyTest.tsx | 101 +++++++++++++++++++++++++++++++++ src/pages/test.tsx | 4 +- 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 public/kick.mp3 create mode 100644 src/components/LatencyTest.tsx diff --git a/public/kick.mp3 b/public/kick.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..f4b56f89551bb878d36428021638bd9972585904 GIT binary patch literal 15716 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 Q6VS|#Sg6W^eQsO;pP{n?YO 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 XZy)~yGVlWwes{wEfLi=Fhwy&@FL21S literal 0 HcmV?d00001 diff --git a/src/components/LatencyTest.tsx b/src/components/LatencyTest.tsx new file mode 100644 index 00000000..e50ad9e9 --- /dev/null +++ b/src/components/LatencyTest.tsx @@ -0,0 +1,101 @@ +import { Button, Text } from "@chakra-ui/react"; +import { useEffect, useMemo, useRef, useState } from "react"; + +// NOTE: +// 60,000 / BPM = [one beat in ms] +// BPM = 60000 / [one beat in ms] +const MS_IN_M = 60_000; +const tempo = 120; // 120 BPM + +const MIN_BEATS = 10; + +async function loadSound(audioContext: AudioContext) { + let response = await fetch("./kick.mp3", { mode: "no-cors" }); + let arrayBuffer = await response.arrayBuffer(); + let audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + return audioBuffer; +} + +function playSound(audioContext: AudioContext, audioBuffer: AudioBuffer) { + const player = audioContext.createBufferSource(); + player.buffer = audioBuffer; + player.connect(audioContext.destination); + player.loop = false; + player.start(); +} + +export function LatencyTest() { + const initialized = useRef(false); + const audioContext = useRef(null); + const audioBuffer = useRef(null); + + const [isRunning, setIsRunning] = useState(false); + const [beatTimes, setBeatTimes] = useState([]); + const [userTimes, setUserTimes] = useState([]); + + useEffect(() => { + if (initialized.current) return; + initialized.current = true; + + const initialize = async () => { + audioContext.current = new AudioContext(); + audioBuffer.current = await loadSound(audioContext.current); + }; + + initialize(); + }, [initialized]); + + const userTempo = useMemo(() => { + if (userTimes.length < 2) return 0; + + const userTimesMs = userTimes.map((time, index) => { + if (index === 0) return 0; + return time.getTime() - userTimes[index - 1].getTime(); + }); + + const average = + userTimesMs.reduce((acc, curr) => acc + curr, 0) / userTimesMs.length; + return MS_IN_M / average; + }, [userTimes]); + + useEffect(() => { + if (!isRunning) return; + + let currentBpmInMs = MS_IN_M / tempo; + const interval = setInterval(() => { + if (!audioContext.current || !audioBuffer.current) return; + playSound(audioContext.current, audioBuffer.current); + setBeatTimes((beatTimes) => [...beatTimes, new Date()]); + }, currentBpmInMs); + + return () => clearInterval(interval); + }, [isRunning]); + + return ( + <> + Latency Test + + Tempo: {tempo.toFixed(2)} + User tempo: {userTempo} + + ); +} diff --git a/src/pages/test.tsx b/src/pages/test.tsx index 5ab74b22..cb2eb60c 100644 --- a/src/pages/test.tsx +++ b/src/pages/test.tsx @@ -1,3 +1,4 @@ +import { LatencyTest } from "@/src/components/LatencyTest"; import { Store } from "@/src/types/Store"; import { StoreContext } from "@/src/types/StoreContext"; import { Box, ChakraProvider, theme } from "@chakra-ui/react"; @@ -25,7 +26,8 @@ export default function Test() {

Embedded conjurer test:

- {initialized && } + {/* {initialized && } */} +
From cc2fbbac656f1a3cb376151f9fe10a2671124e8f Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Sun, 10 Nov 2024 22:23:35 -0800 Subject: [PATCH 04/19] create a latency test --- src/components/LatencyTest.tsx | 134 +++++++++++++++++++++++---------- src/pages/test.tsx | 4 +- 2 files changed, 97 insertions(+), 41 deletions(-) diff --git a/src/components/LatencyTest.tsx b/src/components/LatencyTest.tsx index e50ad9e9..71acc0d3 100644 --- a/src/components/LatencyTest.tsx +++ b/src/components/LatencyTest.tsx @@ -1,4 +1,4 @@ -import { Button, Text } from "@chakra-ui/react"; +import { Button, HStack, Text, VStack } from "@chakra-ui/react"; import { useEffect, useMemo, useRef, useState } from "react"; // NOTE: @@ -24,10 +24,32 @@ function playSound(audioContext: AudioContext, audioBuffer: AudioBuffer) { player.start(); } -export function LatencyTest() { +// Source: https://gist.github.com/AlexJWayne/1d99b3cd81d610ac7351 +const accurateInterval = (time: number, fn: () => void) => { + let cancel: any, nextAt: any, timeout: any, wrapper: any, _ref: any; + nextAt = new Date().getTime() + time; + timeout = null; + if (typeof time === "function") + (_ref = [time, fn]), (fn = _ref[0]), (time = _ref[1]); + wrapper = () => { + nextAt += time; + timeout = setTimeout(wrapper, nextAt - new Date().getTime()); + return fn(); + }; + cancel = () => clearTimeout(timeout); + timeout = setTimeout(wrapper, nextAt - new Date().getTime()); + return { cancel }; +}; + +export function LatencyTest({ + setLatency, +}: { + setLatency: (latency: number) => void; +}) { const initialized = useRef(false); const audioContext = useRef(null); const audioBuffer = useRef(null); + const lastLatency = useRef(0); const [isRunning, setIsRunning] = useState(false); const [beatTimes, setBeatTimes] = useState([]); @@ -45,57 +67,93 @@ export function LatencyTest() { initialize(); }, [initialized]); - const userTempo = useMemo(() => { + // const userTempo = useMemo(() => { + // if (userTimes.length < 2) return 0; + + // const userTimesMs = userTimes + // .map((time, index) => { + // if (index === 0) return null; + // return time.getTime() - userTimes[index - 1].getTime(); + // }) + // .filter((time) => time !== null); + + // const average = + // userTimesMs.reduce((acc, curr) => acc + curr, 0) / userTimesMs.length; + // return MS_IN_M / average; + // }, [userTimes]); + + const userLatency = useMemo(() => { if (userTimes.length < 2) return 0; + if (beatTimes.length > userTimes.length) return lastLatency.current; - const userTimesMs = userTimes.map((time, index) => { - if (index === 0) return 0; - return time.getTime() - userTimes[index - 1].getTime(); - }); + const userLatencies = beatTimes + .map((beatTime, index) => { + if (index >= userTimes.length) return null; + return userTimes[index].getTime() - beatTime.getTime(); + }) + .filter((time) => time !== null); - const average = - userTimesMs.reduce((acc, curr) => acc + curr, 0) / userTimesMs.length; - return MS_IN_M / average; - }, [userTimes]); + lastLatency.current = + userLatencies.reduce((acc, curr) => acc + curr, 0) / userLatencies.length; + + return lastLatency.current; + }, [beatTimes, userTimes]); useEffect(() => { if (!isRunning) return; let currentBpmInMs = MS_IN_M / tempo; - const interval = setInterval(() => { + const { cancel } = accurateInterval(currentBpmInMs, () => { if (!audioContext.current || !audioBuffer.current) return; playSound(audioContext.current, audioBuffer.current); setBeatTimes((beatTimes) => [...beatTimes, new Date()]); - }, currentBpmInMs); + }); - return () => clearInterval(interval); + return () => cancel(); }, [isRunning]); return ( - <> - Latency Test - - Tempo: {tempo.toFixed(2)} - User tempo: {userTempo} - + + setIsRunning(true); + setBeatTimes([]); + setUserTimes([]); + }} + > + Tap + + + + +
); } diff --git a/src/pages/test.tsx b/src/pages/test.tsx index cb2eb60c..5ab74b22 100644 --- a/src/pages/test.tsx +++ b/src/pages/test.tsx @@ -1,4 +1,3 @@ -import { LatencyTest } from "@/src/components/LatencyTest"; import { Store } from "@/src/types/Store"; import { StoreContext } from "@/src/types/StoreContext"; import { Box, ChakraProvider, theme } from "@chakra-ui/react"; @@ -26,8 +25,7 @@ export default function Test() {

Embedded conjurer test:

- {/* {initialized && } */} - + {initialized && }
From ec4974cbc4fc1c936602597ddddab936a09f469e Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Sun, 10 Nov 2024 22:39:37 -0800 Subject: [PATCH 05/19] implement LatencyModal --- src/components/LatencyModal/LatencyModal.tsx | 38 +++++++++++++++++++ .../{ => LatencyModal}/LatencyTest.tsx | 0 src/components/Menu/MenuBar.tsx | 7 ++++ src/types/UIStore.ts | 1 + src/utils/timeout.ts | 5 +++ 5 files changed, 51 insertions(+) create mode 100644 src/components/LatencyModal/LatencyModal.tsx rename src/components/{ => LatencyModal}/LatencyTest.tsx (100%) create mode 100644 src/utils/timeout.ts diff --git a/src/components/LatencyModal/LatencyModal.tsx b/src/components/LatencyModal/LatencyModal.tsx new file mode 100644 index 00000000..b39da796 --- /dev/null +++ b/src/components/LatencyModal/LatencyModal.tsx @@ -0,0 +1,38 @@ +import { observer } from "mobx-react-lite"; +import { + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, +} from "@chakra-ui/react"; +import { useStore } from "@/src/types/StoreContext"; +import { LatencyTest } from "@/src/components/LatencyModal/LatencyTest"; +import { action } from "mobx"; + +export const LatencyModal = observer(function LatencyModal() { + const store = useStore(); + const { audioStore, uiStore } = store; + + const isOpen = uiStore.showingLatencyModal; + const onClose = action(() => (uiStore.showingLatencyModal = false)); + + return ( + + + + Set audio latency + + { + audioStore.audioLatency = latency; + onClose(); + })} + /> + + + + + ); +}); diff --git a/src/components/LatencyTest.tsx b/src/components/LatencyModal/LatencyTest.tsx similarity index 100% rename from src/components/LatencyTest.tsx rename to src/components/LatencyModal/LatencyTest.tsx diff --git a/src/components/Menu/MenuBar.tsx b/src/components/Menu/MenuBar.tsx index 1a8bb189..cbd60084 100644 --- a/src/components/Menu/MenuBar.tsx +++ b/src/components/Menu/MenuBar.tsx @@ -30,6 +30,7 @@ import { KeyboardShortcuts } from "@/src/components/KeyboardShortcuts"; import { useSaveExperience } from "@/src/hooks/experience"; import { DisplayMode } from "@/src/types/UIStore"; import { action } from "mobx"; +import { LatencyModal } from "@/src/components/LatencyModal/LatencyModal"; export const MenuBar = observer(function MenuBar() { const store = useStore(); @@ -81,6 +82,7 @@ export const MenuBar = observer(function MenuBar() { + Transmit data to canopy + (uiStore.showingLatencyModal = true))} + > + Set audio latency + )} diff --git a/src/types/UIStore.ts b/src/types/UIStore.ts index cf3a0ff7..9a070390 100644 --- a/src/types/UIStore.ts +++ b/src/types/UIStore.ts @@ -37,6 +37,7 @@ export class UIStore { showingViewerInstructionsModal = false; showingSaveBeatMapModal = false; showingLoadBeatMapModal = false; + showingLatencyModal = false; pendingAction: "open" | "save" | "" = ""; diff --git a/src/utils/timeout.ts b/src/utils/timeout.ts new file mode 100644 index 00000000..293a7e44 --- /dev/null +++ b/src/utils/timeout.ts @@ -0,0 +1,5 @@ +export function timeout(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} From 966e61fda0f96fc06c5dc0094d96e215b3b2f08f Mon Sep 17 00:00:00 2001 From: Ben Rollin Date: Sun, 10 Nov 2024 23:01:50 -0800 Subject: [PATCH 06/19] respond to audio latency changes --- src/components/LatencyModal/LatencyTest.tsx | 2 +- .../Wavesurfer/WavesurferWaveform.tsx | 36 ++++++++----------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/LatencyModal/LatencyTest.tsx b/src/components/LatencyModal/LatencyTest.tsx index 71acc0d3..fbadbb11 100644 --- a/src/components/LatencyModal/LatencyTest.tsx +++ b/src/components/LatencyModal/LatencyTest.tsx @@ -148,7 +148,7 @@ export function LatencyTest({ Stop