Compare commits

...

13 Commits

Author SHA1 Message Date
0adfec2e63 fixes regression loading config on startup
patch bump
2023-11-19 23:09:02 +00:00
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
14 changed files with 350 additions and 174 deletions

View File

@@ -131,9 +131,11 @@ For Gate BP Sidechain, Attack, Hold, Release you may use:
To reset a slider back to its default value you may use `Control + Shift + R`.
#### `Menu`
### Menu
A single menu item `Voicemeeter` can be opened using `Alt` and then `v`. The menu allows you to:
#### `Voicemeeter`
The `Voicemeeter` menu can be opened using `Alt` and then `v`. It offers the following options:
- Restart Voicemeeter audio engine
- Save/Load current settings (as an xml file)
@@ -143,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.
### `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.

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]
groups = ["default", "build", "lint", "test"]
cross_platform = true
static_urls = false
lock_version = "4.3"
content_hash = "sha256:680eff1b532e55860290380d4e2f331dc29af6fb898a0df16fdb033843bf15a4"
strategy = ["cross_platform"]
lock_version = "4.4"
content_hash = "sha256:4295f6824f37484ec423b53bd425334b0e039a51d81261b52c8890928fcf3948"
[[package]]
name = "altgraph"
@@ -75,6 +74,31 @@ files = [
{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]]
name = "mypy-extensions"
version = "1.0.0"
@@ -249,15 +273,25 @@ files = [
{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]]
name = "voicemeeter-api"
version = "2.4.11"
version = "2.5.2"
requires_python = ">=3.10,<4.0"
summary = "A Python wrapper for the Voiceemeter API"
dependencies = [
"tomli<3.0.0,>=2.0.1; python_version < \"3.11\"",
]
files = [
{file = "voicemeeter_api-2.4.11-py3-none-any.whl", hash = "sha256:7a1b290d90c851204438c18e2d343e568c242fcd1e664c5b88d4019a553d44e1"},
{file = "voicemeeter_api-2.4.11.tar.gz", hash = "sha256:875591ad326a7a13ef141536cca83953edcda81da256191bab3844bac46a0e70"},
{file = "voicemeeter_api-2.5.2-py3-none-any.whl", hash = "sha256:304c14a6eef5f95d8b883e8e1752d23f17c3d662d25a28d114d33134f62e93ec"},
{file = "voicemeeter_api-2.5.2.tar.gz", hash = "sha256:e2b9558a38ed290b184a3e46b598c0a716cef2e64521431f84a46e1180d539ca"},
]

View File

@@ -1,6 +1,6 @@
[project]
name = "nvda_voicemeeter"
version = "0.5.2"
version = "0.6.1"
description = "A Voicemeeter app compatible with NVDA"
authors = [
{ name = "onyx-and-iris", email = "code@onyxandiris.online" },
@@ -8,7 +8,7 @@ authors = [
dependencies = [
"pysimplegui>=4.60.5",
"pyparsing>=3.1.1",
"voicemeeter-api>=2.4.11",
"voicemeeter-api>=2.5.2",
]
requires-python = ">=3.10,<3.12"
readme = "README.md"
@@ -23,6 +23,7 @@ build = [
lint = [
"black>=23.7.0",
"ruff>=0.0.291",
"mypy>=1.7.0",
]
test = [
"psgdemos>=1.12.1",

View File

@@ -26,7 +26,6 @@ class Builder:
steps = (
self.make_tab0_row0,
self.make_tab0_row1,
self.make_tab0_row2,
self.make_tab0_row3,
self.make_tab0_row4,
self.make_tab0_row5,
@@ -93,6 +92,8 @@ class Builder:
return [[menu], [tab_group]]
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 = [
[
"&Voicemeeter",
@@ -103,6 +104,7 @@ class Builder:
"Load Settings on Startup ::MENU",
],
],
["&Theme", themes],
]
return psg.Menu(menu_def, key="menus")
@@ -152,47 +154,6 @@ class Builder:
[step(hardware_out) for step in (add_physical_device_opts,)]
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:
"""tab0 row3 represents patch composite"""
@@ -202,7 +163,7 @@ class Builder:
[
psg.ButtonMenu(
f"PC{i + 1}",
size=(6, 2),
size=(5, 2),
menu_def=["", outputs],
key=f"PATCH COMPOSITE||PC{i + 1}",
)
@@ -381,7 +342,7 @@ class Builder:
if i == self.kind.phys_in + 1:
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("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
],
@@ -389,7 +350,7 @@ class Builder:
else:
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("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
],

View File

@@ -5,7 +5,7 @@ from pathlib import Path
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":
raise NVDAVMError("Only Windows OS supported")

View File

@@ -13,7 +13,10 @@ class LabelSlider(psg.Frame):
if param in ("AUDIBILITY", "DENOISER"):
size = 7
else:
size = 4
if psg.theme() == "HighContrast":
size = 5
else:
size = 4
layout = [
[
psg.Text(param.capitalize(), size=size),

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

@@ -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)},
}
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}||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:
params |= {
**{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:
params = {}
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:
params = {}
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

@@ -48,6 +48,30 @@ class Popup:
if 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):
if "Strip" in tab:
if index < self.kind.phys_in:
@@ -94,6 +118,50 @@ class Popup:
return data
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:
buffer = [
[
@@ -109,27 +177,79 @@ class Popup:
return psg.Frame("BUFFERING", buffer)
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,)
for step in steps:
layout.append([step()])
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}
for driver in ("MME", "WDM", "KS", "ASIO"):
popup[f"BUFFER {driver}"].Widget.config(**buttonmenu_opts)
popup[f"BUFFER {driver}"].bind("<FocusIn>", "||FOCUS IN")
popup[f"BUFFER {driver}"].bind("<space>", "||KEY SPACE", propagate=False)
popup[f"BUFFER {driver}"].bind("<Return>", "||KEY ENTER", propagate=False)
popup["Exit"].bind("<FocusIn>", "||FOCUS IN")
popup["Exit"].bind("<Return>", "||KEY ENTER")
self.popup[f"BUFFER {driver}"].Widget.config(**buttonmenu_opts)
self.popup[f"BUFFER {driver}"].bind("<FocusIn>", "||FOCUS IN")
self.popup[f"BUFFER {driver}"].bind("<space>", "||KEY SPACE", propagate=False)
self.popup[f"BUFFER {driver}"].bind("<Return>", "||KEY ENTER", propagate=False)
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 = popup.read()
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 [["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"]:
if values[event] == "Default":
if "MME" in event:
@@ -149,27 +269,14 @@ class Popup:
val = int(self.window.vm.get(f"option.buffer.{driver.lower()}"))
self.window.nvda.speak(f"{driver} BUFFER {val if val else 'default'}")
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"]]:
self.window.nvda.speak(button)
case [_, ["KEY", "ENTER"]]:
popup.find_element_with_focus().click()
self.popup.find_element_with_focus().click()
self.logger.debug(f"parsed::{parsed_cmd}")
popup.close()
def on_pdirty(self):
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())
)
self.window.vm.observer.remove(self.on_pdirty)
self.popup.close()
def compressor(self, index, title=None):
self.index = index

View File

@@ -1,7 +1,9 @@
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:
return 2 * num - 2
return 2 * num - 1
@@ -192,3 +194,49 @@ def get_slider_modes() -> Iterable:
"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
from . import models, util
from . import configuration, models, util
from .builder import Builder
from .nvda import Nvda
from .parser import Parser
@@ -12,18 +12,19 @@ from .popup import Popup
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):
"""Represents the main window of the Voicemeeter NVDA application"""
SETTINGS = "settings.json"
def __init__(self, title, vm):
self.vm = vm
self.kind = self.vm.kind
self.logger = logger.getChild(type(self).__name__)
self.logger.debug(f"loaded with theme: {psg.theme()}")
self.cache = {
"hw_ins": models._make_hardware_ins_cache(self.vm),
"hw_outs": models._make_hardware_outs_cache(self.vm),
@@ -58,22 +59,16 @@ class NVDAVMWindow(psg.Window):
for i in range(self.kind.num_bus):
self[f"BUS {i}||SLIDER GAIN"].Widget.config(**slider_opts)
self[f"BUS {i}||MODE"].Widget.config(**buttonmenu_opts)
if self.kind.name != "basic":
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["tabgroup"].set_focus()
def __enter__(self):
settings_path = Path.cwd() / self.SETTINGS
settings_path = configuration.SETTINGS
if settings_path.exists():
try:
with open(settings_path, "r") as f:
data = json.load(f)
defaultconfig = Path(data["default_config"])
if defaultconfig.exists():
defaultconfig = Path(configuration.get("default_config", "")) # coerce the type
if defaultconfig.is_file() and defaultconfig.exists():
self.vm.set("command.load", str(defaultconfig))
self.logger.debug(f"config {defaultconfig} loaded")
self.TKroot.after(
@@ -82,7 +77,7 @@ class NVDAVMWindow(psg.Window):
f"config {defaultconfig.stem} has been loaded",
)
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.observer.add(self.on_pdirty)
@@ -124,10 +119,6 @@ class NVDAVMWindow(psg.Window):
for i in range(self.kind.num_bus):
self[f"BUS {i}||SLIDER GAIN"].update(value=self.vm.bus[i].gain)
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():
identifier, i = key.split("||")
partial = util.get_channel_identifier_list(self.vm)[int(i)]
@@ -195,12 +186,6 @@ class NVDAVMWindow(psg.Window):
self["HARDWARE OUT||A2"].bind("<space>", "||KEY SPACE", 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
if self.kind.name != "basic":
for i in range(self.kind.composite):
@@ -237,9 +222,14 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER")
else:
for param in ("MONO", "SOLO", "MUTE"):
self[f"STRIP {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
self[f"STRIP {i}||{param}"].bind("<Return>", "||KEY ENTER")
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("<Return>", "||KEY ENTER")
# Strip Sliders
for i in range(self.kind.num_strip):
@@ -519,17 +509,27 @@ class NVDAVMWindow(psg.Window):
file_types=(("XML", ".xml"),),
):
filepath = Path(filepath)
with open(self.SETTINGS, "w") as f:
json.dump({"default_config": str(filepath)}, f)
configuration.set("default_config", str(filepath))
self.TKroot.after(
200,
self.nvda.speak,
f"config {filepath.stem} set as default on startup",
)
else:
with open(self.SETTINGS, "wb") as f:
f.truncate()
self.logger.debug("settings.json was truncated")
configuration.delete("default_config")
self.logger.debug("default_config removed from settings.json")
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
case ["tabgroup"] | [["tabgroup"], ["FOCUS", "IN"]]:
@@ -586,20 +586,6 @@ class NVDAVMWindow(psg.Window):
case [["HARDWARE", "OUT"], [key], ["KEY", "SPACE" | "ENTER"]]:
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
case [["PATCH", "COMPOSITE"], [key]]:
val = values[f"PATCH COMPOSITE||{key}"]
@@ -613,10 +599,12 @@ class NVDAVMWindow(psg.Window):
else:
index = int(key[-1]) - 1
comp_index = self.vm.patch.composite[index].get()
if self.kind.name == "banana":
if comp_index == 64: # bus channel
comp_index = 0
val = util.get_patch_composite_list(self.kind)[comp_index - 1]
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}")
case [["PATCH", "COMPOSITE"], [key], ["KEY", "SPACE" | "ENTER"]]:
util.open_context_menu_for_buttonmenu(self, f"PATCH COMPOSITE||{key}")
@@ -657,54 +645,36 @@ class NVDAVMWindow(psg.Window):
# Strip Params
case [["STRIP", index], [param]]:
label = self.cache["labels"][f"STRIP {index}||LABEL"]
match param:
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"
phonetic = {"k": "karaoke"}
if actual == "k":
next_val = self.vm.strip[int(index)].k + 1
if next_val == 4:
next_val = 0
setattr(self.vm.strip[int(index)], actual, next_val)
self.cache["strip"][f"STRIP {index}||{param}"] = next_val
self.nvda.speak(["off", "k m", "k 1", "k 2"][next_val])
else:
val = not self.cache["strip"][f"STRIP {index}||{param}"]
setattr(self.vm.strip[int(index)], actual, val)
self.cache["strip"][f"STRIP {index}||{param}"] = val
self.nvda.speak("on" if val else "off")
case "KARAOKE":
opts = ["off", "k m", "k 1", "k 2", "k v"]
next_val = self.vm.strip[int(index)].k + 1
if next_val == len(opts):
next_val = 0
self.vm.strip[int(index)].k = next_val
self.cache["strip"][f"STRIP {index}||{param}"] = next_val
self.nvda.speak(opts[next_val])
case output if param in util._get_bus_assignments(self.kind):
val = not self.cache["strip"][f"STRIP {index}||{output}"]
setattr(self.vm.strip[int(index)], output, val)
self.cache["strip"][f"STRIP {index}||{output}"] = val
self.nvda.speak("on" if val else "off")
case _:
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.nvda.speak("on" if val else "off")
case [["STRIP", index], [param], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None:
val = self.cache["strip"][f"STRIP {index}||{param}"]
match param:
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"}
phonetic = {"KARAOKE": "karaoke"}
label = self.cache["labels"][f"STRIP {index}||LABEL"]
if actual == "k":
if param == "KARAOKE":
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:
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"]]:
self.find_element_with_focus().click()