mirror of
				https://github.com/onyx-and-iris/nvda-voicemeeter.git
				synced 2025-11-04 11:51: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.
 | 
			
		||||
#   https://pdm.fming.dev/#use-with-ide
 | 
			
		||||
.pdm.toml
 | 
			
		||||
.pdm-python
 | 
			
		||||
.pdm-build/
 | 
			
		||||
 | 
			
		||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
 | 
			
		||||
__pypackages__/
 | 
			
		||||
@ -158,3 +160,5 @@ cython_debug/
 | 
			
		||||
#  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.
 | 
			
		||||
#.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
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								pdm.lock
									
									
									
										generated
									
									
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user