mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-12 05:49:15 +00:00
Compare commits
No commits in common. "dfb96777bb5300e86276b8e3aa50bc623510928e" and "d85058117901aad30edbaba564757b8d6f0851e9" have entirely different histories.
dfb96777bb
...
d850581179
237
.github/workflows/release.yml
vendored
237
.github/workflows/release.yml
vendored
@ -1,237 +0,0 @@
|
|||||||
name: Build and Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*.*.*'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: windows-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.12'
|
|
||||||
|
|
||||||
- name: Install PDM
|
|
||||||
uses: pdm-project/setup-pdm@v4
|
|
||||||
with:
|
|
||||||
python-version: '3.12'
|
|
||||||
|
|
||||||
- name: Install Task
|
|
||||||
uses: go-task/setup-task@v1
|
|
||||||
with:
|
|
||||||
version: 3.x
|
|
||||||
|
|
||||||
- name: Download NVDA Controller Client
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
Write-Host "Downloading NVDA Controller Client..."
|
|
||||||
$url = "https://download.nvaccess.org/releases/stable/nvda_2025.3.3_controllerClient.zip"
|
|
||||||
$zipPath = "nvda_controllerClient.zip"
|
|
||||||
|
|
||||||
# Download the zip file
|
|
||||||
Invoke-WebRequest -Uri $url -OutFile $zipPath
|
|
||||||
Write-Host "Downloaded $zipPath"
|
|
||||||
|
|
||||||
# Extract to temp directory
|
|
||||||
$tempDir = "temp_controller"
|
|
||||||
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
|
|
||||||
|
|
||||||
# Find and copy DLL files to correct locations
|
|
||||||
Write-Host "Extracting DLL files..."
|
|
||||||
|
|
||||||
# Create directories if they don't exist
|
|
||||||
New-Item -ItemType Directory -Path "controllerClient/x64" -Force | Out-Null
|
|
||||||
New-Item -ItemType Directory -Path "controllerClient/x86" -Force | Out-Null
|
|
||||||
|
|
||||||
# Find and copy the DLL files
|
|
||||||
$dllFiles = Get-ChildItem -Path $tempDir -Recurse -Name "*.dll" | Where-Object { $_ -like "*controllerClient*" }
|
|
||||||
|
|
||||||
foreach ($dll in $dllFiles) {
|
|
||||||
$fullPath = Join-Path $tempDir $dll
|
|
||||||
$dirName = (Get-Item $fullPath).Directory.Name
|
|
||||||
|
|
||||||
if ($dll -match "x64" -or $dirName -match "x64") {
|
|
||||||
Copy-Item $fullPath "controllerClient/x64/nvdaControllerClient.dll"
|
|
||||||
Write-Host "Copied x64 DLL: $dll"
|
|
||||||
} elseif ($dll -match "x86" -or $dirName -match "x86") {
|
|
||||||
Copy-Item $fullPath "controllerClient/x86/nvdaControllerClient.dll"
|
|
||||||
Write-Host "Copied x86 DLL: $dll"
|
|
||||||
} elseif ($dll -match "arm64" -or $dirName -match "arm64") {
|
|
||||||
Write-Host "Skipping ARM64 DLL: $dll (not needed)"
|
|
||||||
} else {
|
|
||||||
Write-Host "Skipping unknown architecture DLL: $dll"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Clean up
|
|
||||||
Remove-Item $zipPath -Force
|
|
||||||
Remove-Item $tempDir -Recurse -Force
|
|
||||||
|
|
||||||
# Verify files were copied
|
|
||||||
Write-Host "Verifying controller client files..."
|
|
||||||
if (Test-Path "controllerClient/x64/nvdaControllerClient.dll") {
|
|
||||||
Write-Host "[OK] x64 controller client found"
|
|
||||||
} else {
|
|
||||||
Write-Host "[ERROR] x64 controller client missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Test-Path "controllerClient/x86/nvdaControllerClient.dll") {
|
|
||||||
Write-Host "[OK] x86 controller client found"
|
|
||||||
} else {
|
|
||||||
Write-Host "[ERROR] x86 controller client missing"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Fix dependencies for CI
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
echo "Fixing local dependencies for CI build..."
|
|
||||||
|
|
||||||
# Remove local path dependency for voicemeeter-api
|
|
||||||
pdm remove -dG dev voicemeeter-api || true
|
|
||||||
|
|
||||||
echo "Updated dependencies for CI build"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
# Install project dependencies
|
|
||||||
pdm install
|
|
||||||
|
|
||||||
# Verify PyInstaller is available
|
|
||||||
Write-Host "Verifying PyInstaller installation..."
|
|
||||||
pdm list | Select-String pyinstaller
|
|
||||||
|
|
||||||
- name: Get PDM executable path
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
$pdmPath = Get-Command pdm | Select-Object -ExpandProperty Source
|
|
||||||
Write-Host "PDM path: $pdmPath"
|
|
||||||
echo "PDM_BIN=$pdmPath" >> $env:GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build artifacts with dynamic taskfile
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
PDM_BIN: ${{ env.PDM_BIN }}
|
|
||||||
run: |
|
|
||||||
Write-Host "Building all executables using dynamic builder..."
|
|
||||||
task --taskfile Taskfile.dynamic.yml build-all
|
|
||||||
|
|
||||||
- name: Compress build artifacts
|
|
||||||
shell: pwsh
|
|
||||||
env:
|
|
||||||
PDM_BIN: ${{ env.PDM_BIN }}
|
|
||||||
run: |
|
|
||||||
Write-Host "Compressing build artifacts..."
|
|
||||||
task --taskfile Taskfile.dynamic.yml compress-all
|
|
||||||
|
|
||||||
- name: Verify build outputs
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
Write-Host "Verifying build outputs..."
|
|
||||||
|
|
||||||
$expectedFiles = @(
|
|
||||||
"dist/basic.zip",
|
|
||||||
"dist/banana.zip",
|
|
||||||
"dist/potato.zip"
|
|
||||||
)
|
|
||||||
|
|
||||||
$missingFiles = @()
|
|
||||||
$foundFiles = @()
|
|
||||||
|
|
||||||
foreach ($file in $expectedFiles) {
|
|
||||||
if (Test-Path $file) {
|
|
||||||
$size = [math]::Round((Get-Item $file).Length / 1MB, 2)
|
|
||||||
$foundFiles += "$file ($size MB)"
|
|
||||||
} else {
|
|
||||||
$missingFiles += $file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Found files:"
|
|
||||||
$foundFiles | ForEach-Object { Write-Host " $_" }
|
|
||||||
|
|
||||||
if ($missingFiles.Count -gt 0) {
|
|
||||||
Write-Host -ForegroundColor Red "Missing files:"
|
|
||||||
$missingFiles | ForEach-Object { Write-Host " $_" }
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host -ForegroundColor Green "All expected files found!"
|
|
||||||
|
|
||||||
- name: Upload build artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: nvda-voicemeeter-builds
|
|
||||||
path: |
|
|
||||||
dist/basic.zip
|
|
||||||
dist/banana.zip
|
|
||||||
dist/potato.zip
|
|
||||||
|
|
||||||
- name: Build Summary
|
|
||||||
shell: pwsh
|
|
||||||
run: |
|
|
||||||
Write-Host -ForegroundColor Green "Build completed successfully!"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "Built artifacts:"
|
|
||||||
Write-Host " - nvda-voicemeeter-basic.zip"
|
|
||||||
Write-Host " - nvda-voicemeeter-banana.zip"
|
|
||||||
Write-Host " - nvda-voicemeeter-potato.zip"
|
|
||||||
|
|
||||||
release:
|
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
run: |
|
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
|
||||||
gh release create $TAG_NAME \
|
|
||||||
--title "NVDA-Voicemeeter $TAG_NAME" \
|
|
||||||
--notes "## NVDA-Voicemeeter Release $TAG_NAME
|
|
||||||
|
|
||||||
### Downloads
|
|
||||||
- **nvda-voicemeeter-basic.zip** - Basic version with dependencies
|
|
||||||
- **nvda-voicemeeter-banana.zip** - Banana version with dependencies
|
|
||||||
- **nvda-voicemeeter-potato.zip** - Potato version with dependencies
|
|
||||||
|
|
||||||
### Requirements
|
|
||||||
- Windows 10/11
|
|
||||||
- Voicemeeter (Basic/Banana/Potato) installed
|
|
||||||
- NVDA screen reader
|
|
||||||
|
|
||||||
### Installation
|
|
||||||
1. Download the appropriate zip for your Voicemeeter version
|
|
||||||
2. Extract and run the executable - no installation required
|
|
||||||
3. The application will integrate with NVDA automatically
|
|
||||||
|
|
||||||
### Notes
|
|
||||||
- Built with dynamic build system using PyInstaller
|
|
||||||
- Includes NVDA Controller Client for screen reader integration"
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Upload release assets
|
|
||||||
run: |
|
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
|
||||||
find . -name "*.zip" -exec gh release upload $TAG_NAME {} \;
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
19
.github/workflows/ruff.yml
vendored
19
.github/workflows/ruff.yml
vendored
@ -1,19 +0,0 @@
|
|||||||
name: Ruff
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
branches: [main]
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
ruff:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: astral-sh/ruff-action@v3
|
|
||||||
with:
|
|
||||||
args: 'format --check --diff'
|
|
||||||
@ -1,76 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
# Dynamic build system - no spec files needed!
|
|
||||||
# Examples:
|
|
||||||
# - task -t Taskfile.dynamic.yml build KINDS="basic banana"
|
|
||||||
# - task -t Taskfile.dynamic.yml build-all
|
|
||||||
# KINDS can be a space-separated list of kinds to build, or "all" to build everything.
|
|
||||||
#
|
|
||||||
# Compression tasks are also dynamic, allowing you to specify which kind to compress or compress all kinds at once.
|
|
||||||
# Examples:
|
|
||||||
# - task -t Taskfile.dynamic.yml compress KIND=basic
|
|
||||||
# - task -t Taskfile.dynamic.yml compress-all
|
|
||||||
|
|
||||||
vars:
|
|
||||||
KINDS: '{{.KINDS | default "all"}}'
|
|
||||||
SHELL: pwsh
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
build:
|
|
||||||
desc: Build specified kinds dynamically (no spec files needed)
|
|
||||||
preconditions:
|
|
||||||
- sh: |
|
|
||||||
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
|
||||||
cmds:
|
|
||||||
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py {{.KINDS}}
|
|
||||||
|
|
||||||
build-all:
|
|
||||||
desc: Build all kinds
|
|
||||||
preconditions:
|
|
||||||
- sh: |
|
|
||||||
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
|
||||||
cmds:
|
|
||||||
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py all
|
|
||||||
|
|
||||||
compress:
|
|
||||||
desc: Compress artifacts for specified kind
|
|
||||||
cmds:
|
|
||||||
- task: compress-{{.KIND}}
|
|
||||||
|
|
||||||
compress-all:
|
|
||||||
desc: Compress artifacts for all kinds
|
|
||||||
cmds:
|
|
||||||
- for:
|
|
||||||
matrix:
|
|
||||||
KIND: [basic, banana, potato]
|
|
||||||
task: compress-{{.ITEM.KIND}}
|
|
||||||
|
|
||||||
compress-basic:
|
|
||||||
desc: Compress basic build artifacts
|
|
||||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/basic -DestinationPath dist/basic.zip -Force"'
|
|
||||||
generates:
|
|
||||||
- dist/basic.zip
|
|
||||||
|
|
||||||
compress-banana:
|
|
||||||
desc: Compress banana build artifacts
|
|
||||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/banana -DestinationPath dist/banana.zip -Force"'
|
|
||||||
generates:
|
|
||||||
- dist/banana.zip
|
|
||||||
|
|
||||||
compress-potato:
|
|
||||||
desc: Compress potato build artifacts
|
|
||||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/potato -DestinationPath dist/potato.zip -Force"'
|
|
||||||
generates:
|
|
||||||
- dist/potato.zip
|
|
||||||
|
|
||||||
clean:
|
|
||||||
desc: Clean all build artifacts
|
|
||||||
cmds:
|
|
||||||
- |
|
|
||||||
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nvda-voicemeeter"
|
name = "nvda-voicemeeter"
|
||||||
version = "1.1.0"
|
version = "1.0.0"
|
||||||
description = "A Voicemeeter app compatible with NVDA"
|
description = "A Voicemeeter app compatible with NVDA"
|
||||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@ -438,20 +438,13 @@ class Builder:
|
|||||||
def make_tab3_button_row(self, i) -> psg.Frame:
|
def make_tab3_button_row(self, i) -> psg.Frame:
|
||||||
"""tab3 row represents bus composite toggle"""
|
"""tab3 row represents bus composite toggle"""
|
||||||
|
|
||||||
def add_bus_buttons(layout):
|
def add_strip_outputs(layout):
|
||||||
busmono = util.get_bus_mono()
|
params = ['MONO', 'EQ', 'MUTE']
|
||||||
params = ['EQ', 'MUTE']
|
|
||||||
if self.kind.name == 'basic':
|
if self.kind.name == 'basic':
|
||||||
params.remove('EQ')
|
params.remove('EQ')
|
||||||
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
|
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.ButtonMenu(
|
|
||||||
'Mono',
|
|
||||||
size=(6, 2),
|
|
||||||
menu_def=['', busmono],
|
|
||||||
key=f'BUS {i}||MONO',
|
|
||||||
),
|
|
||||||
*[
|
*[
|
||||||
psg.Button(
|
psg.Button(
|
||||||
param.capitalize(),
|
param.capitalize(),
|
||||||
@ -461,7 +454,7 @@ class Builder:
|
|||||||
for param in params
|
for param in params
|
||||||
],
|
],
|
||||||
psg.ButtonMenu(
|
psg.ButtonMenu(
|
||||||
'Bus Mode',
|
'BUSMODE',
|
||||||
size=(12, 2),
|
size=(12, 2),
|
||||||
menu_def=['', busmodes],
|
menu_def=['', busmodes],
|
||||||
key=f'BUS {i}||MODE',
|
key=f'BUS {i}||MODE',
|
||||||
@ -470,7 +463,7 @@ class Builder:
|
|||||||
)
|
)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
[step(outputs) for step in (add_bus_buttons,)]
|
[step(outputs) for step in (add_strip_outputs,)]
|
||||||
return psg.Frame(
|
return psg.Frame(
|
||||||
self.window.cache['labels'][f'BUS {i}||LABEL'],
|
self.window.cache['labels'][f'BUS {i}||LABEL'],
|
||||||
outputs,
|
outputs,
|
||||||
|
|||||||
@ -155,10 +155,6 @@ def get_bus_modes(vm) -> list:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_bus_mono() -> list:
|
|
||||||
return ['off', 'on', 'stereo reverse']
|
|
||||||
|
|
||||||
|
|
||||||
def check_bounds(val, bounds: tuple) -> int | float:
|
def check_bounds(val, bounds: tuple) -> int | float:
|
||||||
lower, upper = bounds
|
lower, upper = bounds
|
||||||
if val > upper:
|
if val > upper:
|
||||||
|
|||||||
@ -58,7 +58,6 @@ class NVDAVMWindow(psg.Window):
|
|||||||
self[f'STRIP {i}||SLIDER LIMIT'].Widget.config(**slider_opts)
|
self[f'STRIP {i}||SLIDER LIMIT'].Widget.config(**slider_opts)
|
||||||
for i in range(self.kind.num_bus):
|
for i in range(self.kind.num_bus):
|
||||||
self[f'BUS {i}||SLIDER GAIN'].Widget.config(**slider_opts)
|
self[f'BUS {i}||SLIDER GAIN'].Widget.config(**slider_opts)
|
||||||
self[f'BUS {i}||MONO'].Widget.config(**buttonmenu_opts)
|
|
||||||
self[f'BUS {i}||MODE'].Widget.config(**buttonmenu_opts)
|
self[f'BUS {i}||MODE'].Widget.config(**buttonmenu_opts)
|
||||||
|
|
||||||
self.register_events()
|
self.register_events()
|
||||||
@ -252,16 +251,13 @@ class NVDAVMWindow(psg.Window):
|
|||||||
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||||
|
|
||||||
# Bus Params
|
# Bus Params
|
||||||
params = ['EQ', 'MUTE']
|
params = ['MONO', 'EQ', 'MUTE']
|
||||||
if self.kind.name == 'basic':
|
if self.kind.name == 'basic':
|
||||||
params.remove('EQ')
|
params.remove('EQ')
|
||||||
for i in range(self.kind.num_bus):
|
for i in range(self.kind.num_bus):
|
||||||
for param in params:
|
for param in params:
|
||||||
self[f'BUS {i}||{param}'].bind('<FocusIn>', '||FOCUS IN')
|
self[f'BUS {i}||{param}'].bind('<FocusIn>', '||FOCUS IN')
|
||||||
self[f'BUS {i}||{param}'].bind('<Return>', '||KEY ENTER')
|
self[f'BUS {i}||{param}'].bind('<Return>', '||KEY ENTER')
|
||||||
self[f'BUS {i}||MONO'].bind('<FocusIn>', '||FOCUS IN')
|
|
||||||
self[f'BUS {i}||MONO'].bind('<space>', '||KEY SPACE', propagate=False)
|
|
||||||
self[f'BUS {i}||MONO'].bind('<Return>', '||KEY ENTER')
|
|
||||||
self[f'BUS {i}||MODE'].bind('<FocusIn>', '||FOCUS IN')
|
self[f'BUS {i}||MODE'].bind('<FocusIn>', '||FOCUS IN')
|
||||||
self[f'BUS {i}||MODE'].bind('<space>', '||KEY SPACE', propagate=False)
|
self[f'BUS {i}||MODE'].bind('<space>', '||KEY SPACE', propagate=False)
|
||||||
self[f'BUS {i}||MODE'].bind('<Return>', '||KEY ENTER', propagate=False)
|
self[f'BUS {i}||MODE'].bind('<Return>', '||KEY ENTER', propagate=False)
|
||||||
@ -310,7 +306,7 @@ class NVDAVMWindow(psg.Window):
|
|||||||
mode = None
|
mode = None
|
||||||
continue
|
continue
|
||||||
|
|
||||||
match parsed_cmd := self.parser.match.parse_string(event):
|
match parsed_cmd := self.parser.match.parseString(event):
|
||||||
# Slider mode
|
# Slider mode
|
||||||
case [['ALT', 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' as direction], ['PRESS' | 'RELEASE' as e]]:
|
case [['ALT', 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' as direction], ['PRESS' | 'RELEASE' as e]]:
|
||||||
if mode:
|
if mode:
|
||||||
@ -976,7 +972,7 @@ class NVDAVMWindow(psg.Window):
|
|||||||
self.nvda.speak,
|
self.nvda.speak,
|
||||||
'on' if val else 'off',
|
'on' if val else 'off',
|
||||||
)
|
)
|
||||||
case 'MUTE':
|
case 'MONO' | 'MUTE':
|
||||||
val = not val
|
val = not val
|
||||||
setattr(self.vm.bus[int(index)], param.lower(), val)
|
setattr(self.vm.bus[int(index)], param.lower(), val)
|
||||||
self.cache['bus'][event] = val
|
self.cache['bus'][event] = val
|
||||||
@ -985,15 +981,6 @@ class NVDAVMWindow(psg.Window):
|
|||||||
self.nvda.speak,
|
self.nvda.speak,
|
||||||
'on' if val else 'off',
|
'on' if val else 'off',
|
||||||
)
|
)
|
||||||
case 'MONO':
|
|
||||||
chosen = values[event]
|
|
||||||
self.vm.bus[int(index)].mono = util.get_bus_mono().index(chosen)
|
|
||||||
self.cache['bus'][event] = chosen
|
|
||||||
self.TKroot.after(
|
|
||||||
200,
|
|
||||||
self.nvda.speak,
|
|
||||||
f'mono {chosen}',
|
|
||||||
)
|
|
||||||
case 'MODE':
|
case 'MODE':
|
||||||
chosen = util._bus_mode_map_reversed[values[event]]
|
chosen = util._bus_mode_map_reversed[values[event]]
|
||||||
setattr(self.vm.bus[int(index)].mode, chosen, True)
|
setattr(self.vm.bus[int(index)].mode, chosen, True)
|
||||||
@ -1009,19 +996,11 @@ class NVDAVMWindow(psg.Window):
|
|||||||
val = self.cache['bus'][f'BUS {index}||{param}']
|
val = self.cache['bus'][f'BUS {index}||{param}']
|
||||||
if param == 'MODE':
|
if param == 'MODE':
|
||||||
self.nvda.speak(f'{label} bus {param} {util._bus_mode_map[val]}')
|
self.nvda.speak(f'{label} bus {param} {util._bus_mode_map[val]}')
|
||||||
elif param == 'MONO':
|
|
||||||
busmode = util.get_bus_mono()[val]
|
|
||||||
if busmode in ('on', 'off'):
|
|
||||||
self.nvda.speak(f'{label} {param} {busmode}')
|
|
||||||
else:
|
|
||||||
self.nvda.speak(f'{label} {busmode}')
|
|
||||||
else:
|
else:
|
||||||
self.nvda.speak(f'{label} {param} {"on" if val else "off"}')
|
self.nvda.speak(f'{label} {param} {"on" if val else "off"}')
|
||||||
case [['BUS', index], [param], ['KEY', 'SPACE' | 'ENTER']]:
|
case [['BUS', index], [param], ['KEY', 'SPACE' | 'ENTER']]:
|
||||||
if param == 'MODE':
|
if param == 'MODE':
|
||||||
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MODE')
|
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MODE')
|
||||||
elif param == 'MONO':
|
|
||||||
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MONO')
|
|
||||||
else:
|
else:
|
||||||
self.find_element_with_focus().click()
|
self.find_element_with_focus().click()
|
||||||
|
|
||||||
|
|||||||
@ -1,302 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Dynamic build system for nvda-voicemeeter.
|
|
||||||
|
|
||||||
This script generates PyInstaller spec files on-the-fly and builds executables
|
|
||||||
without storing intermediate files in the repository.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python tools/dynamic_builder.py # Build all kinds
|
|
||||||
python tools/dynamic_builder.py basic # Build basic only
|
|
||||||
python tools/dynamic_builder.py basic banana # Build specific kinds
|
|
||||||
python tools/dynamic_builder.py all # Build all kinds (explicit)
|
|
||||||
|
|
||||||
Requirements:
|
|
||||||
- PDM environment with PyInstaller installed
|
|
||||||
- controllerClient DLL files in controllerClient/{x64,x86}/
|
|
||||||
- nvda_voicemeeter package installed in environment
|
|
||||||
|
|
||||||
Environment Variables:
|
|
||||||
- PDM_BIN: Path to PDM executable (default: 'pdm')
|
|
||||||
|
|
||||||
Exit Codes:
|
|
||||||
- 0: All builds successful
|
|
||||||
- 1: One or more builds failed
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
# Build configuration
|
|
||||||
KINDS = ['basic', 'banana', 'potato']
|
|
||||||
|
|
||||||
# Templates
|
|
||||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
|
||||||
|
|
||||||
import nvda_voicemeeter
|
|
||||||
|
|
||||||
KIND_ID = '{kind}'
|
|
||||||
|
|
||||||
with voicemeeterlib.api(KIND_ID) as vm:
|
|
||||||
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
|
||||||
window.run()
|
|
||||||
"""
|
|
||||||
|
|
||||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
added_files = [
|
|
||||||
( '{controller_x64_path}', 'controllerClient/x64' ),
|
|
||||||
( '{controller_x86_path}', 'controllerClient/x86' ),
|
|
||||||
]
|
|
||||||
|
|
||||||
a = Analysis(
|
|
||||||
['{script_path}'],
|
|
||||||
pathex=[],
|
|
||||||
binaries=[],
|
|
||||||
datas=added_files,
|
|
||||||
hiddenimports=[],
|
|
||||||
hookspath=[],
|
|
||||||
hooksconfig={{}},
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
win_no_prefer_redirects=False,
|
|
||||||
win_private_assemblies=False,
|
|
||||||
cipher=block_cipher,
|
|
||||||
noarchive=False,
|
|
||||||
)
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
|
||||||
|
|
||||||
exe = EXE(
|
|
||||||
pyz,
|
|
||||||
a.scripts,
|
|
||||||
[],
|
|
||||||
exclude_binaries=True,
|
|
||||||
name='{kind}',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
console=False,
|
|
||||||
disable_windowed_traceback=False,
|
|
||||||
argv_emulation=False,
|
|
||||||
target_arch=None,
|
|
||||||
codesign_identity=None,
|
|
||||||
entitlements_file=None,
|
|
||||||
)
|
|
||||||
coll = COLLECT(
|
|
||||||
exe,
|
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
upx_exclude=[],
|
|
||||||
name='{kind}',
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicBuilder:
|
|
||||||
def __init__(self, base_dir: Path, dist_dir: Path):
|
|
||||||
self.base_dir = base_dir
|
|
||||||
self.dist_dir = dist_dir
|
|
||||||
self.temp_dir = None
|
|
||||||
|
|
||||||
def validate_environment(self) -> bool:
|
|
||||||
"""Validate that all required files and dependencies are present."""
|
|
||||||
|
|
||||||
# Check for controller client DLLs
|
|
||||||
x64_dll = self.base_dir / 'controllerClient' / 'x64' / 'nvdaControllerClient.dll'
|
|
||||||
x86_dll = self.base_dir / 'controllerClient' / 'x86' / 'nvdaControllerClient.dll'
|
|
||||||
|
|
||||||
if not x64_dll.exists():
|
|
||||||
print(f'[ERROR] Missing x64 controller client: {x64_dll}')
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not x86_dll.exists():
|
|
||||||
print(f'[ERROR] Missing x86 controller client: {x86_dll}')
|
|
||||||
return False
|
|
||||||
|
|
||||||
print('[OK] Controller client DLLs found')
|
|
||||||
|
|
||||||
# Check PyInstaller availability
|
|
||||||
try:
|
|
||||||
result = subprocess.run(['pdm', 'list'], capture_output=True, text=True)
|
|
||||||
if 'pyinstaller' not in result.stdout.lower():
|
|
||||||
print('[ERROR] PyInstaller not found in PDM environment')
|
|
||||||
return False
|
|
||||||
print('[OK] PyInstaller available')
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
print('[ERROR] Failed to check PDM environment')
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
# Validate environment first
|
|
||||||
if not self.validate_environment():
|
|
||||||
print('[ERROR] Environment validation failed!')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
self.temp_dir = Path(tempfile.mkdtemp(prefix='nvda_voicemeeter_build_'))
|
|
||||||
print(f'Using temp directory: {self.temp_dir}')
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
||||||
if self.temp_dir and self.temp_dir.exists():
|
|
||||||
shutil.rmtree(self.temp_dir)
|
|
||||||
print(f'Cleaned up temp directory: {self.temp_dir}')
|
|
||||||
|
|
||||||
def create_python_file(self, kind: str) -> Path:
|
|
||||||
"""Create a temporary Python launcher file."""
|
|
||||||
content = PYTHON_TEMPLATE.format(kind=kind)
|
|
||||||
|
|
||||||
py_file = self.temp_dir / f'{kind}.py'
|
|
||||||
with open(py_file, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
return py_file
|
|
||||||
|
|
||||||
def create_spec_file(self, kind: str, py_file: Path) -> Path:
|
|
||||||
"""Create a temporary PyInstaller spec file."""
|
|
||||||
controller_x64_path = (self.base_dir / 'controllerClient' / 'x64').as_posix()
|
|
||||||
controller_x86_path = (self.base_dir / 'controllerClient' / 'x86').as_posix()
|
|
||||||
|
|
||||||
content = SPEC_TEMPLATE.format(
|
|
||||||
script_path=py_file.as_posix(),
|
|
||||||
controller_x64_path=controller_x64_path,
|
|
||||||
controller_x86_path=controller_x86_path,
|
|
||||||
kind=kind,
|
|
||||||
)
|
|
||||||
|
|
||||||
spec_file = self.temp_dir / f'{kind}.spec'
|
|
||||||
with open(spec_file, 'w') as f:
|
|
||||||
f.write(content)
|
|
||||||
|
|
||||||
return spec_file
|
|
||||||
|
|
||||||
def build_variant(self, kind: str) -> bool:
|
|
||||||
"""Build a single kind variant."""
|
|
||||||
print(f'Building {kind}...')
|
|
||||||
|
|
||||||
# Validate kind
|
|
||||||
if kind not in KINDS:
|
|
||||||
print(f'[ERROR] Unknown kind: {kind}. Valid kinds: {", ".join(KINDS)}')
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Create temporary files
|
|
||||||
py_file = self.create_python_file(kind)
|
|
||||||
spec_file = self.create_spec_file(kind, py_file)
|
|
||||||
|
|
||||||
# Build with PyInstaller
|
|
||||||
dist_path = self.dist_dir / kind
|
|
||||||
pdm_bin = os.getenv('PDM_BIN', 'pdm')
|
|
||||||
cmd = [
|
|
||||||
pdm_bin,
|
|
||||||
'run',
|
|
||||||
'pyinstaller',
|
|
||||||
'--noconfirm',
|
|
||||||
'--distpath',
|
|
||||||
str(dist_path.parent),
|
|
||||||
str(spec_file),
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(cmd, cwd=self.base_dir, capture_output=True, text=True)
|
|
||||||
if result.returncode == 0:
|
|
||||||
print(f'[OK] Built {kind}')
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
print(f'[FAIL] Failed to build {kind}')
|
|
||||||
print(f'Error: {result.stderr}')
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
print(f'[ERROR] Exception building {kind}: {e}')
|
|
||||||
return False
|
|
||||||
|
|
||||||
def build_all_kinds(self) -> Dict[str, bool]:
|
|
||||||
"""Build all kind variants."""
|
|
||||||
results = {}
|
|
||||||
|
|
||||||
for kind in KINDS:
|
|
||||||
success = self.build_variant(kind)
|
|
||||||
results[kind] = success
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(description='Dynamic build system for nvda-voicemeeter')
|
|
||||||
parser.add_argument(
|
|
||||||
'kinds',
|
|
||||||
nargs='*',
|
|
||||||
choices=KINDS + ['all'],
|
|
||||||
help='Kinds to build (default: all)',
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--dist-dir',
|
|
||||||
type=Path,
|
|
||||||
default=Path('dist'),
|
|
||||||
help='Distribution output directory',
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.kinds or 'all' in args.kinds:
|
|
||||||
kinds_to_build = KINDS
|
|
||||||
else:
|
|
||||||
kinds_to_build = args.kinds
|
|
||||||
|
|
||||||
base_dir = Path.cwd()
|
|
||||||
args.dist_dir.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
print(f'Building kinds: {", ".join(kinds_to_build)}')
|
|
||||||
|
|
||||||
all_results = {}
|
|
||||||
|
|
||||||
with DynamicBuilder(base_dir, args.dist_dir) as builder:
|
|
||||||
if 'all' in kinds_to_build or len(kinds_to_build) == len(KINDS):
|
|
||||||
# Build all kinds
|
|
||||||
results = builder.build_all_kinds()
|
|
||||||
all_results.update(results)
|
|
||||||
else:
|
|
||||||
# Build specific kinds
|
|
||||||
for kind in kinds_to_build:
|
|
||||||
success = builder.build_variant(kind)
|
|
||||||
all_results[kind] = success
|
|
||||||
|
|
||||||
# Report results
|
|
||||||
print('\n' + '=' * 50)
|
|
||||||
print('BUILD SUMMARY')
|
|
||||||
print('=' * 50)
|
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
total_count = 0
|
|
||||||
|
|
||||||
for build_name, success in all_results.items():
|
|
||||||
status = '[OK]' if success else '[FAIL]'
|
|
||||||
print(f'{status} {build_name}')
|
|
||||||
if success:
|
|
||||||
success_count += 1
|
|
||||||
total_count += 1
|
|
||||||
|
|
||||||
print(f'\nSuccess: {success_count}/{total_count}')
|
|
||||||
|
|
||||||
if success_count == total_count:
|
|
||||||
print('All builds completed successfully!')
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print('Some builds failed!')
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
Loading…
x
Reference in New Issue
Block a user