17 Commits

Author SHA1 Message Date
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
d95a2280c6 minor bump
update README with example bits in config
2025-01-24 00:49:55 +00:00
5534381981 reduce min supported version to 2022.1.0 (unsure if this is earliest it will work so we'll see) 2025-01-24 00:49:41 +00:00
0522b69420 read bits from config, defaults to cdll.BITS
reflect change in script_announce_voicemeeter_version
2025-01-24 00:48:59 +00:00
12 changed files with 126 additions and 122 deletions

View File

@@ -1,64 +1,56 @@
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
steps: steps:
- uses: actions/checkout@v3 - name: Checkout code
uses: actions/checkout@v4
- run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt - name: Set up PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: '3.11'
- name: Set up Python - name: Install dependencies
uses: actions/setup-python@v4 run: |
with: pdm sync -d -G build
python-version: 3.9
cache: 'pip'
- name: Install dependencies - name: Build addon
run: | run: pdm run scons
python -m pip install --upgrade pip wheel
pip install -r requirements.txt
sudo apt-get update -y
sudo apt-get install -y gettext
- name: Code checks - name: Upload build artifacts
run: export SKIP=no-commit-to-branch; pre-commit run --all if: success()
uses: actions/upload-artifact@v4
- name: building addon with:
run: scons name: packaged_addon
path: ./*.nvda-addon
- uses: actions/upload-artifact@v3
with:
name: packaged_addon
path: ./*.nvda-addon
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

@@ -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
@@ -85,6 +85,7 @@ example:
```json ```json
{ {
"voicemeeter": "banana", "voicemeeter": "banana",
"bits": 64,
"keybinds": { "keybinds": {
"NVDA+alt+k": "strip_mode", "NVDA+alt+k": "strip_mode",
"NVDA+alt+l": "bus_mode", "NVDA+alt+l": "bus_mode",
@@ -120,6 +121,7 @@ example:
Would make the following changes: Would make the following changes:
- load the plugin in `banana` mode (default is potato) - load the plugin in `banana` mode (default is potato)
- override the bits of Voicemeeter GUI to 64 (default is 32)
- change the `strip_mode` and `bus_mode` binds to `NVDA+alt+k` and `NVDA+alt+l` respectively - change the `strip_mode` and `bus_mode` binds to `NVDA+alt+k` and `NVDA+alt+l` respectively
- change the `announce_voicemeeter_version` bind to `NVDA+shift+z` - change the `announce_voicemeeter_version` bind to `NVDA+shift+z`
- changes the bus assignment binds to `NVDA+control+number` - changes the bus assignment binds to `NVDA+control+number`

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:
@@ -41,5 +41,5 @@ class Binds:
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

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,15 +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)
if BITS == 64: libc = ct.WinDLL(str(dll_path))
libc = ct.CDLL(str(dll_path))
else:
libc = ct.WinDLL(str(dll_path))

View File

@@ -8,7 +8,7 @@ class CommandsMixin:
### ANNOUNCEMENTS ### ### ANNOUNCEMENTS ###
def script_announce_voicemeeter_version(self, _): def script_announce_voicemeeter_version(self, _):
ui.message(f'Running Voicemeeter {self.kind} {self.controller.version}') ui.message(f'Running Voicemeeter {self.kind} {self.controller.version} {self.controller.bits} bit')
def script_announce_controller(self, _): def script_announce_controller(self, _):
ui.message(f'Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}') ui.message(f'Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}')

View File

@@ -2,6 +2,7 @@ import ctypes as ct
from logHandler import log from logHandler import log
from . import config
from .binds import Binds from .binds import Binds
from .cdll import BITS from .cdll import BITS
from .context import Context, StripStrategy from .context import Context, StripStrategy
@@ -11,6 +12,7 @@ from .kinds import KindId
class Controller(Binds): class Controller(Binds):
def __init__(self): def __init__(self):
self.ctx = Context(StripStrategy(self, 0)) self.ctx = Context(StripStrategy(self, 0))
self.bits = config.get('bits', BITS)
def login(self): def login(self):
retval = self.call(self.bind_login, ok=(0, 1)) retval = self.call(self.bind_login, ok=(0, 1))
@@ -40,8 +42,8 @@ class Controller(Binds):
def run_voicemeeter(self, kind_id): def run_voicemeeter(self, kind_id):
val = kind_id.value val = kind_id.value
if val == 3 and BITS == 64: if self.bits == 64:
val = 6 val += 3
self.call(self.bind_run_voicemeeter, val) self.call(self.bind_run_voicemeeter, val)
def __clear(self): def __clear(self):

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
@@ -14,11 +14,6 @@ class KindId(Enum):
@dataclass @dataclass
class KindMapClass: class KindMapClass:
name: str name: str
ins: tuple
outs: tuple
vban: tuple
asio: tuple
insert: int
@property @property
def phys_in(self) -> int: def phys_in(self) -> int:
@@ -50,7 +45,6 @@ class KindMapClass:
@dataclass @dataclass
class BasicMap(KindMapClass): class BasicMap(KindMapClass):
name: str
ins: tuple = (2, 1) ins: tuple = (2, 1)
outs: tuple = (1, 1) outs: tuple = (1, 1)
vban: tuple = (4, 4, 1, 1) vban: tuple = (4, 4, 1, 1)
@@ -60,7 +54,6 @@ class BasicMap(KindMapClass):
@dataclass @dataclass
class BananaMap(KindMapClass): class BananaMap(KindMapClass):
name: str
ins: tuple = (3, 2) ins: tuple = (3, 2)
outs: tuple = (3, 2) outs: tuple = (3, 2)
vban: tuple = (8, 8, 1, 1) vban: tuple = (8, 8, 1, 1)
@@ -70,7 +63,6 @@ class BananaMap(KindMapClass):
@dataclass @dataclass
class PotatoMap(KindMapClass): class PotatoMap(KindMapClass):
name: str
ins: tuple = (5, 3) ins: tuple = (5, 3)
outs: tuple = (5, 3) outs: tuple = (5, 3)
vban: tuple = (8, 8, 1, 1) vban: tuple = (8, 8, 1, 1)
@@ -86,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)
@@ -95,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

@@ -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.0.0', 'addon_version': '1.1.1',
# 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
@@ -38,7 +38,7 @@ The add-on requires Voicemeeter to be installed."""
# Documentation file name # Documentation file name
'addon_docFileName': 'readme.html', 'addon_docFileName': 'readme.html',
# Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional)
'addon_minimumNVDAVersion': '2024.4.0', 'addon_minimumNVDAVersion': '2022.1.0',
# Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version)
'addon_lastTestedNVDAVersion': '2024.4.2', 'addon_lastTestedNVDAVersion': '2024.4.2',
# Add-on update channel (default is None, denoting stable releases, # Add-on update channel (default is None, denoting stable releases,

View File

@@ -1,27 +1,22 @@
[project] [project]
name = "nvda-addon-voicemeeter" name = "nvda-addon-voicemeeter"
version = "1.0.0" version = "1.1.1"
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 = [
@@ -51,14 +46,16 @@ exclude = [
line-length = 120 line-length = 120
indent-width = 4 indent-width = 4
# Assume Python 3.10 # Assume Python 3.11
target-version = "py310" 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",
]