Compare commits

...

20 Commits

Author SHA1 Message Date
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
14 changed files with 385 additions and 228 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`
#### `Themes`
The `Themes` 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

13
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:ca47eaae0de5aa6bcc3fde33b6c1fa7dc2476aeb680f00bb1c550fe06ad67c55"
[[package]]
name = "altgraph"
@@ -251,13 +250,13 @@ files = [
[[package]]
name = "voicemeeter-api"
version = "2.4.10"
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.10-py3-none-any.whl", hash = "sha256:2f75acb7b472e56b6bd8d4f1141f32d948c55ef9b30d5a08e085a1c8e76e2464"},
{file = "voicemeeter_api-2.4.10.tar.gz", hash = "sha256:1d8dfc1e8922179f8b97c90b90b9ed051082018c6af5feb1d48250140a02d40c"},
{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.0b1"
version = "0.5.7b1"
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.10",
"voicemeeter-api>=2.5.2",
]
requires-python = ">=3.10,<3.12"
readme = "README.md"

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,61 +154,20 @@ 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"""
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(
[
psg.ButtonMenu(
f"PC{i + 1}",
size=(6, 2),
size=(5, 2),
menu_def=["", outputs],
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:
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"),
],
@@ -478,18 +439,26 @@ class Builder:
"""tab3 row represents bus composite toggle"""
def add_strip_outputs(layout):
params = ["MONO", "EQ", "MUTE", "MODE"]
if self.vm.kind.name == "basic":
params = ["MONO", "EQ", "MUTE"]
if self.kind.name == "basic":
params.remove("EQ")
label = {"MODE": "BUSMODE"}
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
layout.append(
[
psg.Button(
label.get(param, param.capitalize()),
size=(12 if param == "MODE" else 6, 2),
key=f"BUS {i}||{param}",
)
for param in params
*[
psg.Button(
param.capitalize(),
size=(6, 2),
key=f"BUS {i}||{param}",
)
for param in params
],
psg.ButtonMenu(
"BUSMODE",
size=(12, 2),
menu_def=["", busmodes],
key=f"BUS {i}||MODE",
),
]
)

View File

@@ -33,7 +33,7 @@ def get_nvdapath():
try:
NVDA_PATH = Path(get_nvdapath()) / "nvda.exe"
except FileNotFoundError as e:
except FileNotFoundError:
NVDA_PATH = ""

View File

@@ -97,7 +97,7 @@ class CompSlider(psg.Slider):
return {
"range": (-24, 24),
"default_value": self.vm.strip[self.index].comp.gainout,
"resolution": 0.01,
"resolution": 0.1,
"disabled": True,
}

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:
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":
hw_outs |= {f"HARDWARE OUT||A2": vm.bus[1].device.name}
hw_outs |= {"HARDWARE OUT||A2": vm.bus[1].device.name}
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)},
}
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
@@ -48,8 +50,11 @@ def get_patch_composite_list(kind) -> list:
for i in range(kind.phys_out):
[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):
[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
@@ -108,6 +113,24 @@ def get_channel_identifier_list(vm) -> list:
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:
if vm.kind.name == "basic":
return [
@@ -157,3 +180,45 @@ def get_full_slider_params(i, kind) -> Iterable:
if kind.name == "basic":
params.remove("LIMIT")
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)]
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",
]

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,14 +12,12 @@ from .popup import Popup
logger = logging.getLogger(__name__)
psg.theme("Dark Blue 3")
psg.theme(configuration.get("default_theme", "Dark Blue 3"))
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
@@ -45,9 +43,9 @@ class NVDAVMWindow(psg.Window):
for i in range(self.kind.phys_out):
self[f"HARDWARE OUT||A{i + 1}"].Widget.config(**buttonmenu_opts)
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":
[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}
for i in range(self.kind.num_strip):
for param in util.get_slider_params(i, self.kind):
@@ -57,21 +55,16 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||SLIDER LIMIT"].Widget.config(**slider_opts)
for i in range(self.kind.num_bus):
self[f"BUS {i}||SLIDER GAIN"].Widget.config(**slider_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[f"BUS {i}||MODE"].Widget.config(**buttonmenu_opts)
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"])
defaultconfig = Path(configuration.get("default_config", "")) # coerce the type
if defaultconfig.exists():
self.vm.set("command.load", str(defaultconfig))
self.logger.debug(f"config {defaultconfig} loaded")
@@ -81,7 +74,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)
@@ -123,10 +116,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)]
@@ -179,30 +168,24 @@ class NVDAVMWindow(psg.Window):
self.bind(f"<Alt-Control-{event}-{direction}>", f"ALT CTRL {direction.upper()}||{event_id}")
# 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("<space>", "||KEY SPACE", propagate=False)
self[f"HARDWARE IN||{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
# 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("<space>", "||KEY SPACE", propagate=False)
self[f"HARDWARE OUT||A{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
if self.vm.kind.name == "basic":
self[f"HARDWARE OUT||A2"].bind("<FocusIn>", "||FOCUS IN")
self[f"HARDWARE OUT||A2"].bind("<space>", "||KEY SPACE", propagate=False)
self[f"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")
if self.kind.name == "basic":
self["HARDWARE OUT||A2"].bind("<FocusIn>", "||FOCUS IN")
self["HARDWARE OUT||A2"].bind("<space>", "||KEY SPACE", propagate=False)
self["HARDWARE OUT||A2"].bind("<Return>", "||KEY ENTER", propagate=False)
# Patch Composite
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("<space>", "||KEY SPACE", propagate=False)
self[f"PATCH COMPOSITE||PC{i + 1}"].bind("<Return>", "||KEY ENTER", propagate=False)
@@ -236,9 +219,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):
@@ -260,13 +248,16 @@ class NVDAVMWindow(psg.Window):
self[f"STRIP {i}||SLIDER {param}"].bind("<Control-Shift-KeyPress-R>", "||KEY CTRL SHIFT R")
# Bus Params
params = ["MONO", "EQ", "MUTE", "MODE"]
if self.vm.kind.name == "basic":
params = ["MONO", "EQ", "MUTE"]
if self.kind.name == "basic":
params.remove("EQ")
for i in range(self.kind.num_bus):
for param in params:
self[f"BUS {i}||{param}"].bind("<FocusIn>", "||FOCUS IN")
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
for i in range(self.kind.num_bus):
@@ -300,13 +291,15 @@ class NVDAVMWindow(psg.Window):
self.logger.debug(f"values::{values}")
if event in (psg.WIN_CLOSED, "Exit"):
break
elif event.endswith("MODE"):
elif event in util.get_slider_modes():
mode = event
self.nvda.speak(f"{mode} enabled")
self.logger.debug(f"entered slider mode {mode}")
continue
elif event == "ESCAPE":
if mode:
self.nvda.speak(f"{mode} disabled")
self.logger.debug(f"exited from slider mode {mode}")
mode = None
continue
@@ -423,7 +416,7 @@ class NVDAVMWindow(psg.Window):
identifier, partial = focus.Key.split("||")
_, index = identifier.split()
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
continue
match tab:
@@ -452,16 +445,16 @@ class NVDAVMWindow(psg.Window):
case "tab||Settings":
self.write_event_value("ADVANCED SETTINGS", None)
case "tab||Physical Strip":
if self.kind.name != "potato":
continue
if values["tabgroup||Physical Strip"] == "tab||Physical Strip||sliders":
if focus := self.find_element_with_focus():
identifier, partial = focus.key.split("||")
_, index = identifier.split()
if "SLIDER COMP" in partial:
self.popup.compressor(int(index), title="Advanced Compressor")
elif "SLIDER GATE" in partial:
self.popup.gate(int(index), title="Advanced Gate")
match self.kind.name:
case "potato":
if "SLIDER COMP" in partial:
self.popup.compressor(int(index), title="Advanced Compressor")
elif "SLIDER GATE" in partial:
self.popup.gate(int(index), title="Advanced Gate")
# Menus
case [["Restart", "Audio", "Engine"], ["MENU"]]:
@@ -513,17 +506,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_settings", 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_settings")
self.logger.debug("default_settings 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"]]:
@@ -580,33 +583,25 @@ 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}"]
index = int(key[-1]) - 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"]]:
if self.find_element_with_focus() is not None:
if values[f"PATCH COMPOSITE||{key}"]:
val = values[f"PATCH COMPOSITE||{key}"]
else:
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}")
case [["PATCH", "COMPOSITE"], [key], ["KEY", "SPACE" | "ENTER"]]:
util.open_context_menu_for_buttonmenu(self, f"PATCH COMPOSITE||{key}")
@@ -647,54 +642,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()
@@ -964,6 +941,7 @@ class NVDAVMWindow(psg.Window):
self.vm.strip[int(index)].gain = 0
self[f"STRIP {index}||SLIDER {param}"].update(value=0)
case "COMP" | "GATE" | "DENOISER":
target = getattr(self.vm.strip[int(index)], param.lower())
setattr(target, "knob", 0)
self[f"STRIP {index}||SLIDER {param}"].update(value=0)
case "AUDIBILITY":
@@ -1001,40 +979,27 @@ class NVDAVMWindow(psg.Window):
"on" if val else "off",
)
case "MODE":
bus_modes = util.get_bus_modes(self.vm)
next_index = bus_modes.index(val) + 1
if next_index == len(bus_modes):
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
chosen = util._bus_mode_map_reversed[values[event]]
setattr(self.vm.bus[int(index)].mode, chosen, True)
self.cache["bus"][event] = chosen
self.TKroot.after(
200,
self.nvda.speak,
phonetic.get(next_bus, next_bus),
util._bus_mode_map[chosen],
)
case [["BUS", index], [param], ["FOCUS", "IN"]]:
if self.find_element_with_focus() is not None:
label = self.cache["labels"][f"BUS {index}||LABEL"]
val = self.cache["bus"][f"BUS {index}||{param}"]
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} bus {param} {'on' if val else 'off'}")
case [["BUS", index], [param], ["KEY", "ENTER"]]:
self.find_element_with_focus().click()
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:
self.find_element_with_focus().click()
# Bus Sliders
case [["BUS", index], ["SLIDER", "GAIN"]]: