From c64807cf94aac876d180c6dcaf5906771a10fc1d Mon Sep 17 00:00:00 2001 From: vimae <34780309+vimaexd@users.noreply.github.com> Date: Fri, 10 Jul 2026 05:06:46 +0100 Subject: [PATCH] feat: musical speed controls (#2191) * feat: musical speed controls --- src/i18n/locales/en.json | 4 +- .../player/components/player-config.tsx | 83 ++++++++++++++++++- .../components/general/control-settings.tsx | 20 +++++ src/renderer/store/settings.store.ts | 5 ++ 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 2a484a46c..cf5a4b9f1 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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": { diff --git a/src/renderer/features/player/components/player-config.tsx b/src/renderer/features/player/components/player-config.tsx index af1cfc9b1..f78bc180a 100644 --- a/src/renderer/features/player/components/player-config.tsx +++ b/src/renderer/features/player/components/player-config.tsx @@ -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 ? : <>, + id: 'pitchControls', + label: '', + }, { component: ( { () => (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 ( { ]} 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 ( + + + {microtonal && ( + + )} + + {speed.toFixed(2)}% {speedToPitch(speed) > 0 && '+'} + {speedToPitch(speed) == 0 && '±'} + {speedToPitch(speed).toFixed(2)}st + + {microtonal && ( + + )} + + + ); +}; diff --git a/src/renderer/features/settings/components/general/control-settings.tsx b/src/renderer/features/settings/components/general/control-settings.tsx index 66c2c2628..2989394ae 100644 --- a/src/renderer/features/settings/components/general/control-settings.tsx +++ b/src/renderer/features/settings/components/general/control-settings.tsx @@ -501,6 +501,26 @@ export const ControlSettings = memo(() => { }, ] : []), + { + control: ( + + setSettings({ + general: { + ...settings, + microtonalPitchControls: e.currentTarget.checked, + }, + }) + } + /> + ), + description: t('setting.microtonalPitchControls', { + context: 'description', + }), + isHidden: false, + title: t('setting.microtonalPitchControls'), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 37b204956..78825e032 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -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);