From 1500165e19a535dd01af9a2ff1fdded700675baf Mon Sep 17 00:00:00 2001 From: Adityaonx Date: Sun, 16 Aug 2026 22:24:11 +0530 Subject: [PATCH] feat(audio): implement 3D virtual surround sound and audio filters --- src-tauri/src/store/ui_state_store.rs | 45 +++ src/App.vue | 9 + src/components/PlayerControls.vue | 7 +- .../player-controls/RightControls.vue | 226 ++++++++++++- src/composables/useAppBootstrap.ts | 8 +- src/composables/useMenuControls.ts | 12 +- src/composables/useSurroundSound.ts | 312 ++++++++++++++++++ src/panels/SettingsPanel.vue | 173 ++++++++++ src/styles/panels.css | 166 ++++++++++ 9 files changed, 949 insertions(+), 9 deletions(-) create mode 100644 src/composables/useSurroundSound.ts diff --git a/src-tauri/src/store/ui_state_store.rs b/src-tauri/src/store/ui_state_store.rs index f9ff876..328715c 100644 --- a/src-tauri/src/store/ui_state_store.rs +++ b/src-tauri/src/store/ui_state_store.rs @@ -39,6 +39,8 @@ pub struct UiState { #[serde(default)] pub audio: Option, #[serde(default)] + pub surround_sound: Option, + #[serde(default)] pub playback_adjustments: Option, #[serde(default)] pub playback: Option, @@ -65,6 +67,7 @@ impl Default for UiState { settings: None, rendering: None, audio: None, + surround_sound: None, playback_adjustments: None, playback: None, subtitle_appearance: None, @@ -86,6 +89,12 @@ impl UiState { settings: incoming.settings.or(self.settings), rendering: incoming.rendering.or(self.rendering), audio: incoming.audio.or(self.audio), + surround_sound: match (self.surround_sound, incoming.surround_sound) { + (Some(curr), Some(inc)) => Some(curr.merge(inc)), + (None, Some(inc)) => Some(inc), + (Some(curr), None) => Some(curr), + (None, None) => None, + }, playback_adjustments: incoming .playback_adjustments .or(self.playback_adjustments), @@ -146,6 +155,42 @@ pub struct ColorAdjustmentsState { pub hue: Option, } +#[derive(Serialize, Deserialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct SurroundSoundState { + #[serde(default)] + pub global_enabled: Option, + #[serde(default)] + pub enabled: Option, + #[serde(default)] + pub preset: Option, + #[serde(default)] + pub surround_depth: Option, + #[serde(default)] + pub ambience: Option, + #[serde(default)] + pub clarity: Option, + #[serde(default)] + pub bass_boost: Option, + #[serde(default)] + pub dynamic_boost: Option, +} + +impl SurroundSoundState { + fn merge(self, incoming: SurroundSoundState) -> SurroundSoundState { + SurroundSoundState { + global_enabled: incoming.global_enabled.or(self.global_enabled), + enabled: incoming.enabled.or(self.enabled), + preset: incoming.preset.or(self.preset), + surround_depth: incoming.surround_depth.or(self.surround_depth), + ambience: incoming.ambience.or(self.ambience), + clarity: incoming.clarity.or(self.clarity), + bass_boost: incoming.bass_boost.or(self.bass_boost), + dynamic_boost: incoming.dynamic_boost.or(self.dynamic_boost), + } + } +} + #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SubtitleAppearanceState { diff --git a/src/App.vue b/src/App.vue index f658ac0..9c71a4d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,6 +36,7 @@ import { usePlaylistCreationPrompt } from "./composables/usePlaylistCreationProm import { usePlaybackContextMenu } from "./composables/usePlaybackContextMenu"; import { useRemoteControlQrDialog } from "./composables/useRemoteControlQrDialog"; import { useAudioOutput } from "./composables/useAudioOutput"; +import { useSurroundSound } from "./composables/useSurroundSound"; import { tauriCoreClient } from "./core-client/tauriPlaybackClient"; import { tauriPlaylistSourceClient } from "./core-client/tauriPlaylistSourceClient"; @@ -65,6 +66,7 @@ const { isPipEnabled, schedulePointerRefresh, shouldKeepControlsVisible, + showSurroundMenu, hideAllMenus, toggleMenu, closeAllMenus, @@ -83,6 +85,11 @@ const { normalizeStoredPanel, } = useAppBootstrap(tauriCoreClient); +const { + applySurroundForMedia, + reapplyFilters: reapplySurround, +} = useSurroundSound(); + const clearNavSelectionDuringLoad = ref(false); const playbackLoadingState = usePlaybackLoadingState(); const { isLoading, loadingUrl } = playbackLoadingState; @@ -455,6 +462,7 @@ const onFileLoaded = async () => { await tracks.setSubtitlesDisabled(true); } await adjustments.applyColorAdjustmentsForMedia(player.state.media.url); + await applySurroundForMedia(player.state.media.url); await subtitleAppearance.applySubtitleAppearanceOptions(); }; @@ -704,6 +712,7 @@ useAppStartupBindings({ :playback-rates="speed.playbackRates" :show-speed-menu="speed.showSpeedMenu.value" :show-settings-menu="adjustments.showSettingsMenu.value" + :show-surround-menu="showSurroundMenu" :audio-delay="adjustments.audioDelay.value" :sub-delay="adjustments.subDelay.value" :secondary-sub-delay="adjustments.secondarySubDelay.value" diff --git a/src/components/PlayerControls.vue b/src/components/PlayerControls.vue index 6528ca0..261163c 100644 --- a/src/components/PlayerControls.vue +++ b/src/components/PlayerControls.vue @@ -23,6 +23,7 @@ const props = defineProps<{ playbackRates: number[]; showSpeedMenu: boolean; showSettingsMenu: boolean; + showSurroundMenu: boolean; audioDelay: number; subDelay: number; secondarySubDelay: number; @@ -60,7 +61,10 @@ const emit = defineEmits<{ (e: "toggle-play-pause"): void; (e: "stop-playback"): void; (e: "next-track"): void; - (e: "toggle-menu", menuName: "audio" | "sub" | "speed" | "settings"): void; + ( + e: "toggle-menu", + menuName: "audio" | "sub" | "speed" | "settings" | "surround", + ): void; (e: "toggle-loop-one"): void; (e: "set-speed", rate: number): void; (e: "set-speed-continuously", rate: number): void; @@ -253,6 +257,7 @@ onUnmounted(() => { :playback-rates="playbackRates" :show-speed-menu="showSpeedMenu" :show-settings-menu="showSettingsMenu" + :show-surround-menu="showSurroundMenu" :speed-locked="audioPassthroughActive" :audio-delay="audioDelay" :sub-delay="subDelay" diff --git a/src/components/player-controls/RightControls.vue b/src/components/player-controls/RightControls.vue index 0af1aae..c93d070 100644 --- a/src/components/player-controls/RightControls.vue +++ b/src/components/player-controls/RightControls.vue @@ -13,12 +13,17 @@ import { getSubtitleTrackTitle, } from "../../utils/trackDisplay"; import ControlSlider from "./ControlSlider.vue"; +import { + useSurroundSound, + type SurroundPreset, +} from "../../composables/useSurroundSound"; const props = defineProps<{ currentSpeed: number; playbackRates: number[]; showSpeedMenu: boolean; showSettingsMenu: boolean; + showSurroundMenu: boolean; speedLocked: boolean; audioDelay: number; subDelay: number; @@ -52,7 +57,10 @@ const props = defineProps<{ }>(); const emit = defineEmits<{ - (e: "toggle-menu", menuName: "audio" | "sub" | "speed" | "settings"): void; + ( + e: "toggle-menu", + menuName: "audio" | "sub" | "speed" | "settings" | "surround", + ): void; (e: "toggle-loop-one"): void; (e: "set-speed", rate: number): void; (e: "set-speed-continuously", rate: number): void; @@ -114,6 +122,16 @@ const subtitleFontOptions = [ const isSameTrackId = (left: MediaTrack["id"], right: MediaTrack["id"]) => String(left) === String(right); + +const showSurroundMenu = computed(() => props.showSurroundMenu); +const { + surroundState, + globalSurroundEnabled, + setGlobalSurroundEnabled, + setEnabled: setSurroundEnabled, + setPreset: setSurroundPreset, + setParam: setSurroundParam, +} = useSurroundSound(); const activeSubTarget = computed(() => props.activeSubTarget); const activeSubFontFamily = computed(() => props.primarySubFontFamily, @@ -946,6 +964,212 @@ watch( +
+ + + +
+
+ 3D Audio +
+ + +
+
+ +
+ +
+ + +
+ + +
+
+
+ Surround Depth + Stereo field width +
+ + {{ surroundState.surroundDepth }} +
+ +
+
+ Ambience + Room reverb / echo +
+ + {{ surroundState.ambience }} +
+ +
+
+ Clarity + Treble presence +
+ + {{ surroundState.clarity }} +
+ +
+
+ Bass Boost + Low-frequency gain +
+ + {{ surroundState.bassBoost }} +
+ +
+
+ Dynamic Boost + Loudness levelling +
+ + {{ surroundState.dynamicBoost }} +
+
+
+
+
+
+ + + +