mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-04-19 07:43:30 +00:00
Compare commits
22 Commits
add-gain-s
...
09f052b4a3
| Author | SHA1 | Date | |
|---|---|---|---|
| 09f052b4a3 | |||
| 577f1fbdbb | |||
| f9614f44ac | |||
| 9b757b743b | |||
| 7f8f6f2b6c | |||
| 0c806b8e4c | |||
| 645361c731 | |||
| 6baa0000c8 | |||
| d722154325 | |||
| c94d763569 | |||
| ac14b7ad0d | |||
| 78b7e01fa6 | |||
| 42fad86905 | |||
| 6c54d28210 | |||
| d958167ded | |||
| 0570138963 | |||
| 1fdb39a041 | |||
| 3d7b33c8be | |||
| cf5c8f81aa | |||
| c248808c7b | |||
| d93f078365 | |||
| e0f36b2411 |
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nvda_voicemeeter"
|
||||
version = "0.2.1a3"
|
||||
version = "0.2.2a1"
|
||||
description = "A Voicemeeter app compatible with NVDA"
|
||||
authors = [
|
||||
{ name = "onyx-and-iris", email = "code@onyxandiris.online" },
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import PySimpleGUI as psg
|
||||
|
||||
from .util import (
|
||||
get_asio_checkbox_index,
|
||||
get_input_device_list,
|
||||
get_insert_checkbox_index,
|
||||
get_output_device_list,
|
||||
get_patch_composite_list,
|
||||
get_tabs_labels,
|
||||
)
|
||||
from . import util
|
||||
from .compound import LabelSlider
|
||||
|
||||
|
||||
class Builder:
|
||||
@@ -91,7 +85,7 @@ class Builder:
|
||||
return psg.Tab(identifier, [[tabgroup]], key=f"tab||{identifier}")
|
||||
|
||||
tabs = []
|
||||
for tab in get_tabs_labels():
|
||||
for tab in util.get_tabs_labels():
|
||||
tabs.append(_make_tabs(tab))
|
||||
|
||||
tab_group = psg.TabGroup([tabs], change_submits=True, enable_events=True, key="tabgroup")
|
||||
@@ -116,7 +110,7 @@ class Builder:
|
||||
"""tab0 row0 represents hardware ins"""
|
||||
|
||||
def add_physical_device_opts(layout):
|
||||
devices = get_input_device_list(self.vm)
|
||||
devices = util.get_input_device_list(self.vm)
|
||||
devices.append("- remove device selection -")
|
||||
layout.append(
|
||||
[
|
||||
@@ -147,7 +141,7 @@ class Builder:
|
||||
psg.ButtonMenu(
|
||||
f"A{i + 1}",
|
||||
size=(6, 3),
|
||||
menu_def=["", get_output_device_list(i, self.vm)],
|
||||
menu_def=["", util.get_output_device_list(i, self.vm)],
|
||||
key=f"HARDWARE OUT||A{i + 1}",
|
||||
)
|
||||
for i in range(num_outs)
|
||||
@@ -167,7 +161,7 @@ class Builder:
|
||||
[
|
||||
psg.Spin(
|
||||
nums,
|
||||
initial_value=self.window.cache["asio"][f"ASIO CHECKBOX||{get_asio_checkbox_index(0, i)}"],
|
||||
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",
|
||||
@@ -178,7 +172,7 @@ class Builder:
|
||||
[
|
||||
psg.Spin(
|
||||
nums,
|
||||
initial_value=self.window.cache["asio"][f"ASIO CHECKBOX||{get_asio_checkbox_index(1, i)}"],
|
||||
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",
|
||||
@@ -199,7 +193,7 @@ class Builder:
|
||||
"""tab0 row3 represents patch composite"""
|
||||
|
||||
def add_physical_device_opts(layout):
|
||||
outputs = get_patch_composite_list(self.vm.kind)
|
||||
outputs = util.get_patch_composite_list(self.vm.kind)
|
||||
layout.append(
|
||||
[
|
||||
psg.ButtonMenu(
|
||||
@@ -226,7 +220,7 @@ class Builder:
|
||||
[
|
||||
psg.Checkbox(
|
||||
text=channel,
|
||||
default=self.vm.patch.insert[get_insert_checkbox_index(self.kind, j, i)].on,
|
||||
default=self.vm.patch.insert[util.get_insert_checkbox_index(self.kind, j, i)].on,
|
||||
enable_events=True,
|
||||
key=f"INSERT CHECKBOX||IN{i} {j}",
|
||||
)
|
||||
@@ -239,7 +233,7 @@ class Builder:
|
||||
[
|
||||
psg.Checkbox(
|
||||
text=channel,
|
||||
default=self.vm.patch.insert[get_insert_checkbox_index(self.kind, j, i)].on,
|
||||
default=self.vm.patch.insert[util.get_insert_checkbox_index(self.kind, j, i)].on,
|
||||
enable_events=True,
|
||||
key=f"INSERT CHECKBOX||IN{i} {j}",
|
||||
)
|
||||
@@ -314,6 +308,7 @@ class Builder:
|
||||
def add_gain_slider(layout):
|
||||
layout.append(
|
||||
[
|
||||
psg.Text("Gain"),
|
||||
psg.Slider(
|
||||
range=(-60, 12),
|
||||
default_value=self.vm.strip[i].gain,
|
||||
@@ -323,13 +318,36 @@ class Builder:
|
||||
enable_events=True,
|
||||
orientation="horizontal",
|
||||
key=f"STRIP {i}||SLIDER GAIN",
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
outputs = []
|
||||
[step(outputs) for step in (add_gain_slider,)]
|
||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], outputs, key=f"STRIP {i}||LABEL||SLIDER")
|
||||
def add_param_sliders(layout):
|
||||
layout.append([LabelSlider(self.window, i, param) for param in util.get_slider_params(i, self.vm)])
|
||||
|
||||
def add_limit_slider(layout):
|
||||
layout.append(
|
||||
[
|
||||
psg.Text("Limit"),
|
||||
psg.Slider(
|
||||
range=(-40, 12),
|
||||
default_value=self.vm.strip[i].limit,
|
||||
resolution=1,
|
||||
disable_number_display=True,
|
||||
expand_x=True,
|
||||
enable_events=True,
|
||||
orientation="horizontal",
|
||||
key=f"STRIP {i}||SLIDER LIMIT",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
layout = []
|
||||
steps = (add_gain_slider, add_param_sliders)
|
||||
if self.kind.name in ("banana", "potato"):
|
||||
steps += (add_limit_slider,)
|
||||
[step(layout) for step in steps]
|
||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], layout, key=f"STRIP {i}||LABEL||SLIDER")
|
||||
|
||||
def make_tab1_slider_rows(self) -> psg.Frame:
|
||||
layout = [[self.make_tab1_slider_row(i)] for i in range(self.kind.phys_in)]
|
||||
@@ -382,6 +400,7 @@ class Builder:
|
||||
def add_gain_slider(layout):
|
||||
layout.append(
|
||||
[
|
||||
psg.Text("Gain"),
|
||||
psg.Slider(
|
||||
range=(-60, 12),
|
||||
default_value=self.vm.strip[i].gain,
|
||||
@@ -391,13 +410,45 @@ class Builder:
|
||||
enable_events=True,
|
||||
orientation="horizontal",
|
||||
key=f"STRIP {i}||SLIDER GAIN",
|
||||
)
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
outputs = []
|
||||
[step(outputs) for step in (add_gain_slider,)]
|
||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], outputs, key=f"STRIP {i}||LABEL||SLIDER")
|
||||
def add_param_sliders(layout):
|
||||
if self.kind.name in ("basic", "banana"):
|
||||
for param in util.get_slider_params(i, self.vm):
|
||||
layout.append([LabelSlider(self.window, i, param, range_=(-12, 12))])
|
||||
else:
|
||||
layout.append(
|
||||
[
|
||||
LabelSlider(self.window, i, param, range_=(-12, 12))
|
||||
for param in util.get_slider_params(i, self.vm)
|
||||
]
|
||||
)
|
||||
|
||||
def add_limit_slider(layout):
|
||||
layout.append(
|
||||
[
|
||||
psg.Text("Limit"),
|
||||
psg.Slider(
|
||||
range=(-40, 12),
|
||||
default_value=self.vm.strip[i].limit,
|
||||
resolution=1,
|
||||
disable_number_display=True,
|
||||
expand_x=True,
|
||||
enable_events=True,
|
||||
orientation="horizontal",
|
||||
key=f"STRIP {i}||SLIDER LIMIT",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
layout = []
|
||||
steps = (add_gain_slider, add_param_sliders)
|
||||
if self.kind.name in ("banana", "potato"):
|
||||
steps += (add_limit_slider,)
|
||||
[step(layout) for step in steps]
|
||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], layout, key=f"STRIP {i}||LABEL||SLIDER")
|
||||
|
||||
def make_tab2_slider_rows(self) -> psg.Frame:
|
||||
layout = [
|
||||
|
||||
35
src/nvda_voicemeeter/compound.py
Normal file
35
src/nvda_voicemeeter/compound.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import PySimpleGUI as psg
|
||||
|
||||
|
||||
class LabelSlider(psg.Frame):
|
||||
"""Compound Label Slider element"""
|
||||
|
||||
def __init__(self, parent, i, param, range_=(0, 10), *args, **kwargs):
|
||||
self.parent = parent
|
||||
if param in ("AUDIBILITY", "DENOISER"):
|
||||
size = 7
|
||||
else:
|
||||
size = 4
|
||||
layout = [
|
||||
[
|
||||
psg.Text(param.capitalize(), size=size),
|
||||
psg.Slider(
|
||||
range=range_,
|
||||
default_value=self.default_value(i, param),
|
||||
resolution=0.1,
|
||||
disable_number_display=True,
|
||||
size=(12, 16),
|
||||
expand_x=True,
|
||||
enable_events=True,
|
||||
orientation="horizontal",
|
||||
key=f"STRIP {i}||SLIDER {param}",
|
||||
),
|
||||
]
|
||||
]
|
||||
super().__init__(None, layout=layout, border_width=0, pad=0, *args, **kwargs)
|
||||
|
||||
def default_value(self, i, param):
|
||||
target = getattr(self.parent.vm.strip[i], param.lower())
|
||||
if param in ("COMP", "GATE", "DENOISER"):
|
||||
return target.knob
|
||||
return target
|
||||
162
src/nvda_voicemeeter/popup.py
Normal file
162
src/nvda_voicemeeter/popup.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import PySimpleGUI as psg
|
||||
|
||||
from . import util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Popup:
|
||||
def __init__(self, window):
|
||||
self.window = window
|
||||
self.logger = logger.getChild(type(self).__name__)
|
||||
|
||||
def save_as(self, message, title=None, initial_folder=None):
|
||||
layout = [
|
||||
[psg.Text(message)],
|
||||
[
|
||||
psg.FileSaveAs("Browse", initial_folder=str(initial_folder), file_types=(("XML", ".xml"),)),
|
||||
psg.Button("Cancel"),
|
||||
],
|
||||
]
|
||||
popup = psg.Window(title, layout, finalize=True)
|
||||
popup["Browse"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Browse"].bind("<Return>", "||KEY ENTER")
|
||||
popup["Cancel"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Cancel"].bind("<Return>", "||KEY ENTER")
|
||||
filepath = None
|
||||
while True:
|
||||
event, values = popup.read()
|
||||
self.logger.debug(f"event::{event}")
|
||||
self.logger.debug(f"values::{values}")
|
||||
if event in (psg.WIN_CLOSED, "Cancel"):
|
||||
break
|
||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
||||
case [[button], ["FOCUS", "IN"]]:
|
||||
if values["Browse"]:
|
||||
filepath = values["Browse"]
|
||||
break
|
||||
self.window.nvda.speak(button)
|
||||
case [[button], ["KEY", "ENTER"]]:
|
||||
popup.find_element_with_focus().click()
|
||||
self.logger.debug(f"parsed::{parsed_cmd}")
|
||||
popup.close()
|
||||
if filepath:
|
||||
return Path(filepath)
|
||||
|
||||
def rename(self, message, title=None, tab=None):
|
||||
if tab == "Physical Strip":
|
||||
upper = self.window.kind.phys_in + 1
|
||||
elif tab == "Virtual Strip":
|
||||
upper = self.window.kind.virt_in + 1
|
||||
elif tab == "Buses":
|
||||
upper = self.window.kind.num_bus + 1
|
||||
|
||||
layout = [
|
||||
[psg.Text(message)],
|
||||
[
|
||||
[
|
||||
psg.Spin(
|
||||
list(range(1, upper)), initial_value=1, size=2, enable_events=True, key=f"Index", readonly=True
|
||||
),
|
||||
psg.Input(key="Edit"),
|
||||
],
|
||||
[psg.Button("Ok"), psg.Button("Cancel")],
|
||||
],
|
||||
]
|
||||
popup = psg.Window(title, layout, finalize=True)
|
||||
popup["Index"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Edit"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Ok"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Ok"].bind("<Return>", "||KEY ENTER")
|
||||
popup["Cancel"].bind("<FocusIn>", "||FOCUS IN")
|
||||
popup["Cancel"].bind("<Return>", "||KEY ENTER")
|
||||
data = {}
|
||||
while True:
|
||||
event, values = popup.read()
|
||||
self.logger.debug(f"event::{event}")
|
||||
self.logger.debug(f"values::{values}")
|
||||
if event in (psg.WIN_CLOSED, "Cancel"):
|
||||
break
|
||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
||||
case ["Index"]:
|
||||
val = values["Index"]
|
||||
self.window.nvda.speak(f"Index {val}")
|
||||
case [[button], ["FOCUS", "IN"]]:
|
||||
if button == "Index":
|
||||
val = values["Index"]
|
||||
self.window.nvda.speak(f"Index {val}")
|
||||
else:
|
||||
self.window.nvda.speak(button)
|
||||
case [[button], ["KEY", "ENTER"]]:
|
||||
popup.find_element_with_focus().click()
|
||||
case ["Ok"]:
|
||||
data = values
|
||||
break
|
||||
self.logger.debug(f"parsed::{parsed_cmd}")
|
||||
popup.close()
|
||||
return data
|
||||
|
||||
def advanced_settings(self, title):
|
||||
def _make_buffering_frame() -> psg.Frame:
|
||||
buffer = [
|
||||
[
|
||||
psg.ButtonMenu(
|
||||
driver,
|
||||
size=(14, 2),
|
||||
menu_def=["", util.get_asio_samples_list(driver)],
|
||||
key=f"BUFFER {driver}",
|
||||
)
|
||||
for driver in ("MME", "WDM", "KS", "ASIO")
|
||||
],
|
||||
]
|
||||
return psg.Frame("BUFFERING", buffer)
|
||||
|
||||
layout = []
|
||||
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)
|
||||
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")
|
||||
while True:
|
||||
event, values = popup.read()
|
||||
self.logger.debug(f"event::{event}")
|
||||
self.logger.debug(f"values::{values}")
|
||||
if event in (psg.WIN_CLOSED, "Exit"):
|
||||
break
|
||||
match parsed_cmd := self.parser.match.parseString(event):
|
||||
case ["BUFFER MME" | "BUFFER WDM" | "BUFFER KS" | "BUFFER ASIO"]:
|
||||
if values[event] == "Default":
|
||||
if "MME" in event:
|
||||
val = 1024
|
||||
elif "WDM" in event or "KS" in event:
|
||||
val = 512
|
||||
else:
|
||||
val = 0
|
||||
else:
|
||||
val = int(values[event])
|
||||
driver = event.split()[1]
|
||||
self.vm.set(f"option.buffer.{driver.lower()}", val)
|
||||
self.TKroot.after(200, self.window.nvda.speak, f"{driver} BUFFER {val if val else 'default'}")
|
||||
case [["BUFFER", driver], ["FOCUS", "IN"]]:
|
||||
val = int(self.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}")
|
||||
case [[button], ["FOCUS", "IN"]]:
|
||||
self.window.nvda.speak(button)
|
||||
case [[button], ["KEY", "ENTER"]]:
|
||||
popup.find_element_with_focus().click()
|
||||
self.logger.debug(f"parsed::{parsed_cmd}")
|
||||
popup.close()
|
||||
@@ -1,3 +1,6 @@
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
def get_asio_checkbox_index(channel, num) -> int:
|
||||
if channel == 0:
|
||||
return 2 * num - 2
|
||||
@@ -8,8 +11,7 @@ def get_insert_checkbox_index(kind, channel, num) -> int:
|
||||
if num <= kind.phys_in:
|
||||
if channel == 0:
|
||||
return 2 * num - 2
|
||||
else:
|
||||
return 2 * num - 1
|
||||
return 2 * num - 1
|
||||
return (2 * kind.phys_in) + (8 * (num - kind.phys_in - 1)) + channel
|
||||
|
||||
|
||||
@@ -130,10 +132,21 @@ def get_bus_modes(vm) -> list:
|
||||
]
|
||||
|
||||
|
||||
def check_bounds(val, bounds):
|
||||
def check_bounds(val, bounds: tuple) -> int | float:
|
||||
lower, upper = bounds
|
||||
if val > upper:
|
||||
val = upper
|
||||
elif val < lower:
|
||||
val = lower
|
||||
return val
|
||||
|
||||
|
||||
def get_slider_params(i, vm) -> Iterable:
|
||||
if i < vm.kind.phys_in:
|
||||
if vm.kind.name == "basic":
|
||||
return ("AUDIBILITY",)
|
||||
if vm.kind.name == "banana":
|
||||
return ("COMP", "GATE")
|
||||
if vm.kind.name == "potato":
|
||||
return ("COMP", "GATE", "DENOISER")
|
||||
return ("BASS", "MID", "TREBLE")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user