Compare commits

..

28 Commits

Author SHA1 Message Date
dacc972b17 rename section in readme 2023-11-19 22:50:29 +00:00
2ed1cad666 minor bump
closes #19
2023-11-19 22:45:57 +00:00
64361b2011 High Contrast theme added.
Sets GUI font to Arial, size 14

version bumped to 0.5.7b2

Issue #19
2023-11-19 01:22:40 +00:00
36003fe73f theme list updated.
themes section added to readme

version bumped to 0.5.7b1

Issue #19
2023-11-16 15:43:39 +00:00
cb82033e1c theme menu initial implementation
bump to 0.5.7a1

Issue #19
2023-11-15 23:34:53 +00:00
bde9020471 closes #17 2023-10-29 19:26:03 +00:00
364b0deeb4 bump to v0.5.6b1
addresses issue #17
2023-10-26 21:06:06 +01:00
a80e4e2d83 Patch BUS to A1 ASIO Outputs implemented
Patch ASIO Inputs to Strips moved to Advanced Settings
2023-10-26 20:12:49 +01:00
6e146bac50 upd images 2023-10-04 05:22:08 +01:00
c385476cc4 adjust cache to match strip layout
event keys updated

_get_bus_assignments added to util.py

patch bump
2023-09-29 20:08:18 +01:00
1c09556c61 adds missing karaoke mode k v
patch bump
2023-09-29 18:30:53 +01:00
421688eff8 resizes the patch composite buttons
fixes possible (but unlikely) index out of range.

patch bump
2023-09-29 13:38:40 +01:00
bdd570738a fix version bump... 2023-09-29 01:18:57 +01:00
71b137a9c2 use self.kind 2023-09-29 01:16:48 +01:00
912eb8c14d number of patch composite buttons fixed
for banana and potato kinds

voicemeeter-api dependency bump

patch bump
2023-09-29 01:09:18 +01:00
9cd65737e5 bump to version 0.5.1
closes #16
2023-09-28 23:35:02 +01:00
4a6ca2a353 check against slider modes explicitly
add enter/exit slider mode debug logging
2023-09-28 22:43:06 +01:00
cc99b14e89 define space bind event for bus buttons 2023-09-28 21:35:05 +01:00
23458debaa initial busmode buttonmenu implementation
bump to 0.5.1a1
Issue #16
2023-09-28 19:44:42 +01:00
496cc35321 fixes comp output gain resolution 2023-09-28 18:18:26 +01:00
d758db9dee lint/format fixes 2023-09-28 16:32:52 +01:00
876de55ad2 Advanced Comp|Gate sliders implemented
bump to 0.5.0

closes #14
2023-09-27 23:34:07 +01:00
eab4b1c6a9 fixes unboundlocalerror 2023-09-27 13:36:48 +01:00
0aeb33608f implements reset binds
Advanced Comp|Gate section added to README.

bump to version 0.5.0b1
2023-09-26 23:51:34 +01:00
5befe72ca1 adds pdirty events for advanced comp/gate 2023-09-26 19:38:55 +01:00
abab560281 +10, +50 step binds added for gate
bump to version 0.5.0a2
Initial gate slider implementation
for Issue #14
2023-09-26 17:20:15 +01:00
6882adb47b gate as no Knee parameter 2023-09-26 15:56:12 +01:00
af602e087d GateSlider added to compound
Advanced Gate slider layout and events defined
needs more testing.
2023-09-26 15:53:43 +01:00
14 changed files with 900 additions and 304 deletions

View File

@@ -107,15 +107,35 @@ All sliders may be controlled in three different ways:
- `Shift + Left|Right arrow` to move a slider by 0.1 steps. - `Shift + Left|Right arrow` to move a slider by 0.1 steps.
- `Control + Left|Right arrow` to move a slider by 3 steps. - `Control + Left|Right arrow` to move a slider by 3 steps.
To reset a slider back to its default value you may use `Control + Shift + R`.
To rename a strip/bus channel focus on the channel in question and press `F2`. Then enter the new channel name into the text input widget and press the `Ok` button. To rename a strip/bus channel focus on the channel in question and press `F2`. Then enter the new channel name into the text input widget and press the `Ok` button.
Pressing the `OK` button with an empty text input will clear the label. In this case the label will be read as a default value for that channel. For example, if the leftmost Strip label were cleared, the screen reader will now read `Hardware Input 1`. Pressing the `OK` button with an empty text input will clear the label. In this case the label will be read as a default value for that channel. For example, if the leftmost Strip label were cleared, the screen reader will now read `Hardware Input 1`.
Pressing `Cancel` will close the popup window with no affect on the label. Pressing `Cancel` will close the popup window with no affect on the label.
#### `Menu` #### `Advanced Compressor|Gate`
A single menu item `Voicemeeter` can be opened using `Alt` and then `v`. The menu allows you to: For potato version only, you may access advanced Compressor and Gate sliders. Simply focus any Gate or Compressor slider and press `Control + A`. This will open a popup window where you can navigate between the different sliders with `TAB`. Move the sliders with the same binds you would for normal sliders. However, there are a couple of extra binds for certain controls.
For Compressor Release you may use:
- `Alt + Left|Right arrow` to move the slider by 10 steps.
- `Alt + Control + Left|Right arrow` to move the slider by 50 steps.
For Gate BP Sidechain, Attack, Hold, Release you may use:
- `Alt + Left|Right arrow` to move the slider by 10 steps.
- `Alt + Control + Left|Right arrow` to move the slider by 50 steps.
To reset a slider back to its default value you may use `Control + Shift + R`.
### Menu
#### `Voicemeeter`
The `Voicemeeter` menu can be opened using `Alt` and then `v`. It offers the following options:
- Restart Voicemeeter audio engine - Restart Voicemeeter audio engine
- Save/Load current settings (as an xml file) - Save/Load current settings (as an xml file)
@@ -125,7 +145,11 @@ The `Save Settings` option opens a popup window with two buttons, `Browse` and `
`Load Settings` and `Load on Startup` both open an Open dialog box immediately. `Load Settings` and `Load on Startup` both open an Open dialog box immediately.
### `Quick access binds` #### `Theme`
The `Theme` menu can be opened using `Alt` and then `t`. Use this menu to select from a list of coloured themes. Some themes offer higher contrast colours. An application restart is required to load a new theme. Once a theme is selected it will become the default for future startups.
### Quick access binds
There are a number of quick binds available to assist with faster navigation and general use. There are a number of quick binds available to assist with faster navigation and general use.

BIN
img/busmode_buttonmenu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 777 KiB

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

48
pdm.lock generated
View File

@@ -3,10 +3,9 @@
[metadata] [metadata]
groups = ["default", "build", "lint", "test"] groups = ["default", "build", "lint", "test"]
cross_platform = true strategy = ["cross_platform"]
static_urls = false lock_version = "4.4"
lock_version = "4.3" content_hash = "sha256:4295f6824f37484ec423b53bd425334b0e039a51d81261b52c8890928fcf3948"
content_hash = "sha256:680eff1b532e55860290380d4e2f331dc29af6fb898a0df16fdb033843bf15a4"
[[package]] [[package]]
name = "altgraph" name = "altgraph"
@@ -75,6 +74,31 @@ files = [
{file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"}, {file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"},
] ]
[[package]]
name = "mypy"
version = "1.7.0"
requires_python = ">=3.8"
summary = "Optional static typing for Python"
dependencies = [
"mypy-extensions>=1.0.0",
"tomli>=1.1.0; python_version < \"3.11\"",
"typing-extensions>=4.1.0",
]
files = [
{file = "mypy-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5da84d7bf257fd8f66b4f759a904fd2c5a765f70d8b52dde62b521972a0a2357"},
{file = "mypy-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3637c03f4025f6405737570d6cbfa4f1400eb3c649317634d273687a09ffc2f"},
{file = "mypy-1.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b633f188fc5ae1b6edca39dae566974d7ef4e9aaaae00bc36efe1f855e5173ac"},
{file = "mypy-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d6ed9a3997b90c6f891138e3f83fb8f475c74db4ccaa942a1c7bf99e83a989a1"},
{file = "mypy-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:1fe46e96ae319df21359c8db77e1aecac8e5949da4773c0274c0ef3d8d1268a9"},
{file = "mypy-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:df67fbeb666ee8828f675fee724cc2cbd2e4828cc3df56703e02fe6a421b7401"},
{file = "mypy-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a79cdc12a02eb526d808a32a934c6fe6df07b05f3573d210e41808020aed8b5d"},
{file = "mypy-1.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65f385a6f43211effe8c682e8ec3f55d79391f70a201575def73d08db68ead1"},
{file = "mypy-1.7.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e81ffd120ee24959b449b647c4b2fbfcf8acf3465e082b8d58fd6c4c2b27e46"},
{file = "mypy-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:f29386804c3577c83d76520abf18cfcd7d68264c7e431c5907d250ab502658ee"},
{file = "mypy-1.7.0-py3-none-any.whl", hash = "sha256:96650d9a4c651bc2a4991cf46f100973f656d69edc7faf91844e87fe627f7e96"},
{file = "mypy-1.7.0.tar.gz", hash = "sha256:1e280b5697202efa698372d2f39e9a6713a0395a756b1c6bd48995f8d72690dc"},
]
[[package]] [[package]]
name = "mypy-extensions" name = "mypy-extensions"
version = "1.0.0" version = "1.0.0"
@@ -249,15 +273,25 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
[[package]]
name = "typing-extensions"
version = "4.8.0"
requires_python = ">=3.8"
summary = "Backported and Experimental Type Hints for Python 3.8+"
files = [
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
]
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.4.10" version = "2.5.2"
requires_python = ">=3.10,<4.0" requires_python = ">=3.10,<4.0"
summary = "A Python wrapper for the Voiceemeter API" summary = "A Python wrapper for the Voiceemeter API"
dependencies = [ dependencies = [
"tomli<3.0.0,>=2.0.1; python_version < \"3.11\"", "tomli<3.0.0,>=2.0.1; python_version < \"3.11\"",
] ]
files = [ files = [
{file = "voicemeeter_api-2.4.10-py3-none-any.whl", hash = "sha256:2f75acb7b472e56b6bd8d4f1141f32d948c55ef9b30d5a08e085a1c8e76e2464"}, {file = "voicemeeter_api-2.5.2-py3-none-any.whl", hash = "sha256:304c14a6eef5f95d8b883e8e1752d23f17c3d662d25a28d114d33134f62e93ec"},
{file = "voicemeeter_api-2.4.10.tar.gz", hash = "sha256:1d8dfc1e8922179f8b97c90b90b9ed051082018c6af5feb1d48250140a02d40c"}, {file = "voicemeeter_api-2.5.2.tar.gz", hash = "sha256:e2b9558a38ed290b184a3e46b598c0a716cef2e64521431f84a46e1180d539ca"},
] ]

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "nvda_voicemeeter" name = "nvda_voicemeeter"
version = "0.4.2a1" version = "0.6.0"
description = "A Voicemeeter app compatible with NVDA" description = "A Voicemeeter app compatible with NVDA"
authors = [ authors = [
{ name = "onyx-and-iris", email = "code@onyxandiris.online" }, { name = "onyx-and-iris", email = "code@onyxandiris.online" },
@@ -8,7 +8,7 @@ authors = [
dependencies = [ dependencies = [
"pysimplegui>=4.60.5", "pysimplegui>=4.60.5",
"pyparsing>=3.1.1", "pyparsing>=3.1.1",
"voicemeeter-api>=2.4.10", "voicemeeter-api>=2.5.2",
] ]
requires-python = ">=3.10,<3.12" requires-python = ">=3.10,<3.12"
readme = "README.md" readme = "README.md"
@@ -23,6 +23,7 @@ build = [
lint = [ lint = [
"black>=23.7.0", "black>=23.7.0",
"ruff>=0.0.291", "ruff>=0.0.291",
"mypy>=1.7.0",
] ]
test = [ test = [
"psgdemos>=1.12.1", "psgdemos>=1.12.1",
@@ -35,16 +36,60 @@ shell = "build.ps1"
line-length = 119 line-length = 119
[tool.ruff] [tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. select = [
select = ["E", "F"] "E",
# Avoid enforcing line-length violations (`E501`). Let Black deal with this. "F",
ignore = ["E501"] ]
ignore = [
# Allow autofix for all enabled rules (when `--fix`) is provided. "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"] ]
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 = [] unfixable = []
# Exclude a variety of commonly ignored directories.
exclude = [ exclude = [
".bzr", ".bzr",
".direnv", ".direnv",
@@ -68,19 +113,15 @@ exclude = [
"node_modules", "node_modules",
"venv", "venv",
] ]
# Same as Black.
line-length = 119 line-length = 119
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10
target-version = "py310" target-version = "py310"
[tool.ruff.mccabe] [tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10 max-complexity = 10
[tool.ruff.per-file-ignores] [tool.ruff.per-file-ignores]
"__init__.py" = ["E402", "F401"] # Ignore unused import and variable not accessed violations "__init__.py" = [
"E402",
"F401",
]

View File

@@ -26,7 +26,6 @@ class Builder:
steps = ( steps = (
self.make_tab0_row0, self.make_tab0_row0,
self.make_tab0_row1, self.make_tab0_row1,
self.make_tab0_row2,
self.make_tab0_row3, self.make_tab0_row3,
self.make_tab0_row4, self.make_tab0_row4,
self.make_tab0_row5, self.make_tab0_row5,
@@ -93,6 +92,8 @@ class Builder:
return [[menu], [tab_group]] return [[menu], [tab_group]]
def make_menu(self) -> psg.Menu: def make_menu(self) -> psg.Menu:
themes = [f"{theme}::MENU THEME" for theme in util.get_themes_list()]
themes.append("Default::MENU THEME")
menu_def = [ menu_def = [
[ [
"&Voicemeeter", "&Voicemeeter",
@@ -103,6 +104,7 @@ class Builder:
"Load Settings on Startup ::MENU", "Load Settings on Startup ::MENU",
], ],
], ],
["&Theme", themes],
] ]
return psg.Menu(menu_def, key="menus") return psg.Menu(menu_def, key="menus")
@@ -152,61 +154,20 @@ class Builder:
[step(hardware_out) for step in (add_physical_device_opts,)] [step(hardware_out) for step in (add_physical_device_opts,)]
return psg.Frame("Hardware Out", hardware_out) return psg.Frame("Hardware Out", hardware_out)
def make_tab0_row2(self) -> psg.Frame:
"""tab0 row2 represents patch asio inputs to strips"""
def add_asio_checkboxes(layout, i):
nums = list(range(99))
layout.append(
[
psg.Spin(
nums,
initial_value=self.window.cache["asio"][
f"ASIO CHECKBOX||{util.get_asio_checkbox_index(0, i)}"
],
size=2,
enable_events=True,
key=f"ASIO CHECKBOX||IN{i} 0",
)
],
)
layout.append(
[
psg.Spin(
nums,
initial_value=self.window.cache["asio"][
f"ASIO CHECKBOX||{util.get_asio_checkbox_index(1, i)}"
],
size=2,
enable_events=True,
key=f"ASIO CHECKBOX||IN{i} 1",
)
],
)
inner = []
asio_checkboxlists = ([] for _ in range(self.kind.phys_out))
for i, checkbox_list in enumerate(asio_checkboxlists):
[step(checkbox_list, i + 1) for step in (add_asio_checkboxes,)]
inner.append(psg.Frame(f"In#{i + 1}", checkbox_list))
asio_checkboxes = [inner]
return psg.Frame("PATCH ASIO Inputs to Strips", asio_checkboxes)
def make_tab0_row3(self) -> psg.Frame: def make_tab0_row3(self) -> psg.Frame:
"""tab0 row3 represents patch composite""" """tab0 row3 represents patch composite"""
def add_physical_device_opts(layout): def add_physical_device_opts(layout):
outputs = util.get_patch_composite_list(self.vm.kind) outputs = util.get_patch_composite_list(self.kind)
layout.append( layout.append(
[ [
psg.ButtonMenu( psg.ButtonMenu(
f"PC{i + 1}", f"PC{i + 1}",
size=(6, 2), size=(5, 2),
menu_def=["", outputs], menu_def=["", outputs],
key=f"PATCH COMPOSITE||PC{i + 1}", key=f"PATCH COMPOSITE||PC{i + 1}",
) )
for i in range(self.kind.phys_out) for i in range(self.kind.composite)
] ]
) )
@@ -381,7 +342,7 @@ class Builder:
if i == self.kind.phys_in + 1: if i == self.kind.phys_in + 1:
layout.append( layout.append(
[ [
psg.Button("K", size=(6, 2), key=f"STRIP {i}||MONO"), psg.Button("K", size=(6, 2), key=f"STRIP {i}||KARAOKE"),
psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"), psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"),
psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"), psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
], ],
@@ -389,7 +350,7 @@ class Builder:
else: else:
layout.append( layout.append(
[ [
psg.Button("MC", size=(6, 2), key=f"STRIP {i}||MONO"), psg.Button("MC", size=(6, 2), key=f"STRIP {i}||MC"),
psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"), psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"),
psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"), psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
], ],
@@ -478,18 +439,26 @@ class Builder:
"""tab3 row represents bus composite toggle""" """tab3 row represents bus composite toggle"""
def add_strip_outputs(layout): def add_strip_outputs(layout):
params = ["MONO", "EQ", "MUTE", "MODE"] params = ["MONO", "EQ", "MUTE"]
if self.vm.kind.name == "basic": if self.kind.name == "basic":
params.remove("EQ") params.remove("EQ")
label = {"MODE": "BUSMODE"} busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
layout.append( layout.append(
[ [
*[
psg.Button( psg.Button(
label.get(param, param.capitalize()), param.capitalize(),
size=(12 if param == "MODE" else 6, 2), size=(6, 2),
key=f"BUS {i}||{param}", key=f"BUS {i}||{param}",
) )
for param in params for param in params
],
psg.ButtonMenu(
"BUSMODE",
size=(12, 2),
menu_def=["", busmodes],
key=f"BUS {i}||MODE",
),
] ]
) )

View File

@@ -5,7 +5,7 @@ from pathlib import Path
from .errors import NVDAVMError from .errors import NVDAVMError
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32 bits = 64 if ct.sizeof(ct.c_void_p) == 8 else 32
if platform.system() != "Windows": if platform.system() != "Windows":
raise NVDAVMError("Only Windows OS supported") raise NVDAVMError("Only Windows OS supported")
@@ -33,7 +33,7 @@ def get_nvdapath():
try: try:
NVDA_PATH = Path(get_nvdapath()) / "nvda.exe" NVDA_PATH = Path(get_nvdapath()) / "nvda.exe"
except FileNotFoundError as e: except FileNotFoundError:
NVDA_PATH = "" NVDA_PATH = ""

View File

@@ -1,5 +1,9 @@
from typing import Union
import PySimpleGUI as psg import PySimpleGUI as psg
from . import util
class LabelSlider(psg.Frame): class LabelSlider(psg.Frame):
"""Compound Label Slider Strip element""" """Compound Label Slider Strip element"""
@@ -8,6 +12,9 @@ class LabelSlider(psg.Frame):
self.parent = parent self.parent = parent
if param in ("AUDIBILITY", "DENOISER"): if param in ("AUDIBILITY", "DENOISER"):
size = 7 size = 7
else:
if psg.theme() == "HighContrast":
size = 5
else: else:
size = 4 size = 4
layout = [ layout = [
@@ -46,6 +53,7 @@ class CompSlider(psg.Slider):
expand_x=True, expand_x=True,
enable_events=True, enable_events=True,
orientation="horizontal", orientation="horizontal",
key=f"COMPRESSOR||SLIDER {param}",
**self.default_params(param), **self.default_params(param),
) )
@@ -57,61 +65,144 @@ class CompSlider(psg.Slider):
"default_value": self.vm.strip[self.index].comp.gainin, "default_value": self.vm.strip[self.index].comp.gainin,
"resolution": 0.1, "resolution": 0.1,
"disabled": True, "disabled": True,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "RATIO": case "RATIO":
return { return {
"range": (1, 8), "range": (1, 8),
"default_value": self.vm.strip[self.index].comp.ratio, "default_value": self.vm.strip[self.index].comp.ratio,
"resolution": 0.1, "resolution": 0.1,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "THRESHOLD": case "THRESHOLD":
return { return {
"range": (-40, -3), "range": (-40, -3),
"default_value": self.vm.strip[self.index].comp.threshold, "default_value": self.vm.strip[self.index].comp.threshold,
"resolution": 0.1, "resolution": 0.1,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "ATTACK": case "ATTACK":
return { return {
"range": (0, 200), "range": (0, 200),
"default_value": self.vm.strip[self.index].comp.attack, "default_value": self.vm.strip[self.index].comp.attack,
"resolution": 0.1, "resolution": 0.1,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "RELEASE": case "RELEASE":
return { return {
"range": (0, 5000), "range": (0, 5000),
"default_value": self.vm.strip[self.index].comp.release, "default_value": self.vm.strip[self.index].comp.release,
"resolution": 0.1, "resolution": 0.1,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "KNEE": case "KNEE":
return { return {
"range": (0, 1), "range": (0, 1),
"default_value": self.vm.strip[self.index].comp.knee, "default_value": self.vm.strip[self.index].comp.knee,
"resolution": 0.01, "resolution": 0.01,
"key": f"COMPRESSOR||SLIDER {param}",
} }
case "OUTPUT GAIN": case "OUTPUT GAIN":
return { return {
"range": (-24, 24), "range": (-24, 24),
"default_value": self.vm.strip[self.index].comp.gainout, "default_value": self.vm.strip[self.index].comp.gainout,
"resolution": 0.01, "resolution": 0.1,
"disabled": True, "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):
label_map = {
"DAMPING": "Damping Max",
"BPSIDECHAIN": "BP Sidechain",
}
layout = [ layout = [
[ [
psg.Text(param.capitalize(), size=8), psg.Text(label_map.get(param, param.title()), size=10),
CompSlider(parent.vm, index, param), slider_cls(parent.vm, index, param),
] ]
] ]
super().__init__(None, layout=layout, border_width=0, pad=0, *args, **kwargs) super().__init__(None, layout=layout, border_width=0, pad=0, *args, **kwargs)

View File

@@ -0,0 +1,34 @@
import json
from pathlib import Path
SETTINGS = Path.cwd() / "settings.json"
def config_from_json():
data = {}
if not SETTINGS.exists():
return data
with open(SETTINGS, "r") as f:
data = json.load(f)
return data
config = config_from_json()
def get(key, default=None):
if key in config:
return config[key]
return default
def set(key, value):
config[key] = value
with open(SETTINGS, "w") as f:
json.dump(config, f)
def delete(key):
del config[key]
with open(SETTINGS, "w") as f:
json.dump(config, f)

View File

@@ -5,7 +5,7 @@ def _make_hardware_ins_cache(vm) -> dict:
def _make_hardware_outs_cache(vm) -> dict: def _make_hardware_outs_cache(vm) -> dict:
hw_outs = {**{f"HARDWARE OUT||A{i + 1}": vm.bus[i].device.name for i in range(vm.kind.phys_out)}} hw_outs = {**{f"HARDWARE OUT||A{i + 1}": vm.bus[i].device.name for i in range(vm.kind.phys_out)}}
if vm.kind.name == "basic": if vm.kind.name == "basic":
hw_outs |= {f"HARDWARE OUT||A2": vm.bus[1].device.name} hw_outs |= {"HARDWARE OUT||A2": vm.bus[1].device.name}
return hw_outs return hw_outs
@@ -38,10 +38,15 @@ def _make_param_cache(vm, channel_type) -> dict:
**{f"STRIP {i}||B3": vm.strip[i].B3 for i in range(vm.kind.num_strip)}, **{f"STRIP {i}||B3": vm.strip[i].B3 for i in range(vm.kind.num_strip)},
} }
params |= { params |= {
**{f"STRIP {i}||MONO": vm.strip[i].mono for i in range(vm.kind.num_strip)}, **{f"STRIP {i}||MONO": vm.strip[i].mono for i in range(vm.kind.phys_in)},
**{f"STRIP {i}||SOLO": vm.strip[i].solo for i in range(vm.kind.num_strip)}, **{f"STRIP {i}||SOLO": vm.strip[i].solo for i in range(vm.kind.num_strip)},
**{f"STRIP {i}||MUTE": vm.strip[i].mute for i in range(vm.kind.num_strip)}, **{f"STRIP {i}||MUTE": vm.strip[i].mute for i in range(vm.kind.num_strip)},
} }
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in):
if i == vm.kind.phys_in + 1:
params[f"STRIP {i}||KARAOKE"] = vm.strip[i].k
else:
params[f"STRIP {i}||MC"] = vm.strip[i].mc
else: else:
params |= { params |= {
**{f"BUS {i}||MONO": vm.bus[i].mono for i in range(vm.kind.num_bus)}, **{f"BUS {i}||MONO": vm.bus[i].mono for i in range(vm.kind.num_bus)},
@@ -74,10 +79,17 @@ def _make_label_cache(vm) -> dict:
def _make_patch_asio_cache(vm) -> dict: def _make_patch_asio_cache(vm) -> dict:
params = {}
if vm.kind.name != "basic": if vm.kind.name != "basic":
return {**{f"ASIO CHECKBOX||{i}": vm.patch.asio[i].get() for i in range(vm.kind.phys_out * 2)}} params |= {**{f"ASIO INPUT SPINBOX||{i}": vm.patch.asio[i].get() for i in range(vm.kind.phys_out * 2)}}
for i in range(vm.kind.phys_out - 1):
target = getattr(vm.patch, f"A{i + 2}")
params |= {**{f"ASIO OUTPUT A{i + 2} SPINBOX||{j}": target[j].get() for j in range(vm.kind.num_bus)}}
return params
def _make_patch_insert_cache(vm) -> dict: def _make_patch_insert_cache(vm) -> dict:
params = {}
if vm.kind.name != "basic": if vm.kind.name != "basic":
return {**{f"INSERT CHECKBOX||{i}": vm.patch.insert[i].on for i in range(vm.kind.num_strip_levels)}} params |= {**{f"INSERT CHECKBOX||{i}": vm.patch.insert[i].on for i in range(vm.kind.num_strip_levels)}}
return params

View File

@@ -4,7 +4,7 @@ from pathlib import Path
import PySimpleGUI as psg import PySimpleGUI as psg
from . import util from . import util
from .compound import LabelSliderCompressor from .compound import CompSlider, GateSlider, LabelSliderAdvanced
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -48,6 +48,30 @@ class Popup:
if filepath: if filepath:
return Path(filepath) return Path(filepath)
def on_pdirty(self):
if self.popup.Title == "Advanced Settings":
if self.kind.name != "basic":
for key, value in self.window.cache["asio"].items():
if "INPUT" in key:
identifier, i = key.split("||")
partial = util.get_channel_identifier_list(self.window.vm)[int(i)]
self.popup[f"{identifier}||{partial}"].update(value=value)
elif "OUTPUT" in key:
self.popup[key].update(value=value)
if self.popup.Title == "Advanced Compressor":
for param in ("RATIO", "THRESHOLD", "ATTACK", "RELEASE", "KNEE"):
self.popup[f"COMPRESSOR||SLIDER {param}"].update(
value=getattr(self.window.vm.strip[self.index].comp, param.lower())
)
self.popup["COMPRESSOR||SLIDER INPUT GAIN"].update(value=self.window.vm.strip[self.index].comp.gainin)
self.popup["COMPRESSOR||SLIDER OUTPUT GAIN"].update(value=self.window.vm.strip[self.index].comp.gainout)
elif self.popup.Title == "Advanced Gate":
for param in ("THRESHOLD", "DAMPING", "BPSIDECHAIN", "ATTACK", "HOLD", "RELEASE"):
self.popup[f"GATE||SLIDER {param}"].update(
value=getattr(self.window.vm.strip[self.index].gate, param.lower())
)
def rename(self, message, index, title=None, tab=None): def rename(self, message, index, title=None, tab=None):
if "Strip" in tab: if "Strip" in tab:
if index < self.kind.phys_in: if index < self.kind.phys_in:
@@ -94,6 +118,50 @@ class Popup:
return data return data
def advanced_settings(self, title): def advanced_settings(self, title):
def add_patch_asio_input_to_strips(layout, i):
nums = list(range(99))
layout.append(
[
psg.Spin(
nums,
initial_value=self.window.cache["asio"][
f"ASIO INPUT SPINBOX||{util.get_asio_input_spinbox_index(0, i)}"
],
size=2,
enable_events=True,
key=f"ASIO INPUT SPINBOX||IN{i} 0",
)
],
)
layout.append(
[
psg.Spin(
nums,
initial_value=self.window.cache["asio"][
f"ASIO INPUT SPINBOX||{util.get_asio_input_spinbox_index(1, i)}"
],
size=2,
enable_events=True,
key=f"ASIO INPUT SPINBOX||IN{i} 1",
)
],
)
def add_patch_bus_to_asio_outputs(layout, i):
nums = list(range(99))
layout.append(
[
psg.Spin(
nums,
initial_value=self.window.cache["asio"][f"ASIO OUTPUT A{i} SPINBOX||{j}"],
size=2,
enable_events=True,
key=f"ASIO OUTPUT A{i} SPINBOX||{j}",
)
for j in range(self.kind.num_bus)
],
)
def _make_buffering_frame() -> psg.Frame: def _make_buffering_frame() -> psg.Frame:
buffer = [ buffer = [
[ [
@@ -109,27 +177,79 @@ class Popup:
return psg.Frame("BUFFERING", buffer) return psg.Frame("BUFFERING", buffer)
layout = [] layout = []
if self.kind.name != "basic":
inner = []
patch_input_to_strips = ([] for _ in range(self.kind.phys_in))
for i, checkbox_list in enumerate(patch_input_to_strips):
[step(checkbox_list, i + 1) for step in (add_patch_asio_input_to_strips,)]
inner.append(psg.Frame(f"In#{i + 1}", checkbox_list))
layout.append([psg.Frame("PATCH ASIO Inputs to Strips", [inner])])
inner_2 = []
patch_output_to_bus = ([] for _ in range(self.kind.phys_out - 1))
for i, checkbox_list in enumerate(patch_output_to_bus):
[step(checkbox_list, i + 2) for step in (add_patch_bus_to_asio_outputs,)]
inner_2.append([psg.Frame(f"OutA{i + 2}", checkbox_list)])
layout.append([psg.Frame("PATCH BUS to A1 ASIO Outputs", [*inner_2])])
steps = (_make_buffering_frame,) steps = (_make_buffering_frame,)
for step in steps: for step in steps:
layout.append([step()]) layout.append([step()])
layout.append([psg.Button("Exit", size=(8, 2))]) layout.append([psg.Button("Exit", size=(8, 2))])
popup = psg.Window(title, layout, finalize=True) self.popup = psg.Window(title, layout, finalize=True)
if self.kind.name != "basic":
for i in range(self.kind.phys_out):
self.popup[f"ASIO INPUT SPINBOX||IN{i + 1} 0"].Widget.config(state="readonly")
self.popup[f"ASIO INPUT SPINBOX||IN{i + 1} 1"].Widget.config(state="readonly")
for i in range(self.kind.phys_out - 1):
for j in range(self.kind.num_bus):
self.popup[f"ASIO OUTPUT A{i + 2} SPINBOX||{j}"].Widget.config(state="readonly")
if self.kind.name != "basic":
for i in range(self.kind.phys_out):
self.popup[f"ASIO INPUT SPINBOX||IN{i + 1} 0"].bind("<FocusIn>", "||FOCUS IN")
self.popup[f"ASIO INPUT SPINBOX||IN{i + 1} 1"].bind("<FocusIn>", "||FOCUS IN")
for i in range(self.kind.phys_out - 1):
for j in range(self.kind.num_bus):
self.popup[f"ASIO OUTPUT A{i + 2} SPINBOX||{j}"].bind("<FocusIn>", "||FOCUS IN")
buttonmenu_opts = {"takefocus": 1, "highlightthickness": 1} buttonmenu_opts = {"takefocus": 1, "highlightthickness": 1}
for driver in ("MME", "WDM", "KS", "ASIO"): for driver in ("MME", "WDM", "KS", "ASIO"):
popup[f"BUFFER {driver}"].Widget.config(**buttonmenu_opts) self.popup[f"BUFFER {driver}"].Widget.config(**buttonmenu_opts)
popup[f"BUFFER {driver}"].bind("<FocusIn>", "||FOCUS IN") self.popup[f"BUFFER {driver}"].bind("<FocusIn>", "||FOCUS IN")
popup[f"BUFFER {driver}"].bind("<space>", "||KEY SPACE", propagate=False) self.popup[f"BUFFER {driver}"].bind("<space>", "||KEY SPACE", propagate=False)
popup[f"BUFFER {driver}"].bind("<Return>", "||KEY ENTER", propagate=False) self.popup[f"BUFFER {driver}"].bind("<Return>", "||KEY ENTER", propagate=False)
popup["Exit"].bind("<FocusIn>", "||FOCUS IN") self.popup["Exit"].bind("<FocusIn>", "||FOCUS IN")
popup["Exit"].bind("<Return>", "||KEY ENTER") self.popup["Exit"].bind("<Return>", "||KEY ENTER")
self.window.vm.observer.add(self.on_pdirty)
while True: while True:
event, values = popup.read() event, values = self.popup.read()
self.logger.debug(f"event::{event}") self.logger.debug(f"event::{event}")
self.logger.debug(f"values::{values}") self.logger.debug(f"values::{values}")
if event in (psg.WIN_CLOSED, "Exit"): if event in (psg.WIN_CLOSED, "Exit"):
break break
match parsed_cmd := self.window.parser.match.parseString(event): match parsed_cmd := self.window.parser.match.parseString(event):
case [["ASIO", "INPUT", "SPINBOX"], [in_num, channel]]:
index = util.get_asio_input_spinbox_index(int(channel), int(in_num[-1]))
val = values[f"ASIO INPUT SPINBOX||{in_num} {channel}"]
self.window.vm.patch.asio[index].set(val)
channel = ("left", "right")[int(channel)]
self.window.nvda.speak(str(val))
case [["ASIO", "INPUT", "SPINBOX"], [in_num, channel], ["FOCUS", "IN"]]:
if self.popup.find_element_with_focus() is not None:
val = values[f"ASIO INPUT SPINBOX||{in_num} {channel}"]
channel = ("left", "right")[int(channel)]
num = int(in_num[-1])
self.window.nvda.speak(f"Patch ASIO inputs to strips IN#{num} {channel} {val}")
case [["ASIO", "OUTPUT", param, "SPINBOX"], [index]]:
target = getattr(self.window.vm.patch, param)[int(index)]
target.set(values[event])
self.window.nvda.speak(str(values[event]))
case [["ASIO", "OUTPUT", param, "SPINBOX"], [index], ["FOCUS", "IN"]]:
if self.popup.find_element_with_focus() is not None:
val = values[f"ASIO OUTPUT {param} SPINBOX||{index}"]
self.window.nvda.speak(
f"Patch BUS to A1 ASIO Outputs OUT {param} channel {int(index) + 1} {val}"
)
case ["BUFFER MME" | "BUFFER WDM" | "BUFFER KS" | "BUFFER ASIO"]: case ["BUFFER MME" | "BUFFER WDM" | "BUFFER KS" | "BUFFER ASIO"]:
if values[event] == "Default": if values[event] == "Default":
if "MME" in event: if "MME" in event:
@@ -149,18 +269,21 @@ class Popup:
val = int(self.window.vm.get(f"option.buffer.{driver.lower()}")) val = int(self.window.vm.get(f"option.buffer.{driver.lower()}"))
self.window.nvda.speak(f"{driver} BUFFER {val if val else 'default'}") self.window.nvda.speak(f"{driver} BUFFER {val if val else 'default'}")
case [["BUFFER", driver], ["KEY", "SPACE" | "ENTER"]]: case [["BUFFER", driver], ["KEY", "SPACE" | "ENTER"]]:
util.open_context_menu_for_buttonmenu(popup, f"BUFFER {driver}") util.open_context_menu_for_buttonmenu(self.popup, f"BUFFER {driver}")
case [[button], ["FOCUS", "IN"]]: case [[button], ["FOCUS", "IN"]]:
self.window.nvda.speak(button) self.window.nvda.speak(button)
case [_, ["KEY", "ENTER"]]: case [_, ["KEY", "ENTER"]]:
popup.find_element_with_focus().click() self.popup.find_element_with_focus().click()
self.logger.debug(f"parsed::{parsed_cmd}") self.logger.debug(f"parsed::{parsed_cmd}")
popup.close() self.window.vm.observer.remove(self.on_pdirty)
self.popup.close()
def compressor(self, index, title=None): def compressor(self, index, title=None):
self.index = index
def _make_comp_frame() -> psg.Frame: def _make_comp_frame() -> psg.Frame:
comp_layout = [ 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") for param in ("INPUT GAIN", "RATIO", "THRESHOLD", "ATTACK", "RELEASE", "KNEE", "OUTPUT GAIN")
] ]
return psg.Frame("ADVANCED COMPRESSOR", comp_layout) return psg.Frame("ADVANCED COMPRESSOR", comp_layout)
@@ -171,37 +294,39 @@ class Popup:
layout.append([step()]) layout.append([step()])
layout.append([psg.Button("MAKEUP", size=(12, 1)), psg.Button("Exit", size=(8, 1))]) layout.append([psg.Button("MAKEUP", size=(12, 1)), psg.Button("Exit", size=(8, 1))])
popup = psg.Window(title, layout, return_keyboard_events=False, finalize=True) self.popup = psg.Window(title, layout, return_keyboard_events=False, finalize=True)
buttonmenu_opts = {"takefocus": 1, "highlightthickness": 1} buttonmenu_opts = {"takefocus": 1, "highlightthickness": 1}
for param in ("INPUT GAIN", "RATIO", "THRESHOLD", "ATTACK", "RELEASE", "KNEE", "OUTPUT GAIN"): for param in ("INPUT GAIN", "RATIO", "THRESHOLD", "ATTACK", "RELEASE", "KNEE", "OUTPUT GAIN"):
popup[f"COMPRESSOR||SLIDER {param}"].Widget.config(**buttonmenu_opts) self.popup[f"COMPRESSOR||SLIDER {param}"].Widget.config(**buttonmenu_opts)
popup[f"COMPRESSOR||SLIDER {param}"].bind("<FocusIn>", "||FOCUS IN") self.popup[f"COMPRESSOR||SLIDER {param}"].bind("<FocusIn>", "||FOCUS IN")
popup[f"COMPRESSOR||SLIDER {param}"].bind("<FocusOut>", "||FOCUS OUT") self.popup[f"COMPRESSOR||SLIDER {param}"].bind("<FocusOut>", "||FOCUS OUT")
for event in ("KeyPress", "KeyRelease"): for event in ("KeyPress", "KeyRelease"):
event_id = event.removeprefix("Key").upper() event_id = event.removeprefix("Key").upper()
for direction in ("Left", "Right", "Up", "Down"): for direction in ("Left", "Right", "Up", "Down"):
popup[f"COMPRESSOR||SLIDER {param}"].bind( self.popup[f"COMPRESSOR||SLIDER {param}"].bind(
f"<{event}-{direction}>", f"||KEY {direction.upper()} {event_id}" f"<{event}-{direction}>", f"||KEY {direction.upper()} {event_id}"
) )
popup[f"COMPRESSOR||SLIDER {param}"].bind( self.popup[f"COMPRESSOR||SLIDER {param}"].bind(
f"<Shift-{event}-{direction}>", f"||KEY SHIFT {direction.upper()} {event_id}" f"<Shift-{event}-{direction}>", f"||KEY SHIFT {direction.upper()} {event_id}"
) )
popup[f"COMPRESSOR||SLIDER {param}"].bind( self.popup[f"COMPRESSOR||SLIDER {param}"].bind(
f"<Control-{event}-{direction}>", f"||KEY CTRL {direction.upper()} {event_id}" f"<Control-{event}-{direction}>", f"||KEY CTRL {direction.upper()} {event_id}"
) )
if param == "RELEASE": if param == "RELEASE":
popup[f"COMPRESSOR||SLIDER {param}"].bind( self.popup[f"COMPRESSOR||SLIDER {param}"].bind(
f"<Alt-{event}-{direction}>", f"||KEY ALT {direction.upper()} {event_id}" f"<Alt-{event}-{direction}>", f"||KEY ALT {direction.upper()} {event_id}"
) )
popup[f"COMPRESSOR||SLIDER {param}"].bind( self.popup[f"COMPRESSOR||SLIDER {param}"].bind(
f"<Control-Alt-{event}-{direction}>", f"||KEY CTRL ALT {direction.upper()} {event_id}" f"<Control-Alt-{event}-{direction}>", f"||KEY CTRL ALT {direction.upper()} {event_id}"
) )
popup["MAKEUP"].bind("<FocusIn>", "||FOCUS IN") self.popup[f"COMPRESSOR||SLIDER {param}"].bind("<Control-Shift-KeyPress-R>", "||KEY CTRL SHIFT R")
popup["MAKEUP"].bind("<Return>", "||KEY ENTER") self.popup["MAKEUP"].bind("<FocusIn>", "||FOCUS IN")
popup["Exit"].bind("<FocusIn>", "||FOCUS IN") self.popup["MAKEUP"].bind("<Return>", "||KEY ENTER")
popup["Exit"].bind("<Return>", "||KEY ENTER") self.popup["Exit"].bind("<FocusIn>", "||FOCUS IN")
self.popup["Exit"].bind("<Return>", "||KEY ENTER")
self.window.vm.observer.add(self.on_pdirty)
while True: while True:
event, values = popup.read() event, values = self.popup.read()
self.logger.debug(f"event::{event}") self.logger.debug(f"event::{event}")
self.logger.debug(f"values::{values}") self.logger.debug(f"values::{values}")
if event in (psg.WIN_CLOSED, "Exit"): if event in (psg.WIN_CLOSED, "Exit"):
@@ -232,20 +357,10 @@ class Popup:
else: else:
val -= 1 val -= 1
match param: val = CompSlider.check_bounds(param, val)
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))
setattr(self.window.vm.strip[index].comp, param.lower(), val) setattr(self.window.vm.strip[index].comp, param.lower(), val)
popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
if param == "KNEE": if param == "KNEE":
self.window.nvda.speak(str(round(val, 2))) self.window.nvda.speak(str(round(val, 2)))
else: else:
@@ -277,20 +392,10 @@ class Popup:
else: else:
val -= 3 val -= 3
match param: val = CompSlider.check_bounds(param, val)
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))
setattr(self.window.vm.strip[index].comp, param.lower(), val) setattr(self.window.vm.strip[index].comp, param.lower(), val)
popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
if param == "KNEE": if param == "KNEE":
self.window.nvda.speak(str(round(val, 2))) self.window.nvda.speak(str(round(val, 2)))
else: else:
@@ -318,20 +423,10 @@ class Popup:
else: else:
val -= 0.1 val -= 0.1
match param: val = CompSlider.check_bounds(param, val)
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))
setattr(self.window.vm.strip[index].comp, param.lower(), val) setattr(self.window.vm.strip[index].comp, param.lower(), val)
popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
if param == "KNEE": if param == "KNEE":
self.window.nvda.speak(str(round(val, 2))) self.window.nvda.speak(str(round(val, 2)))
else: else:
@@ -355,7 +450,7 @@ class Popup:
val = util.check_bounds(val, (0, 5000)) val = util.check_bounds(val, (0, 5000))
self.window.vm.strip[index].comp.release = val self.window.vm.strip[index].comp.release = val
popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
self.window.nvda.speak(str(round(val, 1))) self.window.nvda.speak(str(round(val, 1)))
else: else:
self.window.vm.event.pdirty = True self.window.vm.event.pdirty = True
@@ -376,7 +471,7 @@ class Popup:
val = util.check_bounds(val, (0, 5000)) val = util.check_bounds(val, (0, 5000))
self.window.vm.strip[index].comp.release = val self.window.vm.strip[index].comp.release = val
popup[f"COMPRESSOR||SLIDER {param}"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
self.window.nvda.speak(str(round(val, 1))) self.window.nvda.speak(str(round(val, 1)))
else: else:
self.window.vm.event.pdirty = True self.window.vm.event.pdirty = True
@@ -412,7 +507,7 @@ class Popup:
self.window.vm.strip[index].comp.gainin = val self.window.vm.strip[index].comp.gainin = val
else: else:
self.window.vm.strip[index].comp.gainout = val self.window.vm.strip[index].comp.gainout = val
popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val)
self.window.nvda.speak(str(round(val, 1))) self.window.nvda.speak(str(round(val, 1)))
else: else:
self.window.vm.event.pdirty = True self.window.vm.event.pdirty = True
@@ -439,7 +534,7 @@ class Popup:
self.window.vm.strip[index].comp.gainin = val self.window.vm.strip[index].comp.gainin = val
else: else:
self.window.vm.strip[index].comp.gainout = val self.window.vm.strip[index].comp.gainout = val
popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val)
self.window.nvda.speak(str(round(val, 1))) self.window.nvda.speak(str(round(val, 1)))
else: else:
self.window.vm.event.pdirty = True self.window.vm.event.pdirty = True
@@ -466,21 +561,262 @@ class Popup:
self.window.vm.strip[index].comp.gainin = val self.window.vm.strip[index].comp.gainin = val
else: else:
self.window.vm.strip[index].comp.gainout = val self.window.vm.strip[index].comp.gainout = val
popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val) self.popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=val)
self.window.nvda.speak(str(round(val, 1))) self.window.nvda.speak(str(round(val, 1)))
else: else:
self.window.vm.event.pdirty = True self.window.vm.event.pdirty = True
case [
["COMPRESSOR"],
["SLIDER", "INPUT" | "OUTPUT" as direction, "GAIN"],
["KEY", "CTRL", "SHIFT", "R"],
]:
if direction == "INPUT":
self.window.vm.strip[index].comp.gainin = 0
else:
self.window.vm.strip[index].comp.gainout = 0
self.popup[f"COMPRESSOR||SLIDER {direction} GAIN"].update(value=0)
self.window.nvda.speak(str(0))
case [["COMPRESSOR"], ["SLIDER", param], ["KEY", "CTRL", "SHIFT", "R"]]:
match param:
case "RATIO":
val = 1
case "THRESHOLD":
val = -20
case "ATTACK":
val = 10
case "RELEASE":
val = 50
case "KNEE":
val = 0.5
setattr(self.window.vm.strip[index].comp, param.lower(), val)
self.popup[f"COMPRESSOR||SLIDER {param}"].update(value=val)
self.window.nvda.speak(str(round(val, 1)))
case ["MAKEUP"]: case ["MAKEUP"]:
val = not self.window.vm.strip[index].comp.makeup val = not self.window.vm.strip[index].comp.makeup
self.window.vm.strip[index].comp.makeup = val self.window.vm.strip[index].comp.makeup = val
self.window.nvda.speak("on" if val else "off") self.window.nvda.speak("on" if val else "off")
case [[button], ["FOCUS", "IN"]]: case [[button], ["FOCUS", "IN"]]:
if button == "MAKEUP": 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: else:
self.window.nvda.speak(button) self.window.nvda.speak(button)
case [_, ["KEY", "ENTER"]]: case [_, ["KEY", "ENTER"]]:
popup.find_element_with_focus().click() self.popup.find_element_with_focus().click()
self.logger.debug(f"parsed::{parsed_cmd}") self.logger.debug(f"parsed::{parsed_cmd}")
popup.close() self.window.vm.observer.remove(self.on_pdirty)
self.popup.close()
def gate(self, index, title=None):
self.index = index
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))])
self.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"):
self.popup[f"GATE||SLIDER {param}"].Widget.config(**buttonmenu_opts)
self.popup[f"GATE||SLIDER {param}"].bind("<FocusIn>", "||FOCUS IN")
self.popup[f"GATE||SLIDER {param}"].bind("<FocusOut>", "||FOCUS OUT")
for event in ("KeyPress", "KeyRelease"):
event_id = event.removeprefix("Key").upper()
for direction in ("Left", "Right", "Up", "Down"):
self.popup[f"GATE||SLIDER {param}"].bind(
f"<{event}-{direction}>", f"||KEY {direction.upper()} {event_id}"
)
self.popup[f"GATE||SLIDER {param}"].bind(
f"<Shift-{event}-{direction}>", f"||KEY SHIFT {direction.upper()} {event_id}"
)
self.popup[f"GATE||SLIDER {param}"].bind(
f"<Control-{event}-{direction}>", f"||KEY CTRL {direction.upper()} {event_id}"
)
if param in ("BPSIDECHAIN", "ATTACK", "HOLD", "RELEASE"):
self.popup[f"GATE||SLIDER {param}"].bind(
f"<Alt-{event}-{direction}>", f"||KEY ALT {direction.upper()} {event_id}"
)
self.popup[f"GATE||SLIDER {param}"].bind(
f"<Control-Alt-{event}-{direction}>", f"||KEY CTRL ALT {direction.upper()} {event_id}"
)
self.popup[f"GATE||SLIDER {param}"].bind("<Control-Shift-KeyPress-R>", "||KEY CTRL SHIFT R")
self.popup["Exit"].bind("<FocusIn>", "||FOCUS IN")
self.popup["Exit"].bind("<Return>", "||KEY ENTER")
self.window.vm.observer.add(self.on_pdirty)
while True:
event, values = self.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"]]:
label_map = {
"DAMPING": "Damping Max",
"BPSIDECHAIN": "BP Sidechain",
}
self.window.nvda.speak(f"{label_map.get(param, 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)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
if param == "BPSIDECHAIN":
self.window.nvda.speak(str(int(val)))
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)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
if param == "BPSIDECHAIN":
self.window.nvda.speak(str(int(val)))
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)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
if param == "BPSIDECHAIN":
self.window.nvda.speak(str(int(val)))
else:
self.window.nvda.speak(str(round(val, 1)))
else:
self.window.vm.event.pdirty = True
case [
["GATE"],
["SLIDER", "BPSIDECHAIN" | "ATTACK" | "HOLD" | "RELEASE" as param],
["KEY", "ALT", "LEFT" | "RIGHT" 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 += 10
case "LEFT" | "DOWN":
val -= 10
val = GateSlider.check_bounds(param, val)
setattr(self.window.vm.strip[index].gate, param.lower(), val)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
if param == "BPSIDECHAIN":
self.window.nvda.speak(str(int(val)))
else:
self.window.nvda.speak(str(round(val, 1)))
else:
self.window.vm.event.pdirty = True
case [
["GATE"],
["SLIDER", "BPSIDECHAIN" | "ATTACK" | "HOLD" | "RELEASE" as param],
["KEY", "CTRL", "ALT", "LEFT" | "RIGHT" 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 += 50
case "LEFT" | "DOWN":
val -= 50
val = GateSlider.check_bounds(param, val)
setattr(self.window.vm.strip[index].gate, param.lower(), val)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
if param == "BPSIDECHAIN":
self.window.nvda.speak(str(int(val)))
else:
self.window.nvda.speak(str(round(val, 1)))
else:
self.window.vm.event.pdirty = True
case [["GATE"], ["SLIDER", param], ["KEY", "CTRL", "SHIFT", "R"]]:
match param:
case "THRESHOLD":
val = -60
case "DAMPING":
val = -60
case "BPSIDECHAIN":
val = 100
case "ATTACK":
val = 0
case "HOLD":
val = 500
case "RELEASE":
val = 1000
setattr(self.window.vm.strip[index].gate, param.lower(), val)
self.popup[f"GATE||SLIDER {param}"].update(value=val)
self.window.nvda.speak(str(round(val, 1)))
case [[button], ["FOCUS", "IN"]]:
self.window.nvda.speak(button)
case [_, ["KEY", "ENTER"]]:
self.popup.find_element_with_focus().click()
self.logger.debug(f"parsed::{parsed_cmd}")
self.window.vm.observer.remove(self.on_pdirty)
self.popup.close()

View File

@@ -1,7 +1,9 @@
from typing import Iterable from typing import Iterable
import PySimpleGUI as psg
def get_asio_checkbox_index(channel, num) -> int:
def get_asio_input_spinbox_index(channel, num) -> int:
if channel == 0: if channel == 0:
return 2 * num - 2 return 2 * num - 2
return 2 * num - 1 return 2 * num - 1
@@ -48,8 +50,11 @@ def get_patch_composite_list(kind) -> list:
for i in range(kind.phys_out): for i in range(kind.phys_out):
[temp.append(f"IN#{i + 1} {channel}") for channel in ("Left", "Right")] [temp.append(f"IN#{i + 1} {channel}") for channel in ("Left", "Right")]
for i in range(kind.phys_out, kind.phys_out + kind.virt_out): for i in range(kind.phys_out, kind.phys_out + kind.virt_out):
[temp.append(f"IN#{i + 1} {channel}") for channel in ("Left", "Right", "Center", "LFE", "SL", "SR", "BL", "BR")] [
temp.append(f"BUS Channel") temp.append(f"IN#{i + 1} {channel}")
for channel in ("Left", "Right", "Center", "LFE", "SL", "SR", "BL", "BR")
]
temp.append("BUS Channel")
return temp return temp
@@ -108,6 +113,24 @@ def get_channel_identifier_list(vm) -> list:
return identifiers return identifiers
_bus_mode_map = {
"normal": "Normal",
"amix": "Mix Down A",
"bmix": "Mix Down B",
"repeat": "Stereo Repeat",
"composite": "Composite",
"tvmix": "Up Mix TV",
"upmix21": "Up Mix 2.1",
"upmix41": "Up Mix 4.1",
"upmix61": "Up Mix 6.1",
"centeronly": "Center Only",
"lfeonly": "Low Frequency Effect Only",
"rearonly": "Rear Only",
}
_bus_mode_map_reversed = dict((reversed(item) for item in _bus_mode_map.items()))
def get_bus_modes(vm) -> list: def get_bus_modes(vm) -> list:
if vm.kind.name == "basic": if vm.kind.name == "basic":
return [ return [
@@ -157,3 +180,63 @@ def get_full_slider_params(i, kind) -> Iterable:
if kind.name == "basic": if kind.name == "basic":
params.remove("LIMIT") params.remove("LIMIT")
return params return params
def get_slider_modes() -> Iterable:
return (
"GAIN MODE",
"BASS MODE",
"MID MODE",
"TREBLE MODE",
"AUDIBILITY MODE",
"COMP MODE",
"GATE MODE",
"DENOISER MODE",
"LIMIT MODE",
)
def _get_bus_assignments(kind) -> list:
return [f"A{i}" for i in range(1, kind.phys_out + 1)] + [f"B{i}" for i in range(1, kind.virt_out + 1)]
psg.theme_add_new(
"HighContrast",
{
"BACKGROUND": "#FFFFFF",
"TEXT": "#000000",
"INPUT": "#FAF9F6",
"TEXT_INPUT": "#000000",
"SCROLL": "#FAF9F6",
"BUTTON": ("#000000", "#FFFFFF"),
"PROGRESS": ("#000000", "#FFFFFF"),
"BORDER": 2,
"SLIDER_DEPTH": 3,
"PROGRESS_DEPTH": 0,
},
)
def get_themes_list() -> list:
return [
"Bright Colors",
"Dark Blue 14",
"Dark Brown 2",
"Dark Brown 3",
"Dark Green 2",
"Dark Grey 2",
"Dark Teal1",
"Dark Teal6",
"Kayak",
"Light Blue 2",
"Light Brown 2",
"Light Brown 5",
"Light Green",
"Light Green 3",
"Light Grey 2",
"Light Purple",
"Neutral Blue",
"Reds",
"Sandy Beach",
"High Contrast",
]

View File

@@ -4,7 +4,7 @@ from pathlib import Path
import PySimpleGUI as psg import PySimpleGUI as psg
from . import models, util from . import configuration, models, util
from .builder import Builder from .builder import Builder
from .nvda import Nvda from .nvda import Nvda
from .parser import Parser from .parser import Parser
@@ -12,18 +12,19 @@ from .popup import Popup
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
psg.theme("Dark Blue 3") psg.theme(configuration.get("default_theme", "Dark Blue 3"))
if psg.theme() == "HighContrast":
psg.set_options(font=("Arial", 14))
class NVDAVMWindow(psg.Window): class NVDAVMWindow(psg.Window):
"""Represents the main window of the Voicemeeter NVDA application""" """Represents the main window of the Voicemeeter NVDA application"""
SETTINGS = "settings.json"
def __init__(self, title, vm): def __init__(self, title, vm):
self.vm = vm self.vm = vm
self.kind = self.vm.kind self.kind = self.vm.kind
self.logger = logger.getChild(type(self).__name__) self.logger = logger.getChild(type(self).__name__)
self.logger.debug(f"loaded with theme: {psg.theme()}")
self.cache = { self.cache = {
"hw_ins": models._make_hardware_ins_cache(self.vm), "hw_ins": models._make_hardware_ins_cache(self.vm),
"hw_outs": models._make_hardware_outs_cache(self.vm), "hw_outs": models._make_hardware_outs_cache(self.vm),
@@ -45,9 +46,9 @@ class NVDAVMWindow(psg.Window):
for i in range(self.kind.phys_out): for i in range(self.kind.phys_out):
self[f"HARDWARE OUT||A{i + 1}"].Widget.config(**buttonmenu_opts) self[f"HARDWARE OUT||A{i + 1}"].Widget.config(**buttonmenu_opts)
if self.kind.name == "basic": if self.kind.name == "basic":
self[f"HARDWARE OUT||A2"].Widget.config(**buttonmenu_opts) self["HARDWARE OUT||A2"].Widget.config(**buttonmenu_opts)
if self.kind.name != "basic": if self.kind.name != "basic":
[self[f"PATCH COMPOSITE||PC{i + 1}"].Widget.config(**buttonmenu_opts) for i in range(self.kind.phys_out)] [self[f"PATCH COMPOSITE||PC{i + 1}"].Widget.config(**buttonmenu_opts) for i in range(self.kind.composite)]
slider_opts = {"takefocus": 1, "highlightthickness": 1} slider_opts = {"takefocus": 1, "highlightthickness": 1}
for i in range(self.kind.num_strip): for i in range(self.kind.num_strip):
for param in util.get_slider_params(i, self.kind): for param in util.get_slider_params(i, self.kind):
@@ -57,22 +58,17 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||SLIDER LIMIT"].Widget.config(**slider_opts) self[f"STRIP {i}||SLIDER LIMIT"].Widget.config(**slider_opts)
for i in range(self.kind.num_bus): for i in range(self.kind.num_bus):
self[f"BUS {i}||SLIDER GAIN"].Widget.config(**slider_opts) self[f"BUS {i}||SLIDER GAIN"].Widget.config(**slider_opts)
if self.kind.name != "basic": self[f"BUS {i}||MODE"].Widget.config(**buttonmenu_opts)
for i in range(self.kind.phys_out):
self[f"ASIO CHECKBOX||IN{i + 1} 0"].Widget.config(state="readonly")
self[f"ASIO CHECKBOX||IN{i + 1} 1"].Widget.config(state="readonly")
self.register_events() self.register_events()
self["tabgroup"].set_focus() self["tabgroup"].set_focus()
def __enter__(self): def __enter__(self):
settings_path = Path.cwd() / self.SETTINGS settings_path = configuration.SETTINGS
if settings_path.exists(): if settings_path.exists():
try: try:
with open(settings_path, "r") as f: defaultconfig = Path(configuration.get("default_config", "")) # coerce the type
data = json.load(f) if defaultconfig.is_file() and defaultconfig.exists():
defaultconfig = Path(data["default_config"])
if defaultconfig.exists():
self.vm.set("command.load", str(defaultconfig)) self.vm.set("command.load", str(defaultconfig))
self.logger.debug(f"config {defaultconfig} loaded") self.logger.debug(f"config {defaultconfig} loaded")
self.TKroot.after( self.TKroot.after(
@@ -81,7 +77,7 @@ class NVDAVMWindow(psg.Window):
f"config {defaultconfig.stem} has been loaded", f"config {defaultconfig.stem} has been loaded",
) )
except json.JSONDecodeError: except json.JSONDecodeError:
self.logger.debug("no data in settings.json. silently continuing...") self.logger.debug("no default_config in settings.json. silently continuing...")
self.vm.init_thread() self.vm.init_thread()
self.vm.observer.add(self.on_pdirty) self.vm.observer.add(self.on_pdirty)
@@ -123,10 +119,6 @@ class NVDAVMWindow(psg.Window):
for i in range(self.kind.num_bus): for i in range(self.kind.num_bus):
self[f"BUS {i}||SLIDER GAIN"].update(value=self.vm.bus[i].gain) self[f"BUS {i}||SLIDER GAIN"].update(value=self.vm.bus[i].gain)
if self.kind.name != "basic": if self.kind.name != "basic":
for key, value in self.cache["asio"].items():
identifier, i = key.split("||")
partial = util.get_channel_identifier_list(self.vm)[int(i)]
self[f"{identifier}||{partial}"].update(value=value)
for key, value in self.cache["insert"].items(): for key, value in self.cache["insert"].items():
identifier, i = key.split("||") identifier, i = key.split("||")
partial = util.get_channel_identifier_list(self.vm)[int(i)] partial = util.get_channel_identifier_list(self.vm)[int(i)]
@@ -179,30 +171,24 @@ class NVDAVMWindow(psg.Window):
self.bind(f"<Alt-Control-{event}-{direction}>", f"ALT CTRL {direction.upper()}||{event_id}") self.bind(f"<Alt-Control-{event}-{direction}>", f"ALT CTRL {direction.upper()}||{event_id}")
# Hardware In # Hardware In
for i in range(self.vm.kind.phys_in): for i in range(self.kind.phys_in):
self[f"HARDWARE IN||{i + 1}"].bind("<FocusIn>", "||FOCUS IN") self[f"HARDWARE IN||{i + 1}"].bind("<FocusIn>", "||FOCUS IN")
self[f"HARDWARE IN||{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False) self[f"HARDWARE IN||{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"HARDWARE IN||{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False) self[f"HARDWARE IN||{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
# Hardware Out # Hardware Out
for i in range(self.vm.kind.phys_out): for i in range(self.kind.phys_out):
self[f"HARDWARE OUT||A{i + 1}"].bind("<FocusIn>", "||FOCUS IN") self[f"HARDWARE OUT||A{i + 1}"].bind("<FocusIn>", "||FOCUS IN")
self[f"HARDWARE OUT||A{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False) self[f"HARDWARE OUT||A{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"HARDWARE OUT||A{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False) self[f"HARDWARE OUT||A{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
if self.vm.kind.name == "basic": if self.kind.name == "basic":
self[f"HARDWARE OUT||A2"].bind("<FocusIn>", "||FOCUS IN") self["HARDWARE OUT||A2"].bind("<FocusIn>", "||FOCUS IN")
self[f"HARDWARE OUT||A2"].bind("<space>", "||KEY SPACE", propagate=False) self["HARDWARE OUT||A2"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"HARDWARE OUT||A2"].bind("<Return>", "||KEY ENTER", propagate=False) self["HARDWARE OUT||A2"].bind("<Return>", "||KEY ENTER", propagate=False)
# Patch ASIO
if self.kind.name != "basic":
for i in range(self.kind.phys_out):
self[f"ASIO CHECKBOX||IN{i + 1} 0"].bind("<FocusIn>", "||FOCUS IN")
self[f"ASIO CHECKBOX||IN{i + 1} 1"].bind("<FocusIn>", "||FOCUS IN")
# Patch Composite # Patch Composite
if self.kind.name != "basic": if self.kind.name != "basic":
for i in range(self.vm.kind.phys_out): for i in range(self.kind.composite):
self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<FocusIn>", "||FOCUS IN") self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<FocusIn>", "||FOCUS IN")
self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False) self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False) self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
@@ -236,7 +222,12 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN") self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER") self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER")
else: else:
for param in ("MONO", "SOLO", "MUTE"): if i == self.kind.phys_in + 1:
for param in ("KARAOKE", "SOLO", "MUTE"):
self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER")
else:
for param in ("MC", "SOLO", "MUTE"):
self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN") self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER") self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER")
@@ -260,13 +251,16 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||SLIDER {param}"].bind("<Control-Shift-KeyPress-R>", "||KEY CTRL SHIFT R") self[f"STRIP {i}||SLIDER {param}"].bind("<Control-Shift-KeyPress-R>", "||KEY CTRL SHIFT R")
# Bus Params # Bus Params
params = ["MONO", "EQ", "MUTE", "MODE"] params = ["MONO", "EQ", "MUTE"]
if self.vm.kind.name == "basic": if self.kind.name == "basic":
params.remove("EQ") params.remove("EQ")
for i in range(self.kind.num_bus): for i in range(self.kind.num_bus):
for param in params: for param in params:
self[f"BUS {i}||{param}"].bind("<FocusIn>", "||FOCUS IN") self[f"BUS {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"BUS {i}||{param}"].bind("<Return>", "||KEY ENTER") self[f"BUS {i}||{param}"].bind("<Return>", "||KEY ENTER")
self[f"BUS {i}||MODE"].bind("<FocusIn>", "||FOCUS IN")
self[f"BUS {i}||MODE"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"BUS {i}||MODE"].bind("<Return>", "||KEY ENTER", propagate=False)
# Bus Sliders # Bus Sliders
for i in range(self.kind.num_bus): for i in range(self.kind.num_bus):
@@ -300,13 +294,15 @@ class NVDAVMWindow(psg.Window):
self.logger.debug(f"values::{values}") self.logger.debug(f"values::{values}")
if event in (psg.WIN_CLOSED, "Exit"): if event in (psg.WIN_CLOSED, "Exit"):
break break
elif event.endswith("MODE"): elif event in util.get_slider_modes():
mode = event mode = event
self.nvda.speak(f"{mode} enabled") self.nvda.speak(f"{mode} enabled")
self.logger.debug(f"entered slider mode {mode}")
continue continue
elif event == "ESCAPE": elif event == "ESCAPE":
if mode: if mode:
self.nvda.speak(f"{mode} disabled") self.nvda.speak(f"{mode} disabled")
self.logger.debug(f"exited from slider mode {mode}")
mode = None mode = None
continue continue
@@ -423,7 +419,7 @@ class NVDAVMWindow(psg.Window):
identifier, partial = focus.Key.split("||") identifier, partial = focus.Key.split("||")
_, index = identifier.split() _, index = identifier.split()
index = int(index) index = int(index)
data = self.popup.rename("Label", index, title=f"Rename", tab=tab) data = self.popup.rename("Label", index, title="Rename", tab=tab)
if not data: # cancel was pressed if not data: # cancel was pressed
continue continue
match tab: match tab:
@@ -456,8 +452,12 @@ class NVDAVMWindow(psg.Window):
if focus := self.find_element_with_focus(): if focus := self.find_element_with_focus():
identifier, partial = focus.key.split("||") identifier, partial = focus.key.split("||")
_, index = identifier.split() _, index = identifier.split()
match self.kind.name:
case "potato":
if "SLIDER COMP" in partial: if "SLIDER COMP" in partial:
self.popup.compressor(int(index), title="Advanced Compressor") self.popup.compressor(int(index), title="Advanced Compressor")
elif "SLIDER GATE" in partial:
self.popup.gate(int(index), title="Advanced Gate")
# Menus # Menus
case [["Restart", "Audio", "Engine"], ["MENU"]]: case [["Restart", "Audio", "Engine"], ["MENU"]]:
@@ -509,17 +509,27 @@ class NVDAVMWindow(psg.Window):
file_types=(("XML", ".xml"),), file_types=(("XML", ".xml"),),
): ):
filepath = Path(filepath) filepath = Path(filepath)
with open(self.SETTINGS, "w") as f: configuration.set("default_settings", str(filepath))
json.dump({"default_config": str(filepath)}, f)
self.TKroot.after( self.TKroot.after(
200, 200,
self.nvda.speak, self.nvda.speak,
f"config {filepath.stem} set as default on startup", f"config {filepath.stem} set as default on startup",
) )
else: else:
with open(self.SETTINGS, "wb") as f: configuration.delete("default_settings")
f.truncate() self.logger.debug("default_settings removed from settings.json")
self.logger.debug("settings.json was truncated")
case [theme, ["MENU", "THEME"]]:
chosen = " ".join(theme)
if chosen == "Default":
chosen = "Dark Blue 3"
configuration.set("default_theme", chosen)
self.TKroot.after(
200,
self.nvda.speak,
f"theme {chosen} selected.",
)
self.logger.debug(f"theme {chosen} selected")
# Tabs # Tabs
case ["tabgroup"] | [["tabgroup"], ["FOCUS", "IN"]]: case ["tabgroup"] | [["tabgroup"], ["FOCUS", "IN"]]:
@@ -576,33 +586,25 @@ class NVDAVMWindow(psg.Window):
case [["HARDWARE", "OUT"], [key], ["KEY", "SPACE" | "ENTER"]]: case [["HARDWARE", "OUT"], [key], ["KEY", "SPACE" | "ENTER"]]:
util.open_context_menu_for_buttonmenu(self, f"HARDWARE OUT||{key}") util.open_context_menu_for_buttonmenu(self, f"HARDWARE OUT||{key}")
# Patch ASIO
case [["ASIO", "CHECKBOX"], [in_num, channel]]:
index = util.get_asio_checkbox_index(int(channel), int(in_num[-1]))
val = values[f"ASIO CHECKBOX||{in_num} {channel}"]
self.vm.patch.asio[index].set(val)
channel = ("left", "right")[int(channel)]
self.nvda.speak(str(val))
case [["ASIO", "CHECKBOX"], [in_num, channel], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None:
val = values[f"ASIO CHECKBOX||{in_num} {channel}"]
channel = ("left", "right")[int(channel)]
num = int(in_num[-1])
self.nvda.speak(f"Patch ASIO inputs to strips IN#{num} {channel} {val}")
# Patch COMPOSITE # Patch COMPOSITE
case [["PATCH", "COMPOSITE"], [key]]: case [["PATCH", "COMPOSITE"], [key]]:
val = values[f"PATCH COMPOSITE||{key}"] val = values[f"PATCH COMPOSITE||{key}"]
index = int(key[-1]) - 1 index = int(key[-1]) - 1
self.vm.patch.composite[index].set(util.get_patch_composite_list(self.kind).index(val) + 1) self.vm.patch.composite[index].set(util.get_patch_composite_list(self.kind).index(val) + 1)
self.TKroot.after(200, self.nvda.speak, f"PATCH COMPOSITE {key[-1]} set {val}") self.TKroot.after(200, self.nvda.speak, val)
case [["PATCH", "COMPOSITE"], [key], ["FOCUS", "IN"]]: case [["PATCH", "COMPOSITE"], [key], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None: if self.find_element_with_focus() is not None:
if values[f"PATCH COMPOSITE||{key}"]: if values[f"PATCH COMPOSITE||{key}"]:
val = values[f"PATCH COMPOSITE||{key}"] val = values[f"PATCH COMPOSITE||{key}"]
else: else:
index = int(key[-1]) - 1 index = int(key[-1]) - 1
val = util.get_patch_composite_list(self.kind)[self.vm.patch.composite[index].get() - 1] comp_index = self.vm.patch.composite[index].get()
comp_list = util.get_patch_composite_list(self.kind)
try:
val = comp_list[comp_index - 1]
except IndexError as e:
val = comp_list[-1]
self.logger.error(f"{type(e).__name__}: {e}")
self.nvda.speak(f"Patch COMPOSITE {key[-1]} {val}") self.nvda.speak(f"Patch COMPOSITE {key[-1]} {val}")
case [["PATCH", "COMPOSITE"], [key], ["KEY", "SPACE" | "ENTER"]]: case [["PATCH", "COMPOSITE"], [key], ["KEY", "SPACE" | "ENTER"]]:
util.open_context_menu_for_buttonmenu(self, f"PATCH COMPOSITE||{key}") util.open_context_menu_for_buttonmenu(self, f"PATCH COMPOSITE||{key}")
@@ -643,54 +645,36 @@ class NVDAVMWindow(psg.Window):
# Strip Params # Strip Params
case [["STRIP", index], [param]]: case [["STRIP", index], [param]]:
label = self.cache["labels"][f"STRIP {index}||LABEL"]
match param: match param:
case "MONO": case "KARAOKE":
if int(index) < self.kind.phys_in: opts = ["off", "k m", "k 1", "k 2", "k v"]
actual = param.lower()
elif int(index) == self.kind.phys_in + 1:
actual = "k"
else:
actual = "mc"
phonetic = {"k": "karaoke"}
if actual == "k":
next_val = self.vm.strip[int(index)].k + 1 next_val = self.vm.strip[int(index)].k + 1
if next_val == 4: if next_val == len(opts):
next_val = 0 next_val = 0
setattr(self.vm.strip[int(index)], actual, next_val) self.vm.strip[int(index)].k = next_val
self.cache["strip"][f"STRIP {index}||{param}"] = next_val self.cache["strip"][f"STRIP {index}||{param}"] = next_val
self.nvda.speak(["off", "k m", "k 1", "k 2"][next_val]) self.nvda.speak(opts[next_val])
else: case output if param in util._get_bus_assignments(self.kind):
val = not self.cache["strip"][f"STRIP {index}||{param}"] val = not self.cache["strip"][f"STRIP {index}||{output}"]
setattr(self.vm.strip[int(index)], actual, val) setattr(self.vm.strip[int(index)], output, val)
self.cache["strip"][f"STRIP {index}||{param}"] = val self.cache["strip"][f"STRIP {index}||{output}"] = val
self.nvda.speak("on" if val else "off") self.nvda.speak("on" if val else "off")
case _: case _:
val = not self.cache["strip"][f"STRIP {index}||{param}"] val = not self.cache["strip"][f"STRIP {index}||{param}"]
setattr(self.vm.strip[int(index)], param if param[0] in ("A", "B") else param.lower(), val) setattr(self.vm.strip[int(index)], param.lower(), val)
self.cache["strip"][f"STRIP {index}||{param}"] = val self.cache["strip"][f"STRIP {index}||{param}"] = val
self.nvda.speak("on" if val else "off") self.nvda.speak("on" if val else "off")
case [["STRIP", index], [param], ["FOCUS", "IN"]]: case [["STRIP", index], [param], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None: if self.find_element_with_focus() is not None:
val = self.cache["strip"][f"STRIP {index}||{param}"] val = self.cache["strip"][f"STRIP {index}||{param}"]
match param: phonetic = {"KARAOKE": "karaoke"}
case "MONO":
if int(index) < self.kind.phys_in:
actual = param.lower()
elif int(index) == self.kind.phys_in + 1:
actual = "k"
else:
actual = "mc"
case _:
actual = param
phonetic = {"k": "karaoke"}
label = self.cache["labels"][f"STRIP {index}||LABEL"] label = self.cache["labels"][f"STRIP {index}||LABEL"]
if actual == "k": if param == "KARAOKE":
self.nvda.speak( self.nvda.speak(
f"{label} {phonetic.get(actual, actual)} {['off', 'k m', 'k 1', 'k 2'][self.cache['strip'][f'STRIP {int(index)}||{param}']]}" f"{label} {phonetic.get(param, param)} {['off', 'k m', 'k 1', 'k 2', 'k v'][self.cache['strip'][f'STRIP {int(index)}||{param}']]}"
) )
else: else:
self.nvda.speak(f"{label} {phonetic.get(actual, actual)} {'on' if val else 'off'}") self.nvda.speak(f"{label} {phonetic.get(param, param)} {'on' if val else 'off'}")
case [["STRIP", index], [param], ["KEY", "ENTER"]]: case [["STRIP", index], [param], ["KEY", "ENTER"]]:
self.find_element_with_focus().click() self.find_element_with_focus().click()
@@ -960,6 +944,7 @@ class NVDAVMWindow(psg.Window):
self.vm.strip[int(index)].gain = 0 self.vm.strip[int(index)].gain = 0
self[f"STRIP {index}||SLIDER {param}"].update(value=0) self[f"STRIP {index}||SLIDER {param}"].update(value=0)
case "COMP" | "GATE" | "DENOISER": case "COMP" | "GATE" | "DENOISER":
target = getattr(self.vm.strip[int(index)], param.lower())
setattr(target, "knob", 0) setattr(target, "knob", 0)
self[f"STRIP {index}||SLIDER {param}"].update(value=0) self[f"STRIP {index}||SLIDER {param}"].update(value=0)
case "AUDIBILITY": case "AUDIBILITY":
@@ -971,7 +956,7 @@ class NVDAVMWindow(psg.Window):
case "LIMIT": case "LIMIT":
self.vm.strip[int(index)].limit = 12 self.vm.strip[int(index)].limit = 12
self[f"STRIP {index}||SLIDER {param}"].update(value=12) self[f"STRIP {index}||SLIDER {param}"].update(value=12)
self.nvda.speak(f"{param} {12 if param == 'LABEL' else 0}") self.nvda.speak(f"{12 if param == 'LIMIT' else 0}")
# Bus Params # Bus Params
case [["BUS", index], [param]]: case [["BUS", index], [param]]:
@@ -997,39 +982,26 @@ class NVDAVMWindow(psg.Window):
"on" if val else "off", "on" if val else "off",
) )
case "MODE": case "MODE":
bus_modes = util.get_bus_modes(self.vm) chosen = util._bus_mode_map_reversed[values[event]]
next_index = bus_modes.index(val) + 1 setattr(self.vm.bus[int(index)].mode, chosen, True)
if next_index == len(bus_modes): self.cache["bus"][event] = chosen
next_index = 0
next_bus = bus_modes[next_index]
phonetic = {
"amix": "Mix Down A",
"bmix": "Mix Down B",
"repeat": "Stereo Repeat",
"tvmix": "Up Mix TV",
"upmix21": "Up Mix 2.1",
"upmix41": "Up Mix 4.1",
"upmix61": "Up Mix 6.1",
"centeronly": "Center Only",
"lfeonly": "Low Frequency Effect Only",
"rearonly": "Rear Only",
}
setattr(self.vm.bus[int(index)].mode, next_bus, True)
self.cache["bus"][event] = next_bus
self.TKroot.after( self.TKroot.after(
200, 200,
self.nvda.speak, self.nvda.speak,
phonetic.get(next_bus, next_bus), util._bus_mode_map[chosen],
) )
case [["BUS", index], [param], ["FOCUS", "IN"]]: case [["BUS", index], [param], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None: if self.find_element_with_focus() is not None:
label = self.cache["labels"][f"BUS {index}||LABEL"] label = self.cache["labels"][f"BUS {index}||LABEL"]
val = self.cache["bus"][f"BUS {index}||{param}"] val = self.cache["bus"][f"BUS {index}||{param}"]
if param == "MODE": if param == "MODE":
self.nvda.speak(f"{label} bus {param} {val}") self.nvda.speak(f"{label} bus {param} {util._bus_mode_map[val]}")
else:
self.nvda.speak(f"{label} {param} {'on' if val else 'off'}")
case [["BUS", index], [param], ["KEY", "SPACE" | "ENTER"]]:
if param == "MODE":
util.open_context_menu_for_buttonmenu(self, f"BUS {index}||MODE")
else: else:
self.nvda.speak(f"{label} bus {param} {'on' if val else 'off'}")
case [["BUS", index], [param], ["KEY", "ENTER"]]:
self.find_element_with_focus().click() self.find_element_with_focus().click()
# Bus Sliders # Bus Sliders
@@ -1104,7 +1076,7 @@ class NVDAVMWindow(psg.Window):
case [["BUS", index], ["SLIDER", "GAIN"], ["KEY", "CTRL", "SHIFT", "R"]]: case [["BUS", index], ["SLIDER", "GAIN"], ["KEY", "CTRL", "SHIFT", "R"]]:
self.vm.bus[int(index)].gain = 0 self.vm.bus[int(index)].gain = 0
self[f"BUS {index}||SLIDER GAIN"].update(value=0) self[f"BUS {index}||SLIDER GAIN"].update(value=0)
self.nvda.speak(str(val)) self.nvda.speak(str(0))
# Unknown # Unknown
case _: case _: