Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature]: Add Equalizer #308

Closed
wants to merge 3 commits into from
Closed
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
9 changes: 8 additions & 1 deletion src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { getMpvProperties } from '/@/renderer/features/settings/components/playb
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
import '@ag-grid-community/styles/ag-grid.css';
import { bandsToAudioFilter } from '/@/renderer/utils';

ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);

Expand All @@ -38,6 +39,7 @@ const remote = isElectron() ? window.electron.remote : null;
export const App = () => {
const theme = useTheme();
const contentFont = useSettingsStore((state) => state.general.fontContent);
const audioBands = useSettingsStore((state) => state.audio.bands);
const { type: playbackType } = usePlaybackSettings();
const { bindings } = useHotkeySettings();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
Expand Down Expand Up @@ -66,12 +68,15 @@ export const App = () => {
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
};

const volume = properties.volume;
properties.af = bandsToAudioFilter(audioBands);

mpvPlayer?.initialize({
extraParameters,
properties,
});

mpvPlayer?.volume(properties.volume);
mpvPlayer?.volume(volume);
}
mpvPlayer?.restoreQueue();
};
Expand All @@ -85,6 +90,8 @@ export const App = () => {
mpvPlayer?.stop();
mpvPlayer?.cleanup();
};
// audioBands should NOT cause a cleanup of this function
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [clearQueue, playbackType]);

useEffect(() => {
Expand Down
40 changes: 37 additions & 3 deletions src/renderer/components/audio-player/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import {
crossfadeHandler,
gaplessHandler,
} from '/@/renderer/components/audio-player/utils/list-handlers';
import { useSettingsStore } from '/@/renderer/store/settings.store';
import { useAudioSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import type { CrossfadeStyle } from '/@/renderer/types';
import { PlaybackStyle, PlayerStatus } from '/@/renderer/types';
import { AudioFrequencies, octaveToQFactor } from '/@/renderer/utils';

interface AudioPlayerProps extends ReactPlayerProps {
crossfadeDuration: number;
Expand All @@ -35,6 +36,7 @@ const getDuration = (ref: any) => {

type WebAudio = {
context: AudioContext;
filters: BiquadFilterNode[];
gain: GainNode;
};

Expand All @@ -54,6 +56,8 @@ export const AudioPlayer = forwardRef(
}: AudioPlayerProps,
ref: any,
) => {
const { bands, octave } = useAudioSettings();

const player1Ref = useRef<ReactPlayer>(null);
const player2Ref = useRef<ReactPlayer>(null);
const [isTransitioning, setIsTransitioning] = useState(false);
Expand Down Expand Up @@ -122,9 +126,22 @@ export const AudioPlayer = forwardRef(
sampleRate: playback.audioSampleRateHz || undefined,
});
const gain = context.createGain();
gain.connect(context.destination);
const filters: BiquadFilterNode[] = [];

let priorNode = gain;

for (let i = 0; i < AudioFrequencies.length; i += 1) {
const filter = context.createBiquadFilter();
filter.frequency.value = AudioFrequencies[i];
filter.type = 'peaking';
priorNode.connect(filter);
priorNode = filter;
filters.push(filter);
}

setWebAudio({ context, gain });
priorNode.connect(context.destination);

setWebAudio({ context, filters, gain });

return () => {
return context.close();
Expand All @@ -135,6 +152,23 @@ export const AudioPlayer = forwardRef(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (webAudio) {
const qValue = octaveToQFactor(octave);
for (const filter of webAudio.filters) {
filter.Q.value = qValue;
}
}
}, [octave, webAudio]);

useEffect(() => {
if (webAudio) {
bands.forEach((band, idx) => {
webAudio.filters[idx].gain.value = band.gain;
});
}
}, [bands, webAudio]);

useImperativeHandle(ref, () => ({
get player1() {
return player1Ref?.current;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Divider, Stack } from '@mantine/core';
import { Equalizer } from '/@/renderer/features/settings/components/advanced-audio/equalizer';

export const AdvancedAudioTab = () => {
return (
<Stack spacing="md">
<Equalizer />
<Divider />
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useEffect, useState } from 'react';
import { Group, NumberInputProps, Stack, rem } from '@mantine/core';
import { useMove, usePrevious } from '@mantine/hooks';
import { Text } from '/@/renderer/components/text';
import { NumberInput } from '/@/renderer/components';
import styled from 'styled-components';

interface VerticalSliderProps {
onChange: (value: number) => void;

title?: string;
value: number;
}

export const DB_RADIUS = 6;
const DB_DIAMATER = 2 * DB_RADIUS;
const DB_SCALE = 100 / DB_DIAMATER;

const EqualizerNumberInput = styled(NumberInput)<NumberInputProps>`
& .mantine-NumberInput-input {
text-align: center;
}
`;

export const EqualizerSlider = ({ value, title, onChange }: VerticalSliderProps) => {
const [seekingValue, setValue] = useState(value);

const { ref, active } = useMove(({ y }) => {
const value = Math.round((0.5 - y) * DB_DIAMATER * 10) / 10;
setValue(value);
});

const wasActive = usePrevious(active);

useEffect(() => {
if (wasActive && !active) {
onChange(seekingValue);
}
}, [active, onChange, seekingValue, wasActive]);

const displayValue = active ? seekingValue : value;

return (
<>
<Stack align="center">
{title && (
<Text
mt="sm"
ta="center"
>
{title}
</Text>
)}
<div
ref={ref}
style={{
backgroundColor: 'var(--input-bg)',
height: rem(120),
position: 'relative',
width: rem(16),
}}
>
<div
style={{
backgroundColor: 'var(--primary-color)',
bottom: 0,
height: `${(displayValue + DB_RADIUS) * DB_SCALE}%`,
position: 'absolute',
width: rem(16),
}}
/>
<div
style={{
backgroundColor: 'var(--input-active-fg)',
bottom: `calc(${(displayValue + DB_RADIUS) * DB_SCALE}% - ${rem(8)})`,
height: rem(16),
left: 0,
position: 'absolute',
width: rem(16),
}}
/>
</div>
<Group spacing={5}>
<EqualizerNumberInput
// why a key? Apparently without it the number input would
// not update its value when the slider changed......
key={displayValue}
max={DB_RADIUS}
min={-DB_RADIUS}
precision={1}
radius="xs"
rightSection="db"
step={0.1}
value={displayValue}
variant="unstyled"
width={rem(74)}
onChange={(val) => {
onChange(Number(val));
}}
/>
</Group>
</Stack>
</>
);
};
139 changes: 139 additions & 0 deletions src/renderer/features/settings/components/advanced-audio/equalizer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { Grid, SelectItem } from '@mantine/core';
import { EqualizerSlider } from '/@/renderer/features/settings/components/advanced-audio/eqalizer-slider';
import { useAudioSettings, useSettingsStoreActions } from '/@/renderer/store';
import isElectron from 'is-electron';
import { useCallback } from 'react';
import { Button, Select } from '/@/renderer/components';
import { AudioFrequencies, Octave, bandsToAudioFilter } from '/@/renderer/utils';
import {
SettingOption,
SettingsSection,
} from '/@/renderer/features/settings/components/settings-section';

const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;

const POSSIBLE_OCTAVES: SelectItem[] = [
{
label: 'Full octave',
value: Octave.Full,
},
{
label: 'Third octave',
value: Octave.Third,
},
];

export const Equalizer = () => {
const settings = useAudioSettings();
const { setSettings } = useSettingsStoreActions();

const setOctave = useCallback(
(octave: Octave) => {
setSettings({
audio: {
...settings,
octave,
},
});

if (mpvPlayer) {
mpvPlayer.setProperties({
af: bandsToAudioFilter(settings.bands, octave),
});
}
},
[setSettings, settings],
);

const setBandSetting = useCallback(
(gain: number, index: number) => {
const bands = [...settings.bands];
bands[index].gain = gain;

setSettings({
audio: {
...settings,
bands,
},
});

if (mpvPlayer) {
mpvPlayer.setProperties({
af: bandsToAudioFilter(bands, settings.octave),
});
}
},
[setSettings, settings],
);

const resetBand = useCallback(() => {
const bands = AudioFrequencies.map((frequency) => ({
frequency,
gain: 0,
}));

setSettings({
audio: {
...settings,
bands,
},
});

if (mpvPlayer) {
mpvPlayer.setProperties({
af: bandsToAudioFilter(bands, settings.octave),
});
}
}, [setSettings, settings]);

const options: SettingOption[] = [
{
control: (
<Select
data={POSSIBLE_OCTAVES}
value={settings.octave}
onChange={setOctave}
/>
),
description: 'Specifies the width (in octaves) of the filter',
title: 'Filter width',
},
{
control: (
<Button
compact
variant="filled"
onClick={() => resetBand()}
>
Reset to default
</Button>
),
description: 'Rest audio bands to 0',
title: 'Audio Equalizations',
},
];

return (
<>
<SettingsSection options={options} />
<Grid
columns={AudioFrequencies.length}
gutter={20}
justify="center"
>
{settings.bands.map((band, idx) => (
<Grid.Col
key={band.frequency}
span="auto"
>
<EqualizerSlider
title={`${band.frequency} Hz`}
value={band.gain}
onChange={(gain) => setBandSetting(gain, idx)}
/>
</Grid.Col>
))}
</Grid>
</>
);
};
Loading