Compare commits

..

No commits in common. "dev" and "v.0.1" have entirely different histories.
dev ... v.0.1

19 changed files with 247 additions and 1051 deletions

View File

@ -1,56 +1,64 @@
name: Build Addon name: build addon
on: on:
push: push:
tags: tags: ["*"]
- 'v*.*.*' # To build on main/master branch, uncomment the following line:
# 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:
- name: Checkout code - uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up PDM - run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt
uses: pdm-project/setup-pdm@v4
with:
python-version: '3.11'
- name: Install dependencies - name: Set up Python
run: | uses: actions/setup-python@v4
pdm sync -d -G build with:
python-version: 3.9
cache: 'pip'
- name: Build addon - name: Install dependencies
run: pdm run scons run: |
python -m pip install --upgrade pip wheel
pip install -r requirements.txt
sudo apt-get update -y
sudo apt-get install -y gettext
- name: Upload build artifacts - name: Code checks
if: success() run: export SKIP=no-commit-to-branch; pre-commit run --all
uses: actions/upload-artifact@v4
with: - name: building addon
name: packaged_addon run: scons
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: steps:
- name: Download releases files - uses: actions/checkout@v3
uses: actions/download-artifact@v4.1.7 - name: download releases files
with: uses: actions/download-artifact@v3
name: packaged_addon - name: Display structure of downloaded files
run: ls -R
- name: Display structure of downloaded files - name: Release
run: tree uses: softprops/action-gh-release@v1
with:
- name: Release files: packaged_addon/*.nvda-addon
uses: softprops/action-gh-release@v1 fail_on_unmatched_files: true
with: prerelease: ${{ contains(github.ref, '-') }}
files: ./*.nvda-addon
fail_on_unmatched_files: true
prerelease: ${{ contains(github.ref, '-') }}

178
.gitignore vendored
View File

@ -1,179 +1,3 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# 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/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# NVDA
addon/doc/*.css addon/doc/*.css
addon/doc/en/ addon/doc/en/
*_docHandler.py *_docHandler.py
@ -186,5 +10,5 @@ manifest.ini
.sconsign.dblite .sconsign.dblite
/[0-9]*.[0-9]*.[0-9]*.json /[0-9]*.[0-9]*.[0-9]*.json
# testing .venv/
keybinds.json keybinds.json

View File

@ -1,5 +1,8 @@
{ {
"python.analysis.diagnosticSeverityOverrides": { "python.analysis.diagnosticSeverityOverrides": {
"reportMissingImports": "none" "reportMissingImports": "none"
} },
"black-formatter.args": [
"--line-length=120"
]
} }

136
README.md
View File

@ -1,137 +1,3 @@
[![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)
# NVDA Addon Voicemeeter # NVDA Addon Voicemeeter
Control [Voicemeeter][voicemeeter] with global hotkeys. Control Voicemeeter GUI with customisable hotkeys.
## Requirements
- [NVDA screen reader][nvda]
## Install
This addon can be installed through the Add-on store, `Install from external source`. Simply download the [latest Release](https://github.com/onyx-and-iris/nvda-addon-voicemeeter/releases) and load it with NVDA.
## Default Keybinds
### Channel Controllers
- `NVDA+alt+s`: Enable strip controller
- `NVDA+alt+b`: Enable bus controller.
- `NVDA+alt+1`: Enable controller index 1 (strip|bus)
- `NVDA+alt+2`: Enable controller index 2 (strip|bus)
- `NVDA+alt+3`: Enable controller index 3 (strip|bus)
- `NVDA+alt+4`: Enable controller index 4 (strip|bus)
- `NVDA+alt+5`: Enable controller index 5 (strip|bus)
- `NVDA+alt+6`: Enable controller index 6 (strip|bus)
- `NVDA+alt+7`: Enable controller index 7 (strip|bus)
- `NVDA+alt+8`: Enable controller index 8 (strip|bus)
### Slider Controllers
- `NVDA+alt+g`: Enable gain slider controller.
- `NVDA+alt+c`: Enable comp slider controller.
- `NVDA+alt+t`: Enable gate slider controller.
- `NVDA+alt+d`: Enable denoiser slider controller.
- `NVDA+alt+a`: Enable audibility slider controller.
### Sliders
- `NVDA+shift+upArrow`: Move slider up by 1 step
- `NVDA+shift+downArrow`: Move slider down by 1 step
- `NVDA+shift+alt+upArrow`: Move slider up by 0.1 step
- `NVDA+shift+alt+downArrow`: Move slider down by 0.1 step
- `NVDA+shift+control+upArrow`: Move slider up by 3 steps
- `NVDA+shift+control+downArrow`: Move slider down by 3 steps
### Channel Parameters
- `NVDA+shift+o`: Mono
- `NVDA+shift+s`: Solo
- `NVDA+shift+m`: Mute
- `NVDA+shift+c`: MC
- `NVDA+shift+k`: Karaoke
- `NVDA+shift+n`: Next Bus Mode
- `NVDA+shift+p`: Previous Bus Mode
### Bus Assignments (A1-A5|B1-B3)
- `NVDA+shift+1`: Toggle BUS assignment 1
- `NVDA+shift+2`: Toggle BUS assignment 2
- `NVDA+shift+3`: Toggle BUS assignment 3
- `NVDA+shift+4`: Toggle BUS assignment 4
- `NVDA+shift+5`: Toggle BUS assignment 5
- `NVDA+shift+6`: Toggle BUS assignment 6
- `NVDA+shift+7`: Toggle BUS assignment 7
- `NVDA+shift+8`: Toggle BUS assignment 8
### Announcements
- `NVDA+shift+q`: Announce current controller.
- `NVDA+shift+a`: Announce Voicemeeter kind and version.
## Configuration
By placing a file named `nvda_settings.json` in `User Home Directory / Documents / Voicemeeter` (the same place as your Voicemeeter xml profiles) you can change most of the default keybinds.
The `voicemeeter` key can take one of three values:
- `basic`
- `banana`
- `potato`
example:
```json
{
"voicemeeter": "banana",
"bits": 64,
"keybinds": {
"NVDA+alt+k": "strip_mode",
"NVDA+alt+l": "bus_mode",
"NVDA+alt+g": "gain_mode",
"NVDA+alt+c": "comp_mode",
"NVDA+alt+t": "gate_mode",
"NVDA+alt+d": "denoiser_mode",
"NVDA+alt+a": "audibility_mode",
"NVDA+shift+q": "announce_controller",
"NVDA+shift+z": "announce_voicemeeter_version",
"NVDA+shift+o": "rotate_mono",
"NVDA+shift+s": "toggle_solo",
"NVDA+shift+m": "toggle_mute",
"NVDA+shift+c": "toggle_mc",
"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+downArrow": "slider_decrease_by_point_one",
"NVDA+shift+alt+upArrow": "slider_increase_by_one",
"NVDA+shift+alt+downArrow": "slider_decrease_by_one",
"NVDA+shift+control+upArrow": "slider_increase_by_three",
"NVDA+shift+control+downArrow": "slider_decrease_by_three",
"NVDA+control+1": "bus_assignment",
"NVDA+control+2": "bus_assignment",
"NVDA+control+3": "bus_assignment",
"NVDA+control+4": "bus_assignment",
"NVDA+control+5": "bus_assignment",
"NVDA+control+6": "bus_assignment",
"NVDA+control+7": "bus_assignment",
"NVDA+control+8": "bus_assignment"
}
}
```
Would make the following changes:
- 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 `announce_voicemeeter_version` bind to `NVDA+shift+z`
- changes the bus assignment binds to `NVDA+control+number`
All other binds would then be defaults.
[voicemeeter]: https://voicemeeter.com/
[nvda]: https://www.nvaccess.org/

View File

@ -1,41 +0,0 @@
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

@ -1,16 +1,56 @@
import json
import time import time
from pathlib import Path
import globalPluginHandler import globalPluginHandler
from logHandler import log
from . import config, util
from .commands import CommandsMixin from .commands import CommandsMixin
from .controller import Controller from .controller import Controller
from .kinds import KindId, request_kind_map from .kinds import KindId, request_kind_map
class GlobalPlugin(CommandsMixin, globalPluginHandler.GlobalPlugin): def _make_gestures():
__kind_id = config.get('voicemeeter', 'potato') defaults = {
__gestures = util._make_gestures(__kind_id) "kb:NVDA+alt+s": "strip_mode",
"kb:NVDA+alt+b": "bus_mode",
"kb:NVDA+shift+q": "announce_controller",
"kb:NVDA+shift+a": "announce_voicemeeter_version",
"kb:NVDA+shift+o": "toggle_mono",
"kb:NVDA+shift+s": "toggle_solo",
"kb:NVDA+shift+m": "toggle_mute",
"kb:NVDA+shift+upArrow": "increase_gain",
"kb:NVDA+shift+downArrow": "decrease_gain",
"kb:NVDA+shift+alt+upArrow": "increase_gain",
"kb:NVDA+shift+alt+downArrow": "decrease_gain",
"kb:NVDA+shift+control+upArrow": "increase_gain",
"kb:NVDA+shift+control+downArrow": "decrease_gain",
}
overrides = None
pn = Path.home() / "Documents" / "Voicemeeter" / "keybinds.json"
if pn.exists():
with open(pn, "r") as f:
data = json.load(f)
overrides = {f"kb:{v}": k for k, v in data.items()}
log.info("INFO - loading settings from keybinds.json")
if overrides:
return {**defaults, **overrides}
return defaults
def _get_kind_id():
pn = Path.home() / "Documents" / "Voicemeeter" / "settings.json"
if pn.exists():
with open(pn, "r") as f:
data = json.load(f)
return data["voicemeeter"]
return "potato"
class GlobalPlugin(globalPluginHandler.GlobalPlugin, CommandsMixin):
__gestures = _make_gestures()
__kind_id = _get_kind_id()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -18,7 +58,12 @@ class GlobalPlugin(CommandsMixin, globalPluginHandler.GlobalPlugin):
if self.controller.login() == 1: if self.controller.login() == 1:
self.controller.run_voicemeeter(KindId[self.__kind_id.upper()]) self.controller.run_voicemeeter(KindId[self.__kind_id.upper()])
time.sleep(1) time.sleep(1)
self.kind = request_kind_map(self.__kind_id) self.kind = request_kind_map(self.controller.kind_id)
for i in range(1, self.kind.num_strip + 1):
self.bindGesture(f"kb:NVDA+alt+{i}", "index")
for i in range(1, self.kind.phys_out + self.kind.virt_out + 1):
self.bindGesture(f"kb:NVDA+shift+{i}", "bus_assignment")
def terminate(self, *args, **kwargs): def terminate(self, *args, **kwargs):
super().terminate(*args, **kwargs) super().terminate(*args, **kwargs)

View File

@ -1,69 +1,51 @@
import ctypes as ct import ctypes as ct
from ctypes.wintypes import CHAR, FLOAT, LONG import winreg
from pathlib import Path
from .cdll import libc from .error import VMError
from .error import VMAddonCAPIError
class Binds: class Binds:
bind_login = libc.VBVMR_Login VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
bind_login.restype = LONG BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
bind_login.argtypes = None
bind_logout = libc.VBVMR_Logout def __init__(self):
bind_logout.restype = LONG dll_path = Path(self.__vmpath()).parent.joinpath(
bind_logout.argtypes = None f'VoicemeeterRemote{"64" if self.BITS == 64 else ""}.dll'
)
if self.BITS == 64:
self.libc = ct.CDLL(str(dll_path))
else:
self.libc = ct.WinDLL(str(dll_path))
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter def __vmpath(self):
bind_run_voicemeeter.restype = LONG with winreg.OpenKey(
bind_run_voicemeeter.argtypes = [LONG] winreg.HKEY_LOCAL_MACHINE,
r"{}".format(
"\\".join(
(
"\\".join(
filter(
None,
(
"SOFTWARE",
"WOW6432Node" if self.BITS == 64 else "",
"Microsoft",
"Windows",
"CurrentVersion",
"Uninstall",
),
)
),
self.VM_KEY,
)
)
),
) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType def call(self, fn, *args, ok=(0,)):
bind_get_voicemeeter_type.restype = LONG retval = getattr(self.libc, fn)(*args)
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
bind_get_voicemeeter_version.restype = LONG
bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
bind_is_parameters_dirty.restype = LONG
bind_is_parameters_dirty.argtypes = None
bind_get_parameter_float = libc.VBVMR_GetParameterFloat
bind_get_parameter_float.restype = LONG
bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
bind_set_parameter_float = libc.VBVMR_SetParameterFloat
bind_set_parameter_float.restype = LONG
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
def _call(self, fn, *args, ok=(0,)):
retval = fn(*args)
if retval not in ok: if retval not in ok:
raise VMAddonCAPIError(fn.__name__, retval) raise VMError(f"{fn} returned {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,54 +0,0 @@
import ctypes as ct
import platform
from pathlib import Path
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
VM_KEY = 'VB:Voicemeeter {17359A74-1236-5467}'
REG_KEY = '\\'.join(
filter(
None,
(
'SOFTWARE',
'WOW6432Node' if BITS == 64 else '',
'Microsoft',
'Windows',
'CurrentVersion',
'Uninstall',
),
)
)
def get_vmpath():
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'{}'.format('\\'.join((REG_KEY, VM_KEY)))) as vm_key:
return winreg.QueryValueEx(vm_key, r'UninstallString')[0]
try:
vm_parent = Path(get_vmpath()).parent
except FileNotFoundError as 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_path = vm_parent.joinpath(DLL_NAME)
if not dll_path.is_file():
ERR_MSG = f'Could not find {dll_path}'
raise VMAddonError(ERR_MSG)
libc = ct.WinDLL(str(dll_path))

View File

@ -1,227 +1,65 @@
import ui import ui
from logHandler import log from logHandler import log
from . import context, util from . import context
class CommandsMixin: 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} {self.controller.bits} bit') ui.message(f"Running Voicemeeter {self.kind}")
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}")
### ALTER THE CONTEXT ### ### ALTER THE CONTEXT ###
def script_strip_mode(self, _): def script_strip_mode(self, _):
if self.controller.ctx.index >= self.kind.num_strip: if self.controller.ctx.index >= self.kind.num_strip:
ui.message(f'Controller strip {self.controller.ctx.index + 1} does not exist for Voicemeeter {self.kind}') ui.message(f"Controller strip {self.controller.ctx.index + 1} does not exist for Voicemeeter {self.kind}")
return return
self.controller.ctx.strategy = context.StripStrategy(self.controller, self.controller.ctx.index) self.controller.ctx.strategy = context.StripStrategy(self.controller, self.controller.ctx.index)
ui.message(f'Controller for strip {self.controller.ctx.index + 1}') ui.message(f"Controller for strip {self.controller.ctx.index + 1}")
log.info(f'INFO - strip {self.controller.ctx.index} mode') log.info(f"INFO - strip {self.controller.ctx.index} mode")
def script_bus_mode(self, _): def script_bus_mode(self, _):
if self.controller.ctx.index >= self.kind.num_bus: if self.controller.ctx.index >= self.kind.num_bus:
ui.message(f'Controller bus {self.controller.ctx.index + 1} does not exist for Voicemeeter {self.kind}') ui.message(f"Controller bus {self.controller.ctx.index + 1} does not exist for Voicemeeter {self.kind}")
return return
self.controller.ctx.strategy = context.BusStrategy(self.controller, self.controller.ctx.index) self.controller.ctx.strategy = context.BusStrategy(self.controller, self.controller.ctx.index)
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}")
log.info(f'INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode') log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode")
def script_index(self, gesture): def script_index(self, gesture):
proposed = int(gesture.displayName[-1]) proposed = int(gesture.displayName[-1])
self.controller.ctx.index = proposed - 1 self.controller.ctx.index = proposed - 1
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}")
log.info(f'INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode') log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode")
def __set_slider_mode(self, mode): ### BOOLEAN PARMETERS ###
self.controller.ctx.slider_mode = mode
ui.message(f'{mode} mode enabled')
def script_gain_mode(self, _): def script_toggle_mono(self, _):
self.__set_slider_mode('gain') val = not self.controller.ctx.get_bool("mono")
self.controller.ctx.set_bool("mono", val)
def script_comp_mode(self, _): ui.message("on" if val else "off")
self.__set_slider_mode('comp')
def script_gate_mode(self, _):
self.__set_slider_mode('gate')
def script_denoiser_mode(self, _):
self.__set_slider_mode('denoiser')
def script_audibility_mode(self, _):
self.__set_slider_mode('audibility')
### STRIP|BUS PARAMETERS ###
def script_rotate_mono(self, _):
if isinstance(self.controller.ctx.strategy, context.StripStrategy):
val = not self.controller.ctx.get_bool('mono')
self.controller.ctx.set_bool('mono', val)
ui.message('on' if val else 'off')
else:
opts = ['off', 'on', 'stereo reverse']
val = self.controller.ctx.get_int('mono')
new_val = (val + 1) % len(opts)
self.controller.ctx.set_int('mono', new_val)
ui.message(opts[new_val])
def script_toggle_mute(self, _):
val = not self.controller.ctx.get_bool('mute')
self.controller.ctx.set_bool('mute', val)
ui.message('on' if val else 'off')
### STRIP PARAMETERS ###
def script_toggle_solo(self, _): def script_toggle_solo(self, _):
if not isinstance(self.controller.ctx.strategy, context.StripStrategy): val = not self.controller.ctx.get_bool("solo")
ui.message('Solo only available for strips') self.controller.ctx.set_bool("solo", val)
return ui.message("on" if val else "off")
val = not self.controller.ctx.get_bool('solo') def script_toggle_mute(self, _):
self.controller.ctx.set_bool('solo', val) val = not self.controller.ctx.get_bool("mute")
ui.message('on' if val else 'off') self.controller.ctx.set_bool("mute", val)
ui.message("on" if val else "off")
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')
self.controller.ctx.set_bool('mc', val)
ui.message('on' if val else 'off')
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']
val = self.controller.ctx.get_int('karaoke')
new_val = (val + 1) % len(opts)
self.controller.ctx.set_int('karaoke', new_val)
ui.message(opts[new_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}"
else: else:
output = f'B{proposed - self.kind.phys_out}' output = f"B{proposed - self.kind.phys_out}"
val = not self.controller.ctx.get_bool(output) val = not self.controller.ctx.get_bool(output)
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 ###
def script_slider_increase_by_point_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 0.1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_decrease_by_point_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 0.1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_increase_by_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_decrease_by_one(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 1
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_increase_by_three(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_decrease_by_three(self, _):
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - 3
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))

View File

@ -1,20 +0,0 @@
import json
from pathlib import Path
def config_from_json():
pn = Path.home() / 'Documents' / 'Voicemeeter' / 'nvda_settings.json'
data = None
if pn.exists():
with open(pn, 'r') as f:
data = json.load(f)
return data or {}
__config = config_from_json()
def get(name, default=None):
if name in __config:
return __config[name]
return default

View File

@ -5,11 +5,6 @@ class Strategy(ABC):
def __init__(self, controller, index): def __init__(self, controller, index):
self._controller = controller self._controller = controller
self._index = index self._index = index
self._slider_mode = 'gain'
@abstractmethod
def identifier(self):
pass
@property @property
def index(self): def index(self):
@ -19,49 +14,29 @@ class Strategy(ABC):
def index(self, val): def index(self, val):
self._index = val self._index = val
@property
def slider_mode(self):
return self._slider_mode
@slider_mode.setter
def slider_mode(self, 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:
return round(self._controller.get(f'{self.identifier}.{param}'), 1)
def set_float(self, param: str, val: float):
self._controller.set(f'{self.identifier}.{param}', val)
def get_int(self, param: str) -> int:
return int(self._controller.get(f'{self.identifier}.{param}'))
def set_int(self, param: str, val: int):
self._controller.set(f'{self.identifier}.{param}', val)
class StripStrategy(Strategy): class StripStrategy(Strategy):
def __str__(self): def __str__(self):
return 'Strip' return "Strip"
@property @property
def identifier(self): def identifier(self):
return f'{self}[{self._index}]' return f"strip[{self._index}]"
class BusStrategy(Strategy): class BusStrategy(Strategy):
def __str__(self): def __str__(self):
return 'Bus' return "Bus"
@property @property
def identifier(self): def identifier(self):
return f'{self}[{self._index}]' return f"bus[{self._index}]"
class Context: class Context:
@ -84,14 +59,8 @@ class Context:
def index(self, val): def index(self, val):
self._strategy._index = val self._strategy._index = val
@property def get_bool(self, *args):
def slider_mode(self): return self._strategy.get_bool(*args)
return self._strategy._slider_mode
@slider_mode.setter def set_bool(self, *args):
def slider_mode(self, val): self._strategy.set_bool(*args)
self._strategy._slider_mode = val
def __getattr__(self, name):
"""Delegate method calls to the strategy object."""
return getattr(self._strategy, name)

View File

@ -2,60 +2,46 @@ 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 .context import Context, StripStrategy from .context import Context, StripStrategy
from .kinds import KindId from .kinds import KindId
class Controller: class Controller:
def __init__(self): def __init__(self):
self._binds = Binds() self.binds = Binds()
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._binds.login() retval = self.binds.call("VBVMR_Login", ok=(0, 1))
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._binds.logout() self.binds.call("VBVMR_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._binds.get_voicemeeter_type(c_type) self.binds.call("VBVMR_GetVoicemeeterType", ct.byref(c_type))
return KindId(c_type.value).name.lower() return KindId(c_type.value).name.lower()
@property
def version(self):
ver = ct.c_long()
self._binds.get_voicemeeter_version(ver)
return '{}.{}.{}.{}'.format(
(ver.value & 0xFF000000) >> 24,
(ver.value & 0x00FF0000) >> 16,
(ver.value & 0x0000FF00) >> 8,
ver.value & 0x000000FF,
)
def run_voicemeeter(self, kind_id): def run_voicemeeter(self, kind_id):
val = kind_id.value val = kind_id.value
if self.bits == 64: if val == 3 and Binds.BITS == 64:
val += 3 val = 6
self._binds.run_voicemeeter(val) self.binds.call("VBVMR_RunVoicemeeter", val)
def __clear(self): def __clear(self):
while self._binds.is_parameters_dirty() == 1: while self.binds.call("VBVMR_IsParametersDirty", ok=(0, 1)) == 1:
pass pass
def get(self, param): def _get(self, param):
self.__clear() self.__clear()
buf = ct.c_float() buf = ct.c_float()
self._binds.get_parameter_float(param.encode(), buf) self.binds.call("VBVMR_GetParameterFloat", param.encode(), ct.byref(buf))
return buf.value return buf.value
def set(self, param, val): def _set(self, param, val):
self._binds.set_parameter_float(param.encode(), ct.c_float(float(val))) self.binds.call("VBVMR_SetParameterFloat", param.encode(), ct.c_float(float(val)))

View File

@ -1,11 +1,2 @@
class VMAddonError(Exception): class VMError(Exception):
"""Base voicemeeter add-on exception class""" """Base VMError class"""
class VMAddonCAPIError(VMAddonError):
"""Exception raised when the Voicemeeter C-API returns an error code"""
def __init__(self, fn_name, code):
self.fn_name = fn_name
self.code = code
super().__init__(f'{self.fn_name} returned {self.code}')

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 VMAddonError from .error import VMError
@unique @unique
@ -14,6 +14,11 @@ 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:
@ -45,6 +50,7 @@ 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)
@ -54,6 +60,7 @@ 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)
@ -63,6 +70,7 @@ 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)
@ -71,15 +79,14 @@ class PotatoMap(KindMapClass):
def kind_factory(kind_id): def kind_factory(kind_id):
if kind_id == 'basic': if kind_id == "basic":
_kind_map = BasicMap _kind_map = BasicMap
elif kind_id == 'banana': elif kind_id == "banana":
_kind_map = BananaMap _kind_map = BananaMap
elif kind_id == 'potato': elif kind_id == "potato":
_kind_map = PotatoMap _kind_map = PotatoMap
else: else:
ERR_MSG = f'Unknown Voicemeeter kind {kind_id}' raise ValueError(f"Unknown Voicemeeter kind {kind_id}")
raise ValueError(ERR_MSG)
return _kind_map(name=kind_id) return _kind_map(name=kind_id)
@ -88,5 +95,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 VMAddonError(str(e)) from e raise VMError(str(e)) from e
return KIND_obj return KIND_obj

View File

@ -1,97 +0,0 @@
from . import config
from .kinds import request_kind_map
def remove_prefix(input_string, prefix):
if prefix and input_string.startswith(prefix):
return input_string[len(prefix) :]
return input_string
def remove_suffix(input_string, suffix):
if suffix and input_string.endswith(suffix):
return input_string[: -len(suffix)]
return input_string
def _make_gestures(kind_id):
kind = request_kind_map(kind_id)
defaults = {
'kb:NVDA+alt+s': 'strip_mode',
'kb:NVDA+alt+b': 'bus_mode',
'kb:NVDA+alt+g': 'gain_mode',
'kb:NVDA+alt+c': 'comp_mode',
'kb:NVDA+alt+t': 'gate_mode',
'kb:NVDA+alt+d': 'denoiser_mode',
'kb:NVDA+alt+a': 'audibility_mode',
'kb:NVDA+shift+q': 'announce_controller',
'kb:NVDA+shift+v': 'announce_voicemeeter_version',
'kb:NVDA+shift+o': 'rotate_mono',
'kb:NVDA+shift+s': 'toggle_solo',
'kb:NVDA+shift+m': 'toggle_mute',
'kb:NVDA+shift+c': 'toggle_mc',
'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+downArrow': 'slider_decrease_by_point_one',
'kb:NVDA+shift+alt+upArrow': 'slider_increase_by_one',
'kb:NVDA+shift+alt+downArrow': 'slider_decrease_by_one',
'kb:NVDA+shift+control+upArrow': 'slider_increase_by_three',
'kb:NVDA+shift+control+downArrow': 'slider_decrease_by_three',
}
for i in range(1, kind.num_strip + 1):
defaults[f'kb:NVDA+alt+{i}'] = 'index'
for i in range(1, kind.phys_out + kind.virt_out + 1):
defaults[f'kb:NVDA+shift+{i}'] = 'bus_assignment'
abc = config.get('keybinds')
if abc:
overrides = {f'kb:{remove_prefix(k, "kb:")}': v for k, v in abc.items()}
matching_values = set(defaults.values()).intersection(set(overrides.values()))
defaults = {k: v for k, v in defaults.items() if v not in matching_values}
return {**defaults, **overrides}
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)

25
build.ps1 Normal file
View File

@ -0,0 +1,25 @@
param(
[switch]$build
)
function Copy-FilestoScratchpad {
$source = Join-Path $PSScriptRoot "addon" "globalPlugins" "voicemeeter"
$target = Join-Path $env:appdata "nvda" "scratchpad" "globalPlugins"
Copy-Item -Path $source -Destination $target -Recurse -Force
}
function main {
"Copying files to Scratchpad" | Write-Host
Copy-FilestoScratchpad
if ($build) {
Invoke-Expression ".venv/Scripts/Activate.ps1"
"Building add-on" | Write-Host
scons
deactivate
}
}
if ($MyInvocation.InvocationName -ne '.') { main }

View File

@ -16,39 +16,39 @@ def _(arg):
# Add-on information variables # Add-on information variables
addon_info = { addon_info = {
# add-on Name/identifier, internal for NVDA # add-on Name/identifier, internal for NVDA
'addon_name': 'voicemeeter', "addon_name": "voicemeeter",
# Add-on summary, usually the user visible name of the addon. # Add-on summary, usually the user visible name of the addon.
# Translators: Summary for this add-on # Translators: Summary for this add-on
# to be shown on installation and add-on information found in Add-ons Manager. # to be shown on installation and add-on information found in Add-ons Manager.
'addon_summary': _('Voicemeeter Controller'), "addon_summary": _("Voicemeeter Controller"),
# Add-on description # Add-on description
# Translators: Long description to be shown for this add-on on add-on information from add-ons manager # Translators: Long description to be shown for this add-on on add-on information from add-ons manager
'addon_description': _( "addon_description": _(
"""This add-on uses Voicemeeter's Remote API to control it's GUI. """This add-on uses Voicemeeter's Remote API to control it's GUI.
The add-on requires Voicemeeter to be installed.""" The add-on requires Voicemeeter to be installed."""
), ),
# version # version
'addon_version': '1.3.0', "addon_version": "0.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
'addon_url': None, "addon_url": None,
# URL for the add-on repository where the source code can be found # URL for the add-on repository where the source code can be found
'addon_sourceURL': 'https://github.com/onyx-and-iris/nvda-addon-voicemeeter', "addon_sourceURL": "https://github.com/onyx-and-iris/nvda-addon-voicemeeter",
# 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': '2022.1.0', "addon_minimumNVDAVersion": "2023.2",
# 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": "2023.2",
# Add-on update channel (default is None, denoting stable releases, # Add-on update channel (default is None, denoting stable releases,
# and for development releases, use "dev".) # and for development releases, use "dev".)
# Do not change unless you know what you are doing! # Do not change unless you know what you are doing!
'addon_updateChannel': 'dev', "addon_updateChannel": "dev",
# Add-on license such as GPL 2 # Add-on license such as GPL 2
'addon_license': 'GPL 2', "addon_license": "GPL 2",
# URL for the license document the ad-on is licensed under # URL for the license document the ad-on is licensed under
'addon_licenseURL': 'https://github.com/onyx-and-iris/nvda-addon-voicemeeter/blob/main/LICENSE', "addon_licenseURL": "https://github.com/onyx-and-iris/nvda-addon-voicemeeter/blob/main/LICENSE",
} }
# Define the python files that are the sources of your add-on. # Define the python files that are the sources of your add-on.
@ -60,11 +60,11 @@ The add-on requires Voicemeeter to be installed."""
# For more information on SCons Glob expressions please take a look at: # For more information on SCons Glob expressions please take a look at:
# https://scons.org/doc/production/HTML/scons-user/apd.html # https://scons.org/doc/production/HTML/scons-user/apd.html
pythonSources = [ pythonSources = [
'addon/globalPlugins/voicemeeter/*.py', "addon/globalPlugins/voicemeeter/*.py",
] ]
# Files that contain strings for translation. Usually your python sources # Files that contain strings for translation. Usually your python sources
i18nSources = pythonSources + ['buildVars.py'] i18nSources = pythonSources + ["buildVars.py"]
# Files that will be ignored when building the nvda-addon file # Files that will be ignored when building the nvda-addon file
# Paths are relative to the addon directory, not to the root directory of your addon sources. # Paths are relative to the addon directory, not to the root directory of your addon sources.
@ -73,7 +73,7 @@ excludedFiles = []
# Base language for the NVDA add-on # Base language for the NVDA add-on
# If your add-on is written in a language other than english, modify this variable. # If your add-on is written in a language other than english, modify this variable.
# For example, set baseLanguage to "es" if your add-on is primarily written in spanish. # For example, set baseLanguage to "es" if your add-on is primarily written in spanish.
baseLanguage = 'en' baseLanguage = "en"
# Markdown extensions for add-on documentation # Markdown extensions for add-on documentation
# Most add-ons do not require additional Markdown extensions. # Most add-ons do not require additional Markdown extensions.

36
pdm.lock generated
View File

@ -1,36 +0,0 @@
# This file is @generated by PDM.
# It is not intended for manual editing.
[metadata]
groups = ["default", "build"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:ffb180ef920ab37ffd5773fd707e211323fdcdf938a50189f57238ca6222d2c6"
[[metadata.targets]]
requires_python = "==3.11.*"
[[package]]
name = "markdown"
version = "3.7"
requires_python = ">=3.8"
summary = "Python implementation of John Gruber's Markdown."
groups = ["build"]
dependencies = [
"importlib-metadata>=4.4; python_version < \"3.10\"",
]
files = [
{file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"},
{file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"},
]
[[package]]
name = "scons"
version = "4.8.1"
requires_python = ">=3.6"
summary = "Open Source next-generation build tool."
groups = ["build"]
files = [
{file = "SCons-4.8.1-py3-none-any.whl", hash = "sha256:a4c3b434330e2d7d975002fd6783284ba348bf394db94c8f83fdc5bf69cdb8d7"},
{file = "scons-4.8.1.tar.gz", hash = "sha256:5b641357904d2f56f7bfdbb37e165ab996b6143c948b9df0efc7305f54949daa"},
]

View File

@ -1,100 +0,0 @@
[project]
name = "nvda-addon-voicemeeter"
version = "1.3.0"
description = "A GUI-less NVDA Addon for Voicemeeter using the Remote API"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
dependencies = []
requires-python = "==3.11.*"
readme = "README.md"
license = { text = "MIT" }
[dependency-groups]
build = ["scons>=4.8.1", "markdown>=3.7"]
[tool.pdm]
distribution = false
[tool.pdm.scripts]
copy = "task copy"
release = "task build"
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".mypy_cache",
".nox",
".pants.d",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
]
line-length = 120
indent-width = 4
# Assume Python 3.11
target-version = "py311"
[tool.ruff.lint]
# 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
# McCabe complexity (`C901`) by default.
select = ["E4", "E7", "E9", "EM", "F", "B"]
ignore = []
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
# Unlike Black, use single quotes for strings.
quote-style = "single"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
[tool.ruff.lint.mccabe]
max-complexity = 10
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402", "F401"]