mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2024-11-22 10:00:46 +00:00
initial commit
This commit is contained in:
parent
7c0f3327f8
commit
e200462b85
4
.gitignore
vendored
4
.gitignore
vendored
@ -108,6 +108,8 @@ ipython_config.py
|
|||||||
# in version control.
|
# in version control.
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
.pdm.toml
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
@ -158,3 +160,5 @@ cython_debug/
|
|||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
|
controllerClient/
|
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"black-formatter.args": [
|
||||||
|
"--line-length=120"
|
||||||
|
]
|
||||||
|
}
|
9
__main__.py
Normal file
9
__main__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import voicemeeterlib
|
||||||
|
|
||||||
|
import nvda_voicemeeter
|
||||||
|
|
||||||
|
kind_id = "potato"
|
||||||
|
|
||||||
|
with voicemeeterlib.api("potato") as vm:
|
||||||
|
with nvda_voicemeeter.build(f"Voicemeeter {kind_id.capitalize()} NVDA", vm) as window:
|
||||||
|
window.run()
|
51
pdm.lock
Normal file
51
pdm.lock
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# This file is @generated by PDM.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
groups = ["default"]
|
||||||
|
cross_platform = true
|
||||||
|
static_urls = false
|
||||||
|
lock_version = "4.3"
|
||||||
|
content_hash = "sha256:0dfd1ea07c294dd2b837a34ff9d286134e5119c6249e4f9cd6c1ed121de97851"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.1.1"
|
||||||
|
requires_python = ">=3.6.8"
|
||||||
|
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||||
|
files = [
|
||||||
|
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
|
||||||
|
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pysimplegui"
|
||||||
|
version = "4.60.5"
|
||||||
|
summary = "Python GUIs for Humans. Launched in 2018. It's 2022 & PySimpleGUI is an ACTIVE & supported project. Super-simple to create custom GUI's. 325+ Demo programs & Cookbook for rapid start. Extensive documentation. Main docs at www.PySimpleGUI.org. Fun & your success are the focus. Examples using Machine Learning (GUI, OpenCV Integration), Rainmeter Style Desktop Widgets, Matplotlib + Pyplot, PIL support, add GUI to command line scripts, PDF & Image Viewers. Great for beginners & advanced GUI programmers."
|
||||||
|
files = [
|
||||||
|
{file = "PySimpleGUI-4.60.5-py3-none-any.whl", hash = "sha256:004f33311ee685a5287fad54f500f7290b40d7c806044e478b1384f85dedce64"},
|
||||||
|
{file = "PySimpleGUI-4.60.5.tar.gz", hash = "sha256:31014d1cc5eef1373d7e93564ff2604662645cc774a939b1f01aa253e7f9d78b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "A lil' TOML parser"
|
||||||
|
files = [
|
||||||
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "voicemeeter-api"
|
||||||
|
version = "2.4.8"
|
||||||
|
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.8-py3-none-any.whl", hash = "sha256:a3ff9e6f7516d2adedde93f6976526b385d8ae6d86598bfe541a44f498f42ea6"},
|
||||||
|
{file = "voicemeeter_api-2.4.8.tar.gz", hash = "sha256:0d37a9f2af0f68087aa9c76a8cfb2ba44c6b75bb344e8dfa9fd18ad2c862730c"},
|
||||||
|
]
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[project]
|
||||||
|
name = "nvda_voicemeeter"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Voicemeeter app compatible with NVDA"
|
||||||
|
authors = [
|
||||||
|
{name = "onyx-and-iris", email = "code@onyxandiris.online"},
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"pysimplegui>=4.60.5",
|
||||||
|
"pyparsing>=3.1.1",
|
||||||
|
"voicemeeter-api>=2.4.8",
|
||||||
|
]
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
readme = "README.md"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
|
1
src/nvda_voicemeeter/__init__.py
Normal file
1
src/nvda_voicemeeter/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .window import request_window_object as build
|
10
src/nvda_voicemeeter/cdll.py
Normal file
10
src/nvda_voicemeeter/cdll.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import ctypes as ct
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
|
||||||
|
|
||||||
|
controller_path = Path(__file__).parents[2].resolve() / "controllerClient"
|
||||||
|
|
||||||
|
DLL_PATH = controller_path / f"x{64 if bits == 64 else 86}" / f"nvdaControllerClient{bits}.dll"
|
||||||
|
|
||||||
|
libc = ct.CDLL(str(DLL_PATH))
|
26
src/nvda_voicemeeter/models.py
Normal file
26
src/nvda_voicemeeter/models.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
def _make_cache(vm) -> dict:
|
||||||
|
match vm.kind.name:
|
||||||
|
case "basic":
|
||||||
|
return {
|
||||||
|
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
|
}
|
||||||
|
case "banana":
|
||||||
|
return {
|
||||||
|
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
||||||
|
}
|
||||||
|
case "potato":
|
||||||
|
return {
|
||||||
|
**{f"BUTTON||strip {i} A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A4": vm.strip[i].A4 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} A5": vm.strip[i].A5 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
||||||
|
**{f"BUTTON||strip {i} B3": vm.strip[i].B3 for i in range(vm.kind.num_strip)},
|
||||||
|
}
|
29
src/nvda_voicemeeter/nvda.py
Normal file
29
src/nvda_voicemeeter/nvda.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from .cdll import libc
|
||||||
|
|
||||||
|
|
||||||
|
class CBindings:
|
||||||
|
bind_test_if_running = libc.nvdaController_testIfRunning
|
||||||
|
bind_speak_text = libc.nvdaController_speakText
|
||||||
|
bind_cancel_speech = libc.nvdaController_cancelSpeech
|
||||||
|
bind_braille_message = libc.nvdaController_brailleMessage
|
||||||
|
|
||||||
|
def call(self, fn, *args, ok=(0,)):
|
||||||
|
retval = fn(*args)
|
||||||
|
if retval not in ok:
|
||||||
|
raise RuntimeError(f"{fn.__name__} returned {retval}")
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
class Nvda(CBindings):
|
||||||
|
@property
|
||||||
|
def is_running(self):
|
||||||
|
return self.call(self.bind_test_if_running, ok=(0, 1)) == 0
|
||||||
|
|
||||||
|
def speak(self, text):
|
||||||
|
self.call(self.bind_speak_text, text)
|
||||||
|
|
||||||
|
def cancel_speech(self):
|
||||||
|
self.call(self.bind_cancel_speech)
|
||||||
|
|
||||||
|
def braille_message(self, text):
|
||||||
|
self.call(self.bind_braille_message, text)
|
10
src/nvda_voicemeeter/parser.py
Normal file
10
src/nvda_voicemeeter/parser.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from pyparsing import Group, OneOrMore, Optional, Suppress, Word, alphanums
|
||||||
|
|
||||||
|
|
||||||
|
class Parser:
|
||||||
|
def __init__(self):
|
||||||
|
self.widget = Group(OneOrMore(Word(alphanums)))
|
||||||
|
self.token = Suppress("||")
|
||||||
|
self.identifier = Group(OneOrMore(Word(alphanums)))
|
||||||
|
self.event = OneOrMore(Word(alphanums))
|
||||||
|
self.match = self.widget + self.token + self.identifier + Optional(self.token) + Optional(self.event)
|
60
src/nvda_voicemeeter/window.py
Normal file
60
src/nvda_voicemeeter/window.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import PySimpleGUI as psg
|
||||||
|
|
||||||
|
from .models import _make_cache
|
||||||
|
from .nvda import Nvda
|
||||||
|
from .parser import Parser
|
||||||
|
|
||||||
|
|
||||||
|
class Window(psg.Window):
|
||||||
|
def __init__(self, title, vm):
|
||||||
|
self.vm = vm
|
||||||
|
self.kind = self.vm.kind
|
||||||
|
super().__init__(title, self.make_layout(), finalize=True)
|
||||||
|
self.cache = _make_cache(self.vm)
|
||||||
|
self.nvda = Nvda()
|
||||||
|
self.parser = Parser()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def make_layout(self) -> list:
|
||||||
|
"""Builds the window layout step by step"""
|
||||||
|
|
||||||
|
def add_physical_device_opts(layout):
|
||||||
|
devices = ["{type}: {name}".format(**self.vm.device.output(i)) for i in range(self.vm.device.outs)]
|
||||||
|
layout.append(
|
||||||
|
[
|
||||||
|
psg.Combo(
|
||||||
|
devices,
|
||||||
|
size=(22, 4),
|
||||||
|
expand_x=True,
|
||||||
|
enable_events=True,
|
||||||
|
key=f"DEVICE LIST||PHYSOUT {i}",
|
||||||
|
)
|
||||||
|
for i in range(self.kind.phys_out)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
upper_layout = list()
|
||||||
|
[step(upper_layout) for step in (add_physical_device_opts,)]
|
||||||
|
row0 = psg.Frame("Hardware Out", upper_layout)
|
||||||
|
|
||||||
|
return [[row0]]
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Runs the main window until an Close/Exit event"""
|
||||||
|
while True:
|
||||||
|
event, values = self.read()
|
||||||
|
if event in (psg.WIN_CLOSED, "Exit"):
|
||||||
|
break
|
||||||
|
match self.parser.match.parseString(event):
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def request_window_object(title, vm):
|
||||||
|
WINDOW_cls = Window
|
||||||
|
return WINDOW_cls(title, vm)
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user