mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-04-07 18:03:35 +00:00
Compare commits
11 Commits
implement-
...
v0.5.7a1
| Author | SHA1 | Date | |
|---|---|---|---|
| cb82033e1c | |||
| bde9020471 | |||
| 364b0deeb4 | |||
| a80e4e2d83 | |||
| 6e146bac50 | |||
| c385476cc4 | |||
| 1c09556c61 | |||
| 421688eff8 | |||
| bdd570738a | |||
| 71b137a9c2 | |||
| 912eb8c14d |
BIN
img/busmode_buttonmenu.png
Normal file
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 |
BIN
img/settings.png
BIN
img/settings.png
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
13
pdm.lock
generated
13
pdm.lock
generated
@@ -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"},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nvda_voicemeeter"
|
||||
version = "0.5.1"
|
||||
version = "0.5.7a1"
|
||||
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"
|
||||
|
||||
@@ -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"),
|
||||
],
|
||||
@@ -479,7 +440,7 @@ class Builder:
|
||||
|
||||
def add_strip_outputs(layout):
|
||||
params = ["MONO", "EQ", "MUTE"]
|
||||
if self.vm.kind.name == "basic":
|
||||
if self.kind.name == "basic":
|
||||
params.remove("EQ")
|
||||
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
|
||||
layout.append(
|
||||
|
||||
34
src/nvda_voicemeeter/configuration.py
Normal file
34
src/nvda_voicemeeter/configuration.py
Normal 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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
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 +192,24 @@ 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)]
|
||||
|
||||
|
||||
def get_themes_list() -> list:
|
||||
return sorted(
|
||||
[
|
||||
"Black",
|
||||
"Dark Blue",
|
||||
"Dark Blue 3",
|
||||
"Reddit",
|
||||
"Light Gray 1",
|
||||
"Bright Colors",
|
||||
"Dark Amber",
|
||||
"Light Grey 5",
|
||||
"Reds",
|
||||
"Dark Brown 5",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
@@ -47,7 +45,7 @@ class NVDAVMWindow(psg.Window):
|
||||
if self.kind.name == "basic":
|
||||
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):
|
||||
@@ -58,21 +56,15 @@ 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"])
|
||||
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")
|
||||
@@ -82,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)
|
||||
@@ -124,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)]
|
||||
@@ -180,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":
|
||||
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 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.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)
|
||||
@@ -237,7 +219,12 @@ 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"):
|
||||
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")
|
||||
|
||||
@@ -262,7 +249,7 @@ class NVDAVMWindow(psg.Window):
|
||||
|
||||
# Bus Params
|
||||
params = ["MONO", "EQ", "MUTE"]
|
||||
if self.vm.kind.name == "basic":
|
||||
if self.kind.name == "basic":
|
||||
params.remove("EQ")
|
||||
for i in range(self.kind.num_bus):
|
||||
for param in params:
|
||||
@@ -519,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"]]:
|
||||
@@ -586,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}")
|
||||
@@ -653,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":
|
||||
case "KARAOKE":
|
||||
opts = ["off", "k m", "k 1", "k 2", "k v"]
|
||||
next_val = self.vm.strip[int(index)].k + 1
|
||||
if next_val == 4:
|
||||
if next_val == len(opts):
|
||||
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.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(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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user