Skip to content

Commit

Permalink
Add options to specify default loop count and maximum duration.
Browse files Browse the repository at this point in the history
  • Loading branch information
okaxaki committed Dec 17, 2023
1 parent 4f59df7 commit f4422d4
Show file tree
Hide file tree
Showing 6 changed files with 160 additions and 22 deletions.
47 changes: 37 additions & 10 deletions src/contexts/PlayerContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { unmuteAudio } from "../utils/unmute";
import AppGlobal from "./AppGlobal";
import { PlayerContextReducer } from "./PlayerContextReducer";
import { AppProgressContext } from "./AppProgressContext";
import { KSSDecoderStartOptions } from "../kss/kss-decoder-worker";

export type PlayListEntry = {
title?: string | null;
Expand All @@ -32,6 +33,8 @@ export interface PlayerContextState {
currentEntry: PlayListEntry | null;
playState: "playing" | "paused" | "stopped";
playStateChangeCount: number;
defaultLoopCount: number;
defaultDuration: number;
channelMask: KSSChannelMask;
unmute: () => Promise<void>;
}
Expand Down Expand Up @@ -60,6 +63,8 @@ const createDefaultContextState = () => {
playStateChangeCount: 0,
playState: "stopped",
masterGain: 4.0,
defaultLoopCount: 2,
defaultDuration: 300 * 1000,
channelMask: {
psg: 0,
opl: 0,
Expand Down Expand Up @@ -88,6 +93,8 @@ const createDefaultContextState = () => {
state.masterGain = json.masterGain ?? state.masterGain;
state.gainNode.gain.value = state.masterGain;
state.repeatMode = json.repeatMode ?? state.repeatMode;
state.defaultLoopCount = json.defaultLoopCount ?? state.defaultLoopCount;
state.defaultDuration = json.defaultDuration ?? state.defaultDuration;
} catch (e) {
console.error(e);
localStorage.clear();
Expand Down Expand Up @@ -118,7 +125,16 @@ async function applyPlayStateChange(
const { channelMask } = state;
const { dataId, song, duration, fadeDuration } = entry;
const data = await state.storage.get(dataId);
await state.player.play({ channelMask, data, song, duration, fadeDuration });
const options: KSSDecoderStartOptions = {
channelMask,
data,
song,
duration,
fadeDuration,
loop: state.defaultLoopCount,
defaultDuration: state.defaultDuration,
};
await state.player.play(options);
};

if (state.playState == "playing") {
Expand Down Expand Up @@ -163,13 +179,10 @@ export function PlayerContextProvider(props: React.PropsWithChildren) {
const [initialized, setInitialized] = useState(false);

useEffect(() => {
console.log('attach');
console.log(window.opener);
window.addEventListener("message", onWindowMessage, false);
state.player.addEventListener("statechange", onPlayerStateChange);
initialize();
return () => {
console.log('detach');
window.removeEventListener("message", onWindowMessage, false);
state.player.removeEventListener("statechange", onPlayerStateChange);
};
Expand Down Expand Up @@ -214,14 +227,27 @@ export function PlayerContextProvider(props: React.PropsWithChildren) {
};

const save = () => {
const { channelMask, repeatMode, masterGain } = state;
const data = { version: 1, channelMask, masterGain, repeatMode };
const { defaultLoopCount, defaultDuration, channelMask, repeatMode, masterGain } = state;
const data = {
version: 1,
defaultLoopCount,
defaultDuration,
channelMask,
masterGain,
repeatMode,
};
localStorage.setItem("m3disp.playerContext", JSON.stringify(data));
};

useEffect(() => {
save();
}, [state.masterGain, state.channelMask, state.repeatMode]);
}, [
state.masterGain,
state.defaultLoopCount,
state.defaultDuration,
state.channelMask,
state.repeatMode,
]);

const saveEntries = (entries: PlayListEntry[]) => {
const data = JSON.stringify(entries);
Expand All @@ -238,11 +264,12 @@ export function PlayerContextProvider(props: React.PropsWithChildren) {
};

const onWindowMessage = async (ev: MessageEvent) => {
console.log(ev);
if (ev.data instanceof Uint8Array && ev.data.length <= 65536) {
reducer.clearEntries();
const file = new File([ev.data], 'external.mgs');
const entries = await loadEntriesFromFileList(state.storage, [new File([ev.data], file.name)]);
const file = new File([ev.data], "external.mgs");
const entries = await loadEntriesFromFileList(state.storage, [
new File([ev.data], file.name),
]);
reducer.addEntries(entries, 0);
reducer.resume();
reducer.play(0);
Expand Down
12 changes: 12 additions & 0 deletions src/contexts/PlayerContextReducer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,18 @@ export class PlayerContextReducer {
});
}

setDefaultLoopCount(value: number) {
this.setState((state) => {
return { ...state, defaultLoopCount: value };
});
}

setDefaultDuration(value: number) {
this.setState((state) => {
return { ...state, defaultDuration: value };
});
}

setChannelMask(channelMask: KSSChannelMask) {
this.setState((state) => {
if (
Expand Down
43 changes: 36 additions & 7 deletions src/contexts/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { KSSChannelMask } from "../kss/kss-device";
import { PlayerContext } from "./PlayerContext";

export type SettingsContextState = {
defaultLoopCount: number;
defaultDuration: number;
channelMask: KSSChannelMask;
setDefaultLoopCount: (value: number) => void;
setDefaultDuration: (value: number) => void;
setChannelMask: (channelMask: KSSChannelMask) => void;
commit: () => void;
revert: () => void;
Expand All @@ -14,7 +18,11 @@ const noop = () => {
};

const defaultContextState: SettingsContextState = {
defaultLoopCount: 2,
defaultDuration: 300 * 1000,
channelMask: { psg: 0, scc: 0, opll: 0, opl: 0 },
setDefaultLoopCount: noop,
setDefaultDuration: noop,
setChannelMask: noop,
commit: noop,
revert: noop,
Expand All @@ -25,19 +33,38 @@ export const SettingsContext = createContext(defaultContextState);
export function SettingsContextProvider(props: PropsWithChildren) {
const context = useContext(PlayerContext);

const setChannelMask = (channelMask: KSSChannelMask) => {
function setDefaultLoopCount(value: number) {
setState((oldState) => {
return { ...oldState, defaultLoopCount: value };
});
}

function setDefaultDuration(value: number) {
setState((oldState) => {
return { ...oldState, defaultDuration: value };
});
}

function setChannelMask(channelMask: KSSChannelMask) {
setState((oldState) => {
return { ...oldState, channelMask };
});
};
}

const commit = () => {
function commit() {
context.reducer.setDefaultLoopCount(state.defaultLoopCount);
context.reducer.setDefaultDuration(state.defaultDuration);
context.reducer.setChannelMask(state.channelMask);
};
}

const revert = () => {
setState((oldState) => ({ ...oldState, channelMask: { ...context.channelMask } }));
};
function revert() {
setState((oldState) => ({
...oldState,
defaultLoopCount: context.defaultLoopCount,
defaultDuration: context.defaultDuration,
channelMask: { ...context.channelMask },
}));
}

const [state, setState] = useState({
...defaultContextState,
Expand All @@ -48,6 +75,8 @@ export function SettingsContextProvider(props: PropsWithChildren) {
<SettingsContext.Provider
value={{
...state,
setDefaultLoopCount,
setDefaultDuration,
setChannelMask,
commit,
revert,
Expand Down
3 changes: 2 additions & 1 deletion src/kss/kss-decoder-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type KSSDecoderStartOptions = {
cpu?: number | null;
duration?: number | null;
fadeDuration?: number | null;
defaultDuration?: number| null;
rcf?: null | {
resistor: number;
capacitor: number;
Expand Down Expand Up @@ -84,7 +85,7 @@ class KSSDecoderWorker extends AudioDecoderWorker {
this._kssplay.setSilentLimit(15 * 1000);

this._fadeDuration = args.fadeDuration ?? defaultFadeDuration;
this._duration = args.duration ?? defaultDuration;
this._duration = args.duration ?? ((args.defaultDuration ?? defaultDuration) - this._fadeDuration);
this._hasDebugMarker = args.debug ?? false;
this._maxLoop = args.loop ?? defaultLoop;
this._decodeFrames = 0;
Expand Down
35 changes: 31 additions & 4 deletions src/views/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import React, { Fragment, useContext, useState } from "react";
import { AppContext, KeyHighlightColorType } from "../contexts/AppContext";
import { SettingsContext, SettingsContextProvider } from "../contexts/SettingsContext";
import { ColorBall, ColorSelector } from "../widgets/ColorSelector";
import { NumberSelector } from "../widgets/NumberSelector";

interface TabPanelProps {
children?: React.ReactNode;
Expand Down Expand Up @@ -187,6 +188,30 @@ function ColorPanel(props: TabPanelProps) {
);
}

function PlayerPanel(props: TabPanelProps) {
const context = useContext(SettingsContext);

return (
<TabPanel value={props.value} index={props.index}>
<Box sx={{ px: { sm: 2 } }}>
<NumberSelector
label="Loop Count"
values={[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}
value={context.defaultLoopCount}
onChange={context.setDefaultLoopCount}
/>
<NumberSelector
label="Maximum Duration"
values={[120 * 1000, 180 * 1000, 300 * 1000, 600 * 1000, 900 * 1000, 1200 * 1000]}
value={context.defaultDuration}
valueLabelFn={(value) => `${(value / 60 / 1000).toFixed(0)} min.`}
onChange={context.setDefaultDuration}
/>
</Box>
</TabPanel>
);
}

function KeyColorTypeSelector(props: { value: KeyHighlightColorType }) {
const theme = useTheme();
const app = useContext(AppContext);
Expand Down Expand Up @@ -265,19 +290,21 @@ function SettingsDialogBody(props: { id: string }) {
sx={{
minWidth: "300px",
width: { sm: "480px" },
height: { xs: "480px", sm: "480px" },
p: 0,
height: { xs: "480px", sm: "520px" },
p: 1,
backgroundColor: "background.paper",
}}
>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs value={value} onChange={handleChange} variant="fullWidth">
<Tab label="Player" />
<Tab label="Theme" />
<Tab label="Channels" />
</Tabs>
</Box>
<ColorPanel value={value} index={0} />
<MaskPanel value={value} index={1} />
<PlayerPanel value={value} index={0} />
<ColorPanel value={value} index={1} />
<MaskPanel value={value} index={2} />
</DialogContent>
<DialogActions sx={{ backgroundColor: "background.paper" }}>
<Button onClick={onCancel}>Cancel</Button>
Expand Down
42 changes: 42 additions & 0 deletions src/widgets/NumberSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Box, ListSubheader, MenuItem, Select, SelectChangeEvent, Stack } from "@mui/material";

export function NumberSelector(props: {
label: string;
values: number[];
value: number;
valueLabelFn?: (value: number) => string;
onChange: (value: number) => void;
}) {
const { value, values, onChange } = props;
function valueToLabel(value: number) {
return props.valueLabelFn?.(value) ?? value.toString();
}
const items = [];
for (const i in values) {
items.push(
<MenuItem key={i} value={values[i]}>
{valueToLabel(values[i])}
</MenuItem>
);
}
return (
<Stack>
<ListSubheader>{props.label}</ListSubheader>
<Box sx={{ mx: 2 }}>
<Select
fullWidth
size="small"
value={value}
onChange={(e) => {
onChange(e.target.value as number);
}}
renderValue={(value) => {
return <MenuItem sx={{ p: 0 }}>{valueToLabel(value)}</MenuItem>;
}}
>
{items}
</Select>
</Box>
</Stack>
);
}

0 comments on commit f4422d4

Please sign in to comment.