feat: musical speed controls (#2191)

* feat: musical speed controls
This commit is contained in:
vimae
2026-07-10 05:06:46 +01:00
committed by GitHub
parent e3c2605e1d
commit c64807cf94
4 changed files with 107 additions and 5 deletions
+3 -1
View File
@@ -1184,7 +1184,9 @@
"queryBuilderCustomFields_inputLabel": "Label",
"queryBuilderCustomFields_inputTag": "Tag",
"queryBuilderCustomFields": "Custom fields",
"queryBuilderCustomFields_description": "Add custom fields to use in query builders"
"queryBuilderCustomFields_description": "Add custom fields to use in query builders",
"microtonalPitchControls": "Microtonal pitch controls",
"microtonalPitchControls_description": "Add more pitch controls to go between semi-tones"
},
"table": {
"column": {
@@ -14,6 +14,7 @@ import {
} from '/@/renderer/store';
import {
useCombinedLyricsAndVisualizer,
useMicrotonalPitchControls,
usePlaybackSettings,
useSettingsStore,
useSettingsStoreActions,
@@ -21,11 +22,14 @@ import {
useShowVisualizerInSidebar,
} from '/@/renderer/store/settings.store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { Popover } from '/@/shared/components/popover/popover';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Select } from '/@/shared/components/select/select';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { Text } from '/@/shared/components/text/text';
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
const ipc = isElectron() ? window.api.ipc : null;
@@ -93,6 +97,11 @@ export const PlayerConfig = () => {
id: 'playbackSpeed',
label: t('player.playbackSpeed'),
},
{
component: !preservePitch ? <PitchControls /> : <></>,
id: 'pitchControls',
label: '',
},
{
component: (
<Switch
@@ -364,16 +373,15 @@ export const PlaybackSpeedSlider = () => {
() => (value: number) => {
const bpmValue = Number(bpm);
if (bpmValue > 0) {
return `${value} x / ${(bpmValue * value).toFixed(1)} BPM`;
return `${value.toFixed(2)} x / ${(bpmValue * value).toFixed(1)} BPM`;
}
return `${value} x`;
return `${value.toFixed(2)} x`;
},
[bpm],
);
return (
<Slider
defaultValue={speed}
label={formatPlaybackSpeedSliderLabel}
marks={[
{ label: '0.5', value: 0.5 },
@@ -386,14 +394,81 @@ export const PlaybackSpeedSlider = () => {
]}
max={2}
min={0.5}
onChangeEnd={setSpeed}
onChange={setSpeed}
onDoubleClick={() => setSpeed(1)}
step={0.01}
styles={{
markLabel: {},
root: {},
}}
value={speed}
w="100%"
/>
);
};
export const PitchControls = () => {
const microtonal = useMicrotonalPitchControls();
const speed = usePlayerSpeed();
const { setSpeed } = usePlayerActions();
const speedToPitch = (speed: number) => {
return 12 * Math.log2(speed);
};
const pitchToSpeed = (pitch: number) => {
return 2 ** (pitch / 12);
};
const adjustMusicalSpeed = (adjustment: number) => {
const curPitch = speedToPitch(speed);
const newSpeed = pitchToSpeed(curPitch + adjustment);
setSpeed(newSpeed);
};
return (
<Group gap={microtonal ? 'xs' : 'md'} my="sm" w="100%" wrap="nowrap">
<Button
aria-label="-1 semitone"
fullWidth
onClick={() => adjustMusicalSpeed(-1)}
size="compact-xs"
>
-1st
</Button>
{microtonal && (
<Button
aria-label="-10 cents"
fullWidth
onClick={() => adjustMusicalSpeed(-0.1)}
size="compact-xs"
>
-10ct
</Button>
)}
<Text size="xs" style={{ fontFamily: 'monospace' }} ta="center" w="60px">
{speed.toFixed(2)}% {speedToPitch(speed) > 0 && '+'}
{speedToPitch(speed) == 0 && '±'}
{speedToPitch(speed).toFixed(2)}st
</Text>
{microtonal && (
<Button
aria-label="+10 cents"
fullWidth
onClick={() => adjustMusicalSpeed(0.1)}
size="compact-xs"
>
+10ct
</Button>
)}
<Button
aria-label="+1 semitone"
fullWidth
onClick={() => adjustMusicalSpeed(1)}
size="compact-xs"
>
+1st
</Button>
</Group>
);
};
@@ -501,6 +501,26 @@ export const ControlSettings = memo(() => {
},
]
: []),
{
control: (
<Switch
defaultChecked={settings.microtonalPitchControls}
onChange={(e) =>
setSettings({
general: {
...settings,
microtonalPitchControls: e.currentTarget.checked,
},
})
}
/>
),
description: t('setting.microtonalPitchControls', {
context: 'description',
}),
isHidden: false,
title: t('setting.microtonalPitchControls'),
},
];
return <SettingsSection options={controlOptions} title={t('page.setting.controls')} />;
+5
View File
@@ -505,6 +505,7 @@ export const GeneralSettingsSchema = z.object({
lastFM: z.boolean(),
lastfmApiKey: z.string(),
listenBrainz: z.boolean(),
microtonalPitchControls: z.boolean(),
musicBrainz: z.boolean(),
nativeAspectRatio: z.boolean(),
nativeSpotify: z.boolean(),
@@ -1201,6 +1202,7 @@ const initialState: SettingsState = {
lastFM: true,
lastfmApiKey: '',
listenBrainz: true,
microtonalPitchControls: false,
musicBrainz: true,
nativeAspectRatio: false,
nativeSpotify: false,
@@ -2830,3 +2832,6 @@ export const useButterchurnSettings = () => {
};
}, shallow);
};
export const useMicrotonalPitchControls = () =>
useSettingsStore((state) => state.general.microtonalPitchControls, shallow);