diff --git a/package.json b/package.json index 9b6e6a85d..ada6db758 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,8 @@ "@xhayper/discord-rpc": "^1.3.0", "audiomotion-analyzer": "^4.5.1", "axios": "^1.13.2", + "butterchurn": "^2.6.7", + "butterchurn-presets": "^2.4.7", "cheerio": "^1.1.2", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0548c0efd..c9025b34f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,12 @@ importers: axios: specifier: ^1.13.2 version: 1.13.2 + butterchurn: + specifier: ^2.6.7 + version: 2.6.7 + butterchurn-presets: + specifier: ^2.4.7 + version: 2.4.7 cheerio: specifier: ^1.1.2 version: 1.1.2 @@ -2263,6 +2269,9 @@ packages: peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + babel-runtime@6.26.0: + resolution: {integrity: sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2359,6 +2368,12 @@ packages: builder-util@26.0.11: resolution: {integrity: sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==} + butterchurn-presets@2.4.7: + resolution: {integrity: sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg==} + + butterchurn@2.6.7: + resolution: {integrity: sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg==} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2555,6 +2570,10 @@ packages: core-js-compat@3.47.0: resolution: {integrity: sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-util-is@1.0.2: resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} @@ -2779,6 +2798,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecma-proposal-math-extensions@0.0.2: + resolution: {integrity: sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==} + ejs@3.1.10: resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} engines: {node: '>=0.10.0'} @@ -4688,6 +4710,9 @@ packages: regenerate@1.4.2: resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + regenerator-runtime@0.11.1: + resolution: {integrity: sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==} + regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -7998,6 +8023,11 @@ snapshots: transitivePeerDependencies: - supports-color + babel-runtime@6.26.0: + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.11.1 + balanced-match@1.0.2: {} balanced-match@2.0.0: {} @@ -8134,6 +8164,17 @@ snapshots: transitivePeerDependencies: - supports-color + butterchurn-presets@2.4.7: + dependencies: + babel-runtime: 6.26.0 + ecma-proposal-math-extensions: 0.0.2 + lodash: 4.17.21 + + butterchurn@2.6.7: + dependencies: + '@babel/runtime': 7.28.4 + ecma-proposal-math-extensions: 0.0.2 + cac@6.7.14: {} cacache@16.1.3: @@ -8361,6 +8402,8 @@ snapshots: dependencies: browserslist: 4.28.0 + core-js@2.6.12: {} + core-util-is@1.0.2: optional: true @@ -8604,6 +8647,8 @@ snapshots: eastasianwidth@0.2.0: {} + ecma-proposal-math-extensions@0.0.2: {} + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -9526,7 +9571,7 @@ snapshots: i18next@24.2.3(typescript@5.8.3): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.28.4 optionalDependencies: typescript: 5.8.3 @@ -10598,7 +10643,7 @@ snapshots: react-textarea-autosize@8.5.9(@types/react@19.2.5)(react@19.1.0): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.28.4 react: 19.1.0 use-composed-ref: 1.4.0(@types/react@19.2.5)(react@19.1.0) use-latest: 1.3.0(@types/react@19.2.5)(react@19.1.0) @@ -10607,7 +10652,7 @@ snapshots: react-transition-group@4.4.5(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.28.4 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -10672,6 +10717,8 @@ snapshots: regenerate@1.4.2: {} + regenerator-runtime@0.11.1: {} + regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8d4ac0f52..ffb0a42b8 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1095,5 +1095,148 @@ "error_oneFileOnly": "Please only select 1 file", "error_readingFile": "there has been an issue reading the file: {{errorMessage}}", "mainText": "drop a file here" + }, + "visualizer": { + "visualizerType": "Visualizer Type", + "cyclePresets": "Cycle Presets", + "cycleTime": "Cycle Time (seconds)", + "includeAllPresets": "Include All Presets", + "selectedPresets": "Selected Presets", + "randomizeNextPreset": "Randomize Next Preset", + "blendTime": "Blend Time", + "presets": "Presets", + "selectPreset": "Select Preset", + "applyPreset": "Apply Preset", + "saveAsPreset": "Save as Preset", + "updatePreset": "Update Preset", + "copyConfiguration": "Copy Configuration", + "pasteConfiguration": "Paste Configuration", + "pasteConfigurationPlaceholder": "Paste JSON configuration here...", + "pasteFromClipboard": "Paste from Clipboard", + "applyConfiguration": "Apply Configuration", + "configCopied": "Configuration copied to clipboard", + "configCopyFailed": "Failed to copy configuration", + "configPasted": "Configuration applied successfully", + "configPasteFailed": "Failed to apply configuration. Please check the format.", + "configPasteReadFailed": "Failed to read from clipboard", + "presetName": "Preset Name", + "presetNamePlaceholder": "Enter preset name", + "general": "General", + "mode": "Mode", + "mode1To8": "Mode 1 - 8", + "mode10": "Mode 10", + "barSpace": "Bar Space", + "lineWidth": "Line Width", + "fillAlpha": "Fill Alpha", + "channelLayout": "Channel Layout", + "maxFPS": "Max FPS", + "customGradients": "Custom Gradients", + "addCustomGradient": "Add Custom Gradient", + "gradientName": "Gradient Name", + "gradientNamePlaceholder": "Gradient Name", + "vertical": "Vertical", + "horizontal": "Horizontal", + "colorStops": "Color Stops", + "addColor": "Add Color", + "position": "Position", + "level": "Level", + "remove": "Remove", + "custom": "Custom", + "builtIn": "Built-in", + "colors": "Colors", + "colorMode": "Color Mode", + "gradient": "Gradient", + "gradientLeft": "Gradient Left", + "gradientRight": "Gradient Right", + "fft": "FFT", + "fftSize": "FFT Size", + "smoothing": "Smoothing", + "frequencyRangeAndScaling": "Frequency range and scaling", + "minimumFrequency": "Minimum Frequency", + "maximumFrequency": "Maximum Frequency", + "frequencyScale": "Frequency Scale", + "sensitivity": "Sensitivity", + "weightingFilter": "Weighting Filter", + "minimumDecibels": "Minimum Decibels", + "maximumDecibels": "Maximum Decibels", + "linearAmplitude": "Linear Amplitude", + "linearBoost": "Linear Boost", + "peakBehavior": "Peak Behavior", + "showPeaks": "Show Peaks", + "fadePeaks": "Fade Peaks", + "peakLine": "Peak Line", + "gravity": "Gravity", + "peakFadeTime": "Peak Fade Time (ms)", + "peakHoldTime": "Peak Hold Time (ms)", + "radialSpectrum": "Radial Spectrum", + "radial": "Radial", + "radialInvert": "Radial Invert", + "radius": "Radius", + "reflexMirror": "Reflex Mirror", + "reflexFit": "Reflex Fit", + "reflexRatio": "Reflex Ratio", + "reflexAlpha": "Reflex Alpha", + "reflexBrightness": "Reflex Brightness", + "mirror": "Mirror", + "miscellaneousSettings": "Miscellaneous Settings", + "alphaBars": "Alpha Bars", + "ansiBands": "ANSI Bands", + "ledBars": "LED Bars", + "trueLeds": "True LEDs", + "lumiBars": "Lumi Bars", + "outlineBars": "Outline Bars", + "roundBars": "Round Bars", + "lowResolution": "Low Resolution", + "splitGradient": "Split Gradient", + "showFPS": "Show FPS", + "showScaleX": "Show Scale X", + "noteLabels": "Note Labels", + "showScaleY": "Show Scale Y", + "options": { + "mode": { + "bars": "[0] Bars", + "circle": "[1] Circle", + "wave": "[2] Wave", + "rainbow": "[3] Rainbow", + "rings": "[4] Rings", + "mirror": "[5] Mirror", + "line": "[6] Line", + "particles": "[7] Particles", + "fullOctave": "[8] Full octave / 10 bands", + "outlineBars": "[10] Outline bars" + }, + "colorMode": { + "gradient": "Gradient", + "barIndex": "Bar-Index", + "barLevel": "Bar-Level" + }, + "gradient": { + "classic": "Classic", + "prism": "Prism", + "rainbow": "Rainbow", + "steelblue": "Steelblue", + "orangered": "Orangered" + }, + "channelLayout": { + "single": "Single", + "dualCombined": "Dual-Combined", + "dualHorizontal": "Dual-Horizontal", + "dualVertical": "Dual-Vertical" + }, + "frequencyScale": { + "bark": "Bark", + "linear": "Linear", + "log": "Log", + "mel": "Mel" + }, + "weightingFilter": { + "none": "None", + "a": "A", + "b": "B", + "c": "C", + "d": "D", + "z": "Z" + } + } } } diff --git a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx index 3e138c3d7..14d76eef2 100644 --- a/src/renderer/features/now-playing/components/sidebar-play-queue.tsx +++ b/src/renderer/features/now-playing/components/sidebar-play-queue.tsx @@ -8,14 +8,25 @@ import { lyricsQueries } from '/@/renderer/features/lyrics/api/lyrics-api'; import { Lyrics } from '/@/renderer/features/lyrics/lyrics'; import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue'; import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls'; -import { useGeneralSettings, usePlaybackSettings, usePlayerSong } from '/@/renderer/store'; +import { + useGeneralSettings, + usePlaybackSettings, + usePlayerSong, + useSettingsStore, +} from '/@/renderer/store'; import { Divider } from '/@/shared/components/divider/divider'; import { Flex } from '/@/shared/components/flex/flex'; import { Stack } from '/@/shared/components/stack/stack'; import { ItemListKey, PlayerType } from '/@/shared/types/types'; -const Visualizer = lazy(() => - import('/@/renderer/features/player/components/visualizer').then((module) => ({ +const AudioMotionAnalyzerVisualizer = lazy(() => + import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({ + default: module.Visualizer, + })), +); + +const ButterchurnVisualizer = lazy(() => + import('../../visualizer/components/butternchurn/visualizer').then((module) => ({ default: module.Visualizer, })), ); @@ -48,6 +59,7 @@ export const SidebarPlayQueue = () => { const BottomPanel = () => { const { showLyricsInSidebar, showVisualizerInSidebar } = useGeneralSettings(); const { type, webAudio } = usePlaybackSettings(); + const visualizerType = useSettingsStore((store) => store.visualizer.type); const currentSong = usePlayerSong(); const { data: lyricsData } = useQuery( @@ -102,7 +114,11 @@ const BottomPanel = () => { }} > }> - + {visualizerType === 'butterchurn' ? ( + + ) : ( + + )} )} @@ -111,7 +127,11 @@ const BottomPanel = () => { showVisualizer && (
}> - + {visualizerType === 'butterchurn' ? ( + + ) : ( + + )}
) diff --git a/src/renderer/features/player/components/full-screen-player-queue.tsx b/src/renderer/features/player/components/full-screen-player-queue.tsx index 82f84f7d6..e65e26b33 100644 --- a/src/renderer/features/player/components/full-screen-player-queue.tsx +++ b/src/renderer/features/player/components/full-screen-player-queue.tsx @@ -8,7 +8,7 @@ import styles from './full-screen-player-queue.module.css'; import { Lyrics } from '/@/renderer/features/lyrics/lyrics'; import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue'; import { FullScreenSimilarSongs } from '/@/renderer/features/player/components/full-screen-similar-songs'; -import { usePlaybackSettings } from '/@/renderer/store'; +import { usePlaybackSettings, useSettingsStore } from '/@/renderer/store'; import { useFullScreenPlayerStore, useFullScreenPlayerStoreActions, @@ -17,8 +17,14 @@ import { Button } from '/@/shared/components/button/button'; import { Group } from '/@/shared/components/group/group'; import { ItemListKey, PlayerType } from '/@/shared/types/types'; -const Visualizer = lazy(() => - import('/@/renderer/features/player/components/visualizer').then((module) => ({ +const AudioMotionAnalyzerVisualizer = lazy(() => + import('../../visualizer/components/audiomotionanalyzer/visualizer').then((module) => ({ + default: module.Visualizer, + })), +); + +const ButterchurnVisualizer = lazy(() => + import('../../visualizer/components/butternchurn/visualizer').then((module) => ({ default: module.Visualizer, })), ); @@ -28,6 +34,7 @@ export const FullScreenPlayerQueue = () => { const { activeTab, opacity } = useFullScreenPlayerStore(); const { setStore } = useFullScreenPlayerStoreActions(); const { type, webAudio } = usePlaybackSettings(); + const visualizerType = useSettingsStore((store) => store.visualizer.type); const headerItems = useMemo(() => { const items = [ @@ -109,7 +116,11 @@ export const FullScreenPlayerQueue = () => { ) : activeTab === 'visualizer' && type === PlayerType.WEB && webAudio ? ( }> - + {visualizerType === 'butterchurn' ? ( + + ) : ( + + )} ) : null} diff --git a/src/renderer/features/player/components/visualizer.module.css b/src/renderer/features/player/components/visualizer.module.css deleted file mode 100644 index d9fad01d8..000000000 --- a/src/renderer/features/player/components/visualizer.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.container { - z-index: 50; - width: 100%; - height: 100%; - margin: auto; - - canvas { - width: 100%; - margin: auto; - } -} diff --git a/src/renderer/features/player/components/visualizer.tsx b/src/renderer/features/player/components/visualizer.tsx deleted file mode 100644 index 94c4cfd2b..000000000 --- a/src/renderer/features/player/components/visualizer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import AudioMotionAnalyzer from 'audiomotion-analyzer'; -import { createRef, useEffect, useState } from 'react'; - -import styles from './visualizer.module.css'; - -import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio'; -import { useSettingsStore } from '/@/renderer/store'; - -export const Visualizer = () => { - const { webAudio } = useWebAudio(); - const canvasRef = createRef(); - const accent = useSettingsStore((store) => store.general.accent); - const [motion, setMotion] = useState(); - - useEffect(() => { - const { context, gains } = webAudio || {}; - if (gains && context && canvasRef.current && !motion) { - const audioMotion = new AudioMotionAnalyzer(canvasRef.current, { - ansiBands: true, - audioCtx: context, - connectSpeakers: false, - gradient: 'prism', - ledBars: true, - mode: 8, - overlay: true, - showBgColor: false, - showPeaks: false, - showScaleX: false, - showScaleY: false, - smoothing: 0.8, - }); - setMotion(audioMotion); - for (const gain of gains) audioMotion.connectInput(gain); - } - - return () => {}; - }, [accent, canvasRef, motion, webAudio]); - - return
; -}; diff --git a/src/renderer/features/player/utils/open-visualizer-settings-modal.ts b/src/renderer/features/player/utils/open-visualizer-settings-modal.ts new file mode 100644 index 000000000..cbdf012e3 --- /dev/null +++ b/src/renderer/features/player/utils/open-visualizer-settings-modal.ts @@ -0,0 +1,25 @@ +import { openContextModal } from '@mantine/modals'; + +export const openVisualizerSettingsModal = () => { + openContextModal({ + innerProps: {}, + modalKey: 'visualizerSettings', + overlayProps: { + blur: 0, + opacity: 1, + }, + size: 'xl', + styles: { + content: { + height: '90%', + maxWidth: '1400px', + minHeight: '600px', + width: '100%', + }, + }, + title: 'Visualizer Settings', + transitionProps: { + transition: 'pop', + }, + }); +}; diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.module.css b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.module.css new file mode 100644 index 000000000..4182b6a7e --- /dev/null +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.module.css @@ -0,0 +1,11 @@ +.container { + display: flex; + flex-direction: column; + gap: var(--theme-spacing-md); + width: 100%; + margin: 0 auto; +} + +.select-label { + text-align: center; +} diff --git a/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx new file mode 100644 index 000000000..701e99a88 --- /dev/null +++ b/src/renderer/features/visualizer/components/audiomotionanalyzer/visualizer-settings-form.tsx @@ -0,0 +1,2040 @@ +import { ConstructorOptions } from 'audiomotion-analyzer'; +import butterchurnPresets from 'butterchurn-presets'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import styles from './visualizer-settings-form.module.css'; + +import { useSettingsStoreActions, useVisualizerSettings } from '/@/renderer/store/settings.store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Button } from '/@/shared/components/button/button'; +import { Checkbox } from '/@/shared/components/checkbox/checkbox'; +import { ColorInput } from '/@/shared/components/color-input/color-input'; +import { Divider } from '/@/shared/components/divider/divider'; +import { Fieldset } from '/@/shared/components/fieldset/fieldset'; +import { Group } from '/@/shared/components/group/group'; +import { MultiSelect } from '/@/shared/components/multi-select/multi-select'; +import { NumberInput } from '/@/shared/components/number-input/number-input'; +import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control'; +import { Select, SelectProps } from '/@/shared/components/select/select'; +import { Slider, SliderProps } from '/@/shared/components/slider/slider'; +import { Stack } from '/@/shared/components/stack/stack'; +import { TextInput } from '/@/shared/components/text-input/text-input'; +import { Text } from '/@/shared/components/text/text'; +import { Textarea } from '/@/shared/components/textarea/textarea'; +import { toast } from '/@/shared/components/toast/toast'; + +const modeOptions: { label: string; value: ConstructorOptions['mode'] | string }[] = [ + { label: '[0] Bars', value: '0' }, + { label: '[1] Circle', value: '1' }, + { label: '[2] Wave', value: '2' }, + { label: '[3] Rainbow', value: '3' }, + { label: '[4] Rings', value: '4' }, + { label: '[5] Mirror', value: '5' }, + { label: '[6] Line', value: '6' }, + { label: '[7] Particles', value: '7' }, + { label: '[8] Full octave / 10 bands', value: '8' }, + { label: '[10] Outline bars', value: '10' }, +]; + +const colorModeOptions: { label: string; value: ConstructorOptions['colorMode'] }[] = [ + { label: 'Gradient', value: 'gradient' }, + { label: 'Bar-Index', value: 'bar-index' }, + { label: 'Bar-Level', value: 'bar-level' }, +]; + +const gradientOptions: { label: string; value: ConstructorOptions['gradient'] }[] = [ + { label: 'Classic', value: 'classic' }, + { label: 'Prism', value: 'prism' }, + { label: 'Rainbow', value: 'rainbow' }, + { label: 'Steelblue', value: 'steelblue' }, + { label: 'Orangered', value: 'orangered' }, +]; + +const channelLayoutOptions: { label: string; value: ConstructorOptions['channelLayout'] }[] = [ + { label: 'Single', value: 'single' }, + { label: 'Dual-Combined', value: 'dual-combined' }, + { label: 'Dual-Horizontal', value: 'dual-horizontal' }, + { label: 'Dual-Vertical', value: 'dual-vertical' }, +]; + +const fftSizeOptions: { label: string; value: ConstructorOptions['fftSize'] | string }[] = [ + { label: '1024', value: '1024' }, + { label: '2048', value: '2048' }, + { label: '4096', value: '4096' }, + { label: '8192', value: '8192' }, + { label: '16384', value: '16384' }, + { label: '32768', value: '32768' }, +]; + +const frequencyScaleOptions: { label: string; value: ConstructorOptions['frequencyScale'] }[] = [ + { label: 'Bark', value: 'bark' }, + { label: 'Linear', value: 'linear' }, + { label: 'Log', value: 'log' }, + { label: 'Mel', value: 'mel' }, +]; + +const weightingFilterOptions = [ + { label: 'None', value: '' }, + { label: 'A', value: 'A' }, + { label: 'B', value: 'B' }, + { label: 'C', value: 'C' }, + { label: 'D', value: 'D' }, + { label: 'Z', value: 'Z' }, +]; + +const minFreqOptions = [ + { label: '20', value: '20' }, + { label: '30', value: '30' }, + { label: '40', value: '40' }, + { label: '50', value: '50' }, +]; + +const maxFreqOptions = [ + { label: '8000', value: '8000' }, + { label: '10000', value: '10000' }, + { label: '15000', value: '15000' }, + { label: '20000', value: '20000' }, + { label: '22050', value: '22050' }, +]; + +const barSpaceOptions = [ + { label: '0', value: '0' }, + { label: '0.1', value: '0.1' }, + { label: '0.25', value: '0.2' }, + { label: '0.4', value: '0.4' }, + { label: '0.5', value: '0.5' }, + { label: '0.75', value: '0.7' }, + { label: '1.0', value: '1.0' }, +]; + +const useUpdateAudioMotionAnalyzer = () => { + const visualizer = useVisualizerSettings(); + const { setSettings } = useSettingsStoreActions(); + + const updateProperty = ( + property: K, + value: (typeof visualizer.audiomotionanalyzer)[K], + ) => { + setSettings({ + visualizer: { + ...visualizer, + audiomotionanalyzer: { + ...visualizer.audiomotionanalyzer, + [property]: value, + }, + }, + }); + }; + + return { updateProperty, visualizer }; +}; + +const useUpdateButterchurn = () => { + const visualizer = useVisualizerSettings(); + const { setSettings } = useSettingsStoreActions(); + + const updateProperty = ( + property: K, + value: (typeof visualizer.butterchurn)[K], + ) => { + setSettings({ + visualizer: { + ...visualizer, + butterchurn: { + ...visualizer.butterchurn, + [property]: value, + }, + }, + }); + }; + + return { updateProperty, visualizer }; +}; + +export const VisualizerSettingsForm = () => { + const { t } = useTranslation(); + const visualizer = useVisualizerSettings(); + const { setSettings } = useSettingsStoreActions(); + + const visualizerTypeOptions = useMemo( + () => [ + { label: 'AudioMotion Analyzer', value: 'audiomotionanalyzer' }, + { label: 'Butterchurn', value: 'butterchurn' }, + ], + [], + ); + + const handleTypeChange = (value: string) => { + setSettings({ + visualizer: { + ...visualizer, + type: value as 'audiomotionanalyzer' | 'butterchurn', + }, + }); + }; + + return ( +
+
+ + + +
+ {visualizer.type === 'audiomotionanalyzer' && ( + <> + + + + + + + + + + + + + )} + {visualizer.type === 'butterchurn' && ( + <> + + + + )} +
+ ); +}; + +const VisualizerSelect = (props: SelectProps) => { + return