diff --git a/pyproject.toml b/pyproject.toml index 49cbcad..dbcb0ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "nvda_voicemeeter" -version = "0.4.2a1" +version = "0.5.0a1" description = "A Voicemeeter app compatible with NVDA" authors = [ { name = "onyx-and-iris", email = "code@onyxandiris.online" }, @@ -35,16 +35,60 @@ shell = "build.ps1" line-length = 119 [tool.ruff] -# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. -select = ["E", "F"] -# Avoid enforcing line-length violations (`E501`). Let Black deal with this. -ignore = ["E501"] - -# Allow autofix for all enabled rules (when `--fix`) is provided. -fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"] +select = [ + "E", + "F", +] +ignore = [ + "E501", +] +fixable = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "I", + "N", + "Q", + "S", + "T", + "W", + "ANN", + "ARG", + "BLE", + "COM", + "DJ", + "DTZ", + "EM", + "ERA", + "EXE", + "FBT", + "ICN", + "INP", + "ISC", + "NPY", + "PD", + "PGH", + "PIE", + "PL", + "PT", + "PTH", + "PYI", + "RET", + "RSE", + "RUF", + "SIM", + "SLF", + "TCH", + "TID", + "TRY", + "UP", + "YTT", +] unfixable = [] - -# Exclude a variety of commonly ignored directories. exclude = [ ".bzr", ".direnv", @@ -68,19 +112,15 @@ exclude = [ "node_modules", "venv", ] - -# Same as Black. line-length = 119 - -# Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" - -# Assume Python 3.10 target-version = "py310" [tool.ruff.mccabe] -# Unlike Flake8, default to a complexity level of 10. max-complexity = 10 [tool.ruff.per-file-ignores] -"__init__.py" = ["E402", "F401"] # Ignore unused import and variable not accessed violations \ No newline at end of file +"__init__.py" = [ + "E402", + "F401", +] diff --git a/src/nvda_voicemeeter/compound.py b/src/nvda_voicemeeter/compound.py index 2d6bd0f..0dc08c7 100644 --- a/src/nvda_voicemeeter/compound.py +++ b/src/nvda_voicemeeter/compound.py @@ -1,5 +1,9 @@ +from typing import Union + import PySimpleGUI as psg +from . import util + class LabelSlider(psg.Frame): """Compound Label Slider Strip element""" @@ -46,6 +50,7 @@ class CompSlider(psg.Slider): expand_x=True, enable_events=True, orientation="horizontal", + key=f"COMPRESSOR||SLIDER {param}", **self.default_params(param), ) @@ -57,42 +62,36 @@ class CompSlider(psg.Slider): "default_value": self.vm.strip[self.index].comp.gainin, "resolution": 0.1, "disabled": True, - "key": f"COMPRESSOR||SLIDER {param}", } case "RATIO": return { "range": (1, 8), "default_value": self.vm.strip[self.index].comp.ratio, "resolution": 0.1, - "key": f"COMPRESSOR||SLIDER {param}", } case "THRESHOLD": return { "range": (-40, -3), "default_value": self.vm.strip[self.index].comp.threshold, "resolution": 0.1, - "key": f"COMPRESSOR||SLIDER {param}", } case "ATTACK": return { "range": (0, 200), "default_value": self.vm.strip[self.index].comp.attack, "resolution": 0.1, - "key": f"COMPRESSOR||SLIDER {param}", } case "RELEASE": return { "range": (0, 5000), "default_value": self.vm.strip[self.index].comp.release, "resolution": 0.1, - "key": f"COMPRESSOR||SLIDER {param}", } case "KNEE": return { "range": (0, 1), "default_value": self.vm.strip[self.index].comp.knee, "resolution": 0.01, - "key": f"COMPRESSOR||SLIDER {param}", } case "OUTPUT GAIN": return { @@ -100,18 +99,102 @@ class CompSlider(psg.Slider): "default_value": self.vm.strip[self.index].comp.gainout, "resolution": 0.01, "disabled": True, - "key": f"COMPRESSOR||SLIDER {param}", } + @staticmethod + def check_bounds(param, val): + match param: + case "RATIO": + val = util.check_bounds(val, (1, 8)) + case "THRESHOLD": + val = util.check_bounds(val, (-40, -3)) + case "ATTACK": + val = util.check_bounds(val, (0, 200)) + case "RELEASE": + val = util.check_bounds(val, (0, 5000)) + case "KNEE": + val = util.check_bounds(val, (0, 1)) + return val -class LabelSliderCompressor(psg.Frame): - """Compound Label Slider Compressor element""" - def __init__(self, parent, index, param, *args, **kwargs): +class GateSlider(psg.Slider): + def __init__(self, vm, index, param): + self.vm = vm + self.index = index + super().__init__( + disable_number_display=True, + expand_x=True, + enable_events=True, + orientation="horizontal", + key=f"GATE||SLIDER {param}", + **self.default_params(param), + ) + + def default_params(self, param): + match param: + case "THRESHOLD": + return { + "range": (-60, -10), + "default_value": self.vm.strip[self.index].gate.threshold, + "resolution": 0.1, + } + case "DAMPING": + return { + "range": (-60, -10), + "default_value": self.vm.strip[self.index].gate.damping, + "resolution": 0.1, + } + case "BPSIDECHAIN": + return { + "range": (100, 4000), + "default_value": self.vm.strip[self.index].gate.bpsidechain, + "resolution": 1, + } + case "ATTACK": + return { + "range": (0, 1000), + "default_value": self.vm.strip[self.index].gate.attack, + "resolution": 0.1, + } + case "HOLD": + return { + "range": (0, 5000), + "default_value": self.vm.strip[self.index].gate.hold, + "resolution": 0.1, + } + case "RELEASE": + return { + "range": (0, 5000), + "default_value": self.vm.strip[self.index].gate.release, + "resolution": 0.1, + } + + @staticmethod + def check_bounds(param, val): + match param: + case "THRESHOLD": + val = util.check_bounds(val, (-60, -10)) + case "DAMPING MAX": + val = util.check_bounds(val, (-60, -10)) + case "BPSIDECHAIN": + val = util.check_bounds(val, (100, 4000)) + case "ATTACK": + val = util.check_bounds(val, (0, 1000)) + case "HOLD": + val = util.check_bounds(val, (0, 5000)) + case "RELEASE": + val = util.check_bounds(val, (0, 5000)) + return val + + +class LabelSliderAdvanced(psg.Frame): + """Compound Label Slider element for Advanced Comp|Gate""" + + def __init__(self, parent, index, param, slider_cls: Union[CompSlider, GateSlider], *args, **kwargs): layout = [ [ psg.Text(param.capitalize(), size=8), - CompSlider(parent.vm, index, param), + slider_cls(parent.vm, index, param), ] ] super().__init__(None, layout=layout, border_width=0, pad=0, *args, **kwargs) diff --git a/src/nvda_voicemeeter/popup.py b/src/nvda_voicemeeter/popup.py index 5cb061f..0688ca3 100644 --- a/src/nvda_voicemeeter/popup.py +++ b/src/nvda_voicemeeter/popup.py @@ -4,7 +4,7 @@ from pathlib import Path import PySimpleGUI as psg from . import util -from .compound import LabelSliderCompressor +from .compound import CompSlider, GateSlider, LabelSliderAdvanced logger = logging.getLogger(__name__) @@ -160,7 +160,7 @@ class Popup: def compressor(self, index, title=None): def _make_comp_frame() -> psg.Frame: comp_layout = [ - [LabelSliderCompressor(self.window, index, param)] + [LabelSliderAdvanced(self.window, index, param, CompSlider)] for param in ("INPUT GAIN", "RATIO", "THRESHOLD", "ATTACK", "RELEASE", "KNEE", "OUTPUT GAIN") ] return psg.Frame("ADVANCED COMPRESSOR", comp_layout) @@ -232,17 +232,7 @@ class Popup: else: val -= 1 - match param: - case "RATIO": - val = util.check_bounds(val, (1, 8)) - case "THRESHOLD": - val = util.check_bounds(val, (-40, -3)) - case "ATTACK": - val = util.check_bounds(val, (0, 200)) - case "RELEASE": - val = util.check_bounds(val, (0, 5000)) - case "KNEE": - val = util.check_bounds(val, (0, 1)) + val = CompSlider.check_bounds(param, val) setattr(self.window.vm.strip[index].comp, param.lower(), val) popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) @@ -277,17 +267,7 @@ class Popup: else: val -= 3 - match param: - case "RATIO": - val = util.check_bounds(val, (1, 8)) - case "THRESHOLD": - val = util.check_bounds(val, (-40, -3)) - case "ATTACK": - val = util.check_bounds(val, (0, 200)) - case "RELEASE": - val = util.check_bounds(val, (0, 5000)) - case "KNEE": - val = util.check_bounds(val, (0, 1)) + val = CompSlider.check_bounds(param, val) setattr(self.window.vm.strip[index].comp, param.lower(), val) popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) @@ -318,17 +298,7 @@ class Popup: else: val -= 0.1 - match param: - case "RATIO": - val = util.check_bounds(val, (1, 8)) - case "THRESHOLD": - val = util.check_bounds(val, (-40, -3)) - case "ATTACK": - val = util.check_bounds(val, (0, 200)) - case "RELEASE": - val = util.check_bounds(val, (0, 5000)) - case "KNEE": - val = util.check_bounds(val, (0, 1)) + val = CompSlider.check_bounds(param, val) setattr(self.window.vm.strip[index].comp, param.lower(), val) popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) @@ -477,10 +447,140 @@ class Popup: self.window.nvda.speak("on" if val else "off") case [[button], ["FOCUS", "IN"]]: if button == "MAKEUP": - self.window.nvda.speak(f"{button} {'on' if self.window.vm.strip[index].comp.makeup else 'off'}") + self.window.nvda.speak( + f"{button} {'on' if self.window.vm.strip[index].comp.makeup else 'off'}" + ) else: self.window.nvda.speak(button) case [_, ["KEY", "ENTER"]]: popup.find_element_with_focus().click() self.logger.debug(f"parsed::{parsed_cmd}") popup.close() + + def gate(self, index, title=None): + def _make_gate_frame() -> psg.Frame: + gate_layout = [ + [LabelSliderAdvanced(self.window, index, param, GateSlider)] + for param in ("THRESHOLD", "DAMPING", "BPSIDECHAIN", "ATTACK", "HOLD", "RELEASE") + ] + return psg.Frame("ADVANCED GATE", gate_layout) + + layout = [] + steps = (_make_gate_frame,) + for step in steps: + layout.append([step()]) + layout.append([psg.Button("Exit", size=(8, 1))]) + + popup = psg.Window(title, layout, return_keyboard_events=False, finalize=True) + buttonmenu_opts = {"takefocus": 1, "highlightthickness": 1} + for param in ("THRESHOLD", "DAMPING", "BPSIDECHAIN", "ATTACK", "HOLD", "RELEASE"): + popup[f"GATE||SLIDER {param}"].Widget.config(**buttonmenu_opts) + popup[f"GATE||SLIDER {param}"].bind("", "||FOCUS IN") + popup[f"GATE||SLIDER {param}"].bind("", "||FOCUS OUT") + for event in ("KeyPress", "KeyRelease"): + event_id = event.removeprefix("Key").upper() + for direction in ("Left", "Right", "Up", "Down"): + popup[f"GATE||SLIDER {param}"].bind( + f"<{event}-{direction}>", f"||KEY {direction.upper()} {event_id}" + ) + popup[f"GATE||SLIDER {param}"].bind( + f"", f"||KEY SHIFT {direction.upper()} {event_id}" + ) + popup[f"GATE||SLIDER {param}"].bind( + f"", f"||KEY CTRL {direction.upper()} {event_id}" + ) + popup["Exit"].bind("", "||FOCUS IN") + popup["Exit"].bind("", "||KEY ENTER") + while True: + event, values = popup.read() + self.logger.debug(f"event::{event}") + self.logger.debug(f"values::{values}") + if event in (psg.WIN_CLOSED, "Exit"): + break + match parsed_cmd := self.window.parser.match.parseString(event): + case [["GATE"], ["SLIDER", param]]: + setattr(self.window.vm.strip[index].gate, param.lower(), values[event]) + case [["GATE"], ["SLIDER", param], ["FOCUS", "IN"]]: + self.window.nvda.speak(f"{param} {values[f'GATE||SLIDER {param}']}") + + case [ + ["GATE"], + ["SLIDER", param], + ["KEY", "LEFT" | "RIGHT" | "UP" | "DOWN" as input_direction, "PRESS" | "RELEASE" as e], + ]: + if e == "PRESS": + self.window.vm.event.pdirty = False + val = getattr(self.window.vm.strip[index].gate, param.lower()) + + match input_direction: + case "RIGHT" | "UP": + val += 1 + case "LEFT" | "DOWN": + val -= 1 + + val = GateSlider.check_bounds(param, val) + + setattr(self.window.vm.strip[index].gate, param.lower(), val) + popup[f"GATE||SLIDER {param}"].update(value=val) + if param == "KNEE": + self.window.nvda.speak(str(round(val, 2))) + else: + self.window.nvda.speak(str(round(val, 1))) + else: + self.window.vm.event.pdirty = True + case [ + ["GATE"], + ["SLIDER", param], + ["KEY", "CTRL", "LEFT" | "RIGHT" | "UP" | "DOWN" as input_direction, "PRESS" | "RELEASE" as e], + ]: + if e == "PRESS": + self.window.vm.event.pdirty = False + val = getattr(self.window.vm.strip[index].gate, param.lower()) + + match input_direction: + case "RIGHT" | "UP": + val += 3 + case "LEFT" | "DOWN": + val -= 3 + + val = GateSlider.check_bounds(param, val) + + setattr(self.window.vm.strip[index].gate, param.lower(), val) + popup[f"GATE||SLIDER {param}"].update(value=val) + if param == "KNEE": + self.window.nvda.speak(str(round(val, 2))) + else: + self.window.nvda.speak(str(round(val, 1))) + else: + self.window.vm.event.pdirty = True + case [ + ["GATE"], + ["SLIDER", param], + ["KEY", "SHIFT", "LEFT" | "RIGHT" | "UP" | "DOWN" as input_direction, "PRESS" | "RELEASE" as e], + ]: + if e == "PRESS": + self.window.vm.event.pdirty = False + val = getattr(self.window.vm.strip[index].gate, param.lower()) + + match input_direction: + case "RIGHT" | "UP": + val += 0.1 + case "LEFT" | "DOWN": + val -= 0.1 + + val = GateSlider.check_bounds(param, val) + + setattr(self.window.vm.strip[index].gate, param.lower(), val) + popup[f"GATE||SLIDER {param}"].update(value=val) + if param == "KNEE": + self.window.nvda.speak(str(round(val, 2))) + else: + self.window.nvda.speak(str(round(val, 1))) + else: + self.window.vm.event.pdirty = True + + case [_, ["KEY", "ENTER"]]: + popup.find_element_with_focus().click() + + self.logger.debug(f"parsed::{parsed_cmd}") + popup.close() diff --git a/src/nvda_voicemeeter/window.py b/src/nvda_voicemeeter/window.py index 500aed7..a0c63e4 100644 --- a/src/nvda_voicemeeter/window.py +++ b/src/nvda_voicemeeter/window.py @@ -458,6 +458,8 @@ class NVDAVMWindow(psg.Window): _, index = identifier.split() if "SLIDER COMP" in partial: self.popup.compressor(int(index), title="Advanced Compressor") + elif "SLIDER GATE" in partial: + self.popup.gate(int(index), title="Advanced Gate") # Menus case [["Restart", "Audio", "Engine"], ["MENU"]]: