Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions mobile/src/modules/audio/_components/AudioEffectSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { View } from "react-native";

import type { Icon } from "~/resources/icons/type";

import { CachedSlider } from "~/components/Form/Slider";
import { Em } from "~/components/Typography/StyledText";

interface Props extends Omit<
React.ComponentProps<typeof CachedSlider>,
"_className"
> {
displayedValue: string;
/** Optional icon that appears before the `displayedValue`. */
Icon?: (props: Icon) => React.JSX.Element;
}

export function AudioEffectSlider({ displayedValue, Icon, ...props }: Props) {
return (
<View className="flex-row items-center gap-2">
<CachedSlider
hitSlop={10}
trackColor="surfaceContainer"
roundedEndStop
_debounceMultiplier={1}
_className="shrink grow"
{...props}
/>
<View className="w-14 flex-row items-center justify-center gap-2">
{Icon ? <Icon size={20} /> : null}
<Em style={{ fontVariant: ["tabular-nums"] }}>{displayedValue}</Em>
</View>
</View>
);
}
99 changes: 45 additions & 54 deletions mobile/src/modules/audio/_components/PlaybackParameterSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo } from "react";
import { memo, useCallback, useMemo } from "react";
import { View } from "react-native";
import { useSharedValue } from "react-native-reanimated";

Expand All @@ -7,75 +7,66 @@ import { sessionStore, useSessionStore } from "~/stores/Session/store";

import { capitalize } from "~/utils/string";
import { Button } from "~/components/Form/Button";
import { CachedSlider } from "~/components/Form/Slider";
import { SegmentedList } from "~/components/List/Segmented";
import { Em, TStyledText } from "~/components/Typography/StyledText";
import { AudioEffectSlider } from "./AudioEffectSlider";

const PRESET_OPTIONS = [1, 1.25, 1.5, 2] as const;

export function PlaybackParameterSlider(props: {
field: "pitch" | "speed";
onUpdate: (value: number) => void;
Icon: (props: Icon) => React.JSX.Element;
}) {
const fieldName = `playback${capitalize(props.field)}` as const;
const fieldNameKey = `feat.playback.extra.${props.field}` as const;
export const PlaybackParameterSlider = memo(
function PlaybackParameterSlider(props: {
field: "pitch" | "speed";
onUpdate: (value: number) => void;
Icon: (props: Icon) => React.JSX.Element;
}) {
const fieldName = `playback${capitalize(props.field)}` as const;
const fieldNameKey = `feat.playback.extra.${props.field}` as const;

const storedValue = useSessionStore((s) => s[fieldName]);
const cachedValue = useSharedValue(storedValue);
const storedValue = useSessionStore((s) => s[fieldName]);
const cachedValue = useSharedValue(storedValue);

const setField = useCallback(
(value: number) => {
sessionStore.setState({ [fieldName]: value });
props.onUpdate(value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.onUpdate, fieldName],
);
const setField = useCallback(
(value: number) => {
sessionStore.setState({ [fieldName]: value });
props.onUpdate(value);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[props.onUpdate, fieldName],
);

const PresetButtons = useMemo(() => {
return PRESET_OPTIONS.map((preset) => (
<Button
key={preset}
onPress={() => {
setField(preset);
cachedValue.set(preset);
}}
className="min-h-8 flex-1 rounded-full bg-surfaceContainerLow py-2 active:bg-surfaceContainer"
>
<Em>{formatValue(preset)}</Em>
</Button>
));
}, [cachedValue, setField]);
const PresetButtons = useMemo(() => {
return PRESET_OPTIONS.map((preset) => (
<Button
key={preset}
onPress={() => {
setField(preset);
cachedValue.set(preset);
}}
className="min-h-8 flex-1 rounded-full bg-surfaceContainerLow py-2 active:bg-surfaceContainer"
>
<Em>{formatValue(preset)}</Em>
</Button>
));
}, [cachedValue, setField]);

return (
<SegmentedList.CustomItem className="gap-4 p-4">
<TStyledText textKey={fieldNameKey} className="text-sm" />
<View className="flex-row items-center gap-2">
<CachedSlider
return (
<SegmentedList.CustomItem className="gap-4 p-4">
<TStyledText textKey={fieldNameKey} className="text-sm" />
<AudioEffectSlider
initValue={storedValue}
liveValue={cachedValue}
min={0.25}
max={2}
step={0.05}
onChange={setField}
hitSlop={10}
trackColor="surfaceContainer"
roundedEndStop
_debounceMultiplier={1}
_className="shrink grow"
Icon={props.Icon}
displayedValue={`${numberFormatter.format(storedValue)}x`}
/>
<View className="w-14 flex-row items-center justify-center gap-2">
{<props.Icon size={20} />}
<Em style={{ fontVariant: ["tabular-nums"] }}>
{numberFormatter.format(storedValue)}x
</Em>
</View>
</View>
<View className="flex-row items-center gap-4">{PresetButtons}</View>
</SegmentedList.CustomItem>
);
}
<View className="flex-row items-center gap-4">{PresetButtons}</View>
</SegmentedList.CustomItem>
);
},
);

//#region Helpers
const numberFormatter = new Intl.NumberFormat("en-US", {
Expand Down
59 changes: 59 additions & 0 deletions mobile/src/modules/audio/_components/VolumeSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useState } from "react";
import AudioBrowser from "react-native-audio-browser";
import { useAnimatedReaction, useSharedValue } from "react-native-reanimated";
import { scheduleOnRN } from "react-native-worklets";

import { VolumeUp } from "~/resources/icons/VolumeUp";
import { playbackStore, usePlaybackStore } from "~/stores/Playback/store";

import { SegmentedList } from "~/components/List/Segmented";
import { TStyledText } from "~/components/Typography/StyledText";
import { Switch } from "~/components/UI/Switch";
import { AudioEffectSlider } from "./AudioEffectSlider";

export function VolumeSettings() {
const restoreVolume = usePlaybackStore((s) => s.restoreVolume);
const volume = usePlaybackStore((s) => s.volume);
const cachedValue = useSharedValue(volume);
const [_volume, _setVolume] = useState(1);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useAnimatedReaction(
() => cachedValue.get(),
(currVal) => scheduleOnRN(_setVolume, currVal),
);

return (
<SegmentedList>
<SegmentedList.Item
labelTextKey="feat.playback.extra.restoreVolume"
onPress={toggleRestoreVolume}
RightElement={<Switch enabled={restoreVolume} />}
/>
<SegmentedList.CustomItem className="gap-4 p-4">
<TStyledText textKey="feat.playback.extra.volume" className="text-sm" />
<AudioEffectSlider
initValue={volume}
liveValue={cachedValue}
min={0}
max={1}
step={0.01}
onChange={setVolume}
_debounceMultiplier={5}
Icon={VolumeUp}
displayedValue={`${Math.round(_volume * 100)}%`}
/>
</SegmentedList.CustomItem>
</SegmentedList>
);
}

//#region Helpers
function toggleRestoreVolume() {
playbackStore.setState((prev) => ({ restoreVolume: !prev.restoreVolume }));
}

function setVolume(volume: number) {
playbackStore.setState({ volume });
AudioBrowser.setVolume(volume);
}
//#endregion
2 changes: 2 additions & 0 deletions mobile/src/modules/audio/_screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { StaticScreenProps } from "@react-navigation/native";
import { ListLayout } from "~/navigation/layouts/ListLayout";

import { PlaybackParameterSettings } from "./_components/PlaybackParameterSettings";
import { VolumeSettings } from "./_components/VolumeSettings";
import { EqualizerSettings } from "./equalizer/components/EqualizerSettings";
import { ReplayGainSettings } from "./replayGain/components/ReplayGainSettings";

Expand All @@ -18,6 +19,7 @@ function AudioEffectsView({
<EqualizerSettings />
{showHidden ? <PlaybackParameterSettings /> : null}
<ReplayGainSettings />
<VolumeSettings />
</ListLayout>
);
}
Expand Down
46 changes: 16 additions & 30 deletions mobile/src/modules/audio/replayGain/components/PreAmpSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { usePlaybackStore } from "~/stores/Playback/store";
import * as ReplayGain from "../core/actions";
import { DB_OFFSET } from "../core/constants";

import { CachedSlider } from "~/components/Form/Slider";
import { Em, TEm } from "~/components/Typography/StyledText";
import { TEm } from "~/components/Typography/StyledText";
import { AudioEffectSlider } from "../../_components/AudioEffectSlider";

export const PreAmpSlider = memo(function PreAmpSlider(props: {
field: "preAmpWTags" | "preAmpWOTags";
Expand All @@ -18,34 +18,20 @@ export const PreAmpSlider = memo(function PreAmpSlider(props: {
<TEm
textKey={`feat.replayGain.extra.${props.field === "preAmpWTags" ? "adjustWithTags" : "adjustWithoutTags"}`}
/>
<View className="flex-row items-center gap-2">
<CachedSlider
initValue={preAmpValue}
min={DB_OFFSET.min}
max={DB_OFFSET.max}
step={0.1}
onChange={
props.field === "preAmpWTags"
? ReplayGain.updatePreAmpWithTags
: ReplayGain.updatePreAmpWithoutTags
}
disabled={props.disabled}
hitSlop={10}
anchorAt={0}
trackColor="surfaceContainer"
roundedEndStop
_debounceMultiplier={1}
_className="shrink grow"
/>

<Em
style={{ fontVariant: ["tabular-nums"] }}
className="w-14 text-center"
>
{preAmpValue >= 0 ? "+" : ""}
{preAmpValue.toFixed(1)} dB
</Em>
</View>
<AudioEffectSlider
initValue={preAmpValue}
min={DB_OFFSET.min}
max={DB_OFFSET.max}
step={0.1}
onChange={
props.field === "preAmpWTags"
? ReplayGain.updatePreAmpWithTags
: ReplayGain.updatePreAmpWithoutTags
}
disabled={props.disabled}
anchorAt={0}
displayedValue={`${preAmpValue >= 0 ? "+" : ""}${preAmpValue.toFixed(1)} dB`}
/>
</View>
);
});
1 change: 1 addition & 0 deletions mobile/src/modules/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
"options": "Playback Options",
"pitch": "Playback Pitch",
"speed": "Playback Speed",
"restoreVolume": "Restore App Volume",
"volume": "App Volume"
}
},
Expand Down
12 changes: 10 additions & 2 deletions mobile/src/modules/scanning/hooks/useSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ export function useSetup() {
// immediately hydrated.
await revalidateWidgets({ openApp: !playbackStore.getState().isPlaying });

const { repeat, playingFrom, activeKey, isReplayGainEnabled } =
playbackStore.getState();
const {
repeat,
playingFrom,
activeKey,
isReplayGainEnabled,
restoreVolume,
volume,
} = playbackStore.getState();
const { restoreLastPosition, continuePlaybackOnDismiss } =
preferenceStore.getState();
if (restoreLastPosition) {
Expand All @@ -95,6 +101,8 @@ export function useSetup() {
if (repeat === RepeatModes.REPEAT_ONE) {
AudioBrowser.setRepeatMode("track");
}
if (restoreVolume) AudioBrowser.setVolume(volume);
else playbackStore.setState({ volume: 1 });
Comment thread
cyanChill marked this conversation as resolved.
AudioBrowser.setReplayGainStatus(isReplayGainEnabled);

// Ensure equalizer settings are loaded.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@ import AudioBrowser from "react-native-audio-browser";
import { ActivityZone } from "~/resources/icons/ActivityZone";
import { GraphicEQ } from "~/resources/icons/GraphicEQ";
import { VolumeUp } from "~/resources/icons/VolumeUp";
import { usePlaybackStore } from "~/stores/Playback/store";
import { playbackStore, usePlaybackStore } from "~/stores/Playback/store";
import { usePreferenceStore } from "~/stores/Preference/store";
import {
PreferenceSetters,
PreferenceTogglers,
} from "~/stores/Preference/actions";
import { sessionStore, useSessionStore } from "~/stores/Session/store";
import { useLyricStore } from "~/modules/lyric/core/store";
import { toggleLyricVisibility } from "~/modules/lyric/core/actions";

Expand Down Expand Up @@ -42,7 +41,7 @@ export function PlaybackOptionsSheet(props: {
const playbackDelay = usePreferenceStore((s) => s.playbackDelay);
const showLyrics = useLyricStore((s) => s.visible);
const waveformSlider = usePreferenceStore((s) => s.waveformSlider);
const volume = useSessionStore((s) => s.volume);
const volume = usePlaybackStore((s) => s.volume);
const appearanceSheetRef = useSheetRef();
const sheetListHandlers = useEnableSheetScroll(true);

Expand Down Expand Up @@ -157,7 +156,7 @@ const VolumeSliderOptions = {
step: 0.01,
thickness: 48,
onChange: (volume: number) => {
sessionStore.setState({ volume });
playbackStore.setState({ volume });
AudioBrowser.setVolume(volume);
},
overlay: {
Expand Down
7 changes: 7 additions & 0 deletions mobile/src/stores/Playback/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export interface PlaybackStore {
preAmpWTags: number;
/** ReplayGain pre-amp that applies to tracks without the embedded tags. */
preAmpWOTags: number;

/** Whether volume will be persisted. */
restoreVolume: boolean;
/** Percentage of device volume audio will be outputted with. */
volume: number;
}

export const PersistedFields: string[] = [
Expand All @@ -84,5 +89,7 @@ export const PersistedFields: string[] = [
"isReplayGainEnabled",
"preAmpWTags",
"preAmpWOTags",
"restoreVolume",
"volume",
] satisfies Array<keyof PlaybackStore>;
//#endregion
3 changes: 3 additions & 0 deletions mobile/src/stores/Playback/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ export const playbackStore = createPersistedStore<PlaybackStore>(
isReplayGainEnabled: false,
preAmpWTags: 0,
preAmpWOTags: 0,

restoreVolume: false,
volume: 1,
}),
{
name: "music::playback-store",
Expand Down
Loading