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
2 changes: 1 addition & 1 deletion mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"react-i18next": "17.0.8",
"react-native": "0.85.3",
"react-native-android-widget": "0.20.3",
"react-native-audio-browser": "github:MissingCore/react-native-audio-browser#8900e386b4c69c3c4068bc8f062e52ad5fb2196a",
"react-native-audio-browser": "github:MissingCore/react-native-audio-browser#70829572b80c641c64e4267d20817dafc09026d4",
"react-native-bootsplash": "7.3.1",
"react-native-gesture-handler": "3.0.0",
"react-native-keyboard-controller": "1.21.8",
Expand Down
10 changes: 5 additions & 5 deletions mobile/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 15 additions & 1 deletion mobile/src/components/Form/Slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ import type { Icon } from "~/resources/icons/type";
import { Colors } from "~/constants/Styles";
import { OnRTL } from "~/lib/react";
import { cn } from "~/lib/style";
import { countDecimals } from "~/utils/number";
import type { AppColor } from "~/modules/customization/theme/core/constants";
import { useColor } from "~/modules/customization/theme/hooks";
import { Em } from "../Typography/StyledText";

const ACTIVE_OFFSET: [number, number] = [-10, 10];

/**
* Reanimated slider whose render value is handled internally and is
* instantiated on mount.
Expand Down Expand Up @@ -93,6 +96,7 @@ export const CachedSlider = memo(function CachedSlider(props: {
const moveableDistance = props.max - props.min;
const step = useRef(props.step ?? 1);
const debounceMultiplier = useRef(props._debounceMultiplier ?? 5);
const decimalPlaceCount = useRef(countDecimals(props.step ?? 1));

const anchorPoint = props.anchorAt ?? props.min;
const distFromMin = anchorPoint - props.min;
Expand Down Expand Up @@ -145,7 +149,10 @@ export const CachedSlider = memo(function CachedSlider(props: {
if (Math.abs(velocity) > 500) return;
// Don't immediately call `onChange` if we haven't moved `debounceMultipler * step`.
if (
Math.abs(debounceFrom.get() - value) <
roundToDecimal(
Math.abs(debounceFrom.get() - value),
decimalPlaceCount.current,
) <
step.current * debounceMultiplier.current
) {
return;
Expand Down Expand Up @@ -195,6 +202,7 @@ export const CachedSlider = memo(function CachedSlider(props: {

const panGesture = usePanGesture({
enabled: !props.disabled,
[`activeOffset${props.vertical ? "Y" : "X"}`]: ACTIVE_OFFSET,
onBegin: () => setIsInteracting(true),
onActivate: ({ x, y }) => debounceFrom.set(onVerticalWorklet(y, x)),
onUpdate: ({ x, y, velocityX, velocityY }) => {
Expand Down Expand Up @@ -406,4 +414,10 @@ function roundToStep(rawNum: number, step: number) {
if (decimalPlaces === 0) return roundedVal;
return parseFloat(roundedVal.toFixed(decimalPlaces));
}

/** Round number to specified decimal places. */
function roundToDecimal(value: number, decimals: number) {
"worklet";
return Number(value.toFixed(decimals));
}
//#endregion
24 changes: 24 additions & 0 deletions mobile/src/modules/audio/_components/PlaybackParameterSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import AudioBrowser from "react-native-audio-browser";

import { SlowMotionVideo } from "~/resources/icons/SlowMotionVideo";
import { VoiceSelection } from "~/resources/icons/VoiceSelection";

import { SegmentedList } from "~/components/List/Segmented";
import { PlaybackParameterSlider } from "./PlaybackParameterSlider";

export function PlaybackParameterSettings() {
return (
<SegmentedList>
<PlaybackParameterSlider
field="pitch"
onUpdate={AudioBrowser.setPitch}
Icon={VoiceSelection}
/>
<PlaybackParameterSlider
field="speed"
onUpdate={AudioBrowser.setRate}
Icon={SlowMotionVideo}
/>
</SegmentedList>
);
}
89 changes: 89 additions & 0 deletions mobile/src/modules/audio/_components/PlaybackParameterSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useCallback, useMemo } from "react";
import { View } from "react-native";
import { useSharedValue } from "react-native-reanimated";

import type { Icon } from "~/resources/icons/type";
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";

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;

const storedValue = useSessionStore((s) => s[fieldName]);
const cachedValue = useSharedValue(storedValue);
Comment thread
cyanChill marked this conversation as resolved.

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]);

return (
<SegmentedList.CustomItem className="gap-4 p-4">
<TStyledText textKey={fieldNameKey} className="text-sm" />
<View className="flex-row items-center gap-2">
<CachedSlider
initValue={storedValue}
liveValue={cachedValue}
min={0.25}
max={2}
step={0.05}
onChange={setField}
hitSlop={10}
trackColor="surfaceContainer"
roundedEndStop
_debounceMultiplier={1}
_className="shrink grow"
/>
<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>
);
}

//#region Helpers
const numberFormatter = new Intl.NumberFormat("en-US", {
notation: "compact",
maximumFractionDigits: 2,
});

function formatValue(val: number) {
return `${numberFormatter.format(val)}x`;
}
//#endregion
83 changes: 0 additions & 83 deletions mobile/src/modules/audio/_components/PlaybackSpeedSetting.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions mobile/src/modules/audio/_screens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { StaticScreenProps } from "@react-navigation/native";

import { ListLayout } from "~/navigation/layouts/ListLayout";

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

Expand All @@ -16,7 +16,7 @@ function AudioEffectsView({
return (
<ListLayout>
<EqualizerSettings />
{showHidden ? <PlaybackSpeedSetting /> : null}
{showHidden ? <PlaybackParameterSettings /> : null}
<ReplayGainSettings />
</ListLayout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const PreAmpSlider = memo(function PreAmpSlider(props: {
anchorAt={0}
trackColor="surfaceContainer"
roundedEndStop
_debounceMultiplier={0}
_debounceMultiplier={1}
_className="shrink grow"
/>

Expand Down
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 @@ -231,6 +231,7 @@
"extra": {
"delay": "Playback Delay",
"options": "Playback Options",
"pitch": "Playback Pitch",
"speed": "Playback Speed",
"volume": "App Volume"
}
Expand Down
13 changes: 13 additions & 0 deletions mobile/src/resources/icons/VoiceSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Svg, { Path } from "react-native-svg";

import { useColor } from "~/modules/customization/theme/hooks";
import type { Icon } from "./type";

export function VoiceSelection({ size = 24, color }: Icon) {
const usedColor = useColor(color, "onSurface");
return (
<Svg width={size} height={size} viewBox="0 -960 960 960" fill={usedColor}>
<Path d="M728.17-99.96q-11.67 0-19.79-8.24-8.11-8.24-8.11-19.9 0-5.82 2.01-10.81 2.02-4.99 5.8-8.71 43.69-43.69 67.71-100.15 24.02-56.46 24.02-121.1 0-64.17-24.02-120.69-24.02-56.52-67.71-100.21-3.78-3.75-5.8-8.78-2.01-5.03-2.01-11.02 0-11.43 7.99-19.64t19.65-8.21q5.82 0 10.89 2.2 5.06 2.2 8.81 5.91 51.2 51.2 79.68 118.1 28.48 66.9 28.48 142.36 0 75.89-28.48 142.73-28.48 66.85-79.68 118.04-3.76 3.72-8.59 5.92-4.83 2.2-10.85 2.2Zm-104.7-104.85q-11.62 0-19.83-8.14-8.22-8.15-8.22-19.81 0-6.01 2.2-10.83 2.2-4.81 5.92-8.56 23.23-23.23 35.9-53.22 12.68-29.98 12.68-63.32 0-33.35-12.83-63.33-12.83-29.98-35.75-53.21-4.15-3.76-6.14-8.85-1.98-5.08-1.98-11.1 0-11.47 8.24-19.53t19.9-8.06q5.82 0 10.81 2.08t8.71 6.04q30.42 30.61 47.71 70.53 17.29 39.93 17.29 85.43 0 45.5-17.29 85.42-17.29 39.93-47.71 70.35-3.75 4.15-8.78 6.13-5.03 1.98-10.83 1.98ZM336.15-369.89h-92.11v14.08q0 36.27 23.44 63.48 23.44 27.21 58.79 36.21l12 3q34.15 8.93 37.81 45.41 3.65 36.48-29.93 52.53-49.42 24.14-102.38 34.45-52.96 10.31-107.62 12.04-11.65.61-19.86-7.61-8.21-8.23-8.21-19.87 0-11.45 8.22-19.59 8.22-8.15 19.85-8.89 41.35-1.62 81.64-7.89t79.83-21.19q4.61-1.73 4.23-7.02-.39-5.29-6.93-6.83-47.38-18.19-77.11-58.36-29.73-40.18-29.73-90.64v-35.03q0-14.25 9.99-24.24 9.99-10 24.24-10h113.46q5.38 0 8.85-3.46 3.46-3.46 3.46-8.84v-68q0-14.25 9.99-24.24 9.99-9.99 24.24-9.99h88.23q6.92 0 10.48-5.58 3.56-5.58.67-11.73L372.08-780.27q-5.39-10.34-1.67-21.15 3.71-10.81 14.25-16.58 10.34-5.77 21.53-1.77 11.2 4 16.58 14.35l109.5 223.46q16.58 34.23-2.98 65.88-19.55 31.66-57.75 31.66h-67.5v46.65q0 28.35-19.87 48.12-19.86 19.76-48.02 19.76Z" />
</Svg>
);
}
2 changes: 2 additions & 0 deletions mobile/src/stores/Session/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { PopStrategy } from "./types";
export interface SessionStore {
/** The rate at which the media is played (from 0.25 to 2). */
playbackSpeed: number;
/** The factor at which the pitch will be shifted (from 0.25 to 2). */
playbackPitch: number;
/** Percentage of device volume audio will be outputted with. */
volume: number;

Expand Down
1 change: 1 addition & 0 deletions mobile/src/stores/Session/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { SessionStore } from "./constants";

export const sessionStore = createStore<SessionStore>()(() => ({
playbackSpeed: 1,
playbackPitch: 1,
volume: 1,

displayedTrack: null,
Expand Down
7 changes: 7 additions & 0 deletions mobile/src/utils/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export function clamp(min: number, value: number, max: number) {
return Math.min(Math.max(value, min), max);
}

/** Count the number of positions after the decimal in a number. */
export function countDecimals(value: number) {
const asString = value.toString();
if (!asString.includes(".")) return 0;
return asString.split(".").at(-1)!.length;
}

/** Convert epoch time to `YYYY-MM-DD` */
export function formatEpoch(ms: number) {
const date = new Date(ms);
Expand Down