mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-17 16:29:13 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b4a76c484 | |||
| dfb96777bb | |||
| 5aa2af2acf | |||
| cc6e187998 | |||
| 1ea1c59f06 | |||
| 054ad040bb | |||
| 94f0b847a7 | |||
| 8d251d1dea | |||
| 5f7d66ceae | |||
| 4ed17a5476 | |||
| b88955a45a | |||
| c3247fa5bf | |||
| 39c14279b2 | |||
| d850581179 | |||
| 47d38e4468 | |||
| 415f2e2ba3 | |||
| 57b28b3507 | |||
| cc0a1de07b | |||
| 9ee0dac610 | |||
| c7b8622aac | |||
| 2706338fcb | |||
| ff98aa5f89 | |||
| 03252fe30f | |||
| 055e92ab24 | |||
| 32b3ec8a2e | |||
| fbd4962fb2 | |||
| b5ea41c604 | |||
| f48983d08a | |||
| 1c5c8893ad | |||
| e645437d1d | |||
| 9969506698 | |||
| 3a1143199a | |||
| 79f739f250 | |||
| b45b2da4aa | |||
| 0adfec2e63 | |||
| dacc972b17 | |||
| 2ed1cad666 | |||
| 64361b2011 |
237
.github/workflows/release.yml
vendored
Normal file
237
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
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
Normal file
19
.github/workflows/ruff.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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'
|
||||||
78
.gitignore
vendored
78
.gitignore
vendored
@ -1,6 +1,9 @@
|
|||||||
|
# Generated by ignr: github.com/onyx-and-iris/ignr
|
||||||
|
|
||||||
|
## Python ##
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[codz]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
@ -46,7 +49,7 @@ htmlcov/
|
|||||||
nosetests.xml
|
nosetests.xml
|
||||||
coverage.xml
|
coverage.xml
|
||||||
*.cover
|
*.cover
|
||||||
*.py,cover
|
*.py.cover
|
||||||
.hypothesis/
|
.hypothesis/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
cover/
|
cover/
|
||||||
@ -94,23 +97,36 @@ ipython_config.py
|
|||||||
# install all needed dependencies.
|
# install all needed dependencies.
|
||||||
#Pipfile.lock
|
#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
|
# poetry
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
# 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
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
# commonly ignored for libraries.
|
# commonly ignored for libraries.
|
||||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
#poetry.lock
|
#poetry.lock
|
||||||
|
#poetry.toml
|
||||||
|
|
||||||
# pdm
|
# pdm
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||||
|
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||||
#pdm.lock
|
#pdm.lock
|
||||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
#pdm.toml
|
||||||
# in version control.
|
|
||||||
# https://pdm.fming.dev/#use-with-ide
|
|
||||||
.pdm.toml
|
|
||||||
.pdm-python
|
.pdm-python
|
||||||
.pdm-build/
|
.pdm-build/
|
||||||
|
|
||||||
|
# pixi
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||||
|
#pixi.lock
|
||||||
|
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||||
|
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||||
|
.pixi
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
__pypackages__/
|
__pypackages__/
|
||||||
|
|
||||||
@ -123,6 +139,7 @@ celerybeat.pid
|
|||||||
|
|
||||||
# Environments
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
.envrc
|
||||||
.venv
|
.venv
|
||||||
env/
|
env/
|
||||||
venv/
|
venv/
|
||||||
@ -161,17 +178,44 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# nvda files
|
# Abstra
|
||||||
controllerClient/*
|
# Abstra is an AI-powered process automation framework.
|
||||||
!.gitkeep
|
# Ignore directories containing user credentials, local state, and settings.
|
||||||
|
# Learn more at https://abstra.io/docs
|
||||||
|
.abstra/
|
||||||
|
|
||||||
# build files
|
# Visual Studio Code
|
||||||
basic.py
|
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||||
banana.py
|
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||||
potato.py
|
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||||
|
# you could uncomment the following to ignore the entire vscode folder
|
||||||
|
# .vscode/
|
||||||
|
|
||||||
# persistent storage
|
# Ruff stuff:
|
||||||
settings.json
|
.ruff_cache/
|
||||||
|
|
||||||
# quick test
|
# PyPI configuration file
|
||||||
quick.py
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||||
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
|
|
||||||
|
# Marimo
|
||||||
|
marimo/_static/
|
||||||
|
marimo/_lsp/
|
||||||
|
__marimo__/
|
||||||
|
|
||||||
|
# End of ignr
|
||||||
|
|
||||||
|
# Controller Client
|
||||||
|
controllerClient/**/*
|
||||||
|
!controllerClient/*/
|
||||||
|
!controllerClient/**/.gitkeep
|
||||||
|
|
||||||
|
# Spec files
|
||||||
|
spec/*
|
||||||
|
!spec/.gitkeep
|
||||||
|
|||||||
5
.pre-commit-config.yaml
Normal file
5
.pre-commit-config.yaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pdm-project/pdm
|
||||||
|
rev: 2.26.6
|
||||||
|
hooks:
|
||||||
|
- id: pdm-lock-check
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.10.11
|
||||||
20
README.md
20
README.md
@ -1,6 +1,4 @@
|
|||||||
[](https://pdm.fming.dev)
|
[](https://pdm-project.org)
|
||||||
[](https://github.com/psf/black)
|
|
||||||
[](https://pycqa.github.io/isort/)
|
|
||||||
[](https://github.com/astral-sh/ruff)
|
[](https://github.com/astral-sh/ruff)
|
||||||
|
|
||||||
# NVDA Voicemeeter
|
# NVDA Voicemeeter
|
||||||
@ -29,19 +27,17 @@ cd nvda-voicemeeter
|
|||||||
pip install .
|
pip install .
|
||||||
```
|
```
|
||||||
|
|
||||||
Then download the [Controller Client][controller_client] and place the dll files into the directory `controllerClient`.
|
The [Controller Client][controller_client] files are included with the source files. If you decide to download them yourself make sure you keep the directory structure as follows:
|
||||||
|
|
||||||
Your directory structure should look like this:
|
|
||||||
|
|
||||||
├── `controllerClient/`
|
├── `controllerClient/`
|
||||||
|
|
||||||
├── `x64/`
|
├── `x64/`
|
||||||
|
|
||||||
├── nvdaControllerClient64.dll
|
├── nvdaControllerClient.dll
|
||||||
|
|
||||||
├── `x86/`
|
├── `x86/`
|
||||||
|
|
||||||
├── nvdaControllerClient32.dll
|
├── nvdaControllerClient.dll
|
||||||
|
|
||||||
#### `From Releases`
|
#### `From Releases`
|
||||||
|
|
||||||
@ -56,7 +52,7 @@ import voicemeeterlib
|
|||||||
|
|
||||||
import nvda_voicemeeter
|
import nvda_voicemeeter
|
||||||
|
|
||||||
KIND_ID = "potato"
|
KIND_ID = 'potato'
|
||||||
|
|
||||||
with voicemeeterlib.api(KIND_ID) as vm:
|
with voicemeeterlib.api(KIND_ID) as vm:
|
||||||
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||||
@ -145,9 +141,9 @@ The `Save Settings` option opens a popup window with two buttons, `Browse` and `
|
|||||||
|
|
||||||
`Load Settings` and `Load on Startup` both open an Open dialog box immediately.
|
`Load Settings` and `Load on Startup` both open an Open dialog box immediately.
|
||||||
|
|
||||||
#### `Themes`
|
#### `Theme`
|
||||||
|
|
||||||
The `Themes` menu can be opened using `Alt` and then `t`. Use this menu to select from a list of coloured themes. Some themes offer higher contrast colours. An application restart is required to load a new theme. Once a theme is selected it will become the default for future startups.
|
The `Theme` menu can be opened using `Alt` and then `t`. Use this menu to select from a list of coloured themes. Some themes offer higher contrast colours. An application restart is required to load a new theme. Once a theme is selected it will become the default for future startups.
|
||||||
|
|
||||||
### Quick access binds
|
### Quick access binds
|
||||||
|
|
||||||
@ -188,7 +184,7 @@ If you have any questions/suggestions feel free to raise an issue or open a new
|
|||||||
|
|
||||||
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter and its SDK.
|
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter and its SDK.
|
||||||
|
|
||||||
[PySimpleGUI](https://github.com/PySimpleGUI) team for creating an awesome GUI framework.
|
[FreeSimpleGUI](https://github.com/spyoungtech/FreeSimpleGUI) a fork of the now closed source PySimpleGUI project.
|
||||||
|
|
||||||
[spec]: ./SPECIFICATION.md
|
[spec]: ./SPECIFICATION.md
|
||||||
[voicemeeter]: https://voicemeeter.com/
|
[voicemeeter]: https://voicemeeter.com/
|
||||||
|
|||||||
76
Taskfile.dynamic.yml
Normal file
76
Taskfile.dynamic.yml
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
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"
|
||||||
49
Taskfile.yml
Normal file
49
Taskfile.yml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
SHELL: pwsh
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
desc: Prepare artifacts for release
|
||||||
|
cmds:
|
||||||
|
- task: release
|
||||||
|
|
||||||
|
release:
|
||||||
|
desc: Build and compress all artifacts
|
||||||
|
cmds:
|
||||||
|
- task: build
|
||||||
|
- task: compress
|
||||||
|
|
||||||
|
generate-specs:
|
||||||
|
desc: Generate all spec files from templates
|
||||||
|
cmd: pdm run python tools/spec_generator.py
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Build the project
|
||||||
|
deps: [generate-specs]
|
||||||
|
preconditions:
|
||||||
|
- sh: |
|
||||||
|
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
||||||
|
cmds:
|
||||||
|
- for:
|
||||||
|
matrix:
|
||||||
|
KIND: [basic, banana, potato]
|
||||||
|
cmd: pdm run pyinstaller --noconfirm --distpath dist/{{.ITEM.KIND}} spec/{{.ITEM.KIND}}.spec
|
||||||
|
|
||||||
|
compress:
|
||||||
|
desc: Compress the build artifacts
|
||||||
|
cmds:
|
||||||
|
- for:
|
||||||
|
matrix:
|
||||||
|
KIND: [basic, banana, potato]
|
||||||
|
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.KIND}}.zip -Force"'
|
||||||
|
|
||||||
|
clean:
|
||||||
|
desc: Clean the project
|
||||||
|
cmds:
|
||||||
|
- |
|
||||||
|
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
||||||
@ -2,7 +2,7 @@ import voicemeeterlib
|
|||||||
|
|
||||||
import nvda_voicemeeter
|
import nvda_voicemeeter
|
||||||
|
|
||||||
KIND_ID = "potato"
|
KIND_ID = 'potato'
|
||||||
|
|
||||||
with voicemeeterlib.api(KIND_ID) as vm:
|
with voicemeeterlib.api(KIND_ID) as vm:
|
||||||
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||||
|
|||||||
20
build.ps1
20
build.ps1
@ -1,20 +0,0 @@
|
|||||||
function Compress-Builds {
|
|
||||||
$target = Join-Path -Path $PSScriptRoot -ChildPath "dist"
|
|
||||||
@("basic", "banana", "potato") | ForEach-Object {
|
|
||||||
Compress-Archive -Path $(Join-Path -Path $target -ChildPath $_) -DestinationPath $(Join-Path -Path $target -ChildPath "${_}.zip") -Force
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Builds {
|
|
||||||
@("basic", "banana", "potato") | ForEach-Object {
|
|
||||||
pdm run pyinstaller "${_}.spec" --noconfirm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function main {
|
|
||||||
Get-Builds
|
|
||||||
|
|
||||||
Compress-Builds
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($MyInvocation.InvocationName -ne '.') { main }
|
|
||||||
0
controllerClient/x86/.gitkeep
Normal file
0
controllerClient/x86/.gitkeep
Normal file
300
pdm.lock
generated
300
pdm.lock
generated
@ -2,261 +2,217 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
groups = ["default", "build", "lint", "test"]
|
groups = ["default", "build", "dev", "format"]
|
||||||
strategy = ["cross_platform"]
|
strategy = ["inherit_metadata"]
|
||||||
lock_version = "4.4"
|
lock_version = "4.5.0"
|
||||||
content_hash = "sha256:ca47eaae0de5aa6bcc3fde33b6c1fa7dc2476aeb680f00bb1c550fe06ad67c55"
|
content_hash = "sha256:7f8d5a672b82b4ff9d793a255dcdf55415b79773d7cf7e2742f4531366daac58"
|
||||||
|
|
||||||
|
[[metadata.targets]]
|
||||||
|
requires_python = ">=3.10,<3.13"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "altgraph"
|
name = "altgraph"
|
||||||
version = "0.17.3"
|
version = "0.17.5"
|
||||||
summary = "Python graph (network) package"
|
summary = "Python graph (network) package"
|
||||||
|
groups = ["build"]
|
||||||
files = [
|
files = [
|
||||||
{file = "altgraph-0.17.3-py2.py3-none-any.whl", hash = "sha256:c8ac1ca6772207179ed8003ce7687757c04b0b71536f81e2ac5755c6226458fe"},
|
{file = "altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597"},
|
||||||
{file = "altgraph-0.17.3.tar.gz", hash = "sha256:ad33358114df7c9416cdb8fa1eaa5852166c505118717021c6a8c7c7abbd03dd"},
|
{file = "altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "freesimplegui"
|
||||||
version = "23.7.0"
|
version = "5.2.0.post1"
|
||||||
requires_python = ">=3.8"
|
summary = "The free-forever Python GUI framework."
|
||||||
summary = "The uncompromising code formatter."
|
groups = ["default"]
|
||||||
dependencies = [
|
|
||||||
"click>=8.0.0",
|
|
||||||
"mypy-extensions>=0.4.3",
|
|
||||||
"packaging>=22.0",
|
|
||||||
"pathspec>=0.9.0",
|
|
||||||
"platformdirs>=2",
|
|
||||||
"tomli>=1.1.0; python_version < \"3.11\"",
|
|
||||||
]
|
|
||||||
files = [
|
files = [
|
||||||
{file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"},
|
{file = "freesimplegui-5.2.0.post1-py3-none-any.whl", hash = "sha256:3d61eb519324503232f86b2f1bd7f5c6813ce225f6e189d0fd737ddb036af4d5"},
|
||||||
{file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"},
|
{file = "freesimplegui-5.2.0.post1.tar.gz", hash = "sha256:e58a0e6758e9a9e87152256911f94fcc3998356d1309973a9f4d9df2dc55f98a"},
|
||||||
{file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"},
|
|
||||||
{file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"},
|
|
||||||
{file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"},
|
|
||||||
{file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"},
|
|
||||||
{file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "click"
|
|
||||||
version = "8.1.7"
|
|
||||||
requires_python = ">=3.7"
|
|
||||||
summary = "Composable command line interface toolkit"
|
|
||||||
dependencies = [
|
|
||||||
"colorama; platform_system == \"Windows\"",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
|
||||||
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "colorama"
|
|
||||||
version = "0.4.6"
|
|
||||||
requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
|
||||||
summary = "Cross-platform colored terminal text."
|
|
||||||
files = [
|
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "macholib"
|
name = "macholib"
|
||||||
version = "1.16.2"
|
version = "1.16.4"
|
||||||
summary = "Mach-O header analysis and editing"
|
summary = "Mach-O header analysis and editing"
|
||||||
|
groups = ["build"]
|
||||||
|
marker = "sys_platform == \"darwin\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"altgraph>=0.17",
|
"altgraph>=0.17",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "macholib-1.16.2-py2.py3-none-any.whl", hash = "sha256:44c40f2cd7d6726af8fa6fe22549178d3a4dfecc35a9cd15ea916d9c83a688e0"},
|
{file = "macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea"},
|
||||||
{file = "macholib-1.16.2.tar.gz", hash = "sha256:557bbfa1bb255c20e9abafe7ed6cd8046b48d9525db2f9b77d3122a63a2a8bf8"},
|
{file = "macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362"},
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.0.0"
|
|
||||||
requires_python = ">=3.5"
|
|
||||||
summary = "Type system extensions for programs checked with the mypy type checker."
|
|
||||||
files = [
|
|
||||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
|
||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "23.1"
|
version = "26.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "Core utilities for Python packages"
|
summary = "Core utilities for Python packages"
|
||||||
|
groups = ["build"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
{file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"},
|
||||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
{file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"},
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "0.11.2"
|
|
||||||
requires_python = ">=3.7"
|
|
||||||
summary = "Utility library for gitignore style pattern matching of file paths."
|
|
||||||
files = [
|
|
||||||
{file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
|
|
||||||
{file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pefile"
|
name = "pefile"
|
||||||
version = "2023.2.7"
|
version = "2024.8.26"
|
||||||
requires_python = ">=3.6.0"
|
requires_python = ">=3.6.0"
|
||||||
summary = "Python PE parsing module"
|
summary = "Python PE parsing module"
|
||||||
|
groups = ["build"]
|
||||||
|
marker = "sys_platform == \"win32\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"},
|
{file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"},
|
||||||
{file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"},
|
{file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"},
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "platformdirs"
|
|
||||||
version = "3.10.0"
|
|
||||||
requires_python = ">=3.7"
|
|
||||||
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
|
||||||
files = [
|
|
||||||
{file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
|
|
||||||
{file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psgdemos"
|
|
||||||
version = "1.12.1"
|
|
||||||
summary = "Installs the full set of PySimpleGUI Demo Programs and the Demo Browser."
|
|
||||||
dependencies = [
|
|
||||||
"PySimpleGUI",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "psgdemos-1.12.1-py3-none-any.whl", hash = "sha256:a035540dd0ff92f410aed9b7af8d5a641d9d5a9eac3e0072ef115adf06abb447"},
|
|
||||||
{file = "psgdemos-1.12.1.tar.gz", hash = "sha256:4108af795477531a9b1c8675b9aa9b6628c109e660f6954baf8ba2dc3b5806e9"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyinstaller"
|
name = "pyinstaller"
|
||||||
version = "5.13.0"
|
version = "6.19.0"
|
||||||
requires_python = "<3.13,>=3.7"
|
requires_python = "<3.15,>=3.8"
|
||||||
summary = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
summary = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||||
|
groups = ["build"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"altgraph",
|
"altgraph",
|
||||||
|
"importlib-metadata>=4.6; python_version < \"3.10\"",
|
||||||
"macholib>=1.8; sys_platform == \"darwin\"",
|
"macholib>=1.8; sys_platform == \"darwin\"",
|
||||||
|
"packaging>=22.0",
|
||||||
"pefile>=2022.5.30; sys_platform == \"win32\"",
|
"pefile>=2022.5.30; sys_platform == \"win32\"",
|
||||||
"pyinstaller-hooks-contrib>=2021.4",
|
"pyinstaller-hooks-contrib>=2026.0",
|
||||||
"pywin32-ctypes>=0.2.1; sys_platform == \"win32\"",
|
"pywin32-ctypes>=0.2.1; sys_platform == \"win32\"",
|
||||||
"setuptools>=42.0.0",
|
"setuptools>=42.0.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyinstaller-5.13.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:7fdd319828de679f9c5e381eff998ee9b4164bf4457e7fca56946701cf002c3f"},
|
{file = "pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:0df43697c4914285ecd333be968d2cd042ab9b2670124879ee87931d2344eaf5"},
|
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-manylinux2014_i686.whl", hash = "sha256:28d9742c37e9fb518444b12f8c8ab3cb4ba212d752693c34475c08009aa21ccf"},
|
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e5fb17de6c325d3b2b4ceaeb55130ad7100a79096490e4c5b890224406fa42f4"},
|
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:78975043edeb628e23a73fb3ef0a273cda50e765f1716f75212ea3e91b09dede"},
|
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:cd7d5c06f2847195a23d72ede17c60857d6f495d6f0727dc6c9bc1235f2eb79c"},
|
{file = "pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:24009eba63cfdbcde6d2634e9c87f545eb67249ddf3b514e0cd3b2cdaa595828"},
|
{file = "pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:1fde4381155f21d6354dc450dcaa338cd8a40aaacf6bd22b987b0f3e1f96f3ee"},
|
{file = "pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-win32.whl", hash = "sha256:2d03419904d1c25c8968b0ad21da0e0f33d8d65716e29481b5bd83f7f342b0c5"},
|
{file = "pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-win_amd64.whl", hash = "sha256:9fc27c5a853b14a90d39c252707673c7a0efec921cd817169aff3af0fca8c127"},
|
{file = "pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33"},
|
||||||
{file = "pyinstaller-5.13.0-py3-none-win_arm64.whl", hash = "sha256:3a331951f9744bc2379ea5d65d36f3c828eaefe2785f15039592cdc08560b262"},
|
{file = "pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea"},
|
||||||
{file = "pyinstaller-5.13.0.tar.gz", hash = "sha256:5e446df41255e815017d96318e39f65a3eb807e74a796c7e7ff7f13b6366a2e9"},
|
{file = "pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyinstaller-hooks-contrib"
|
name = "pyinstaller-hooks-contrib"
|
||||||
version = "2023.7"
|
version = "2026.3"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "Community maintained hooks for PyInstaller"
|
summary = "Community maintained hooks for PyInstaller"
|
||||||
|
groups = ["build"]
|
||||||
|
dependencies = [
|
||||||
|
"importlib-metadata>=4.6; python_version < \"3.10\"",
|
||||||
|
"packaging>=22.0",
|
||||||
|
"setuptools>=42.0.0",
|
||||||
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyinstaller-hooks-contrib-2023.7.tar.gz", hash = "sha256:0c436a4c3506020e34116a8a7ddfd854c1ad6ddca9a8cd84500bd6e69c9e68f9"},
|
{file = "pyinstaller_hooks_contrib-2026.3-py3-none-any.whl", hash = "sha256:5ecd1068ad262afecadf07556279d2be52ca93a88b049fae17f1a2eb2969254a"},
|
||||||
{file = "pyinstaller_hooks_contrib-2023.7-py2.py3-none-any.whl", hash = "sha256:3c10df14c0f71ab388dfbf1625375b087e7330d9444cbfd2b310ba027fa0cff0"},
|
{file = "pyinstaller_hooks_contrib-2026.3.tar.gz", hash = "sha256:800d3a198a49a6cd0de2d7fb795005fdca7a0222ed9cb47c0691abd1c27b9310"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
version = "3.1.1"
|
version = "3.3.2"
|
||||||
requires_python = ">=3.6.8"
|
requires_python = ">=3.9"
|
||||||
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
summary = "pyparsing - Classes and methods to define and execute parsing grammars"
|
||||||
|
groups = ["default"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb"},
|
{file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"},
|
||||||
{file = "pyparsing-3.1.1.tar.gz", hash = "sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db"},
|
{file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"},
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pysimplegui"
|
|
||||||
version = "4.60.5"
|
|
||||||
summary = "Python GUIs for Humans. Launched in 2018. It's 2022 & PySimpleGUI is an ACTIVE & supported project. Super-simple to create custom GUI's. 325+ Demo programs & Cookbook for rapid start. Extensive documentation. Main docs at www.PySimpleGUI.org. Fun & your success are the focus. Examples using Machine Learning (GUI, OpenCV Integration), Rainmeter Style Desktop Widgets, Matplotlib + Pyplot, PIL support, add GUI to command line scripts, PDF & Image Viewers. Great for beginners & advanced GUI programmers."
|
|
||||||
files = [
|
|
||||||
{file = "PySimpleGUI-4.60.5-py3-none-any.whl", hash = "sha256:004f33311ee685a5287fad54f500f7290b40d7c806044e478b1384f85dedce64"},
|
|
||||||
{file = "PySimpleGUI-4.60.5.tar.gz", hash = "sha256:31014d1cc5eef1373d7e93564ff2604662645cc774a939b1f01aa253e7f9d78b"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pywin32-ctypes"
|
name = "pywin32-ctypes"
|
||||||
version = "0.2.2"
|
version = "0.2.3"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.6"
|
||||||
summary = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
summary = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||||
|
groups = ["build"]
|
||||||
|
marker = "sys_platform == \"win32\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
|
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||||
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
|
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.0.291"
|
version = "0.15.5"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "An extremely fast Python linter, written in Rust."
|
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
|
groups = ["format"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.0.291-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b97d0d7c136a85badbc7fd8397fdbb336e9409b01c07027622f28dcd7db366f2"},
|
{file = "ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c"},
|
||||||
{file = "ruff-0.0.291-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:6ab44ea607967171e18aa5c80335237be12f3a1523375fa0cede83c5cf77feb4"},
|
{file = "ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a04b384f2d36f00d5fb55313d52a7d66236531195ef08157a09c4728090f2ef0"},
|
{file = "ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b727c219b43f903875b7503a76c86237a00d1a39579bb3e21ce027eec9534051"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87671e33175ae949702774071b35ed4937da06f11851af75cd087e1b5a488ac4"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b75f5801547f79b7541d72a211949754c21dc0705c70eddf7f21c88a64de8b97"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b09b94efdcd162fe32b472b2dd5bf1c969fcc15b8ff52f478b048f41d4590e09"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d5b56bc3a2f83a7a1d7f4447c54d8d3db52021f726fdd55d549ca87bca5d747"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681"},
|
||||||
{file = "ruff-0.0.291-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13f0d88e5f367b2dc8c7d90a8afdcfff9dd7d174e324fd3ed8e0b5cb5dc9b7f6"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a"},
|
||||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b3eeee1b1a45a247758ecdc3ab26c307336d157aafc61edb98b825cadb153df3"},
|
{file = "ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca"},
|
||||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6c06006350c3bb689765d71f810128c9cdf4a1121fd01afc655c87bab4fb4f83"},
|
{file = "ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd"},
|
||||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fd17220611047de247b635596e3174f3d7f2becf63bd56301fc758778df9b629"},
|
{file = "ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d"},
|
||||||
{file = "ruff-0.0.291-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5383ba67ad360caf6060d09012f1fb2ab8bd605ab766d10ca4427a28ab106e0b"},
|
{file = "ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752"},
|
||||||
{file = "ruff-0.0.291-py3-none-win32.whl", hash = "sha256:1d5f0616ae4cdc7a938b493b6a1a71c8a47d0300c0d65f6e41c281c2f7490ad3"},
|
{file = "ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2"},
|
||||||
{file = "ruff-0.0.291-py3-none-win_amd64.whl", hash = "sha256:8a69bfbde72db8ca1c43ee3570f59daad155196c3fbe357047cd9b77de65f15b"},
|
{file = "ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74"},
|
||||||
{file = "ruff-0.0.291-py3-none-win_arm64.whl", hash = "sha256:d867384a4615b7f30b223a849b52104214442b5ba79b473d7edd18da3cde22d6"},
|
{file = "ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe"},
|
||||||
{file = "ruff-0.0.291.tar.gz", hash = "sha256:c61109661dde9db73469d14a82b42a88c7164f731e6a3b0042e71394c1c7ceed"},
|
{file = "ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b"},
|
||||||
|
{file = "ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "68.1.2"
|
version = "82.0.1"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.9"
|
||||||
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
|
summary = "Most extensible Python build backend with support for C/C++ extension modules"
|
||||||
|
groups = ["build"]
|
||||||
files = [
|
files = [
|
||||||
{file = "setuptools-68.1.2-py3-none-any.whl", hash = "sha256:3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b"},
|
{file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"},
|
||||||
{file = "setuptools-68.1.2.tar.gz", hash = "sha256:3d4dfa6d95f1b101d695a6160a7626e15583af71a5f52176efa5d39a054d475d"},
|
{file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.4.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "A lil' TOML parser"
|
summary = "A lil' TOML parser"
|
||||||
|
groups = ["default", "dev"]
|
||||||
|
marker = "python_version < \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
{file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"},
|
||||||
|
{file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"},
|
||||||
|
{file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"},
|
||||||
|
{file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"},
|
||||||
|
{file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "voicemeeter-api"
|
name = "voicemeeter-api"
|
||||||
version = "2.5.2"
|
version = "2.7.2"
|
||||||
requires_python = ">=3.10,<4.0"
|
requires_python = ">=3.10"
|
||||||
|
editable = true
|
||||||
|
path = "../voicemeeter-api-python"
|
||||||
summary = "A Python wrapper for the Voiceemeter API"
|
summary = "A Python wrapper for the Voiceemeter API"
|
||||||
|
groups = ["default", "dev"]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tomli<3.0.0,>=2.0.1; python_version < \"3.11\"",
|
"tomli<3.0,>=2.0.1; python_version < \"3.11\"",
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "voicemeeter_api-2.5.2-py3-none-any.whl", hash = "sha256:304c14a6eef5f95d8b883e8e1752d23f17c3d662d25a28d114d33134f62e93ec"},
|
|
||||||
{file = "voicemeeter_api-2.5.2.tar.gz", hash = "sha256:e2b9558a38ed290b184a3e46b598c0a716cef2e64521431f84a46e1180d539ca"},
|
|
||||||
]
|
]
|
||||||
|
|||||||
161
pyproject.toml
161
pyproject.toml
@ -1,94 +1,39 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nvda_voicemeeter"
|
name = "nvda-voicemeeter"
|
||||||
version = "0.5.7b1"
|
version = "1.1.0"
|
||||||
description = "A Voicemeeter app compatible with NVDA"
|
description = "A Voicemeeter app compatible with NVDA"
|
||||||
authors = [
|
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||||
{ name = "onyx-and-iris", email = "code@onyxandiris.online" },
|
|
||||||
]
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pysimplegui>=4.60.5",
|
"pyparsing>=3.2.1",
|
||||||
"pyparsing>=3.1.1",
|
"voicemeeter-api>=2.7.2",
|
||||||
"voicemeeter-api>=2.5.2",
|
"freesimplegui>=5.1.1",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.10,<3.12"
|
requires-python = ">=3.10,<3.13"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
|
||||||
[project.license]
|
[dependency-groups]
|
||||||
text = "MIT"
|
format = ["ruff>=0.9.2"]
|
||||||
|
build = ["pyinstaller>=6.3.0"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
nvda-voicemeeter-basic = "nvda_voicemeeter.gui.basic:run"
|
||||||
|
nvda-voicemeeter-banana = "nvda_voicemeeter.gui.banana:run"
|
||||||
|
nvda-voicemeeter-potato = "nvda_voicemeeter.gui.potato:run"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["pdm-backend"]
|
||||||
|
build-backend = "pdm.backend"
|
||||||
|
|
||||||
|
[tool.pdm]
|
||||||
|
distribution = true
|
||||||
|
|
||||||
[tool.pdm.dev-dependencies]
|
[tool.pdm.dev-dependencies]
|
||||||
build = [
|
dev = [
|
||||||
"pyinstaller>=5.1",
|
"-e file:///${PROJECT_ROOT}/../voicemeeter-api-python#egg=voicemeeter-api",
|
||||||
]
|
]
|
||||||
lint = [
|
|
||||||
"black>=23.7.0",
|
|
||||||
"ruff>=0.0.291",
|
|
||||||
]
|
|
||||||
test = [
|
|
||||||
"psgdemos>=1.12.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.pdm.scripts.build]
|
|
||||||
shell = "build.ps1"
|
|
||||||
|
|
||||||
[tool.black]
|
|
||||||
line-length = 119
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
]
|
|
||||||
ignore = [
|
|
||||||
"E501",
|
|
||||||
]
|
|
||||||
fixable = [
|
|
||||||
"A",
|
|
||||||
"B",
|
|
||||||
"C",
|
|
||||||
"D",
|
|
||||||
"E",
|
|
||||||
"F",
|
|
||||||
"G",
|
|
||||||
"I",
|
|
||||||
"N",
|
|
||||||
"Q",
|
|
||||||
"S",
|
|
||||||
"T",
|
|
||||||
"W",
|
|
||||||
"ANN",
|
|
||||||
"ARG",
|
|
||||||
"BLE",
|
|
||||||
"COM",
|
|
||||||
"DJ",
|
|
||||||
"DTZ",
|
|
||||||
"EM",
|
|
||||||
"ERA",
|
|
||||||
"EXE",
|
|
||||||
"FBT",
|
|
||||||
"ICN",
|
|
||||||
"INP",
|
|
||||||
"ISC",
|
|
||||||
"NPY",
|
|
||||||
"PD",
|
|
||||||
"PGH",
|
|
||||||
"PIE",
|
|
||||||
"PL",
|
|
||||||
"PT",
|
|
||||||
"PTH",
|
|
||||||
"PYI",
|
|
||||||
"RET",
|
|
||||||
"RSE",
|
|
||||||
"RUF",
|
|
||||||
"SIM",
|
|
||||||
"SLF",
|
|
||||||
"TCH",
|
|
||||||
"TID",
|
|
||||||
"TRY",
|
|
||||||
"UP",
|
|
||||||
"YTT",
|
|
||||||
]
|
|
||||||
unfixable = []
|
|
||||||
exclude = [
|
exclude = [
|
||||||
".bzr",
|
".bzr",
|
||||||
".direnv",
|
".direnv",
|
||||||
@ -112,15 +57,57 @@ exclude = [
|
|||||||
"node_modules",
|
"node_modules",
|
||||||
"venv",
|
"venv",
|
||||||
]
|
]
|
||||||
|
|
||||||
line-length = 119
|
line-length = 119
|
||||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
indent-width = 4
|
||||||
|
|
||||||
|
# Assume Python 3.10
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.ruff.mccabe]
|
[tool.ruff.lint]
|
||||||
|
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||||
|
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||||
|
# McCabe complexity (`C901`) by default.
|
||||||
|
select = ["E4", "E7", "E9", "F"]
|
||||||
|
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
|
max-complexity = 10
|
||||||
|
|
||||||
[tool.ruff.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"__init__.py" = [
|
"__init__.py" = ["E402", "F401"]
|
||||||
"E402",
|
|
||||||
"F401",
|
|
||||||
]
|
|
||||||
|
|||||||
0
spec/.gitkeep
Normal file
0
spec/.gitkeep
Normal file
@ -1,4 +1,4 @@
|
|||||||
import PySimpleGUI as psg
|
import FreeSimpleGUI as psg
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
from .compound import LabelSlider
|
from .compound import LabelSlider
|
||||||
@ -16,7 +16,7 @@ class Builder:
|
|||||||
menu = [[self.make_menu()]]
|
menu = [[self.make_menu()]]
|
||||||
|
|
||||||
layout0 = []
|
layout0 = []
|
||||||
if self.kind.name == "basic":
|
if self.kind.name == 'basic':
|
||||||
steps = (
|
steps = (
|
||||||
self.make_tab0_row0,
|
self.make_tab0_row0,
|
||||||
self.make_tab0_row1,
|
self.make_tab0_row1,
|
||||||
@ -62,65 +62,65 @@ class Builder:
|
|||||||
|
|
||||||
def _make_inner_tabgroup(layouts, identifier) -> psg.TabGroup:
|
def _make_inner_tabgroup(layouts, identifier) -> psg.TabGroup:
|
||||||
inner_layout = []
|
inner_layout = []
|
||||||
for i, tabname in enumerate(("buttons", "sliders")):
|
for i, tabname in enumerate(('buttons', 'sliders')):
|
||||||
inner_layout.append([psg.Tab(tabname.capitalize(), layouts[i], key=f"tab||{identifier}||{tabname}")])
|
inner_layout.append([psg.Tab(tabname.capitalize(), layouts[i], key=f'tab||{identifier}||{tabname}')])
|
||||||
return psg.TabGroup(
|
return psg.TabGroup(
|
||||||
inner_layout,
|
inner_layout,
|
||||||
change_submits=True,
|
change_submits=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
key=f"tabgroup||{identifier}",
|
key=f'tabgroup||{identifier}',
|
||||||
)
|
)
|
||||||
|
|
||||||
def _make_tabs(identifier) -> psg.Tab:
|
def _make_tabs(identifier) -> psg.Tab:
|
||||||
match identifier:
|
match identifier:
|
||||||
case "Settings":
|
case 'Settings':
|
||||||
return psg.Tab("Settings", layout0, key="tab||Settings")
|
return psg.Tab('Settings', layout0, key='tab||Settings')
|
||||||
case "Physical Strip":
|
case 'Physical Strip':
|
||||||
tabgroup = _make_inner_tabgroup((layout1_1, layout1_2), identifier)
|
tabgroup = _make_inner_tabgroup((layout1_1, layout1_2), identifier)
|
||||||
case "Virtual Strip":
|
case 'Virtual Strip':
|
||||||
tabgroup = _make_inner_tabgroup((layout2_1, layout2_2), identifier)
|
tabgroup = _make_inner_tabgroup((layout2_1, layout2_2), identifier)
|
||||||
case "Buses":
|
case 'Buses':
|
||||||
tabgroup = _make_inner_tabgroup((layout3_1, layout3_2), identifier)
|
tabgroup = _make_inner_tabgroup((layout3_1, layout3_2), identifier)
|
||||||
return psg.Tab(identifier, [[tabgroup]], key=f"tab||{identifier}")
|
return psg.Tab(identifier, [[tabgroup]], key=f'tab||{identifier}')
|
||||||
|
|
||||||
tabs = []
|
tabs = []
|
||||||
for tab in util.get_tabs_labels():
|
for tab in util.get_tabs_labels():
|
||||||
tabs.append(_make_tabs(tab))
|
tabs.append(_make_tabs(tab))
|
||||||
|
|
||||||
tab_group = psg.TabGroup([tabs], change_submits=True, enable_events=True, key="tabgroup")
|
tab_group = psg.TabGroup([tabs], change_submits=True, enable_events=True, key='tabgroup')
|
||||||
|
|
||||||
return [[menu], [tab_group]]
|
return [[menu], [tab_group]]
|
||||||
|
|
||||||
def make_menu(self) -> psg.Menu:
|
def make_menu(self) -> psg.Menu:
|
||||||
themes = [f"{theme}::MENU THEME" for theme in util.get_themes_list()]
|
themes = [f'{theme}::MENU THEME' for theme in util.get_themes_list()]
|
||||||
themes.append("Default::MENU THEME")
|
themes.append('Default::MENU THEME')
|
||||||
menu_def = [
|
menu_def = [
|
||||||
[
|
[
|
||||||
"&Voicemeeter",
|
'&Voicemeeter',
|
||||||
[
|
[
|
||||||
"Restart Audio Engine::MENU",
|
'Restart Audio Engine::MENU',
|
||||||
"Save Settings::MENU",
|
'Save Settings::MENU',
|
||||||
"Load Settings::MENU",
|
'Load Settings::MENU',
|
||||||
"Load Settings on Startup ::MENU",
|
'Load Settings on Startup ::MENU',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
["&Theme", themes],
|
['&Theme', themes],
|
||||||
]
|
]
|
||||||
return psg.Menu(menu_def, key="menus")
|
return psg.Menu(menu_def, key='menus')
|
||||||
|
|
||||||
def make_tab0_row0(self) -> psg.Frame:
|
def make_tab0_row0(self) -> psg.Frame:
|
||||||
"""tab0 row0 represents hardware ins"""
|
"""tab0 row0 represents hardware ins"""
|
||||||
|
|
||||||
def add_physical_device_opts(layout):
|
def add_physical_device_opts(layout):
|
||||||
devices = util.get_input_device_list(self.vm)
|
devices = util.get_input_device_list(self.vm)
|
||||||
devices.append("- remove device selection -")
|
devices.append('- remove device selection -')
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.ButtonMenu(
|
psg.ButtonMenu(
|
||||||
f"IN {i + 1}",
|
f'IN {i + 1}',
|
||||||
size=(6, 3),
|
size=(6, 3),
|
||||||
menu_def=["", devices],
|
menu_def=['', devices],
|
||||||
key=f"HARDWARE IN||{i + 1}",
|
key=f'HARDWARE IN||{i + 1}',
|
||||||
)
|
)
|
||||||
for i in range(self.kind.phys_in)
|
for i in range(self.kind.phys_in)
|
||||||
]
|
]
|
||||||
@ -128,23 +128,23 @@ class Builder:
|
|||||||
|
|
||||||
hardware_in = []
|
hardware_in = []
|
||||||
[step(hardware_in) for step in (add_physical_device_opts,)]
|
[step(hardware_in) for step in (add_physical_device_opts,)]
|
||||||
return psg.Frame("Hardware In", hardware_in)
|
return psg.Frame('Hardware In', hardware_in)
|
||||||
|
|
||||||
def make_tab0_row1(self) -> psg.Frame:
|
def make_tab0_row1(self) -> psg.Frame:
|
||||||
"""tab0 row1 represents hardware outs"""
|
"""tab0 row1 represents hardware outs"""
|
||||||
|
|
||||||
def add_physical_device_opts(layout):
|
def add_physical_device_opts(layout):
|
||||||
if self.kind.name == "basic":
|
if self.kind.name == 'basic':
|
||||||
num_outs = self.kind.phys_out + self.kind.virt_out
|
num_outs = self.kind.phys_out + self.kind.virt_out
|
||||||
else:
|
else:
|
||||||
num_outs = self.kind.phys_out
|
num_outs = self.kind.phys_out
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.ButtonMenu(
|
psg.ButtonMenu(
|
||||||
f"A{i + 1}",
|
f'A{i + 1}',
|
||||||
size=(6, 3),
|
size=(6, 3),
|
||||||
menu_def=["", util.get_output_device_list(i, self.vm)],
|
menu_def=['', util.get_output_device_list(i, self.vm)],
|
||||||
key=f"HARDWARE OUT||A{i + 1}",
|
key=f'HARDWARE OUT||A{i + 1}',
|
||||||
)
|
)
|
||||||
for i in range(num_outs)
|
for i in range(num_outs)
|
||||||
]
|
]
|
||||||
@ -152,7 +152,7 @@ class Builder:
|
|||||||
|
|
||||||
hardware_out = []
|
hardware_out = []
|
||||||
[step(hardware_out) for step in (add_physical_device_opts,)]
|
[step(hardware_out) for step in (add_physical_device_opts,)]
|
||||||
return psg.Frame("Hardware Out", hardware_out)
|
return psg.Frame('Hardware Out', hardware_out)
|
||||||
|
|
||||||
def make_tab0_row3(self) -> psg.Frame:
|
def make_tab0_row3(self) -> psg.Frame:
|
||||||
"""tab0 row3 represents patch composite"""
|
"""tab0 row3 represents patch composite"""
|
||||||
@ -162,10 +162,10 @@ class Builder:
|
|||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.ButtonMenu(
|
psg.ButtonMenu(
|
||||||
f"PC{i + 1}",
|
f'PC{i + 1}',
|
||||||
size=(5, 2),
|
size=(5, 2),
|
||||||
menu_def=["", outputs],
|
menu_def=['', outputs],
|
||||||
key=f"PATCH COMPOSITE||PC{i + 1}",
|
key=f'PATCH COMPOSITE||PC{i + 1}',
|
||||||
)
|
)
|
||||||
for i in range(self.kind.composite)
|
for i in range(self.kind.composite)
|
||||||
]
|
]
|
||||||
@ -173,7 +173,7 @@ class Builder:
|
|||||||
|
|
||||||
hardware_out = []
|
hardware_out = []
|
||||||
[step(hardware_out) for step in (add_physical_device_opts,)]
|
[step(hardware_out) for step in (add_physical_device_opts,)]
|
||||||
return psg.Frame("PATCH COMPOSITE", hardware_out)
|
return psg.Frame('PATCH COMPOSITE', hardware_out)
|
||||||
|
|
||||||
def make_tab0_row4(self) -> psg.Frame:
|
def make_tab0_row4(self) -> psg.Frame:
|
||||||
"""tab0 row4 represents patch insert"""
|
"""tab0 row4 represents patch insert"""
|
||||||
@ -185,28 +185,28 @@ class Builder:
|
|||||||
[
|
[
|
||||||
psg.Checkbox(
|
psg.Checkbox(
|
||||||
text=channel,
|
text=channel,
|
||||||
default=self.window.cache["insert"][
|
default=self.window.cache['insert'][
|
||||||
f"INSERT CHECKBOX||{util.get_insert_checkbox_index(self.kind, j, i)}"
|
f'INSERT CHECKBOX||{util.get_insert_checkbox_index(self.kind, j, i)}'
|
||||||
],
|
],
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
key=f"INSERT CHECKBOX||IN{i} {j}",
|
key=f'INSERT CHECKBOX||IN{i} {j}',
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
for j, channel in enumerate(("LEFT", "RIGHT"))
|
for j, channel in enumerate(('LEFT', 'RIGHT'))
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Checkbox(
|
psg.Checkbox(
|
||||||
text=channel,
|
text=channel,
|
||||||
default=self.window.cache["insert"][
|
default=self.window.cache['insert'][
|
||||||
f"INSERT CHECKBOX||{util.get_insert_checkbox_index(self.kind, j, i)}"
|
f'INSERT CHECKBOX||{util.get_insert_checkbox_index(self.kind, j, i)}'
|
||||||
],
|
],
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
key=f"INSERT CHECKBOX||IN{i} {j}",
|
key=f'INSERT CHECKBOX||IN{i} {j}',
|
||||||
)
|
)
|
||||||
for j, channel in enumerate(("LEFT", "RIGHT", "C", "LFE", "SL", "SR", "BL", "BR"))
|
for j, channel in enumerate(('LEFT', 'RIGHT', 'C', 'LFE', 'SL', 'SR', 'BL', 'BR'))
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -216,29 +216,29 @@ class Builder:
|
|||||||
for i, checkbox_list in enumerate(checkbox_lists):
|
for i, checkbox_list in enumerate(checkbox_lists):
|
||||||
if i < self.kind.phys_in:
|
if i < self.kind.phys_in:
|
||||||
[step(checkbox_list, i + 1) for step in (add_insert_checkboxes,)]
|
[step(checkbox_list, i + 1) for step in (add_insert_checkboxes,)]
|
||||||
inner.append(psg.Frame(f"In#{i + 1}", checkbox_list))
|
inner.append(psg.Frame(f'In#{i + 1}', checkbox_list))
|
||||||
else:
|
else:
|
||||||
[step(checkbox_list, i + 1) for step in (add_insert_checkboxes,)]
|
[step(checkbox_list, i + 1) for step in (add_insert_checkboxes,)]
|
||||||
asio_checkboxes.append([psg.Frame(f"In#{i + 1}", checkbox_list)])
|
asio_checkboxes.append([psg.Frame(f'In#{i + 1}', checkbox_list)])
|
||||||
asio_checkboxes.insert(0, inner)
|
asio_checkboxes.insert(0, inner)
|
||||||
|
|
||||||
return psg.Frame("PATCH INSERT", asio_checkboxes)
|
return psg.Frame('PATCH INSERT', asio_checkboxes)
|
||||||
|
|
||||||
def make_tab0_row5(self) -> psg.Frame:
|
def make_tab0_row5(self) -> psg.Frame:
|
||||||
"""tab0 row5 represents advanced settings"""
|
"""tab0 row5 represents advanced settings"""
|
||||||
|
|
||||||
return psg.Frame(
|
return psg.Frame(
|
||||||
"ADVANCED SETTINGS",
|
'ADVANCED SETTINGS',
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
psg.Button(
|
psg.Button(
|
||||||
"ADVANCED SETTINGS",
|
'ADVANCED SETTINGS',
|
||||||
size=(20, 2),
|
size=(20, 2),
|
||||||
key="ADVANCED SETTINGS",
|
key='ADVANCED SETTINGS',
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
key="ADVANCED SETTINGS FRAME",
|
key='ADVANCED SETTINGS FRAME',
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_tab1_button_row(self, i) -> psg.Frame:
|
def make_tab1_button_row(self, i) -> psg.Frame:
|
||||||
@ -248,26 +248,26 @@ class Builder:
|
|||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Button(
|
psg.Button(
|
||||||
f"A{j + 1}" if j < self.kind.phys_out else f"B{j - self.kind.phys_out + 1}",
|
f'A{j + 1}' if j < self.kind.phys_out else f'B{j - self.kind.phys_out + 1}',
|
||||||
size=(4, 2),
|
size=(4, 2),
|
||||||
key=f"STRIP {i}||A{j + 1}"
|
key=f'STRIP {i}||A{j + 1}'
|
||||||
if j < self.kind.phys_out
|
if j < self.kind.phys_out
|
||||||
else f"STRIP {i}||B{j - self.kind.phys_out + 1}",
|
else f'STRIP {i}||B{j - self.kind.phys_out + 1}',
|
||||||
)
|
)
|
||||||
for j in range(self.kind.phys_out + self.kind.virt_out)
|
for j in range(self.kind.phys_out + self.kind.virt_out)
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Button("Mono", size=(6, 2), key=f"STRIP {i}||MONO"),
|
psg.Button('Mono', size=(6, 2), key=f'STRIP {i}||MONO'),
|
||||||
psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"),
|
psg.Button('Solo', size=(6, 2), key=f'STRIP {i}||SOLO'),
|
||||||
psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
|
psg.Button('Mute', size=(6, 2), key=f'STRIP {i}||MUTE'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
[step(outputs) for step in (add_strip_outputs,)]
|
[step(outputs) for step in (add_strip_outputs,)]
|
||||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], outputs, key=f"STRIP {i}||LABEL")
|
return psg.Frame(self.window.cache['labels'][f'STRIP {i}||LABEL'], outputs, key=f'STRIP {i}||LABEL')
|
||||||
|
|
||||||
def make_tab1_button_rows(self) -> psg.Frame:
|
def make_tab1_button_rows(self) -> psg.Frame:
|
||||||
layout = [[self.make_tab1_button_row(i)] for i in range(self.kind.phys_in)]
|
layout = [[self.make_tab1_button_row(i)] for i in range(self.kind.phys_in)]
|
||||||
@ -277,7 +277,7 @@ class Builder:
|
|||||||
def add_gain_slider(layout):
|
def add_gain_slider(layout):
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Text("Gain"),
|
psg.Text('Gain'),
|
||||||
psg.Slider(
|
psg.Slider(
|
||||||
range=(-60, 12),
|
range=(-60, 12),
|
||||||
default_value=self.vm.strip[i].gain,
|
default_value=self.vm.strip[i].gain,
|
||||||
@ -286,8 +286,8 @@ class Builder:
|
|||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
disabled=True,
|
disabled=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"STRIP {i}||SLIDER GAIN",
|
key=f'STRIP {i}||SLIDER GAIN',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@ -298,7 +298,7 @@ class Builder:
|
|||||||
def add_limit_slider(layout):
|
def add_limit_slider(layout):
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Text("Limit"),
|
psg.Text('Limit'),
|
||||||
psg.Slider(
|
psg.Slider(
|
||||||
range=(-40, 12),
|
range=(-40, 12),
|
||||||
default_value=self.vm.strip[i].limit,
|
default_value=self.vm.strip[i].limit,
|
||||||
@ -306,18 +306,18 @@ class Builder:
|
|||||||
disable_number_display=True,
|
disable_number_display=True,
|
||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"STRIP {i}||SLIDER LIMIT",
|
key=f'STRIP {i}||SLIDER LIMIT',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
layout = []
|
layout = []
|
||||||
steps = (add_gain_slider, add_param_sliders)
|
steps = (add_gain_slider, add_param_sliders)
|
||||||
if self.kind.name in ("banana", "potato"):
|
if self.kind.name in ('banana', 'potato'):
|
||||||
steps += (add_limit_slider,)
|
steps += (add_limit_slider,)
|
||||||
[step(layout) for step in steps]
|
[step(layout) for step in steps]
|
||||||
return psg.Frame(self.window.cache["labels"][f"STRIP {i}||LABEL"], layout, key=f"STRIP {i}||LABEL||SLIDER")
|
return psg.Frame(self.window.cache['labels'][f'STRIP {i}||LABEL'], layout, key=f'STRIP {i}||LABEL||SLIDER')
|
||||||
|
|
||||||
def make_tab1_slider_rows(self) -> psg.Frame:
|
def make_tab1_slider_rows(self) -> psg.Frame:
|
||||||
layout = [[self.make_tab1_slider_row(i)] for i in range(self.kind.phys_in)]
|
layout = [[self.make_tab1_slider_row(i)] for i in range(self.kind.phys_in)]
|
||||||
@ -330,11 +330,11 @@ class Builder:
|
|||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Button(
|
psg.Button(
|
||||||
f"A{j + 1}" if j < self.kind.phys_out else f"B{j - self.kind.phys_out + 1}",
|
f'A{j + 1}' if j < self.kind.phys_out else f'B{j - self.kind.phys_out + 1}',
|
||||||
size=(4, 2),
|
size=(4, 2),
|
||||||
key=f"STRIP {i}||A{j + 1}"
|
key=f'STRIP {i}||A{j + 1}'
|
||||||
if j < self.kind.phys_out
|
if j < self.kind.phys_out
|
||||||
else f"STRIP {i}||B{j - self.kind.phys_out + 1}",
|
else f'STRIP {i}||B{j - self.kind.phys_out + 1}',
|
||||||
)
|
)
|
||||||
for j in range(self.kind.phys_out + self.kind.virt_out)
|
for j in range(self.kind.phys_out + self.kind.virt_out)
|
||||||
]
|
]
|
||||||
@ -342,26 +342,26 @@ class Builder:
|
|||||||
if i == self.kind.phys_in + 1:
|
if i == self.kind.phys_in + 1:
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Button("K", size=(6, 2), key=f"STRIP {i}||KARAOKE"),
|
psg.Button('K', size=(6, 2), key=f'STRIP {i}||KARAOKE'),
|
||||||
psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"),
|
psg.Button('Solo', size=(6, 2), key=f'STRIP {i}||SOLO'),
|
||||||
psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
|
psg.Button('Mute', size=(6, 2), key=f'STRIP {i}||MUTE'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Button("MC", size=(6, 2), key=f"STRIP {i}||MC"),
|
psg.Button('MC', size=(6, 2), key=f'STRIP {i}||MC'),
|
||||||
psg.Button("Solo", size=(6, 2), key=f"STRIP {i}||SOLO"),
|
psg.Button('Solo', size=(6, 2), key=f'STRIP {i}||SOLO'),
|
||||||
psg.Button("Mute", size=(6, 2), key=f"STRIP {i}||MUTE"),
|
psg.Button('Mute', size=(6, 2), key=f'STRIP {i}||MUTE'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
[step(outputs) for step in (add_strip_outputs,)]
|
[step(outputs) for step in (add_strip_outputs,)]
|
||||||
return psg.Frame(
|
return psg.Frame(
|
||||||
self.window.cache["labels"][f"STRIP {i}||LABEL"],
|
self.window.cache['labels'][f'STRIP {i}||LABEL'],
|
||||||
outputs,
|
outputs,
|
||||||
key=f"STRIP {i}||LABEL",
|
key=f'STRIP {i}||LABEL',
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_tab2_button_rows(self) -> psg.Frame:
|
def make_tab2_button_rows(self) -> psg.Frame:
|
||||||
@ -374,7 +374,7 @@ class Builder:
|
|||||||
def add_gain_slider(layout):
|
def add_gain_slider(layout):
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Text("Gain"),
|
psg.Text('Gain'),
|
||||||
psg.Slider(
|
psg.Slider(
|
||||||
range=(-60, 12),
|
range=(-60, 12),
|
||||||
default_value=self.vm.strip[i].gain,
|
default_value=self.vm.strip[i].gain,
|
||||||
@ -383,14 +383,14 @@ class Builder:
|
|||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
disabled=True,
|
disabled=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"STRIP {i}||SLIDER GAIN",
|
key=f'STRIP {i}||SLIDER GAIN',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_param_sliders(layout):
|
def add_param_sliders(layout):
|
||||||
if self.kind.name in ("basic", "banana"):
|
if self.kind.name in ('basic', 'banana'):
|
||||||
for param in util.get_slider_params(i, self.kind):
|
for param in util.get_slider_params(i, self.kind):
|
||||||
layout.append([LabelSlider(self.window, i, param, range_=(-12, 12))])
|
layout.append([LabelSlider(self.window, i, param, range_=(-12, 12))])
|
||||||
else:
|
else:
|
||||||
@ -404,7 +404,7 @@ class Builder:
|
|||||||
def add_limit_slider(layout):
|
def add_limit_slider(layout):
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Text("Limit"),
|
psg.Text('Limit'),
|
||||||
psg.Slider(
|
psg.Slider(
|
||||||
range=(-40, 12),
|
range=(-40, 12),
|
||||||
default_value=self.vm.strip[i].limit,
|
default_value=self.vm.strip[i].limit,
|
||||||
@ -412,21 +412,21 @@ class Builder:
|
|||||||
disable_number_display=True,
|
disable_number_display=True,
|
||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"STRIP {i}||SLIDER LIMIT",
|
key=f'STRIP {i}||SLIDER LIMIT',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
layout = []
|
layout = []
|
||||||
steps = (add_gain_slider, add_param_sliders)
|
steps = (add_gain_slider, add_param_sliders)
|
||||||
if self.kind.name in ("banana", "potato"):
|
if self.kind.name in ('banana', 'potato'):
|
||||||
steps += (add_limit_slider,)
|
steps += (add_limit_slider,)
|
||||||
[step(layout) for step in steps]
|
[step(layout) for step in steps]
|
||||||
return psg.Frame(
|
return psg.Frame(
|
||||||
self.window.cache["labels"][f"STRIP {i}||LABEL"],
|
self.window.cache['labels'][f'STRIP {i}||LABEL'],
|
||||||
layout,
|
layout,
|
||||||
key=f"STRIP {i}||LABEL||SLIDER",
|
key=f'STRIP {i}||LABEL||SLIDER',
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_tab2_slider_rows(self) -> psg.Frame:
|
def make_tab2_slider_rows(self) -> psg.Frame:
|
||||||
@ -438,36 +438,43 @@ 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_strip_outputs(layout):
|
def add_bus_buttons(layout):
|
||||||
params = ["MONO", "EQ", "MUTE"]
|
busmono = util.get_bus_mono()
|
||||||
if self.kind.name == "basic":
|
params = ['EQ', 'MUTE']
|
||||||
params.remove("EQ")
|
if self.kind.name == 'basic':
|
||||||
|
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(),
|
||||||
size=(6, 2),
|
size=(6, 2),
|
||||||
key=f"BUS {i}||{param}",
|
key=f'BUS {i}||{param}',
|
||||||
)
|
)
|
||||||
for param in params
|
for param in params
|
||||||
],
|
],
|
||||||
psg.ButtonMenu(
|
psg.ButtonMenu(
|
||||||
"BUSMODE",
|
'Bus Mode',
|
||||||
size=(12, 2),
|
size=(12, 2),
|
||||||
menu_def=["", busmodes],
|
menu_def=['', busmodes],
|
||||||
key=f"BUS {i}||MODE",
|
key=f'BUS {i}||MODE',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
[step(outputs) for step in (add_strip_outputs,)]
|
[step(outputs) for step in (add_bus_buttons,)]
|
||||||
return psg.Frame(
|
return psg.Frame(
|
||||||
self.window.cache["labels"][f"BUS {i}||LABEL"],
|
self.window.cache['labels'][f'BUS {i}||LABEL'],
|
||||||
outputs,
|
outputs,
|
||||||
key=f"BUS {i}||LABEL",
|
key=f'BUS {i}||LABEL',
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_tab3_button_rows(self) -> psg.Frame:
|
def make_tab3_button_rows(self) -> psg.Frame:
|
||||||
@ -478,7 +485,7 @@ class Builder:
|
|||||||
def add_gain_slider(layout):
|
def add_gain_slider(layout):
|
||||||
layout.append(
|
layout.append(
|
||||||
[
|
[
|
||||||
psg.Text("Gain"),
|
psg.Text('Gain'),
|
||||||
psg.Slider(
|
psg.Slider(
|
||||||
range=(-60, 12),
|
range=(-60, 12),
|
||||||
default_value=self.vm.bus[i].gain,
|
default_value=self.vm.bus[i].gain,
|
||||||
@ -487,15 +494,15 @@ class Builder:
|
|||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
disabled=True,
|
disabled=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"BUS {i}||SLIDER GAIN",
|
key=f'BUS {i}||SLIDER GAIN',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
outputs = []
|
outputs = []
|
||||||
[step(outputs) for step in (add_gain_slider,)]
|
[step(outputs) for step in (add_gain_slider,)]
|
||||||
return psg.Frame(self.window.cache["labels"][f"BUS {i}||LABEL"], outputs, key=f"BUS {i}||LABEL||SLIDER")
|
return psg.Frame(self.window.cache['labels'][f'BUS {i}||LABEL'], outputs, key=f'BUS {i}||LABEL||SLIDER')
|
||||||
|
|
||||||
def make_tab3_slider_rows(self) -> psg.Frame:
|
def make_tab3_slider_rows(self) -> psg.Frame:
|
||||||
layout = [[self.make_tab3_slider_row(i)] for i in range(self.kind.num_bus)]
|
layout = [[self.make_tab3_slider_row(i)] for i in range(self.kind.num_bus)]
|
||||||
|
|||||||
@ -5,42 +5,42 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .errors import NVDAVMError
|
from .errors import NVDAVMError
|
||||||
|
|
||||||
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
|
BITS = 64 if ct.sizeof(ct.c_void_p) == 8 else 32
|
||||||
|
|
||||||
if platform.system() != "Windows":
|
if platform.system() != 'Windows':
|
||||||
raise NVDAVMError("Only Windows OS supported")
|
raise NVDAVMError('Only Windows OS supported')
|
||||||
|
|
||||||
REG_KEY = "\\".join(
|
REG_KEY = '\\'.join(
|
||||||
filter(
|
filter(
|
||||||
None,
|
None,
|
||||||
(
|
(
|
||||||
"SOFTWARE",
|
'SOFTWARE',
|
||||||
"WOW6432Node" if bits == 64 else "",
|
'WOW6432Node' if BITS == 64 else '',
|
||||||
"Microsoft",
|
'Microsoft',
|
||||||
"Windows",
|
'Windows',
|
||||||
"CurrentVersion",
|
'CurrentVersion',
|
||||||
"Uninstall",
|
'Uninstall',
|
||||||
"NVDA",
|
'NVDA',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_nvdapath():
|
def get_nvdapath():
|
||||||
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"{}".format(REG_KEY)) as nvda_key:
|
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'{}'.format(REG_KEY)) as nvda_key:
|
||||||
return winreg.QueryValueEx(nvda_key, r"UninstallDirectory")[0]
|
return winreg.QueryValueEx(nvda_key, r'UninstallDirectory')[0]
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
NVDA_PATH = Path(get_nvdapath()) / "nvda.exe"
|
NVDA_PATH = Path(get_nvdapath()) / 'nvda.exe'
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
NVDA_PATH = ""
|
NVDA_PATH = ''
|
||||||
|
|
||||||
|
|
||||||
controller_path = Path(__file__).parents[2].resolve() / "controllerClient"
|
controller_path = Path(__file__).parents[2].resolve() / 'controllerClient'
|
||||||
if not controller_path.exists():
|
if not controller_path.exists():
|
||||||
controller_path = Path("controllerClient")
|
controller_path = Path('_internal') / 'controllerClient'
|
||||||
|
|
||||||
DLL_PATH = controller_path / f"x{64 if bits == 64 else 86}" / f"nvdaControllerClient{bits}.dll"
|
DLL_PATH = controller_path / f'x{64 if BITS == 64 else 86}' / 'nvdaControllerClient.dll'
|
||||||
|
|
||||||
libc = ct.CDLL(str(DLL_PATH))
|
libc = ct.CDLL(str(DLL_PATH))
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import PySimpleGUI as psg
|
import FreeSimpleGUI as psg
|
||||||
|
|
||||||
from . import util
|
from . import util
|
||||||
|
|
||||||
@ -10,10 +10,13 @@ class LabelSlider(psg.Frame):
|
|||||||
|
|
||||||
def __init__(self, parent, i, param, range_=(0, 10), *args, **kwargs):
|
def __init__(self, parent, i, param, range_=(0, 10), *args, **kwargs):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
if param in ("AUDIBILITY", "DENOISER"):
|
if param in ('AUDIBILITY', 'DENOISER'):
|
||||||
size = 7
|
size = 7
|
||||||
else:
|
else:
|
||||||
size = 4
|
if psg.theme() == 'HighContrast':
|
||||||
|
size = 5
|
||||||
|
else:
|
||||||
|
size = 4
|
||||||
layout = [
|
layout = [
|
||||||
[
|
[
|
||||||
psg.Text(param.capitalize(), size=size),
|
psg.Text(param.capitalize(), size=size),
|
||||||
@ -25,8 +28,8 @@ class LabelSlider(psg.Frame):
|
|||||||
size=(12, 16),
|
size=(12, 16),
|
||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"STRIP {i}||SLIDER {param}",
|
key=f'STRIP {i}||SLIDER {param}',
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -34,7 +37,7 @@ class LabelSlider(psg.Frame):
|
|||||||
|
|
||||||
def default_value(self, i, param):
|
def default_value(self, i, param):
|
||||||
target = getattr(self.parent.vm.strip[i], param.lower())
|
target = getattr(self.parent.vm.strip[i], param.lower())
|
||||||
if param in ("COMP", "GATE", "DENOISER"):
|
if param in ('COMP', 'GATE', 'DENOISER'):
|
||||||
return target.knob
|
return target.knob
|
||||||
return target
|
return target
|
||||||
|
|
||||||
@ -49,70 +52,70 @@ class CompSlider(psg.Slider):
|
|||||||
disable_number_display=True,
|
disable_number_display=True,
|
||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"COMPRESSOR||SLIDER {param}",
|
key=f'COMPRESSOR||SLIDER {param}',
|
||||||
**self.default_params(param),
|
**self.default_params(param),
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_params(self, param):
|
def default_params(self, param):
|
||||||
match param:
|
match param:
|
||||||
case "INPUT GAIN":
|
case 'INPUT GAIN':
|
||||||
return {
|
return {
|
||||||
"range": (-24, 24),
|
'range': (-24, 24),
|
||||||
"default_value": self.vm.strip[self.index].comp.gainin,
|
'default_value': self.vm.strip[self.index].comp.gainin,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
"disabled": True,
|
'disabled': True,
|
||||||
}
|
}
|
||||||
case "RATIO":
|
case 'RATIO':
|
||||||
return {
|
return {
|
||||||
"range": (1, 8),
|
'range': (1, 8),
|
||||||
"default_value": self.vm.strip[self.index].comp.ratio,
|
'default_value': self.vm.strip[self.index].comp.ratio,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "THRESHOLD":
|
case 'THRESHOLD':
|
||||||
return {
|
return {
|
||||||
"range": (-40, -3),
|
'range': (-40, -3),
|
||||||
"default_value": self.vm.strip[self.index].comp.threshold,
|
'default_value': self.vm.strip[self.index].comp.threshold,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "ATTACK":
|
case 'ATTACK':
|
||||||
return {
|
return {
|
||||||
"range": (0, 200),
|
'range': (0, 200),
|
||||||
"default_value": self.vm.strip[self.index].comp.attack,
|
'default_value': self.vm.strip[self.index].comp.attack,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "RELEASE":
|
case 'RELEASE':
|
||||||
return {
|
return {
|
||||||
"range": (0, 5000),
|
'range': (0, 5000),
|
||||||
"default_value": self.vm.strip[self.index].comp.release,
|
'default_value': self.vm.strip[self.index].comp.release,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "KNEE":
|
case 'KNEE':
|
||||||
return {
|
return {
|
||||||
"range": (0, 1),
|
'range': (0, 1),
|
||||||
"default_value": self.vm.strip[self.index].comp.knee,
|
'default_value': self.vm.strip[self.index].comp.knee,
|
||||||
"resolution": 0.01,
|
'resolution': 0.01,
|
||||||
}
|
}
|
||||||
case "OUTPUT GAIN":
|
case 'OUTPUT GAIN':
|
||||||
return {
|
return {
|
||||||
"range": (-24, 24),
|
'range': (-24, 24),
|
||||||
"default_value": self.vm.strip[self.index].comp.gainout,
|
'default_value': self.vm.strip[self.index].comp.gainout,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
"disabled": True,
|
'disabled': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_bounds(param, val):
|
def check_bounds(param, val):
|
||||||
match param:
|
match param:
|
||||||
case "RATIO":
|
case 'RATIO':
|
||||||
val = util.check_bounds(val, (1, 8))
|
val = util.check_bounds(val, (1, 8))
|
||||||
case "THRESHOLD":
|
case 'THRESHOLD':
|
||||||
val = util.check_bounds(val, (-40, -3))
|
val = util.check_bounds(val, (-40, -3))
|
||||||
case "ATTACK":
|
case 'ATTACK':
|
||||||
val = util.check_bounds(val, (0, 200))
|
val = util.check_bounds(val, (0, 200))
|
||||||
case "RELEASE":
|
case 'RELEASE':
|
||||||
val = util.check_bounds(val, (0, 5000))
|
val = util.check_bounds(val, (0, 5000))
|
||||||
case "KNEE":
|
case 'KNEE':
|
||||||
val = util.check_bounds(val, (0, 1))
|
val = util.check_bounds(val, (0, 1))
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@ -125,64 +128,64 @@ class GateSlider(psg.Slider):
|
|||||||
disable_number_display=True,
|
disable_number_display=True,
|
||||||
expand_x=True,
|
expand_x=True,
|
||||||
enable_events=True,
|
enable_events=True,
|
||||||
orientation="horizontal",
|
orientation='horizontal',
|
||||||
key=f"GATE||SLIDER {param}",
|
key=f'GATE||SLIDER {param}',
|
||||||
**self.default_params(param),
|
**self.default_params(param),
|
||||||
)
|
)
|
||||||
|
|
||||||
def default_params(self, param):
|
def default_params(self, param):
|
||||||
match param:
|
match param:
|
||||||
case "THRESHOLD":
|
case 'THRESHOLD':
|
||||||
return {
|
return {
|
||||||
"range": (-60, -10),
|
'range': (-60, -10),
|
||||||
"default_value": self.vm.strip[self.index].gate.threshold,
|
'default_value': self.vm.strip[self.index].gate.threshold,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "DAMPING":
|
case 'DAMPING':
|
||||||
return {
|
return {
|
||||||
"range": (-60, -10),
|
'range': (-60, -10),
|
||||||
"default_value": self.vm.strip[self.index].gate.damping,
|
'default_value': self.vm.strip[self.index].gate.damping,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "BPSIDECHAIN":
|
case 'BPSIDECHAIN':
|
||||||
return {
|
return {
|
||||||
"range": (100, 4000),
|
'range': (100, 4000),
|
||||||
"default_value": self.vm.strip[self.index].gate.bpsidechain,
|
'default_value': self.vm.strip[self.index].gate.bpsidechain,
|
||||||
"resolution": 1,
|
'resolution': 1,
|
||||||
}
|
}
|
||||||
case "ATTACK":
|
case 'ATTACK':
|
||||||
return {
|
return {
|
||||||
"range": (0, 1000),
|
'range': (0, 1000),
|
||||||
"default_value": self.vm.strip[self.index].gate.attack,
|
'default_value': self.vm.strip[self.index].gate.attack,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "HOLD":
|
case 'HOLD':
|
||||||
return {
|
return {
|
||||||
"range": (0, 5000),
|
'range': (0, 5000),
|
||||||
"default_value": self.vm.strip[self.index].gate.hold,
|
'default_value': self.vm.strip[self.index].gate.hold,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
case "RELEASE":
|
case 'RELEASE':
|
||||||
return {
|
return {
|
||||||
"range": (0, 5000),
|
'range': (0, 5000),
|
||||||
"default_value": self.vm.strip[self.index].gate.release,
|
'default_value': self.vm.strip[self.index].gate.release,
|
||||||
"resolution": 0.1,
|
'resolution': 0.1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_bounds(param, val):
|
def check_bounds(param, val):
|
||||||
match param:
|
match param:
|
||||||
case "THRESHOLD":
|
case 'THRESHOLD':
|
||||||
val = util.check_bounds(val, (-60, -10))
|
val = util.check_bounds(val, (-60, -10))
|
||||||
case "DAMPING MAX":
|
case 'DAMPING MAX':
|
||||||
val = util.check_bounds(val, (-60, -10))
|
val = util.check_bounds(val, (-60, -10))
|
||||||
case "BPSIDECHAIN":
|
case 'BPSIDECHAIN':
|
||||||
val = util.check_bounds(val, (100, 4000))
|
val = util.check_bounds(val, (100, 4000))
|
||||||
case "ATTACK":
|
case 'ATTACK':
|
||||||
val = util.check_bounds(val, (0, 1000))
|
val = util.check_bounds(val, (0, 1000))
|
||||||
case "HOLD":
|
case 'HOLD':
|
||||||
val = util.check_bounds(val, (0, 5000))
|
val = util.check_bounds(val, (0, 5000))
|
||||||
case "RELEASE":
|
case 'RELEASE':
|
||||||
val = util.check_bounds(val, (0, 5000))
|
val = util.check_bounds(val, (0, 5000))
|
||||||
return val
|
return val
|
||||||
|
|
||||||
@ -192,8 +195,8 @@ class LabelSliderAdvanced(psg.Frame):
|
|||||||
|
|
||||||
def __init__(self, parent, index, param, slider_cls: Union[CompSlider, GateSlider], *args, **kwargs):
|
def __init__(self, parent, index, param, slider_cls: Union[CompSlider, GateSlider], *args, **kwargs):
|
||||||
label_map = {
|
label_map = {
|
||||||
"DAMPING": "Damping Max",
|
'DAMPING': 'Damping Max',
|
||||||
"BPSIDECHAIN": "BP Sidechain",
|
'BPSIDECHAIN': 'BP Sidechain',
|
||||||
}
|
}
|
||||||
|
|
||||||
layout = [
|
layout = [
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
SETTINGS = Path.cwd() / "settings.json"
|
SETTINGS = Path.cwd() / 'settings.json'
|
||||||
|
|
||||||
|
|
||||||
def config_from_json():
|
def config_from_json():
|
||||||
data = {}
|
data = {}
|
||||||
if not SETTINGS.exists():
|
if not SETTINGS.exists():
|
||||||
return data
|
return data
|
||||||
with open(SETTINGS, "r") as f:
|
with open(SETTINGS, 'r') as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -24,11 +24,11 @@ def get(key, default=None):
|
|||||||
|
|
||||||
def set(key, value):
|
def set(key, value):
|
||||||
config[key] = value
|
config[key] = value
|
||||||
with open(SETTINGS, "w") as f:
|
with open(SETTINGS, 'w') as f:
|
||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
|
|
||||||
|
|
||||||
def delete(key):
|
def delete(key):
|
||||||
del config[key]
|
del config[key]
|
||||||
with open(SETTINGS, "w") as f:
|
with open(SETTINGS, 'w') as f:
|
||||||
json.dump(config, f)
|
json.dump(config, f)
|
||||||
|
|||||||
@ -8,4 +8,4 @@ class NVDAVMCAPIError(NVDAVMError):
|
|||||||
def __init__(self, fn_name, code):
|
def __init__(self, fn_name, code):
|
||||||
self.fn_name = fn_name
|
self.fn_name = fn_name
|
||||||
self.code = code
|
self.code = code
|
||||||
super().__init__(f"{self.fn_name} returned {self.code}")
|
super().__init__(f'{self.fn_name} returned {self.code}')
|
||||||
|
|||||||
1
src/nvda_voicemeeter/gui/banana/__init__.py
Normal file
1
src/nvda_voicemeeter/gui/banana/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import run
|
||||||
11
src/nvda_voicemeeter/gui/banana/main.py
Normal file
11
src/nvda_voicemeeter/gui/banana/main.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import voicemeeterlib
|
||||||
|
|
||||||
|
import nvda_voicemeeter
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
KIND_ID = 'banana'
|
||||||
|
|
||||||
|
with voicemeeterlib.api(KIND_ID) as vm:
|
||||||
|
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||||
|
window.run()
|
||||||
1
src/nvda_voicemeeter/gui/basic/__init__.py
Normal file
1
src/nvda_voicemeeter/gui/basic/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import run
|
||||||
11
src/nvda_voicemeeter/gui/basic/main.py
Normal file
11
src/nvda_voicemeeter/gui/basic/main.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import voicemeeterlib
|
||||||
|
|
||||||
|
import nvda_voicemeeter
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
KIND_ID = 'basic'
|
||||||
|
|
||||||
|
with voicemeeterlib.api(KIND_ID) as vm:
|
||||||
|
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||||
|
window.run()
|
||||||
1
src/nvda_voicemeeter/gui/potato/__init__.py
Normal file
1
src/nvda_voicemeeter/gui/potato/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .main import run
|
||||||
11
src/nvda_voicemeeter/gui/potato/main.py
Normal file
11
src/nvda_voicemeeter/gui/potato/main.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import voicemeeterlib
|
||||||
|
|
||||||
|
import nvda_voicemeeter
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
KIND_ID = 'potato'
|
||||||
|
|
||||||
|
with voicemeeterlib.api(KIND_ID) as vm:
|
||||||
|
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||||
|
window.run()
|
||||||
@ -1,58 +1,58 @@
|
|||||||
def _make_hardware_ins_cache(vm) -> dict:
|
def _make_hardware_ins_cache(vm) -> dict:
|
||||||
return {**{f"HARDWARE IN||{i + 1}": vm.strip[i].device.name for i in range(vm.kind.phys_in)}}
|
return {**{f'HARDWARE IN||{i + 1}': vm.strip[i].device.name for i in range(vm.kind.phys_in)}}
|
||||||
|
|
||||||
|
|
||||||
def _make_hardware_outs_cache(vm) -> dict:
|
def _make_hardware_outs_cache(vm) -> dict:
|
||||||
hw_outs = {**{f"HARDWARE OUT||A{i + 1}": vm.bus[i].device.name for i in range(vm.kind.phys_out)}}
|
hw_outs = {**{f'HARDWARE OUT||A{i + 1}': vm.bus[i].device.name for i in range(vm.kind.phys_out)}}
|
||||||
if vm.kind.name == "basic":
|
if vm.kind.name == 'basic':
|
||||||
hw_outs |= {"HARDWARE OUT||A2": vm.bus[1].device.name}
|
hw_outs |= {'HARDWARE OUT||A2': vm.bus[1].device.name}
|
||||||
return hw_outs
|
return hw_outs
|
||||||
|
|
||||||
|
|
||||||
def _make_param_cache(vm, channel_type) -> dict:
|
def _make_param_cache(vm, channel_type) -> dict:
|
||||||
params = {}
|
params = {}
|
||||||
if channel_type == "strip":
|
if channel_type == 'strip':
|
||||||
match vm.kind.name:
|
match vm.kind.name:
|
||||||
case "basic":
|
case 'basic':
|
||||||
params |= {
|
params |= {
|
||||||
**{f"STRIP {i}||A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A1': vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B1': vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
}
|
}
|
||||||
case "banana":
|
case 'banana':
|
||||||
params |= {
|
params |= {
|
||||||
**{f"STRIP {i}||A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A1': vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A2': vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A3': vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B1': vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B2': vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
||||||
}
|
}
|
||||||
case "potato":
|
case 'potato':
|
||||||
params |= {
|
params |= {
|
||||||
**{f"STRIP {i}||A1": vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A1': vm.strip[i].A1 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A2": vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A2': vm.strip[i].A2 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A3": vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A3': vm.strip[i].A3 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A4": vm.strip[i].A4 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A4': vm.strip[i].A4 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||A5": vm.strip[i].A5 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||A5': vm.strip[i].A5 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B1": vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B1': vm.strip[i].B1 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B2": vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B2': vm.strip[i].B2 for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||B3": vm.strip[i].B3 for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||B3': vm.strip[i].B3 for i in range(vm.kind.num_strip)},
|
||||||
}
|
}
|
||||||
params |= {
|
params |= {
|
||||||
**{f"STRIP {i}||MONO": vm.strip[i].mono for i in range(vm.kind.phys_in)},
|
**{f'STRIP {i}||MONO': vm.strip[i].mono for i in range(vm.kind.phys_in)},
|
||||||
**{f"STRIP {i}||SOLO": vm.strip[i].solo for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||SOLO': vm.strip[i].solo for i in range(vm.kind.num_strip)},
|
||||||
**{f"STRIP {i}||MUTE": vm.strip[i].mute for i in range(vm.kind.num_strip)},
|
**{f'STRIP {i}||MUTE': vm.strip[i].mute for i in range(vm.kind.num_strip)},
|
||||||
}
|
}
|
||||||
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in):
|
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in):
|
||||||
if i == vm.kind.phys_in + 1:
|
if i == vm.kind.phys_in + 1:
|
||||||
params[f"STRIP {i}||KARAOKE"] = vm.strip[i].k
|
params[f'STRIP {i}||KARAOKE'] = vm.strip[i].k
|
||||||
else:
|
else:
|
||||||
params[f"STRIP {i}||MC"] = vm.strip[i].mc
|
params[f'STRIP {i}||MC'] = vm.strip[i].mc
|
||||||
else:
|
else:
|
||||||
params |= {
|
params |= {
|
||||||
**{f"BUS {i}||MONO": vm.bus[i].mono for i in range(vm.kind.num_bus)},
|
**{f'BUS {i}||MONO': vm.bus[i].mono for i in range(vm.kind.num_bus)},
|
||||||
**{f"BUS {i}||EQ": vm.bus[i].eq.on for i in range(vm.kind.num_bus)},
|
**{f'BUS {i}||EQ': vm.bus[i].eq.on for i in range(vm.kind.num_bus)},
|
||||||
**{f"BUS {i}||MUTE": vm.bus[i].mute for i in range(vm.kind.num_bus)},
|
**{f'BUS {i}||MUTE': vm.bus[i].mute for i in range(vm.kind.num_bus)},
|
||||||
**{f"BUS {i}||MODE": vm.bus[i].mode.get() for i in range(vm.kind.num_bus)},
|
**{f'BUS {i}||MODE': vm.bus[i].mode.get() for i in range(vm.kind.num_bus)},
|
||||||
}
|
}
|
||||||
return params
|
return params
|
||||||
|
|
||||||
@ -60,19 +60,19 @@ def _make_param_cache(vm, channel_type) -> dict:
|
|||||||
def _make_label_cache(vm) -> dict:
|
def _make_label_cache(vm) -> dict:
|
||||||
return {
|
return {
|
||||||
**{
|
**{
|
||||||
f"STRIP {i}||LABEL": vm.strip[i].label if vm.strip[i].label else f"Hardware Input {i + 1}"
|
f'STRIP {i}||LABEL': vm.strip[i].label if vm.strip[i].label else f'Hardware Input {i + 1}'
|
||||||
for i in range(vm.kind.phys_in)
|
for i in range(vm.kind.phys_in)
|
||||||
},
|
},
|
||||||
**{
|
**{
|
||||||
f"STRIP {i}||LABEL": vm.strip[i].label if vm.strip[i].label else f"Virtual Input {i - vm.kind.phys_in + 1}"
|
f'STRIP {i}||LABEL': vm.strip[i].label if vm.strip[i].label else f'Virtual Input {i - vm.kind.phys_in + 1}'
|
||||||
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in)
|
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in)
|
||||||
},
|
},
|
||||||
**{
|
**{
|
||||||
f"BUS {i}||LABEL": vm.bus[i].label if vm.bus[i].label else f"Physical Bus {i + 1}"
|
f'BUS {i}||LABEL': vm.bus[i].label if vm.bus[i].label else f'Physical Bus {i + 1}'
|
||||||
for i in range(vm.kind.phys_out)
|
for i in range(vm.kind.phys_out)
|
||||||
},
|
},
|
||||||
**{
|
**{
|
||||||
f"BUS {i}||LABEL": vm.bus[i].label if vm.bus[i].label else f"Virtual Bus {i - vm.kind.phys_out + 1}"
|
f'BUS {i}||LABEL': vm.bus[i].label if vm.bus[i].label else f'Virtual Bus {i - vm.kind.phys_out + 1}'
|
||||||
for i in range(vm.kind.phys_out, vm.kind.phys_out + vm.kind.virt_out)
|
for i in range(vm.kind.phys_out, vm.kind.phys_out + vm.kind.virt_out)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -80,16 +80,16 @@ def _make_label_cache(vm) -> dict:
|
|||||||
|
|
||||||
def _make_patch_asio_cache(vm) -> dict:
|
def _make_patch_asio_cache(vm) -> dict:
|
||||||
params = {}
|
params = {}
|
||||||
if vm.kind.name != "basic":
|
if vm.kind.name != 'basic':
|
||||||
params |= {**{f"ASIO INPUT SPINBOX||{i}": vm.patch.asio[i].get() for i in range(vm.kind.phys_out * 2)}}
|
params |= {**{f'ASIO INPUT SPINBOX||{i}': vm.patch.asio[i].get() for i in range(vm.kind.phys_out * 2)}}
|
||||||
for i in range(vm.kind.phys_out - 1):
|
for i in range(vm.kind.phys_out - 1):
|
||||||
target = getattr(vm.patch, f"A{i + 2}")
|
target = getattr(vm.patch, f'A{i + 2}')
|
||||||
params |= {**{f"ASIO OUTPUT A{i + 2} SPINBOX||{j}": target[j].get() for j in range(vm.kind.num_bus)}}
|
params |= {**{f'ASIO OUTPUT A{i + 2} SPINBOX||{j}': target[j].get() for j in range(vm.kind.num_bus)}}
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def _make_patch_insert_cache(vm) -> dict:
|
def _make_patch_insert_cache(vm) -> dict:
|
||||||
params = {}
|
params = {}
|
||||||
if vm.kind.name != "basic":
|
if vm.kind.name != 'basic':
|
||||||
params |= {**{f"INSERT CHECKBOX||{i}": vm.patch.insert[i].on for i in range(vm.kind.num_strip_levels)}}
|
params |= {**{f'INSERT CHECKBOX||{i}': vm.patch.insert[i].on for i in range(vm.kind.num_strip_levels)}}
|
||||||
return params
|
return params
|
||||||
|
|||||||
@ -4,10 +4,10 @@ from pyparsing import Group, OneOrMore, Optional, Suppress, Word, alphanums, res
|
|||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.widget = Group(OneOrMore(Word(alphanums)))
|
self.widget = Group(OneOrMore(Word(alphanums)))
|
||||||
self.widget_token = Suppress("||")
|
self.widget_token = Suppress('||')
|
||||||
self.identifier = Group(OneOrMore(Word(alphanums)))
|
self.identifier = Group(OneOrMore(Word(alphanums)))
|
||||||
self.event = Group(OneOrMore(Word(alphanums)))
|
self.event = Group(OneOrMore(Word(alphanums)))
|
||||||
self.menu_token = Suppress("::")
|
self.menu_token = Suppress('::')
|
||||||
self.match = (
|
self.match = (
|
||||||
self.widget + self.widget_token + self.identifier + Optional(self.widget_token) + Optional(self.event)
|
self.widget + self.widget_token + self.identifier + Optional(self.widget_token) + Optional(self.event)
|
||||||
| self.identifier + self.menu_token + self.event
|
| self.identifier + self.menu_token + self.event
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
import PySimpleGUI as psg
|
import FreeSimpleGUI as psg
|
||||||
|
|
||||||
|
|
||||||
def get_asio_input_spinbox_index(channel, num) -> int:
|
def get_asio_input_spinbox_index(channel, num) -> int:
|
||||||
@ -18,9 +18,9 @@ def get_insert_checkbox_index(kind, channel, num) -> int:
|
|||||||
|
|
||||||
|
|
||||||
_rejected_ids = (
|
_rejected_ids = (
|
||||||
"VBAudio100VMVAIO3",
|
'VBAudio100VMVAIO3',
|
||||||
"{F5735BD4-6EAF-4758-9710-9886E5AD0FF3}",
|
'{F5735BD4-6EAF-4758-9710-9886E5AD0FF3}',
|
||||||
"{0239BE07-CEEF-4236-A900-AA778D432FD4}",
|
'{0239BE07-CEEF-4236-A900-AA778D432FD4}',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -28,8 +28,8 @@ def get_input_device_list(vm) -> list:
|
|||||||
devices = []
|
devices = []
|
||||||
for j in range(vm.device.ins):
|
for j in range(vm.device.ins):
|
||||||
device = vm.device.input(j)
|
device = vm.device.input(j)
|
||||||
if device["id"] not in _rejected_ids:
|
if device['id'] not in _rejected_ids:
|
||||||
devices.append("{type}: {name}".format(**device))
|
devices.append('{type}: {name}'.format(**device))
|
||||||
return devices
|
return devices
|
||||||
|
|
||||||
|
|
||||||
@ -37,37 +37,37 @@ def get_output_device_list(i, vm) -> list:
|
|||||||
devices = []
|
devices = []
|
||||||
for j in range(vm.device.outs):
|
for j in range(vm.device.outs):
|
||||||
device = vm.device.output(j)
|
device = vm.device.output(j)
|
||||||
if device["id"] not in _rejected_ids:
|
if device['id'] not in _rejected_ids:
|
||||||
devices.append("{type}: {name}".format(**device))
|
devices.append('{type}: {name}'.format(**device))
|
||||||
if i == 0:
|
if i == 0:
|
||||||
return devices
|
return devices
|
||||||
devices.append("- remove device selection -")
|
devices.append('- remove device selection -')
|
||||||
return [device for device in devices if not device.startswith("asio")]
|
return [device for device in devices if not device.startswith('asio')]
|
||||||
|
|
||||||
|
|
||||||
def get_patch_composite_list(kind) -> list:
|
def get_patch_composite_list(kind) -> list:
|
||||||
temp = []
|
temp = []
|
||||||
for i in range(kind.phys_out):
|
for i in range(kind.phys_out):
|
||||||
[temp.append(f"IN#{i + 1} {channel}") for channel in ("Left", "Right")]
|
[temp.append(f'IN#{i + 1} {channel}') for channel in ('Left', 'Right')]
|
||||||
for i in range(kind.phys_out, kind.phys_out + kind.virt_out):
|
for i in range(kind.phys_out, kind.phys_out + kind.virt_out):
|
||||||
[
|
[
|
||||||
temp.append(f"IN#{i + 1} {channel}")
|
temp.append(f'IN#{i + 1} {channel}')
|
||||||
for channel in ("Left", "Right", "Center", "LFE", "SL", "SR", "BL", "BR")
|
for channel in ('Left', 'Right', 'Center', 'LFE', 'SL', 'SR', 'BL', 'BR')
|
||||||
]
|
]
|
||||||
temp.append("BUS Channel")
|
temp.append('BUS Channel')
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
|
|
||||||
def get_patch_insert_channels() -> list:
|
def get_patch_insert_channels() -> list:
|
||||||
return [
|
return [
|
||||||
"left",
|
'left',
|
||||||
"right",
|
'right',
|
||||||
"center",
|
'center',
|
||||||
"low frequency effect",
|
'low frequency effect',
|
||||||
"surround left",
|
'surround left',
|
||||||
"surround right",
|
'surround right',
|
||||||
"back left",
|
'back left',
|
||||||
"back right",
|
'back right',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -75,8 +75,8 @@ _patch_insert_channels = get_patch_insert_channels()
|
|||||||
|
|
||||||
|
|
||||||
def get_asio_samples_list(driver) -> list:
|
def get_asio_samples_list(driver) -> list:
|
||||||
if driver == "MME":
|
if driver == 'MME':
|
||||||
samples = ["2048", "1536", "1024", "896", "768", "704", "640", "576", "512", "480", "441"]
|
samples = ['2048', '1536', '1024', '896', '768', '704', '640', '576', '512', '480', '441']
|
||||||
else:
|
else:
|
||||||
# fmt: off
|
# fmt: off
|
||||||
samples = [
|
samples = [
|
||||||
@ -84,14 +84,14 @@ def get_asio_samples_list(driver) -> list:
|
|||||||
"352", "320", "288", "256", "224", "192", "160", "128"
|
"352", "320", "288", "256", "224", "192", "160", "128"
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
if driver == "ASIO":
|
if driver == 'ASIO':
|
||||||
samples = [x for x in samples if x not in ("2048", "1536")]
|
samples = [x for x in samples if x not in ('2048', '1536')]
|
||||||
samples.append("Default")
|
samples.append('Default')
|
||||||
return samples
|
return samples
|
||||||
|
|
||||||
|
|
||||||
def get_tabs_labels() -> list:
|
def get_tabs_labels() -> list:
|
||||||
return ["Settings", "Physical Strip", "Virtual Strip", "Buses"]
|
return ['Settings', 'Physical Strip', 'Virtual Strip', 'Buses']
|
||||||
|
|
||||||
|
|
||||||
def open_context_menu_for_buttonmenu(window, identifier) -> None:
|
def open_context_menu_for_buttonmenu(window, identifier) -> None:
|
||||||
@ -106,55 +106,59 @@ def get_channel_identifier_list(vm) -> list:
|
|||||||
identifiers = []
|
identifiers = []
|
||||||
for i in range(vm.kind.phys_in):
|
for i in range(vm.kind.phys_in):
|
||||||
for j in range(2):
|
for j in range(2):
|
||||||
identifiers.append(f"IN{i + 1} {j}")
|
identifiers.append(f'IN{i + 1} {j}')
|
||||||
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in):
|
for i in range(vm.kind.phys_in, vm.kind.phys_in + vm.kind.virt_in):
|
||||||
for j in range(8):
|
for j in range(8):
|
||||||
identifiers.append(f"IN{i + 1} {j}")
|
identifiers.append(f'IN{i + 1} {j}')
|
||||||
return identifiers
|
return identifiers
|
||||||
|
|
||||||
|
|
||||||
_bus_mode_map = {
|
_bus_mode_map = {
|
||||||
"normal": "Normal",
|
'normal': 'Normal',
|
||||||
"amix": "Mix Down A",
|
'amix': 'Mix Down A',
|
||||||
"bmix": "Mix Down B",
|
'bmix': 'Mix Down B',
|
||||||
"repeat": "Stereo Repeat",
|
'repeat': 'Stereo Repeat',
|
||||||
"composite": "Composite",
|
'composite': 'Composite',
|
||||||
"tvmix": "Up Mix TV",
|
'tvmix': 'Up Mix TV',
|
||||||
"upmix21": "Up Mix 2.1",
|
'upmix21': 'Up Mix 2.1',
|
||||||
"upmix41": "Up Mix 4.1",
|
'upmix41': 'Up Mix 4.1',
|
||||||
"upmix61": "Up Mix 6.1",
|
'upmix61': 'Up Mix 6.1',
|
||||||
"centeronly": "Center Only",
|
'centeronly': 'Center Only',
|
||||||
"lfeonly": "Low Frequency Effect Only",
|
'lfeonly': 'Low Frequency Effect Only',
|
||||||
"rearonly": "Rear Only",
|
'rearonly': 'Rear Only',
|
||||||
}
|
}
|
||||||
|
|
||||||
_bus_mode_map_reversed = dict((reversed(item) for item in _bus_mode_map.items()))
|
_bus_mode_map_reversed = dict((reversed(item) for item in _bus_mode_map.items()))
|
||||||
|
|
||||||
|
|
||||||
def get_bus_modes(vm) -> list:
|
def get_bus_modes(vm) -> list:
|
||||||
if vm.kind.name == "basic":
|
if vm.kind.name == 'basic':
|
||||||
return [
|
return [
|
||||||
"normal",
|
'normal',
|
||||||
"amix",
|
'amix',
|
||||||
"repeat",
|
'repeat',
|
||||||
"composite",
|
'composite',
|
||||||
]
|
]
|
||||||
return [
|
return [
|
||||||
"normal",
|
'normal',
|
||||||
"amix",
|
'amix',
|
||||||
"bmix",
|
'bmix',
|
||||||
"repeat",
|
'repeat',
|
||||||
"composite",
|
'composite',
|
||||||
"tvmix",
|
'tvmix',
|
||||||
"upmix21",
|
'upmix21',
|
||||||
"upmix41",
|
'upmix41',
|
||||||
"upmix61",
|
'upmix61',
|
||||||
"centeronly",
|
'centeronly',
|
||||||
"lfeonly",
|
'lfeonly',
|
||||||
"rearonly",
|
'rearonly',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
||||||
@ -166,59 +170,77 @@ def check_bounds(val, bounds: tuple) -> int | float:
|
|||||||
|
|
||||||
def get_slider_params(i, kind) -> Iterable:
|
def get_slider_params(i, kind) -> Iterable:
|
||||||
if i < kind.phys_in:
|
if i < kind.phys_in:
|
||||||
if kind.name == "basic":
|
if kind.name == 'basic':
|
||||||
return ("AUDIBILITY",)
|
return ('AUDIBILITY',)
|
||||||
if kind.name == "banana":
|
if kind.name == 'banana':
|
||||||
return ("COMP", "GATE")
|
return ('COMP', 'GATE')
|
||||||
if kind.name == "potato":
|
if kind.name == 'potato':
|
||||||
return ("COMP", "GATE", "DENOISER")
|
return ('COMP', 'GATE', 'DENOISER')
|
||||||
return ("BASS", "MID", "TREBLE")
|
return ('BASS', 'MID', 'TREBLE')
|
||||||
|
|
||||||
|
|
||||||
def get_full_slider_params(i, kind) -> Iterable:
|
def get_full_slider_params(i, kind) -> Iterable:
|
||||||
params = list(get_slider_params(i, kind) + ("GAIN", "LIMIT"))
|
params = list(get_slider_params(i, kind) + ('GAIN', 'LIMIT'))
|
||||||
if kind.name == "basic":
|
if kind.name == 'basic':
|
||||||
params.remove("LIMIT")
|
params.remove('LIMIT')
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
def get_slider_modes() -> Iterable:
|
def get_slider_modes() -> Iterable:
|
||||||
return (
|
return (
|
||||||
"GAIN MODE",
|
'GAIN MODE',
|
||||||
"BASS MODE",
|
'BASS MODE',
|
||||||
"MID MODE",
|
'MID MODE',
|
||||||
"TREBLE MODE",
|
'TREBLE MODE',
|
||||||
"AUDIBILITY MODE",
|
'AUDIBILITY MODE',
|
||||||
"COMP MODE",
|
'COMP MODE',
|
||||||
"GATE MODE",
|
'GATE MODE',
|
||||||
"DENOISER MODE",
|
'DENOISER MODE',
|
||||||
"LIMIT MODE",
|
'LIMIT MODE',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_bus_assignments(kind) -> list:
|
def _get_bus_assignments(kind) -> list:
|
||||||
return [f"A{i}" for i in range(1, kind.phys_out + 1)] + [f"B{i}" for i in range(1, kind.virt_out + 1)]
|
return [f'A{i}' for i in range(1, kind.phys_out + 1)] + [f'B{i}' for i in range(1, kind.virt_out + 1)]
|
||||||
|
|
||||||
|
|
||||||
|
psg.theme_add_new(
|
||||||
|
'HighContrast',
|
||||||
|
{
|
||||||
|
'BACKGROUND': '#FFFFFF',
|
||||||
|
'TEXT': '#000000',
|
||||||
|
'INPUT': '#FAF9F6',
|
||||||
|
'TEXT_INPUT': '#000000',
|
||||||
|
'SCROLL': '#FAF9F6',
|
||||||
|
'BUTTON': ('#000000', '#FFFFFF'),
|
||||||
|
'PROGRESS': ('#000000', '#FFFFFF'),
|
||||||
|
'BORDER': 2,
|
||||||
|
'SLIDER_DEPTH': 3,
|
||||||
|
'PROGRESS_DEPTH': 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_themes_list() -> list:
|
def get_themes_list() -> list:
|
||||||
return [
|
return [
|
||||||
"Bright Colors",
|
'Bright Colors',
|
||||||
"Dark Blue 14",
|
'Dark Blue 14',
|
||||||
"Dark Brown 2",
|
'Dark Brown 2',
|
||||||
"Dark Brown 3",
|
'Dark Brown 3',
|
||||||
"Dark Green 2",
|
'Dark Green 2',
|
||||||
"Dark Grey 2",
|
'Dark Grey 2',
|
||||||
"Dark Teal1",
|
'Dark Teal1',
|
||||||
"Dark Teal6",
|
'Dark Teal6',
|
||||||
"Kayak",
|
'Kayak',
|
||||||
"Light Blue 2",
|
'Light Blue 2',
|
||||||
"Light Brown 2",
|
'Light Brown 2',
|
||||||
"Light Brown 5",
|
'Light Brown 5',
|
||||||
"Light Green",
|
'Light Green',
|
||||||
"Light Green 3",
|
'Light Green 3',
|
||||||
"Light Grey 2",
|
'Light Grey 2',
|
||||||
"Light Purple",
|
'Light Purple',
|
||||||
"Neutral Blue",
|
'Neutral Blue',
|
||||||
"Reds",
|
'Reds',
|
||||||
"Sandy Beach",
|
'Sandy Beach',
|
||||||
|
'High Contrast',
|
||||||
]
|
]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
302
tools/dynamic_builder.py
Normal file
302
tools/dynamic_builder.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
#!/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()
|
||||||
280
tools/spec_generator.py
Normal file
280
tools/spec_generator.py
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
PyInstaller spec file generator for nvda-voicemeeter project.
|
||||||
|
|
||||||
|
This script generates .spec files for different Voicemeeter kinds (basic, banana, potato).
|
||||||
|
Based on the existing spec file patterns in the spec/ directory.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/spec_generator.py [kind]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
kind: The Voicemeeter kind to generate spec for (basic, banana, potato).
|
||||||
|
If not provided, generates all spec files.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python tools/spec_generator.py basic
|
||||||
|
python tools/spec_generator.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
|
||||||
|
class SpecGenerator:
|
||||||
|
"""Generate PyInstaller spec files for nvda-voicemeeter project."""
|
||||||
|
|
||||||
|
# Supported Voicemeeter kinds
|
||||||
|
KINDS = ['basic', 'banana', 'potato']
|
||||||
|
|
||||||
|
def __init__(self, project_root: Path = None):
|
||||||
|
"""Initialize the spec generator.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
project_root: Path to the project root. If None, uses current directory.
|
||||||
|
"""
|
||||||
|
if project_root is None:
|
||||||
|
# Assume we're running from tools/ directory
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
|
||||||
|
self.project_root = Path(project_root)
|
||||||
|
self.spec_dir = self.project_root / 'spec'
|
||||||
|
|
||||||
|
# Ensure spec directory exists
|
||||||
|
self.spec_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def generate_spec_template(self, kind: str) -> str:
|
||||||
|
"""Generate a PyInstaller spec file template for the given kind.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind: The Voicemeeter kind (basic, banana, potato).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The generated spec file content as a string.
|
||||||
|
"""
|
||||||
|
if kind not in self.KINDS:
|
||||||
|
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
|
||||||
|
|
||||||
|
template = f"""# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
added_files = [
|
||||||
|
( '../controllerClient/x64', 'controllerClient/x64' ),
|
||||||
|
( '../controllerClient/x86', 'controllerClient/x86' ),
|
||||||
|
]
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['{kind}.py'],
|
||||||
|
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}',
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
return template
|
||||||
|
|
||||||
|
def generate_entry_point(self, kind: str) -> str:
|
||||||
|
"""Generate a Python entry point script for the given kind.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind: The Voicemeeter kind (basic, banana, potato).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The generated Python script content as a string.
|
||||||
|
"""
|
||||||
|
if kind not in self.KINDS:
|
||||||
|
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
|
||||||
|
|
||||||
|
entry_point = f"""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()
|
||||||
|
"""
|
||||||
|
return entry_point
|
||||||
|
|
||||||
|
def write_spec_file(self, kind: str, overwrite: bool = False) -> Path:
|
||||||
|
"""Write a spec file for the given kind.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind: The Voicemeeter kind (basic, banana, potato).
|
||||||
|
overwrite: Whether to overwrite existing files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to the created spec file.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileExistsError: If file exists and overwrite is False.
|
||||||
|
"""
|
||||||
|
spec_file = self.spec_dir / f'{kind}.spec'
|
||||||
|
entry_file = self.spec_dir / f'{kind}.py'
|
||||||
|
|
||||||
|
if spec_file.exists() and not overwrite:
|
||||||
|
raise FileExistsError(f'Spec file already exists: {spec_file}')
|
||||||
|
|
||||||
|
if entry_file.exists() and not overwrite:
|
||||||
|
raise FileExistsError(f'Entry point file already exists: {entry_file}')
|
||||||
|
|
||||||
|
# Write spec file
|
||||||
|
spec_content = self.generate_spec_template(kind)
|
||||||
|
spec_file.write_text(spec_content, encoding='utf-8')
|
||||||
|
print(f'Generated spec file: {spec_file}')
|
||||||
|
|
||||||
|
# Write entry point script
|
||||||
|
entry_content = self.generate_entry_point(kind)
|
||||||
|
entry_file.write_text(entry_content, encoding='utf-8')
|
||||||
|
print(f'Generated entry point: {entry_file}')
|
||||||
|
|
||||||
|
return spec_file
|
||||||
|
|
||||||
|
def generate_all_specs(self, overwrite: bool = False) -> List[Path]:
|
||||||
|
"""Generate spec files for all supported kinds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
overwrite: Whether to overwrite existing files.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of paths to the created spec files.
|
||||||
|
"""
|
||||||
|
spec_files = []
|
||||||
|
for kind in self.KINDS:
|
||||||
|
try:
|
||||||
|
spec_file = self.write_spec_file(kind, overwrite=overwrite)
|
||||||
|
spec_files.append(spec_file)
|
||||||
|
except FileExistsError as e:
|
||||||
|
print(f'Skipped {kind}: {e}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
return spec_files
|
||||||
|
|
||||||
|
def list_existing_specs(self) -> List[str]:
|
||||||
|
"""List existing spec files in the spec directory.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of existing spec file kinds.
|
||||||
|
"""
|
||||||
|
existing = []
|
||||||
|
for kind in self.KINDS:
|
||||||
|
spec_file = self.spec_dir / f'{kind}.spec'
|
||||||
|
if spec_file.exists():
|
||||||
|
existing.append(kind)
|
||||||
|
|
||||||
|
return existing
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main entry point for the spec generator."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Generate PyInstaller spec files for nvda-voicemeeter project',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Examples:
|
||||||
|
python tools/spec_generator.py basic # Generate basic.spec
|
||||||
|
python tools/spec_generator.py # Generate all spec files
|
||||||
|
python tools/spec_generator.py --list # List existing spec files
|
||||||
|
python tools/spec_generator.py --force # Overwrite existing files
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'kind',
|
||||||
|
nargs='?',
|
||||||
|
choices=SpecGenerator.KINDS,
|
||||||
|
help=f'Voicemeeter kind to generate spec for ({", ".join(SpecGenerator.KINDS)})',
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--force', '-f', action='store_true', help='Overwrite existing spec files')
|
||||||
|
|
||||||
|
parser.add_argument('--list', '-l', action='store_true', help='List existing spec files')
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
'--project-root', type=Path, help='Path to project root (default: detect from script location)'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Initialize generator
|
||||||
|
generator = SpecGenerator(project_root=args.project_root)
|
||||||
|
|
||||||
|
# List existing files if requested
|
||||||
|
if args.list:
|
||||||
|
existing = generator.list_existing_specs()
|
||||||
|
if existing:
|
||||||
|
print('Existing spec files:')
|
||||||
|
for kind in existing:
|
||||||
|
spec_file = generator.spec_dir / f'{kind}.spec'
|
||||||
|
print(f' {kind}: {spec_file}')
|
||||||
|
else:
|
||||||
|
print('No existing spec files found.')
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.kind:
|
||||||
|
# Generate specific kind
|
||||||
|
generator.write_spec_file(args.kind, overwrite=args.force)
|
||||||
|
print(f'Successfully generated spec for: {args.kind}')
|
||||||
|
else:
|
||||||
|
# Generate all kinds
|
||||||
|
spec_files = generator.generate_all_specs(overwrite=args.force)
|
||||||
|
if spec_files:
|
||||||
|
print(f'Successfully generated {len(spec_files)} spec files.')
|
||||||
|
else:
|
||||||
|
print('No spec files were generated (use --force to overwrite existing files).')
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f'Error: {e}')
|
||||||
|
return 1
|
||||||
|
except FileExistsError as e:
|
||||||
|
print(f'Error: {e}')
|
||||||
|
print('Use --force to overwrite existing files.')
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
exit(main())
|
||||||
Loading…
x
Reference in New Issue
Block a user