Compare commits

...

24 Commits
v1.1.0 ... dev

Author SHA1 Message Date
99b8d0e45c upd pre-commit config 2026-03-21 14:23:49 +00:00
f8fc84b2be minor bump 2026-03-19 22:43:41 +00:00
87d0d6541d add Next Bus Mode and Previous Bus Mode to Channel Parameters section in README
update example config in README
2026-03-19 22:43:20 +00:00
6c9e4b1a89 add bus mode binds (next + previous). 2026-03-19 22:42:43 +00:00
c1b374cce7 patch bump 2026-03-19 06:17:55 +00:00
6b57cfba57 fix valid_index for karaoke for banana kind
work with 0-based indexing throughout the methods and only convert to 1-based indexing for announcements.
2026-03-19 06:17:46 +00:00
0bd28be7b8 fix regression, login should allow a return value of 1. 2026-03-19 05:42:35 +00:00
f458fb8d0e minor bump 2026-03-19 05:33:25 +00:00
4c34028194 improve the feedback for some of the binds:
- solo + mc will give error warning if not currently using a Strip controller.
- karaoke and mc will give error warning if the Strip controller doesn't have the correct index.
- bus assignments will give error warning if not on a Strip controller.

mono now toggles for the Strip controller but rotates through modes for the Bus controller.
2026-03-19 05:33:16 +00:00
84ee479bf1 Controller no longer subclasses Binds.
add some wrapper methods to binds
2026-03-19 05:26:19 +00:00
c3b06cae51 remove on: pull_request 2026-03-19 04:00:42 +00:00
4c176cfd77 patch bump
upd ruff config
2026-03-19 03:58:44 +00:00
df023c67ea add bump task 2026-03-19 03:58:28 +00:00
8b025206b1 rename exceptions + lint fixes 2026-03-19 03:51:36 +00:00
8e8e3ce8a5 upd pdm badge 2026-02-22 11:40:08 +00:00
e565283827 upd tag pattern 2025-02-18 11:25:00 +00:00
2d194e8e67 upd build_addon workflow 2025-02-18 11:24:02 +00:00
3aab5922ab rename build command to avoid overriding built-in 2025-02-14 23:33:34 +00:00
1fb435416f run scons with pdm 2025-02-14 14:32:53 +00:00
40aaeb4c54 add ignore 2025-02-14 11:57:43 +00:00
17cdd84c51 add Taskfile
invoke tasks from pdm
2025-02-14 11:44:52 +00:00
43379f1b09 fix path in release step 2025-01-24 01:41:40 +00:00
dc9ac5ddc3 add name to download action 2025-01-24 01:31:27 +00:00
9764b9d827 upd workflow 2025-01-24 01:05:57 +00:00
15 changed files with 361 additions and 179 deletions

View File

@ -1,40 +1,35 @@
name: build addon name: Build Addon
on: on:
push: push:
tags: ["*"] tags:
# To build on main/master branch, uncomment the following line: - 'v*.*.*'
# branches: [ main , master ]
pull_request:
branches: [ main, master ]
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.11']
os: [ubuntu-latest]
steps: steps:
- uses: actions/checkout@v4 - name: Checkout code
uses: actions/checkout@v4
- name: Set up PDM - name: Set up PDM
uses: pdm-project/setup-pdm@v4 uses: pdm-project/setup-pdm@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: '3.11'
- name: Install dependencies - name: Install dependencies
run: | run: |
pdm sync -d -G build pdm sync -d -G build
- name: building addon - name: Build addon
run: pdm run scons run: pdm run scons
- uses: actions/upload-artifact@v3 - name: Upload build artifacts
if: success()
uses: actions/upload-artifact@v4
with: with:
name: packaged_addon name: packaged_addon
path: ./*.nvda-addon path: ./*.nvda-addon
@ -42,17 +37,20 @@ jobs:
upload_release: upload_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ startsWith(github.ref, 'refs/tags/') }} if: ${{ startsWith(github.ref, 'refs/tags/') }}
needs: ["build"] needs: build
steps:
- uses: actions/checkout@v3
- name: download releases files
uses: actions/download-artifact@v4.1.7
- name: Display structure of downloaded files
run: ls -R
- name: Release steps:
uses: softprops/action-gh-release@v1 - name: Download releases files
with: uses: actions/download-artifact@v4.1.7
files: packaged_addon/*.nvda-addon with:
fail_on_unmatched_files: true name: packaged_addon
prerelease: ${{ contains(github.ref, '-') }}
- name: Display structure of downloaded files
run: tree
- name: Release
uses: softprops/action-gh-release@v1
with:
files: ./*.nvda-addon
fail_on_unmatched_files: true
prerelease: ${{ contains(github.ref, '-') }}

View File

@ -2,6 +2,11 @@ repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.3.0
hooks: hooks:
- id: check-ast
- id: check-case-conflict
- id: check-yaml - id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/pdm-project/pdm
rev: 2.26.6
hooks:
- id: pdm-lock-check

View File

@ -1,4 +1,4 @@
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev) [![pdm-managed](https://img.shields.io/endpoint?url=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Fpdm-project%2F.github%2Fbadge.json)](https://pdm-project.org)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
# NVDA Addon Voicemeeter # NVDA Addon Voicemeeter
@ -15,27 +15,27 @@ This addon can be installed through the Add-on store, `Install from external sou
## Default Keybinds ## Default Keybinds
### Controllers ### Channel Controllers
- `NVDA+alt+s`: Enable strip mode - `NVDA+alt+s`: Enable strip controller
- `NVDA+alt+b`: Enable bus mode. - `NVDA+alt+b`: Enable bus controller.
- `NVDA+alt+1`: Enable controller for channel 1 (strip|bus) - `NVDA+alt+1`: Enable controller index 1 (strip|bus)
- `NVDA+alt+2`: Enable controller for channel 2 (strip|bus) - `NVDA+alt+2`: Enable controller index 2 (strip|bus)
- `NVDA+alt+3`: Enable controller for channel 3 (strip|bus) - `NVDA+alt+3`: Enable controller index 3 (strip|bus)
- `NVDA+alt+4`: Enable controller for channel 4 (strip|bus) - `NVDA+alt+4`: Enable controller index 4 (strip|bus)
- `NVDA+alt+5`: Enable controller for channel 5 (strip|bus) - `NVDA+alt+5`: Enable controller index 5 (strip|bus)
- `NVDA+alt+6`: Enable controller for channel 6 (strip|bus) - `NVDA+alt+6`: Enable controller index 6 (strip|bus)
- `NVDA+alt+7`: Enable controller for channel 7 (strip|bus) - `NVDA+alt+7`: Enable controller index 7 (strip|bus)
- `NVDA+alt+8`: Enable controller for channel 8 (strip|bus) - `NVDA+alt+8`: Enable controller index 8 (strip|bus)
### Slider Modes ### Slider Controllers
- `NVDA+alt+g`: Enable gain slider mode. - `NVDA+alt+g`: Enable gain slider controller.
- `NVDA+alt+c`: Enable comp slider mode. - `NVDA+alt+c`: Enable comp slider controller.
- `NVDA+alt+t`: Enable gate slider mode. - `NVDA+alt+t`: Enable gate slider controller.
- `NVDA+alt+d`: Enable denoiser slider mode. - `NVDA+alt+d`: Enable denoiser slider controller.
- `NVDA+alt+a`: Enable audibility slider mode. - `NVDA+alt+a`: Enable audibility slider controller.
### Sliders ### Sliders
@ -53,22 +53,24 @@ This addon can be installed through the Add-on store, `Install from external sou
- `NVDA+shift+m`: Mute - `NVDA+shift+m`: Mute
- `NVDA+shift+c`: MC - `NVDA+shift+c`: MC
- `NVDA+shift+k`: Karaoke - `NVDA+shift+k`: Karaoke
- `NVDA+shift+n`: Next Bus Mode
- `NVDA+shift+p`: Previous Bus Mode
### Bus Assignments (A1-A5|B1-B3) ### Bus Assignments (A1-A5|B1-B3)
- `NVDA+shift+1`: Toggle BUS assignment 1 for a strip - `NVDA+shift+1`: Toggle BUS assignment 1
- `NVDA+shift+2`: Toggle BUS assignment 2 for a strip - `NVDA+shift+2`: Toggle BUS assignment 2
- `NVDA+shift+3`: Toggle BUS assignment 3 for a strip - `NVDA+shift+3`: Toggle BUS assignment 3
- `NVDA+shift+4`: Toggle BUS assignment 4 for a strip - `NVDA+shift+4`: Toggle BUS assignment 4
- `NVDA+shift+5`: Toggle BUS assignment 5 for a strip - `NVDA+shift+5`: Toggle BUS assignment 5
- `NVDA+shift+6`: Toggle BUS assignment 6 for a strip - `NVDA+shift+6`: Toggle BUS assignment 6
- `NVDA+shift+7`: Toggle BUS assignment 7 for a strip - `NVDA+shift+7`: Toggle BUS assignment 7
- `NVDA+shift+8`: Toggle BUS assignment 8 for a strip - `NVDA+shift+8`: Toggle BUS assignment 8
### Announcements ### Announcements
- `NVDA+shift+q`: Announce current controller. - `NVDA+shift+q`: Announce current controller.
- `NVDA+shift+a`: Announce Voicemeeter kind. - `NVDA+shift+a`: Announce Voicemeeter kind and version.
## Configuration ## Configuration
@ -96,10 +98,13 @@ example:
"NVDA+alt+a": "audibility_mode", "NVDA+alt+a": "audibility_mode",
"NVDA+shift+q": "announce_controller", "NVDA+shift+q": "announce_controller",
"NVDA+shift+z": "announce_voicemeeter_version", "NVDA+shift+z": "announce_voicemeeter_version",
"NVDA+shift+o": "rotate_mono",
"NVDA+shift+s": "toggle_solo", "NVDA+shift+s": "toggle_solo",
"NVDA+shift+m": "toggle_mute", "NVDA+shift+m": "toggle_mute",
"NVDA+shift+c": "toggle_mc", "NVDA+shift+c": "toggle_mc",
"NVDA+shift+k": "karaoke", "NVDA+shift+k": "rotate_karaoke",
"NVDA+shift+n": "rotate_bus_mode_next",
"NVDA+shift+p": "rotate_bus_mode_previous",
"NVDA+shift+upArrow": "slider_increase_by_point_one", "NVDA+shift+upArrow": "slider_increase_by_point_one",
"NVDA+shift+downArrow": "slider_decrease_by_point_one", "NVDA+shift+downArrow": "slider_decrease_by_point_one",
"NVDA+shift+alt+upArrow": "slider_increase_by_one", "NVDA+shift+alt+upArrow": "slider_increase_by_one",

41
Taskfile.yml Normal file
View File

@ -0,0 +1,41 @@
version: '3'
vars:
SHELL: pwsh
tasks:
default:
desc: Build the addon
cmds:
- task: build
copy:
desc: Copy sources files to scratchpad directory
platforms: [windows]
vars:
SOURCE: addon\globalPlugins\voicemeeter
DEST: ${env:appdata}\nvda\scratchpad\globalPlugins\voicemeeter\
cmds:
- "{{.SHELL}} -Command 'robocopy {{.SOURCE}} {{.DEST}} /NDL'"
ignore_error: true
build:
desc: Build the addon
platforms: [windows]
cmds:
- '{{.SHELL}} -Command "pdm run scons"'
bump:
desc: Bump the version number
preconditions:
- sh: '{{.SHELL}} -c "if (Get-Command bump) { exit 0 } else { exit 1 }"'
msg: "The 'bump' command is not available. Run `go install github.com/onyx-and-iris/bump/cmd/bump@latest`."
cmds:
- |
{{if eq .CLI_ARGS "show"}}
{{.SHELL}} -c "bump show -f buildVars.py -p \"'addon_version': '(\d+\.\d+\.\d+)'\""
{{.SHELL}} -c "bump show -f pyproject.toml -p \"version = .(\d+\.\d+\.\d+).\""
{{else}}
{{.SHELL}} -c "bump {{.CLI_ARGS}} -w -f buildVars.py -p \"'addon_version': '(\d+\.\d+\.\d+)'\""
{{.SHELL}} -c "bump {{.CLI_ARGS}} -w -f pyproject.toml -p \"version = .(\d+\.\d+\.\d+).\""
{{end}}

View File

@ -2,7 +2,7 @@ import ctypes as ct
from ctypes.wintypes import CHAR, FLOAT, LONG from ctypes.wintypes import CHAR, FLOAT, LONG
from .cdll import libc from .cdll import libc
from .error import VMCAPIError from .error import VMAddonCAPIError
class Binds: class Binds:
@ -38,8 +38,32 @@ class Binds:
bind_set_parameter_float.restype = LONG bind_set_parameter_float.restype = LONG
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT] bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
def call(self, fn, *args, ok=(0,)): def _call(self, fn, *args, ok=(0,)):
retval = fn(*args) retval = fn(*args)
if retval not in ok: if retval not in ok:
raise VMCAPIError(fn.__name__, retval) raise VMAddonCAPIError(fn.__name__, retval)
return retval return retval
def login(self):
return self._call(self.bind_login, ok=(0, 1))
def logout(self):
return self._call(self.bind_logout)
def run_voicemeeter(self, kind_val):
return self._call(self.bind_run_voicemeeter, kind_val)
def get_voicemeeter_type(self, c_type):
return self._call(self.bind_get_voicemeeter_type, ct.byref(c_type))
def get_voicemeeter_version(self, ver):
return self._call(self.bind_get_voicemeeter_version, ct.byref(ver))
def is_parameters_dirty(self):
return self._call(self.bind_is_parameters_dirty, ok=(0, 1))
def get_parameter_float(self, param, buf):
return self._call(self.bind_get_parameter_float, param, ct.byref(buf))
def set_parameter_float(self, param, val):
return self._call(self.bind_set_parameter_float, param, val)

View File

@ -1,16 +1,22 @@
import ctypes as ct import ctypes as ct
import platform import platform
import winreg
from pathlib import Path from pathlib import Path
from .error import VMError from .error import VMAddonError
try:
import winreg
except ImportError as e:
ERR_MSG = 'winreg module not found, only Windows OS supported'
raise VMAddonError(ERR_MSG) from e
# Defense against edge cases where winreg imports but we're not on Windows
if platform.system() != 'Windows':
ERR_MSG = f'Unsupported OS: {platform.system()}, only Windows OS supported'
raise VMAddonError(ERR_MSG)
BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32 BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
if platform.system() != 'Windows':
raise VMError('Only Windows OS supported')
VM_KEY = 'VB:Voicemeeter {17359A74-1236-5467}' VM_KEY = 'VB:Voicemeeter {17359A74-1236-5467}'
REG_KEY = '\\'.join( REG_KEY = '\\'.join(
filter( filter(
@ -35,12 +41,14 @@ def get_vmpath():
try: try:
vm_parent = Path(get_vmpath()).parent vm_parent = Path(get_vmpath()).parent
except FileNotFoundError as e: except FileNotFoundError as e:
raise VMError('Unable to fetch DLL path from the registry') from e ERR_MSG = 'Voicemeeter installation not found in registry'
raise VMAddonError(ERR_MSG) from e
DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll' DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll'
dll_path = vm_parent.joinpath(DLL_NAME) dll_path = vm_parent.joinpath(DLL_NAME)
if not dll_path.is_file(): if not dll_path.is_file():
raise VMError(f'Could not find {dll_path}') ERR_MSG = f'Could not find {dll_path}'
raise VMAddonError(ERR_MSG)
libc = ct.WinDLL(str(dll_path)) libc = ct.WinDLL(str(dll_path))

View File

@ -1,7 +1,7 @@
import ui import ui
from logHandler import log from logHandler import log
from . import context from . import context, util
class CommandsMixin: class CommandsMixin:
@ -56,37 +56,93 @@ class CommandsMixin:
def script_audibility_mode(self, _): def script_audibility_mode(self, _):
self.__set_slider_mode('audibility') self.__set_slider_mode('audibility')
### BOOLEAN PARAMETERS ### ### STRIP|BUS PARAMETERS ###
def script_toggle_mono(self, _): def script_rotate_mono(self, _):
val = not self.controller.ctx.get_bool('mono') if isinstance(self.controller.ctx.strategy, context.StripStrategy):
self.controller.ctx.set_bool('mono', val) val = not self.controller.ctx.get_bool('mono')
ui.message('on' if val else 'off') self.controller.ctx.set_bool('mono', val)
ui.message('on' if val else 'off')
def script_toggle_solo(self, _): else:
val = not self.controller.ctx.get_bool('solo') opts = ['off', 'on', 'stereo reverse']
self.controller.ctx.set_bool('solo', val) val = self.controller.ctx.get_int('mono')
ui.message('on' if val else 'off') new_val = (val + 1) % len(opts)
self.controller.ctx.set_int('mono', new_val)
ui.message(opts[new_val])
def script_toggle_mute(self, _): def script_toggle_mute(self, _):
val = not self.controller.ctx.get_bool('mute') val = not self.controller.ctx.get_bool('mute')
self.controller.ctx.set_bool('mute', val) self.controller.ctx.set_bool('mute', val)
ui.message('on' if val else 'off') ui.message('on' if val else 'off')
### STRIP PARAMETERS ###
def script_toggle_solo(self, _):
if not isinstance(self.controller.ctx.strategy, context.StripStrategy):
ui.message('Solo only available for strips')
return
val = not self.controller.ctx.get_bool('solo')
self.controller.ctx.set_bool('solo', val)
ui.message('on' if val else 'off')
def script_toggle_mc(self, _): def script_toggle_mc(self, _):
if not isinstance(self.controller.ctx.strategy, context.StripStrategy):
ui.message('MC only available for strips')
return
valid_indices_zero_based = [self.kind.phys_in]
match self.kind.name:
case 'potato':
valid_indices_zero_based.append(self.kind.phys_in + self.kind.virt_in - 1)
if self.controller.ctx.index not in valid_indices_zero_based:
valid_indices_display = [i + 1 for i in valid_indices_zero_based]
if len(valid_indices_display) == 1:
ui.message(f'MC only available for strip {valid_indices_display[0]} for Voicemeeter {self.kind}')
else:
ui.message(
f'MC only available for strips {valid_indices_display[0]} and {valid_indices_display[1]} for Voicemeeter {self.kind}'
)
return
val = not self.controller.ctx.get_bool('mc') val = not self.controller.ctx.get_bool('mc')
self.controller.ctx.set_bool('mc', val) self.controller.ctx.set_bool('mc', val)
ui.message('on' if val else 'off') ui.message('on' if val else 'off')
def script_karaoke(self, _): def script_rotate_karaoke(self, _):
if not isinstance(self.controller.ctx.strategy, context.StripStrategy):
ui.message('Karaoke mode only available for strips')
return
if self.kind.name not in ['banana', 'potato']:
ui.message(f'Karaoke mode not available for Voicemeeter {self.kind}')
return
valid_index_zero_based = None
match self.kind.name:
case 'banana':
valid_index_zero_based = self.kind.phys_in + self.kind.virt_in - 1
case 'potato':
valid_index_zero_based = self.kind.phys_in + self.kind.virt_in - 2
if self.controller.ctx.index != valid_index_zero_based:
ui.message(
f'Karaoke mode only available for strip {valid_index_zero_based + 1} for Voicemeeter {self.kind}'
)
return
opts = ['off', 'k m', 'k 1', 'k 2', 'k v'] opts = ['off', 'k m', 'k 1', 'k 2', 'k v']
val = self.controller.ctx.get_int('karaoke') + 1 val = self.controller.ctx.get_int('karaoke')
if val == len(opts): new_val = (val + 1) % len(opts)
val = 0 self.controller.ctx.set_int('karaoke', new_val)
self.controller.ctx.set_int('karaoke', val) ui.message(opts[new_val])
ui.message(opts[val])
def script_bus_assignment(self, gesture): def script_bus_assignment(self, gesture):
if not isinstance(self.controller.ctx.strategy, context.StripStrategy):
ui.message('Bus assignment only available for strips')
return
proposed = int(gesture.displayName[-1]) proposed = int(gesture.displayName[-1])
if proposed - 1 < self.kind.phys_out: if proposed - 1 < self.kind.phys_out:
output = f'A{proposed}' output = f'A{proposed}'
@ -96,34 +152,76 @@ class CommandsMixin:
self.controller.ctx.set_bool(output, val) self.controller.ctx.set_bool(output, val)
ui.message('on' if val else 'off') ui.message('on' if val else 'off')
### BUS PARAMETERS ###
def script_rotate_bus_mode_next(self, _):
if not isinstance(self.controller.ctx.strategy, context.BusStrategy):
ui.message('Bus mode only available for buses')
return
opts = util._get_bus_mode_opts(self.kind.name)
for mode in opts:
if self.controller.ctx.get_bool(f'mode.{mode}'):
current_mode = mode
log.info(f'INFO - bus {self.controller.ctx.index} current mode {current_mode}')
break
else:
log.warning(f'WARNING - no bus mode found for bus {self.controller.ctx.index}')
return
new_val = (opts.index(current_mode) + 1) % len(opts)
log.info(f'INFO - bus {self.controller.ctx.index} mode {opts[new_val]}')
self.controller.ctx.set_bool(f'mode.{opts[new_val]}', True)
ui.message(util._get_bus_mode_readable_name(opts[new_val]))
def script_rotate_bus_mode_previous(self, _):
if not isinstance(self.controller.ctx.strategy, context.BusStrategy):
ui.message('Bus mode only available for buses')
return
opts = util._get_bus_mode_opts(self.kind.name)
for mode in opts:
if self.controller.ctx.get_bool(f'mode.{mode}'):
current_mode = mode
log.info(f'INFO - bus {self.controller.ctx.index} current mode {current_mode}')
break
else:
log.warning(f'WARNING - no bus mode found for bus {self.controller.ctx.index}')
return
new_val = (opts.index(current_mode) - 1) % len(opts)
log.info(f'INFO - bus {self.controller.ctx.index} mode {opts[new_val]}')
self.controller.ctx.set_bool(f'mode.{opts[new_val]}', True)
ui.message(util._get_bus_mode_readable_name(opts[new_val]))
### CONTROL SLIDERS ### ### CONTROL SLIDERS ###
def script_slider_increase_by_point_one(self, gesture): def script_slider_increase_by_point_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 0.1 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 0.1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_decrease_by_point_one(self, gesture): def script_slider_decrease_by_point_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 0.1 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 0.1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_increase_by_one(self, gesture): def script_slider_increase_by_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 1 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_decrease_by_one(self, gesture): def script_slider_decrease_by_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 1 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_increase_by_three(self, gesture): def script_slider_increase_by_three(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 3 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))
def script_slider_decrease_by_three(self, gesture): def script_slider_decrease_by_three(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 3 val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val) self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1))) ui.message(str(round(val, 1)))

View File

@ -28,22 +28,22 @@ class Strategy(ABC):
self._slider_mode = val self._slider_mode = val
def get_bool(self, param: str) -> bool: def get_bool(self, param: str) -> bool:
return self._controller._get(f'{self.identifier}.{param}') == 1 return self._controller.get(f'{self.identifier}.{param}') == 1
def set_bool(self, param: str, val: bool): def set_bool(self, param: str, val: bool):
self._controller._set(f'{self.identifier}.{param}', 1 if val else 0) self._controller.set(f'{self.identifier}.{param}', 1 if val else 0)
def get_float(self, param: str) -> float: def get_float(self, param: str) -> float:
return round(self._controller._get(f'{self.identifier}.{param}'), 1) return round(self._controller.get(f'{self.identifier}.{param}'), 1)
def set_float(self, param: str, val: float): def set_float(self, param: str, val: float):
self._controller._set(f'{self.identifier}.{param}', val) self._controller.set(f'{self.identifier}.{param}', val)
def get_int(self, param: str) -> int: def get_int(self, param: str) -> int:
return int(self._controller._get(f'{self.identifier}.{param}')) return int(self._controller.get(f'{self.identifier}.{param}'))
def set_int(self, param: str, val: int): def set_int(self, param: str, val: int):
self._controller._set(f'{self.identifier}.{param}', val) self._controller.set(f'{self.identifier}.{param}', val)
class StripStrategy(Strategy): class StripStrategy(Strategy):
@ -92,20 +92,6 @@ class Context:
def slider_mode(self, val): def slider_mode(self, val):
self._strategy._slider_mode = val self._strategy._slider_mode = val
def get_bool(self, *args) -> bool: def __getattr__(self, name):
return self._strategy.get_bool(*args) """Delegate method calls to the strategy object."""
return getattr(self._strategy, name)
def set_bool(self, *args):
self._strategy.set_bool(*args)
def get_float(self, *args) -> float:
return self._strategy.get_float(*args)
def set_float(self, *args):
self._strategy.set_float(*args)
def get_int(self, *args) -> int:
return self._strategy.get_int(*args)
def set_int(self, *args):
self._strategy.set_int(*args)

View File

@ -9,30 +9,31 @@ from .context import Context, StripStrategy
from .kinds import KindId from .kinds import KindId
class Controller(Binds): class Controller:
def __init__(self): def __init__(self):
self._binds = Binds()
self.ctx = Context(StripStrategy(self, 0)) self.ctx = Context(StripStrategy(self, 0))
self.bits = config.get('bits', BITS) self.bits = config.get('bits', BITS)
def login(self): def login(self):
retval = self.call(self.bind_login, ok=(0, 1)) retval = self._binds.login()
log.info('INFO - logged into Voicemeeter Remote API') log.info('INFO - logged into Voicemeeter Remote API')
return retval return retval
def logout(self): def logout(self):
self.call(self.bind_logout) self._binds.logout()
log.info('NFO - logged out of Voicemeeter Remote API') log.info('NFO - logged out of Voicemeeter Remote API')
@property @property
def kind_id(self): def kind_id(self):
c_type = ct.c_long() c_type = ct.c_long()
self.call(self.bind_get_voicemeeter_type, ct.byref(c_type)) self._binds.get_voicemeeter_type(c_type)
return KindId(c_type.value).name.lower() return KindId(c_type.value).name.lower()
@property @property
def version(self): def version(self):
ver = ct.c_long() ver = ct.c_long()
self.call(self.bind_get_voicemeeter_version, ct.byref(ver)) self._binds.get_voicemeeter_version(ver)
return '{}.{}.{}.{}'.format( return '{}.{}.{}.{}'.format(
(ver.value & 0xFF000000) >> 24, (ver.value & 0xFF000000) >> 24,
(ver.value & 0x00FF0000) >> 16, (ver.value & 0x00FF0000) >> 16,
@ -44,17 +45,17 @@ class Controller(Binds):
val = kind_id.value val = kind_id.value
if self.bits == 64: if self.bits == 64:
val += 3 val += 3
self.call(self.bind_run_voicemeeter, val) self._binds.run_voicemeeter(val)
def __clear(self): def __clear(self):
while self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1: while self._binds.is_parameters_dirty() == 1:
pass pass
def _get(self, param): def get(self, param):
self.__clear() self.__clear()
buf = ct.c_float() buf = ct.c_float()
self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf)) self._binds.get_parameter_float(param.encode(), buf)
return buf.value return buf.value
def _set(self, param, val): def set(self, param, val):
self.call(self.bind_set_parameter_float, param.encode(), ct.c_float(float(val))) self._binds.set_parameter_float(param.encode(), ct.c_float(float(val)))

View File

@ -1,9 +1,9 @@
class VMError(Exception): class VMAddonError(Exception):
"""Base voicemeeterlib exception class""" """Base voicemeeter add-on exception class"""
class VMCAPIError(VMError): class VMAddonCAPIError(VMAddonError):
"""Exception raised when the C-API returns an error code""" """Exception raised when the Voicemeeter C-API returns an error code"""
def __init__(self, fn_name, code): def __init__(self, fn_name, code):
self.fn_name = fn_name self.fn_name = fn_name

View File

@ -1,7 +1,7 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum, unique from enum import Enum, unique
from .error import VMError from .error import VMAddonError
@unique @unique
@ -78,7 +78,8 @@ def kind_factory(kind_id):
elif kind_id == 'potato': elif kind_id == 'potato':
_kind_map = PotatoMap _kind_map = PotatoMap
else: else:
raise ValueError(f'Unknown Voicemeeter kind {kind_id}') ERR_MSG = f'Unknown Voicemeeter kind {kind_id}'
raise ValueError(ERR_MSG)
return _kind_map(name=kind_id) return _kind_map(name=kind_id)
@ -87,5 +88,5 @@ def request_kind_map(kind_id):
try: try:
KIND_obj = kind_factory(kind_id) KIND_obj = kind_factory(kind_id)
except ValueError as e: except ValueError as e:
raise VMError(str(e)) from e raise VMAddonError(str(e)) from e
return KIND_obj return KIND_obj

View File

@ -26,11 +26,13 @@ def _make_gestures(kind_id):
'kb:NVDA+alt+a': 'audibility_mode', 'kb:NVDA+alt+a': 'audibility_mode',
'kb:NVDA+shift+q': 'announce_controller', 'kb:NVDA+shift+q': 'announce_controller',
'kb:NVDA+shift+v': 'announce_voicemeeter_version', 'kb:NVDA+shift+v': 'announce_voicemeeter_version',
'kb:NVDA+shift+o': 'toggle_mono', 'kb:NVDA+shift+o': 'rotate_mono',
'kb:NVDA+shift+s': 'toggle_solo', 'kb:NVDA+shift+s': 'toggle_solo',
'kb:NVDA+shift+m': 'toggle_mute', 'kb:NVDA+shift+m': 'toggle_mute',
'kb:NVDA+shift+c': 'toggle_mc', 'kb:NVDA+shift+c': 'toggle_mc',
'kb:NVDA+shift+k': 'karaoke', 'kb:NVDA+shift+k': 'rotate_karaoke',
'kb:NVDA+shift+n': 'rotate_bus_mode_next',
'kb:NVDA+shift+p': 'rotate_bus_mode_previous',
'kb:NVDA+shift+upArrow': 'slider_increase_by_point_one', 'kb:NVDA+shift+upArrow': 'slider_increase_by_point_one',
'kb:NVDA+shift+downArrow': 'slider_decrease_by_point_one', 'kb:NVDA+shift+downArrow': 'slider_decrease_by_point_one',
'kb:NVDA+shift+alt+upArrow': 'slider_increase_by_one', 'kb:NVDA+shift+alt+upArrow': 'slider_increase_by_one',
@ -49,3 +51,47 @@ def _make_gestures(kind_id):
defaults = {k: v for k, v in defaults.items() if v not in matching_values} defaults = {k: v for k, v in defaults.items() if v not in matching_values}
return {**defaults, **overrides} return {**defaults, **overrides}
return defaults return defaults
_bus_mode_map = {
'normal': 'Normal',
'amix': 'Mix Down A',
'bmix': 'Mix Down B',
'repeat': 'Stereo Repeat',
'composite': 'Composite',
'tvmix': 'Up Mix TV',
'upmix21': 'Up Mix 2.1',
'upmix41': 'Up Mix 4.1',
'upmix61': 'Up Mix 6.1',
'centeronly': 'Center Only',
'lfeonly': 'Low Frequency Effect Only',
'rearonly': 'Rear Only',
}
def _get_bus_mode_opts(kind_id):
if kind_id == 'basic':
return [
'normal',
'amix',
'repeat',
'composite',
]
return [
'normal',
'amix',
'bmix',
'repeat',
'composite',
'tvmix',
'upmix21',
'upmix41',
'upmix61',
'centeronly',
'lfeonly',
'rearonly',
]
def _get_bus_mode_readable_name(mode):
return _bus_mode_map.get(mode, mode)

View File

@ -1,25 +0,0 @@
param (
[switch]$build
)
function Copy-FilesToScratchpad {
$source = Join-Path $PSScriptRoot "addon" "globalPlugins" "voicemeeter"
$target = Join-Path $env:appdata "nvda" "scratchpad" "globalPlugins" "voicemeeter"
Robocopy $source $target | Out-Null
}
function Build-Addon {
"Building add-on" | Write-Host
scons
}
function Main {
"Copying updated files to Scratchpad" | Write-Host
Copy-FilesToScratchpad
if ($build) {
Build-Addon
}
}
if ($MyInvocation.InvocationName -ne '.') { Main }

View File

@ -28,7 +28,7 @@ addon_info = {
The add-on requires Voicemeeter to be installed.""" The add-on requires Voicemeeter to be installed."""
), ),
# version # version
'addon_version': '1.1.0', 'addon_version': '1.3.0',
# Author(s) # Author(s)
'addon_author': 'onyx-and-iris <code@onyxandiris.online>', 'addon_author': 'onyx-and-iris <code@onyxandiris.online>',
# URL for the add-on documentation support # URL for the add-on documentation support

View File

@ -1,27 +1,22 @@
[project] [project]
name = "nvda-addon-voicemeeter" name = "nvda-addon-voicemeeter"
version = "1.1.0" version = "1.3.0"
description = "A GUI-less NVDA Addon for Voicemeeter using the Remote API" description = "A GUI-less NVDA Addon for Voicemeeter using the Remote API"
authors = [ authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
{name = "Onyx and Iris", email = "code@onyxandiris.online"},
]
dependencies = [] dependencies = []
requires-python = "==3.11.*" requires-python = "==3.11.*"
readme = "README.md" readme = "README.md"
license = {text = "MIT"} license = { text = "MIT" }
[dependency-groups] [dependency-groups]
build = [ build = ["scons>=4.8.1", "markdown>=3.7"]
"scons>=4.8.1",
"markdown>=3.7",
]
[tool.pdm] [tool.pdm]
distribution = false distribution = false
[tool.pdm.scripts] [tool.pdm.scripts]
copy = "pwsh build.ps1" copy = "task copy"
build = "pwsh build.ps1 -build" release = "task build"
[tool.ruff] [tool.ruff]
exclude = [ exclude = [
@ -56,9 +51,11 @@ target-version = "py311"
[tool.ruff.lint] [tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Enable flake8-errmsg (EM) warnings.
# Enable flake8-bugbear (B) warnings.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default. # McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "F"] select = ["E4", "E7", "E9", "EM", "F", "B"]
ignore = [] ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided. # Allow fix for all enabled rules (when `--fix`) is provided.
@ -100,7 +97,4 @@ docstring-code-line-length = "dynamic"
max-complexity = 10 max-complexity = 10
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = [ "__init__.py" = ["E402", "F401"]
"E402",
"F401",
]