Skip to content

Commit

Permalink
Merge pull request #313 from SotSF/audio-variation-improvement
Browse files Browse the repository at this point in the history
Audio variation improvement and miscellaneous improvement
  • Loading branch information
brollin authored Nov 12, 2024
2 parents 620c6d0 + a470643 commit f050ede
Show file tree
Hide file tree
Showing 26 changed files with 421 additions and 110 deletions.
Binary file added public/kick.mp3
Binary file not shown.
12 changes: 7 additions & 5 deletions src/components/AddPatternButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ export const AddPatternButton = observer(function AddPatternButton() {
const { uiStore } = store;

return (
<Box position="absolute" bottom={0} right={0} m={4}>
<Box position="absolute" bottom={0} right={0} m={6}>
<Button
variant="solid"
bgColor="gray.600"
fontWeight="bold"
borderRadius="50%"
bgColor="orange.500"
_hover={{ backgroundColor: "orange.400" }}
size="md"
borderRadius={"full"}
onClick={action(() => {
store.pause();
uiStore.patternDrawerOpen = true;
})}
zIndex={100}
leftIcon={<AiOutlinePlus size={18} />}
>
<AiOutlinePlus />
Pattern
</Button>
</Box>
);
Expand Down
4 changes: 2 additions & 2 deletions src/components/CameraControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ type CameraControlsProps = {};

export const CameraControls = observer(
function CameraControls({}: CameraControlsProps) {
const { embeddedViewer } = useStore();
const { viewerMode } = useStore();

const cameraRef = useRef<PerspectiveCameraThree>(null);
const initialPositionRef = useRef(new Vector3(0, 0, 20));

useTravelingCamera(cameraRef, embeddedViewer);
useTravelingCamera(cameraRef, viewerMode);

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Display.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RoleSelector } from "@/src/components/RoleSelector";

export const Display = observer(function Display() {
const store = useStore();
const { uiStore, embeddedViewer } = store;
const { uiStore, viewerMode } = store;

const boxRef = useRef<HTMLDivElement>(null);

Expand All @@ -23,7 +23,7 @@ export const Display = observer(function Display() {
position="relative"
height="100%"
>
{!embeddedViewer && (
{!viewerMode && (
<Box transition="all 100ms">
<MenuBar />
<VStack position="absolute" width="100%" marginY="2" zIndex={1}>
Expand Down
38 changes: 38 additions & 0 deletions src/components/LatencyModal/LatencyModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Modal onClose={onClose} isOpen={isOpen} isCentered size="md">
<ModalOverlay />
<ModalContent>
<ModalHeader>Set audio latency</ModalHeader>
<ModalBody>
<LatencyTest
setLatency={action((latency) => {
audioStore.audioLatency = latency;
onClose();
})}
/>
</ModalBody>
<ModalCloseButton />
</ModalContent>
</Modal>
);
});
159 changes: 159 additions & 0 deletions src/components/LatencyModal/LatencyTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Button, HStack, Text, VStack } 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();
}

// 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<AudioContext | null>(null);
const audioBuffer = useRef<AudioBuffer | null>(null);
const lastLatency = useRef(0);

const [isRunning, setIsRunning] = useState(false);
const [beatTimes, setBeatTimes] = useState<Date[]>([]);
const [userTimes, setUserTimes] = useState<Date[]>([]);

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 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 userLatencies = beatTimes
.map((beatTime, index) => {
if (index >= userTimes.length) return null;
return userTimes[index].getTime() - beatTime.getTime();
})
.filter((time) => time !== null);

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 { cancel } = accurateInterval(currentBpmInMs, () => {
if (!audioContext.current || !audioBuffer.current) return;
playSound(audioContext.current, audioBuffer.current);
setBeatTimes((beatTimes) => [...beatTimes, new Date()]);
});

return () => cancel();
}, [isRunning]);

return (
<VStack>
<Text>
Keep clicking Tap until latency stabilizes, then click Stop. When
satisfied, click Use latency.
</Text>
<Text h={8}>
{userLatency ? `Latency: ${userLatency.toFixed(2)}ms` : ""}
</Text>
<HStack>
<Button
onClick={async () => {
if (!audioContext.current || !audioBuffer.current) return;

if (isRunning) {
if (userTimes.length > MIN_BEATS) {
setUserTimes((userTimes) => [
...userTimes.slice(1),
new Date(),
]);
setBeatTimes((beatTimes) => beatTimes.slice(1));
} else setUserTimes((userTimes) => [...userTimes, new Date()]);
return;
}

setIsRunning(true);
setBeatTimes([]);
setUserTimes([]);
}}
>
Tap
</Button>
<Button onClick={() => setIsRunning(false)} isDisabled={!isRunning}>
Stop
</Button>
<Button
onClick={() => setLatency(userLatency / 1000)}
isDisabled={isRunning || userLatency === 0}
>
Use latency
</Button>
</HStack>
</VStack>
);
}
7 changes: 7 additions & 0 deletions src/components/Menu/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -81,6 +82,7 @@ export const MenuBar = observer(function MenuBar() {
</ModalFooter>
</ModalContent>
</Modal>
<LatencyModal />
<HStack>
<Heading
size="md"
Expand Down Expand Up @@ -308,6 +310,11 @@ export const MenuBar = observer(function MenuBar() {
>
Transmit data to canopy
</MenuItemOption>
<MenuItem
onClick={action(() => (uiStore.showingLatencyModal = true))}
>
Set audio latency
</MenuItem>
</MenuList>
</Menu>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/ParameterVariations/NewVariationButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
})}
/>
Expand Down
24 changes: 20 additions & 4 deletions src/components/PatternPlayground/ParameterControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,35 @@ export const ParameterControl = memo(function ParameterControl({
let parameterControl = null;
if (isBooleanParam(patternParam))
parameterControl = (
<BooleanParameterControl {...props} patternParam={patternParam} />
<BooleanParameterControl
{...props}
key={props.key}
patternParam={patternParam}
/>
);
else if (isNumberParam(patternParam))
parameterControl = (
<ScalarParameterControl {...props} patternParam={patternParam} />
<ScalarParameterControl
{...props}
key={props.key}
patternParam={patternParam}
/>
);
else if (isVector4Param(patternParam))
parameterControl = (
<ColorParameterControl {...props} patternParam={patternParam} />
<ColorParameterControl
{...props}
key={props.key}
patternParam={patternParam}
/>
);
else if (isPaletteParam(patternParam))
parameterControl = (
<PaletteParameterControl {...props} patternParam={patternParam} />
<PaletteParameterControl
{...props}
key={props.key}
patternParam={patternParam}
/>
);

return (
Expand Down
4 changes: 2 additions & 2 deletions src/components/PlaylistEditor/PlaylistEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ export const PlaylistEditor = observer(function PlaylistEditor() {
// this is very hacky and I hate it but it works
if (data?.playlist) playlistStore.selectedPlaylist = data.playlist;
});
}, [data?.playlist]);
}, [playlistStore, data?.playlist]);

useEffect(() => {
if (!data?.experiencesAndUsers.length || store.experienceName) return;
// once experiences are fetched, load the first experience in the playlist
store.experienceStore.load(data.experiencesAndUsers[0].experience.name);
}, [data?.experiencesAndUsers]);
}, [store.experienceName, store.experienceStore, data?.experiencesAndUsers]);

const router = useRouter();

Expand Down
8 changes: 8 additions & 0 deletions src/components/RoleSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export const RoleSelector = observer(function RoleSelector() {
>
Experience creator
</MenuItem>
<MenuItem
onClick={action(() => {
store.role = "vj";
router.push("/playground");
})}
>
VJ
</MenuItem>
</MenuList>
</Menu>
);
Expand Down
5 changes: 3 additions & 2 deletions src/components/Timeline/TimerAndWaveform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -33,10 +33,11 @@ export const TimerAndWaveform = observer(function TimerAndWaveform() {
borderColor="black"
spacing={0}
width="150px"
height="80px"
zIndex={18}
bgColor="gray.500"
>
{!embeddedViewer && <TimerReadout />}
<TimerReadout />
<TimerControls />
</VStack>

Expand Down
Loading

0 comments on commit f050ede

Please sign in to comment.