mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-27 13:09:10 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fca7da033 | |||
| 92f357f003 | |||
| d54995dbf1 | |||
| bc0b25032a | |||
| 2a86c05bea | |||
| aae62fa136 | |||
| 5b4a76c484 | |||
| dfb96777bb |
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@ -180,9 +180,9 @@ jobs:
|
|||||||
- name: Build Summary
|
- name: Build Summary
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
Write-Host -ForegroundColor Green "🎉 Build completed successfully!"
|
Write-Host -ForegroundColor Green "Build completed successfully!"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "📦 Built artifacts:"
|
Write-Host "Built artifacts:"
|
||||||
Write-Host " - nvda-voicemeeter-basic.zip"
|
Write-Host " - nvda-voicemeeter-basic.zip"
|
||||||
Write-Host " - nvda-voicemeeter-banana.zip"
|
Write-Host " - nvda-voicemeeter-banana.zip"
|
||||||
Write-Host " - nvda-voicemeeter-potato.zip"
|
Write-Host " - nvda-voicemeeter-potato.zip"
|
||||||
@ -208,22 +208,22 @@ jobs:
|
|||||||
--title "NVDA-Voicemeeter $TAG_NAME" \
|
--title "NVDA-Voicemeeter $TAG_NAME" \
|
||||||
--notes "## NVDA-Voicemeeter Release $TAG_NAME
|
--notes "## NVDA-Voicemeeter Release $TAG_NAME
|
||||||
|
|
||||||
### 📦 Downloads
|
### Downloads
|
||||||
- **nvda-voicemeeter-basic.zip** - Basic version with dependencies
|
- **nvda-voicemeeter-basic.zip** - Basic version with dependencies
|
||||||
- **nvda-voicemeeter-banana.zip** - Banana version with dependencies
|
- **nvda-voicemeeter-banana.zip** - Banana version with dependencies
|
||||||
- **nvda-voicemeeter-potato.zip** - Potato version with dependencies
|
- **nvda-voicemeeter-potato.zip** - Potato version with dependencies
|
||||||
|
|
||||||
### 🔧 Requirements
|
### Requirements
|
||||||
- Windows 10/11
|
- Windows 10/11
|
||||||
- Voicemeeter (Basic/Banana/Potato) installed
|
- Voicemeeter (Basic/Banana/Potato) installed
|
||||||
- NVDA screen reader
|
- NVDA screen reader
|
||||||
|
|
||||||
### 🚀 Installation
|
### Installation
|
||||||
1. Download the appropriate zip for your Voicemeeter version
|
1. Download the appropriate zip for your Voicemeeter version
|
||||||
2. Extract and run the executable - no installation required
|
2. Extract and run the executable - no installation required
|
||||||
3. The application will integrate with NVDA automatically
|
3. The application will integrate with NVDA automatically
|
||||||
|
|
||||||
### 📝 Notes
|
### Notes
|
||||||
- Built with dynamic build system using PyInstaller
|
- Built with dynamic build system using PyInstaller
|
||||||
- Includes NVDA Controller Client for screen reader integration"
|
- Includes NVDA Controller Client for screen reader integration"
|
||||||
env:
|
env:
|
||||||
|
|||||||
@ -22,6 +22,12 @@ tasks:
|
|||||||
build:
|
build:
|
||||||
desc: Build the project
|
desc: Build the project
|
||||||
deps: [generate-specs]
|
deps: [generate-specs]
|
||||||
|
preconditions:
|
||||||
|
- sh: |
|
||||||
|
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
||||||
cmds:
|
cmds:
|
||||||
- for:
|
- for:
|
||||||
matrix:
|
matrix:
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nvda-voicemeeter"
|
name = "nvda-voicemeeter"
|
||||||
version = "1.1.0"
|
version = "1.1.2"
|
||||||
description = "A Voicemeeter app compatible with NVDA"
|
description = "A Voicemeeter app compatible with NVDA"
|
||||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import ctypes as ct
|
import ctypes as ct
|
||||||
import platform
|
import platform
|
||||||
import winreg
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .errors import NVDAVMError
|
from .errors import NVDAVMError
|
||||||
|
|
||||||
BITS = 64 if ct.sizeof(ct.c_void_p) == 8 else 32
|
try:
|
||||||
|
import winreg
|
||||||
|
except ImportError as e:
|
||||||
|
ERR_MSG = 'winreg module not found, only Windows OS supported'
|
||||||
|
raise NVDAVMError(ERR_MSG) from e
|
||||||
|
|
||||||
|
# Defense against edge cases where winreg imports but we're not on Windows
|
||||||
if platform.system() != 'Windows':
|
if platform.system() != 'Windows':
|
||||||
raise NVDAVMError('Only Windows OS supported')
|
raise NVDAVMError('Only Windows OS supported')
|
||||||
|
|
||||||
|
BITS = 64 if ct.sizeof(ct.c_void_p) == 8 else 32
|
||||||
|
|
||||||
REG_KEY = '\\'.join(
|
REG_KEY = '\\'.join(
|
||||||
filter(
|
filter(
|
||||||
None,
|
None,
|
||||||
@ -43,4 +49,4 @@ if not controller_path.exists():
|
|||||||
|
|
||||||
DLL_PATH = controller_path / f'x{64 if BITS == 64 else 86}' / 'nvdaControllerClient.dll'
|
DLL_PATH = controller_path / f'x{64 if BITS == 64 else 86}' / 'nvdaControllerClient.dll'
|
||||||
|
|
||||||
libc = ct.CDLL(str(DLL_PATH))
|
libc = ct.WinDLL(str(DLL_PATH))
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
from .cdll import libc
|
from .cdll import libc
|
||||||
from .errors import NVDAVMCAPIError
|
from .errors import NVDAVMCAPIError
|
||||||
|
|
||||||
|
|
||||||
|
class ServerState(IntEnum):
|
||||||
|
RUNNING = 0
|
||||||
|
UNAVAILABLE = 1722
|
||||||
|
|
||||||
|
|
||||||
class CBindings:
|
class CBindings:
|
||||||
bind_test_if_running = libc.nvdaController_testIfRunning
|
bind_test_if_running = libc.nvdaController_testIfRunning
|
||||||
bind_speak_text = libc.nvdaController_speakText
|
bind_speak_text = libc.nvdaController_speakText
|
||||||
@ -18,7 +25,10 @@ class CBindings:
|
|||||||
class Nvda(CBindings):
|
class Nvda(CBindings):
|
||||||
@property
|
@property
|
||||||
def is_running(self):
|
def is_running(self):
|
||||||
return self.call(self.bind_test_if_running) == 0
|
return (
|
||||||
|
self.call(self.bind_test_if_running, ok=(ServerState.RUNNING, ServerState.UNAVAILABLE))
|
||||||
|
== ServerState.RUNNING
|
||||||
|
)
|
||||||
|
|
||||||
def speak(self, text):
|
def speak(self, text):
|
||||||
self.call(self.bind_speak_text, text)
|
self.call(self.bind_speak_text, text)
|
||||||
|
|||||||
@ -35,7 +35,7 @@ class Popup:
|
|||||||
self.logger.debug(f'values::{values}')
|
self.logger.debug(f'values::{values}')
|
||||||
if event in (psg.WIN_CLOSED, 'Cancel'):
|
if event in (psg.WIN_CLOSED, 'Cancel'):
|
||||||
break
|
break
|
||||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
match parsed_cmd := self.window.parser.match.parse_string(event):
|
||||||
case [[button], ['FOCUS', 'IN']]:
|
case [[button], ['FOCUS', 'IN']]:
|
||||||
if values['Browse']:
|
if values['Browse']:
|
||||||
filepath = values['Browse']
|
filepath = values['Browse']
|
||||||
@ -105,7 +105,7 @@ class Popup:
|
|||||||
self.logger.debug(f'values::{values}')
|
self.logger.debug(f'values::{values}')
|
||||||
if event in (psg.WIN_CLOSED, 'Cancel'):
|
if event in (psg.WIN_CLOSED, 'Cancel'):
|
||||||
break
|
break
|
||||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
match parsed_cmd := self.window.parser.match.parse_string(event):
|
||||||
case [[button], ['FOCUS', 'IN']]:
|
case [[button], ['FOCUS', 'IN']]:
|
||||||
self.window.nvda.speak(button)
|
self.window.nvda.speak(button)
|
||||||
case [_, ['KEY', 'ENTER']]:
|
case [_, ['KEY', 'ENTER']]:
|
||||||
@ -227,7 +227,7 @@ class Popup:
|
|||||||
self.logger.debug(f'values::{values}')
|
self.logger.debug(f'values::{values}')
|
||||||
if event in (psg.WIN_CLOSED, 'Exit'):
|
if event in (psg.WIN_CLOSED, 'Exit'):
|
||||||
break
|
break
|
||||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
match parsed_cmd := self.window.parser.match.parse_string(event):
|
||||||
case [['ASIO', 'INPUT', 'SPINBOX'], [in_num, channel]]:
|
case [['ASIO', 'INPUT', 'SPINBOX'], [in_num, channel]]:
|
||||||
index = util.get_asio_input_spinbox_index(int(channel), int(in_num[-1]))
|
index = util.get_asio_input_spinbox_index(int(channel), int(in_num[-1]))
|
||||||
val = values[f'ASIO INPUT SPINBOX||{in_num} {channel}']
|
val = values[f'ASIO INPUT SPINBOX||{in_num} {channel}']
|
||||||
@ -310,14 +310,16 @@ class Popup:
|
|||||||
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
||||||
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}'
|
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}', propagate=False
|
||||||
)
|
)
|
||||||
if param == 'RELEASE':
|
if param == 'RELEASE':
|
||||||
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
||||||
f'<Alt-{event}-{direction}>', f'||KEY ALT {direction.upper()} {event_id}'
|
f'<Alt-{event}-{direction}>', f'||KEY ALT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
self.popup[f'COMPRESSOR||SLIDER {param}'].bind(
|
||||||
f'<Control-Alt-{event}-{direction}>', f'||KEY CTRL ALT {direction.upper()} {event_id}'
|
f'<Control-Alt-{event}-{direction}>',
|
||||||
|
f'||KEY CTRL ALT {direction.upper()} {event_id}',
|
||||||
|
propagate=False,
|
||||||
)
|
)
|
||||||
self.popup[f'COMPRESSOR||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
self.popup[f'COMPRESSOR||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||||
self.popup['MAKEUP'].bind('<FocusIn>', '||FOCUS IN')
|
self.popup['MAKEUP'].bind('<FocusIn>', '||FOCUS IN')
|
||||||
@ -331,7 +333,7 @@ class Popup:
|
|||||||
self.logger.debug(f'values::{values}')
|
self.logger.debug(f'values::{values}')
|
||||||
if event in (psg.WIN_CLOSED, 'Exit'):
|
if event in (psg.WIN_CLOSED, 'Exit'):
|
||||||
break
|
break
|
||||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
match parsed_cmd := self.window.parser.match.parse_string(event):
|
||||||
case [['COMPRESSOR'], ['SLIDER', param]]:
|
case [['COMPRESSOR'], ['SLIDER', param]]:
|
||||||
setattr(self.window.vm.strip[index].comp, param.lower(), values[event])
|
setattr(self.window.vm.strip[index].comp, param.lower(), values[event])
|
||||||
case [['COMPRESSOR'], ['SLIDER', param], ['FOCUS', 'IN']]:
|
case [['COMPRESSOR'], ['SLIDER', param], ['FOCUS', 'IN']]:
|
||||||
@ -642,14 +644,16 @@ class Popup:
|
|||||||
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self.popup[f'GATE||SLIDER {param}'].bind(
|
self.popup[f'GATE||SLIDER {param}'].bind(
|
||||||
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}'
|
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}', propagate=False
|
||||||
)
|
)
|
||||||
if param in ('BPSIDECHAIN', 'ATTACK', 'HOLD', 'RELEASE'):
|
if param in ('BPSIDECHAIN', 'ATTACK', 'HOLD', 'RELEASE'):
|
||||||
self.popup[f'GATE||SLIDER {param}'].bind(
|
self.popup[f'GATE||SLIDER {param}'].bind(
|
||||||
f'<Alt-{event}-{direction}>', f'||KEY ALT {direction.upper()} {event_id}'
|
f'<Alt-{event}-{direction}>', f'||KEY ALT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self.popup[f'GATE||SLIDER {param}'].bind(
|
self.popup[f'GATE||SLIDER {param}'].bind(
|
||||||
f'<Control-Alt-{event}-{direction}>', f'||KEY CTRL ALT {direction.upper()} {event_id}'
|
f'<Control-Alt-{event}-{direction}>',
|
||||||
|
f'||KEY CTRL ALT {direction.upper()} {event_id}',
|
||||||
|
propagate=False,
|
||||||
)
|
)
|
||||||
self.popup[f'GATE||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
self.popup[f'GATE||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||||
self.popup['Exit'].bind('<FocusIn>', '||FOCUS IN')
|
self.popup['Exit'].bind('<FocusIn>', '||FOCUS IN')
|
||||||
@ -661,7 +665,7 @@ class Popup:
|
|||||||
self.logger.debug(f'values::{values}')
|
self.logger.debug(f'values::{values}')
|
||||||
if event in (psg.WIN_CLOSED, 'Exit'):
|
if event in (psg.WIN_CLOSED, 'Exit'):
|
||||||
break
|
break
|
||||||
match parsed_cmd := self.window.parser.match.parseString(event):
|
match parsed_cmd := self.window.parser.match.parse_string(event):
|
||||||
case [['GATE'], ['SLIDER', param]]:
|
case [['GATE'], ['SLIDER', param]]:
|
||||||
setattr(self.window.vm.strip[index].gate, param.lower(), values[event])
|
setattr(self.window.vm.strip[index].gate, param.lower(), values[event])
|
||||||
case [['GATE'], ['SLIDER', param], ['FOCUS', 'IN']]:
|
case [['GATE'], ['SLIDER', param], ['FOCUS', 'IN']]:
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import FreeSimpleGUI as psg
|
|||||||
|
|
||||||
from . import configuration, models, util
|
from . import configuration, models, util
|
||||||
from .builder import Builder
|
from .builder import Builder
|
||||||
|
from .errors import NVDAVMError
|
||||||
from .nvda import Nvda
|
from .nvda import Nvda
|
||||||
from .parser import Parser
|
from .parser import Parser
|
||||||
from .popup import Popup
|
from .popup import Popup
|
||||||
@ -25,6 +26,10 @@ class NVDAVMWindow(psg.Window):
|
|||||||
self.kind = self.vm.kind
|
self.kind = self.vm.kind
|
||||||
self.logger = logger.getChild(type(self).__name__)
|
self.logger = logger.getChild(type(self).__name__)
|
||||||
self.logger.debug(f'loaded with theme: {psg.theme()}')
|
self.logger.debug(f'loaded with theme: {psg.theme()}')
|
||||||
|
self.nvda = Nvda()
|
||||||
|
if not self.nvda.is_running:
|
||||||
|
self.logger.error('NVDA is not running. Exiting...')
|
||||||
|
raise NVDAVMError('NVDA is not running')
|
||||||
self.cache = {
|
self.cache = {
|
||||||
'hw_ins': models._make_hardware_ins_cache(self.vm),
|
'hw_ins': models._make_hardware_ins_cache(self.vm),
|
||||||
'hw_outs': models._make_hardware_outs_cache(self.vm),
|
'hw_outs': models._make_hardware_outs_cache(self.vm),
|
||||||
@ -34,7 +39,6 @@ class NVDAVMWindow(psg.Window):
|
|||||||
'asio': models._make_patch_asio_cache(self.vm),
|
'asio': models._make_patch_asio_cache(self.vm),
|
||||||
'insert': models._make_patch_insert_cache(self.vm),
|
'insert': models._make_patch_insert_cache(self.vm),
|
||||||
}
|
}
|
||||||
self.nvda = Nvda()
|
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.popup = Popup(self)
|
self.popup = Popup(self)
|
||||||
self.builder = Builder(self)
|
self.builder = Builder(self)
|
||||||
@ -247,7 +251,9 @@ class NVDAVMWindow(psg.Window):
|
|||||||
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self[f'STRIP {i}||SLIDER {param}'].bind(
|
self[f'STRIP {i}||SLIDER {param}'].bind(
|
||||||
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}'
|
f'<Control-{event}-{direction}>',
|
||||||
|
f'||KEY CTRL {direction.upper()} {event_id}',
|
||||||
|
propagate=False,
|
||||||
)
|
)
|
||||||
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||||
|
|
||||||
@ -280,7 +286,7 @@ class NVDAVMWindow(psg.Window):
|
|||||||
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
f'<Shift-{event}-{direction}>', f'||KEY SHIFT {direction.upper()} {event_id}'
|
||||||
)
|
)
|
||||||
self[f'BUS {i}||SLIDER GAIN'].bind(
|
self[f'BUS {i}||SLIDER GAIN'].bind(
|
||||||
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}'
|
f'<Control-{event}-{direction}>', f'||KEY CTRL {direction.upper()} {event_id}', propagate=False
|
||||||
)
|
)
|
||||||
self[f'BUS {i}||SLIDER GAIN'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
self[f'BUS {i}||SLIDER GAIN'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user