Skip to content
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useState } from "react";
import { View } from "react-native";
import AudioBrowser from "react-native-audio-browser";
import type { SharedValue } from "react-native-reanimated";
Expand All @@ -9,21 +8,19 @@ import { sessionStore, useSessionStore } from "~/stores/Session/store";

import { Button } from "~/components/Form/Button";
import { CachedSlider } from "~/components/Form/Slider";
import { DetachedSheet } from "~/components/Sheet";
import type { TrueSheetRef } from "~/components/Sheet/useSheetRef";
import { Em } from "~/components/Typography/StyledText";
import { SegmentedList } from "~/components/List/Segmented";
import { Em, TStyledText } from "~/components/Typography/StyledText";

export function PlaybackSpeedSheet(props: { ref: TrueSheetRef }) {
const [stopDrag, setStopDrag] = useState(false);
export function PlaybackSpeedSetting() {
const playbackSpeed = useSessionStore((s) => s.playbackSpeed);
const cachedPlaybackSpeed = useSharedValue(playbackSpeed);

return (
<DetachedSheet ref={props.ref} draggable={!stopDrag}>
<SegmentedList.CustomItem className="gap-4 p-4">
<TStyledText textKey="feat.playback.extra.speed" className="text-sm" />
<CachedSlider
initValue={playbackSpeed}
liveValue={cachedPlaybackSpeed}
getInteractionStatus={setStopDrag}
{...PlaybackSpeedSliderOptions}
/>
<View className="flex-row items-center gap-4">
Expand All @@ -32,7 +29,7 @@ export function PlaybackSpeedSheet(props: { ref: TrueSheetRef }) {
<PlaybackSpeedPreset preset={1.5} value={cachedPlaybackSpeed} />
<PlaybackSpeedPreset preset={2} value={cachedPlaybackSpeed} />
</View>
</DetachedSheet>
</SegmentedList.CustomItem>
);
}

Expand All @@ -47,7 +44,7 @@ function PlaybackSpeedPreset(props: {
setPlaybackSpeed(props.preset);
props.value.set(props.preset);
}}
className="min-h-8 flex-1 rounded-full py-2 active:bg-surfaceContainer"
className="min-h-8 flex-1 rounded-full bg-surfaceContainerLow py-2 active:bg-surfaceContainer"
>
<Em>{formatValue(props.preset)}</Em>
</Button>
Expand Down Expand Up @@ -76,10 +73,11 @@ const PlaybackSpeedSliderOptions = {
step: 0.05,
thickness: 48,
onChange: setPlaybackSpeed,
trackColor: "surfaceContainer",
overlay: {
accessibilityLabelKey: "feat.playback.extra.speed" as const,
Icon: SlowMotionVideo,
formatValue,
},
};
} as const;
//#endregion
14 changes: 13 additions & 1 deletion mobile/src/modules/audio/_screens.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import type { StaticScreenProps } from "@react-navigation/native";

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

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

function AudioEffectsView() {
type Props = StaticScreenProps<{ showHidden?: boolean }>;

function AudioEffectsView({
route: {
params: { showHidden },
},
}: Props) {
Comment thread
cyanChill marked this conversation as resolved.
return (
<ListLayout>
<EqualizerSettings />
{showHidden ? <PlaybackSpeedSetting /> : null}
<ReplayGainSettings />
</ListLayout>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo } from "react";
import { View, useWindowDimensions } from "react-native";
import { createContext, use, useMemo, useState } from "react";
import { View } from "react-native";
import {
Circle,
Defs,
Expand All @@ -12,6 +12,7 @@ import {
import { useEqualizerStore } from "../core/store";

import { OnRTL } from "~/lib/react";
import { cn } from "~/lib/style";
import { Em } from "~/components/Typography/StyledText";
import { useTheme } from "~/modules/customization/theme/hooks";

Expand All @@ -21,19 +22,29 @@ const ClampedOrdinate = Ordinate - YPadding;
const GraphHeight = Ordinate * 2 + 1;
const XAxisYPos = Ordinate + 1;

/** Store graph width in a context as we may render it in other content. */
const GraphWidthContext = createContext(0);

/** Graph displaying Equalizer configuration based on the Nothing X design. */
export function EQGraph(props: EQLineProps) {
const [graphWidth, setGraphWidth] = useState(0);
return (
<View
style={{ height: GraphHeight }}
className="relative mb-4 w-full rounded-xl bg-surfaceContainerLowest"
>
<Svg style={{ height: "100%", width: "100%" }}>
<GraphAnnotations />
<EQLine points={props.points} />
</Svg>
<GraphLabels />
</View>
<GraphWidthContext value={graphWidth}>
<View
onLayout={(e) => setGraphWidth(e.nativeEvent.layout.width)}
style={{ height: GraphHeight }}
className={cn(
"relative mb-4 w-full rounded-md bg-surfaceContainerLow",
{ "opacity-0": graphWidth === 0 },
)}
>
<Svg style={{ height: "100%", width: "100%" }}>
<GraphAnnotations />
<EQLine points={props.points} />
</Svg>
<GraphLabels />
</View>
</GraphWidthContext>
);
}

Expand All @@ -45,7 +56,7 @@ interface EQLineProps {

function EQLine(props: EQLineProps) {
const { scheme, onSurfaceVariant, surfaceContainerHigh } = useTheme();
const width = useGraphWidth();
const width = use(GraphWidthContext);
const eqBandOrdinate = useEqualizerStore((s) => s.bandOrdinate);

const points = useMemo(
Expand Down Expand Up @@ -130,7 +141,7 @@ const DisplayedFrequencies = [

/** Draws x-axis and tick marks for certain frequencies. */
function GraphAnnotations() {
const width = useGraphWidth();
const width = use(GraphWidthContext);
const { surfaceContainer } = useTheme();
return (
<>
Expand Down Expand Up @@ -159,7 +170,7 @@ function GraphAnnotations() {
* navigating back when in `<GraphAnnotations />`.
*/
function GraphLabels() {
const width = useGraphWidth();
const width = use(GraphWidthContext);
return DisplayedFrequencies.map(({ label, xPosPercent }) => (
<Em
key={label}
Expand All @@ -174,11 +185,3 @@ function GraphLabels() {
));
}
//#endregion

//#region Utils
/** Returns width of the graph (which is the full width of the screen minus gutters). */
function useGraphWidth() {
const { width } = useWindowDimensions();
return width - 32;
}
//#endregion
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { ParseKeys } from "i18next";
import { useMemo } from "react";
import { View } from "react-native";
import { useEqualizerSettings } from "react-native-audio-browser";

import { useEqualizerStore } from "../core/store";
import { toggleEQ, setEQPreset } from "../core/actions";

import { OnRTL } from "~/lib/react";
import { cn } from "~/lib/style";
import { Button } from "~/components/Form/Button";
import { SegmentedList } from "~/components/List/Segmented";
import { TStyledText } from "~/components/Typography/StyledText";
import { Switch } from "~/components/UI/Switch";
import { EQGraph } from "../components/EQGraph";
import { FrequencySlider } from "../components/FrequencySlider";

export function EqualizerSettings() {
const eqFreqs = useEqualizerStore((s) => s.defaultFrequencies);
const eqPresets = useEqualizerStore((s) => s.defaultPresets);
const activePreset = useEqualizerStore((s) => s.preset);

const currEQ = useEqualizerSettings();
const isEQEnabled = Boolean(currEQ?.enabled);

const eqDataPoints = useMemo(
() =>
eqFreqs.map((freq, index) => ({
x: freq,
y: currEQ?.bandLevels[index] ?? 0,
})),
[eqFreqs, currEQ?.bandLevels],
);

return (
<SegmentedList>
<SegmentedList.Item
labelTextKey="feat.equalizer.title"
onPress={toggleEQ}
RightElement={<Switch enabled={isEQEnabled} />}
/>
<SegmentedList.CustomItem className="p-4">
<View
needsOffscreenAlphaCompositing={!isEQEnabled}
renderToHardwareTextureAndroid={!isEQEnabled}
className={cn("gap-4", { "opacity-25": !isEQEnabled })}
>
<EQGraph points={eqDataPoints} />

<View
style={{ flexDirection: OnRTL.decide("row-reverse", "row") }}
className="justify-evenly gap-2"
>
{currEQ?.bandLevels.map((level, index) => (
<FrequencySlider
key={`${currEQ.activePreset}_${index}`}
bandIndex={index}
value={level}
disabled={activePreset !== "Custom" || !currEQ.enabled}
/>
))}
</View>

<View className="flex-row flex-wrap gap-2">
{eqPresets.map((preset) => {
const isActive = activePreset === preset;
return (
<Button
key={preset}
onPress={() => setEQPreset(preset)}
disabled={!currEQ?.enabled}
className={cn(
"min-h-auto rounded-full bg-surfaceContainerLow py-2 active:bg-surfaceContainer disabled:opacity-100",
{ "bg-primary active:bg-primaryDim": isActive },
)}
>
<TStyledText
textKey={`feat.equalizer.extra.${preset}` as ParseKeys}
className={cn("text-xs", { "text-onPrimary": isActive })}
/>
</Button>
);
})}
</View>
</View>
</SegmentedList.CustomItem>
</SegmentedList>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@ export const FrequencySlider = memo(function FrequencySlider(props: Props) {
disabled={props.disabled}
hitSlop={10}
anchorAt={0}
trackColor="surfaceContainer"
roundedEndStop
vertical
_debounceMultiplier={1}
_className="h-48"
/>
<Em style={{ fontVariant: ["tabular-nums"] }}>
{bandValue > 0 ? "+" : ""}
{bandValue >= 0 ? "+" : ""}
{bandValue / 100}
</Em>
</View>
Expand Down
51 changes: 51 additions & 0 deletions mobile/src/modules/audio/replayGain/components/PreAmpSlider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { memo } from "react";
import { View } from "react-native";

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";

export const PreAmpSlider = memo(function PreAmpSlider(props: {
field: "preAmpWTags" | "preAmpWOTags";
disabled: boolean;
}) {
const preAmpValue = usePlaybackStore((s) => s[props.field]);
return (
<View className="gap-2">
<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={0}
_className="shrink grow"
/>

<Em
style={{ fontVariant: ["tabular-nums"] }}
className="w-14 text-center"
>
{preAmpValue >= 0 ? "+" : ""}
{preAmpValue.toFixed(1)} dB
</Em>
</View>
</View>
);
});
Loading