Compare commits

...

60 Commits
v1.9.7 ... main

Author SHA1 Message Date
2185748435 upd shell 2026-03-11 20:33:44 +00:00
d82afc1973 add pyinstaller dep check
remove hardcoded poetry path.

add verify build outputs step.

add build and release summaries.
2026-03-11 20:25:00 +00:00
6be32dcd32 update lockfile 2026-03-11 04:09:41 +00:00
abb55d5090 revert the change. 2026-03-10 23:01:25 +00:00
3271a52f15 use --with build --without dev 2026-03-10 22:17:54 +00:00
eb2ce5360f upd examples 2026-03-10 19:46:26 +00:00
45dbcae804 upd examples 2026-03-10 19:45:26 +00:00
4fab6d9ad9 add examples 2026-03-10 19:38:08 +00:00
fee3fa199b add 1.10.0 to CHANGELOG 2026-03-10 04:17:36 +00:00
cf3205a86f add required release step 2026-03-10 04:02:10 +00:00
c0416d5b7c rename gui entry points 2026-03-10 03:51:34 +00:00
a952f64bab minor bump 2026-03-10 03:47:17 +00:00
3f6172c4bf add publish+ruff actions 2026-03-10 03:46:28 +00:00
96f3fbbee1 fix artifact name patterns 2026-03-10 03:32:19 +00:00
0bc566fa00 access poetry from POETRY_BIN 2026-03-10 03:20:50 +00:00
c9b7f89453 PATH fix attempt 2026-03-10 03:13:48 +00:00
bdba07694b PATH fix attempt 2026-03-10 03:05:12 +00:00
462301cd4e PATH fix attempt 2026-03-10 03:01:04 +00:00
768fed217b remove then readd deps 2026-03-10 02:56:17 +00:00
34299ad84e avoid using local path deps in workflow 2026-03-10 02:49:50 +00:00
7a78d7233e use poetry self add to avoid Windows path issue, see: https://github.com/python-poetry/poetry/issues/10028 2026-03-10 02:42:01 +00:00
971b4a4601 upd shell, see https://github.com/snok/install-poetry?tab=readme-ov-file#running-on-windows 2026-03-10 02:36:34 +00:00
b219511ef8 replace Install Task step with go-task action. 2026-03-10 02:31:05 +00:00
270bda2dc1 add release workflow 2026-03-10 02:21:07 +00:00
6d5bd673a4 add compress tasks to Taskfile.dynamic 2026-03-10 02:18:26 +00:00
16ac188eb4 add shebang + docstring 2026-03-10 01:31:58 +00:00
737dc75cba add dynamic_builder script
add Taskfile for running dynamic builds
2026-03-10 01:31:25 +00:00
03d8415f68 add spec_generator script
add generate-specs target to Taskfile.
add generate-specs target as build dep.
2026-03-10 01:27:04 +00:00
bd868d4613 add .gitkeep files to show the expected dir structure for spec and theme files. 2026-03-10 01:15:13 +00:00
a65f851403 upd lockfile 2026-03-09 22:58:13 +00:00
9e5b5a31a8 bump dep versions 2026-03-09 22:57:39 +00:00
7aa8091de6 fix bus mode/mono button widths.
upd busmono textvariable values
2026-03-09 22:40:34 +00:00
d903faecd9 ensure we get the right bus modes according to the kind 2026-03-09 22:19:10 +00:00
b0d7d734fb make rewrite/restore tasks public 2026-03-09 21:37:59 +00:00
262dcd572b no need to rewrite entry point anymore 2026-03-09 21:37:43 +00:00
d612d38933 add padding to bus mode/mono buttons 2026-03-09 21:34:42 +00:00
5a693b8aaf entry point now accepts an optional theme arg. this makes it easier to test forest/azure themes 2026-03-09 21:34:20 +00:00
9210a26de6 fix source path for theme files
don't create (invisible) info button if azure theme
2026-03-09 21:31:39 +00:00
b0f634f1e8 split up taskfiles
add azure builds
2026-03-09 10:27:47 +00:00
5e5ae33e6a upd README 2026-03-09 05:46:11 +00:00
0d04a2f33e add support for 'host' in toml vban config
fix error with VBANCMDConnectionError error dialog
2026-03-09 05:46:04 +00:00
81a5497a32 add feedback for comp/gate sliders.
bus mode button now reads current mode from tkVar (more reliable).

bus mono button fixed (possible to set stereo reverse)
2026-03-08 21:13:30 +00:00
edc76db88e upd poe version 2026-02-27 20:43:24 +00:00
3b701a074d add path deps to dev.dependencies 2026-01-22 18:57:56 +00:00
66cabb68cf
Merge pull request #19 from onyx-and-iris/dependabot/pip/setuptools-78.1.1
Bump setuptools from 75.8.0 to 78.1.1
2025-05-19 23:58:41 +01:00
dependabot[bot]
37d7e58704
Bump setuptools from 75.8.0 to 78.1.1
Bumps [setuptools](https://github.com/pypa/setuptools) from 75.8.0 to 78.1.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v75.8.0...v78.1.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-19 22:58:20 +00:00
293bccc5ba call task from poe
remove build.ps1 and scripts.py
2025-02-13 16:23:35 +00:00
1d8ffdc756 mark rewrite,restore tasks as internal 2025-02-12 18:26:43 +00:00
e4d87334cb reformat 2025-02-07 22:58:30 +00:00
ad3020809e merge commands 2025-02-07 16:37:00 +00:00
76c6630892 fix deferred task 2025-02-07 15:09:36 +00:00
cc46fc31f8 upd build script, format files/dirs the same as Taskfile 2025-02-07 15:09:21 +00:00
8657e8846a add Taskfile 2025-02-07 14:53:21 +00:00
43aad156a0 upd flags passed to rewriter 2025-02-07 14:53:07 +00:00
5101ff01f2 change --cleanup flag for --restore
run file through ruff formatter
2025-02-07 14:52:40 +00:00
c437ae5843 rename entry points 2025-01-29 15:55:24 +00:00
ae59ba30f9 add 1.9.8 section to CHANGELOG 2025-01-22 16:46:25 +00:00
a3fa227ac1 patch bump 2025-01-22 16:38:52 +00:00
b1b6c66828 reduce the time vban menus are re-enabled after a disconnect 2025-01-22 16:38:44 +00:00
cb00de36f0 add _internal/configs to config paths.
vm-compact dirs now override _internal/config

upd README TOML Files section
2025-01-22 16:30:06 +00:00
34 changed files with 1711 additions and 362 deletions

53
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,53 @@
name: Publish to PyPI
on:
release:
types: [published]
push:
tags:
- 'v*.*.*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Poetry
run: |
pip install poetry==2.3.1
poetry --version
- name: Build package
run: |
poetry install --only-root
poetry build
- uses: actions/upload-artifact@v4
with:
name: dist
path: ./dist
pypi-publish:
needs: build
name: Upload release to PyPI
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/voicemeeter-compact/
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: ./dist
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: ./dist

341
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,341 @@
name: Release Voicemeeter Compact
on:
release:
types: [published]
push:
tags: ['v*.*.*']
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install Poetry
uses: snok/install-poetry@v1
with:
version: latest
virtualenvs-create: true
virtualenvs-in-project: true
- name: Install Task
uses: go-task/setup-task@v1
with:
version: 3.x
- name: Download Forest TTK Theme
run: |
# Clone the Forest theme repository
git clone https://github.com/rdbende/Forest-ttk-theme.git temp-forest-theme
# Copy the required theme files to theme/forest
Copy-Item "temp-forest-theme\forest-dark.tcl" "theme\forest\"
Copy-Item "temp-forest-theme\forest-light.tcl" "theme\forest\"
Copy-Item "temp-forest-theme\forest-dark" "theme\forest\" -Recurse
Copy-Item "temp-forest-theme\forest-light" "theme\forest\" -Recurse
# Clean up
Remove-Item temp-forest-theme -Recurse -Force
shell: pwsh
- name: Download Azure TTK Theme
run: |
# Clone the Azure theme repository
git clone https://github.com/rdbende/Azure-ttk-theme.git temp-azure-theme
# Copy the required theme files to theme/azure
Copy-Item "temp-azure-theme\azure.tcl" "theme\azure\"
Copy-Item "temp-azure-theme\theme" "theme\azure\" -Recurse
# Clean up
Remove-Item temp-azure-theme -Recurse -Force
shell: pwsh
- name: Cache Poetry dependencies
uses: actions/cache@v4
with:
path: .venv
key: venv-${{ runner.os }}-${{ hashFiles('**/poetry.lock') }}
- name: Install Poetry plugins
run: poetry self add poethepoet
shell: bash
- name: Replace path dependencies with PyPI versions
run: |
poetry remove voicemeeter-api vban-cmd || true
poetry add voicemeeter-api vban-cmd
shell: bash
- name: Install dependencies
shell: bash
run: |
# Install project dependencies
poetry install --with build
# Verify PyInstaller is available
echo "Verifying PyInstaller installation..."
poetry show pyinstaller
- name: Get Poetry executable path
shell: bash
run: |
poetryPath=$(which poetry)
echo "Poetry path: $poetryPath"
echo "POETRY_BIN=$poetryPath" >> $GITHUB_ENV
- name: Build artifacts with dynamic taskfile
run: task --taskfile Taskfile.dynamic.yml build-all
shell: bash
env:
POETRY_BIN: ${{ env.POETRY_BIN }}
- name: Create release archives
run: task --taskfile Taskfile.dynamic.yml compress-all
shell: bash
env:
POETRY_BIN: ${{ env.POETRY_BIN }}
- name: Verify build outputs
shell: pwsh
run: |
Write-Host "Verifying build outputs..."
$expectedFiles = @(
"dist/sunvalley-basic.zip",
"dist/sunvalley-banana.zip",
"dist/sunvalley-potato.zip",
"dist/forest-dark-basic.zip",
"dist/forest-dark-banana.zip",
"dist/forest-dark-potato.zip",
"dist/forest-light-basic.zip",
"dist/forest-light-banana.zip",
"dist/forest-light-potato.zip",
"dist/azure-dark-basic.zip",
"dist/azure-dark-banana.zip",
"dist/azure-dark-potato.zip",
"dist/azure-light-basic.zip",
"dist/azure-light-banana.zip",
"dist/azure-light-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!"
# Sunvalley theme variants
- name: Upload build artifacts - Sunvalley Basic
uses: actions/upload-artifact@v4
with:
name: sunvalley-basic
path: dist/sunvalley-basic.zip
- name: Upload build artifacts - Sunvalley Banana
uses: actions/upload-artifact@v4
with:
name: sunvalley-banana
path: dist/sunvalley-banana.zip
- name: Upload build artifacts - Sunvalley Potato
uses: actions/upload-artifact@v4
with:
name: sunvalley-potato
path: dist/sunvalley-potato.zip
# Forest theme variants (dark)
- name: Upload build artifacts - Forest Basic Dark
uses: actions/upload-artifact@v4
with:
name: forest-dark-basic
path: dist/forest-dark-basic.zip
- name: Upload build artifacts - Forest Banana Dark
uses: actions/upload-artifact@v4
with:
name: forest-dark-banana
path: dist/forest-dark-banana.zip
- name: Upload build artifacts - Forest Potato Dark
uses: actions/upload-artifact@v4
with:
name: forest-dark-potato
path: dist/forest-dark-potato.zip
# Forest theme variants (light)
- name: Upload build artifacts - Forest Basic Light
uses: actions/upload-artifact@v4
with:
name: forest-light-basic
path: dist/forest-light-basic.zip
- name: Upload build artifacts - Forest Banana Light
uses: actions/upload-artifact@v4
with:
name: forest-light-banana
path: dist/forest-light-banana.zip
- name: Upload build artifacts - Forest Potato Light
uses: actions/upload-artifact@v4
with:
name: forest-light-potato
path: dist/forest-light-potato.zip
# Azure theme variants (dark)
- name: Upload build artifacts - Azure Basic Dark
uses: actions/upload-artifact@v4
with:
name: azure-dark-basic
path: dist/azure-dark-basic.zip
- name: Upload build artifacts - Azure Banana Dark
uses: actions/upload-artifact@v4
with:
name: azure-dark-banana
path: dist/azure-dark-banana.zip
- name: Upload build artifacts - Azure Potato Dark
uses: actions/upload-artifact@v4
with:
name: azure-dark-potato
path: dist/azure-dark-potato.zip
# Azure theme variants (light)
- name: Upload build artifacts - Azure Basic Light
uses: actions/upload-artifact@v4
with:
name: azure-light-basic
path: dist/azure-light-basic.zip
- name: Upload build artifacts - Azure Banana Light
uses: actions/upload-artifact@v4
with:
name: azure-light-banana
path: dist/azure-light-banana.zip
- name: Upload build artifacts - Azure Potato Light
uses: actions/upload-artifact@v4
with:
name: azure-light-potato
path: dist/azure-light-potato.zip
- name: Build Summary
shell: pwsh
run: |
Write-Host -ForegroundColor Green "Build completed successfully!"
Write-Host ""
Write-Host "Built artifacts (15 theme variants):"
Write-Host " Sunvalley Theme:"
Write-Host " - voicemeeter-compact-sunvalley-basic.zip"
Write-Host " - voicemeeter-compact-sunvalley-banana.zip"
Write-Host " - voicemeeter-compact-sunvalley-potato.zip"
Write-Host " Forest Theme:"
Write-Host " - voicemeeter-compact-forest-dark-basic.zip"
Write-Host " - voicemeeter-compact-forest-dark-banana.zip"
Write-Host " - voicemeeter-compact-forest-dark-potato.zip"
Write-Host " - voicemeeter-compact-forest-light-basic.zip"
Write-Host " - voicemeeter-compact-forest-light-banana.zip"
Write-Host " - voicemeeter-compact-forest-light-potato.zip"
Write-Host " Azure Theme:"
Write-Host " - voicemeeter-compact-azure-dark-basic.zip"
Write-Host " - voicemeeter-compact-azure-dark-banana.zip"
Write-Host " - voicemeeter-compact-azure-dark-potato.zip"
Write-Host " - voicemeeter-compact-azure-light-basic.zip"
Write-Host " - voicemeeter-compact-azure-light-banana.zip"
Write-Host " - voicemeeter-compact-azure-light-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 "Voicemeeter Compact $TAG_NAME" \
--notes "## Voicemeeter Compact Release $TAG_NAME
### Theme Variants
Choose your preferred theme and Voicemeeter version:
**Sunvalley Theme**
- **sunvalley-basic.zip** - For Voicemeeter Basic
- **sunvalley-banana.zip** - For Voicemeeter Banana
- **sunvalley-potato.zip** - For Voicemeeter Potato
**Forest Theme**
- **forest-dark-basic.zip** - Dark theme for Voicemeeter Basic
- **forest-dark-banana.zip** - Dark theme for Voicemeeter Banana
- **forest-dark-potato.zip** - Dark theme for Voicemeeter Potato
- **forest-light-basic.zip** - Light theme for Voicemeeter Basic
- **forest-light-banana.zip** - Light theme for Voicemeeter Banana
- **forest-light-potato.zip** - Light theme for Voicemeeter Potato
**Azure Theme**
- **azure-dark-basic.zip** - Dark theme for Voicemeeter Basic
- **azure-dark-banana.zip** - Dark theme for Voicemeeter Banana
- **azure-dark-potato.zip** - Dark theme for Voicemeeter Potato
- **azure-light-basic.zip** - Light theme for Voicemeeter Basic
- **azure-light-banana.zip** - Light theme for Voicemeeter Banana
- **azure-light-potato.zip** - Light theme for Voicemeeter Potato
### Requirements
- Windows 10/11
- Voicemeeter (Basic/Banana/Potato) installed
- Python 3.10+ (if running from source)
### Installation
1. Download the zip file matching your Voicemeeter version and preferred theme
2. Extract and run the executable - no installation required
3. The application will automatically detect your Voicemeeter installation
### Notes
- Built with PyInstaller for standalone execution
- Each variant is scaled for its specific Voicemeeter version
- Themes provide different visual styles while maintaining full functionality"
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
View 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@v6
- uses: astral-sh/ruff-action@v3
with:
args: 'format --check --diff'

114
.gitignore vendored
View File

@ -1,9 +1,9 @@
# quick test # Generated by ignr: github.com/onyx-and-iris/ignr
quick.py
## 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
@ -23,7 +23,6 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
@ -50,9 +49,10 @@ htmlcov/
nosetests.xml nosetests.xml
coverage.xml coverage.xml
*.cover *.cover
*.py,cover *.py.cover
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
cover/
# Translations # Translations
*.mo *.mo
@ -75,6 +75,7 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@ -85,7 +86,9 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
.python-version # For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv # pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
@ -94,7 +97,37 @@ ipython_config.py
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow # UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# 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.toml
.pdm-python
.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
__pypackages__/ __pypackages__/
# Celery stuff # Celery stuff
@ -106,13 +139,13 @@ celerybeat.pid
# Environments # Environments
.env .env
.envrc
.venv .venv
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
venv_vmcompact/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
@ -132,8 +165,65 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# build # pytype static type analyzer
theme/ .pytype/
spec/
.vscode/ # Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# 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/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.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
# Test files
test-*.py
# Forest/Azure theme files
theme/**/*
!theme/*/
!theme/**/.gitkeep
# Spec files
spec/**/*
!spec/*/
!spec/**/.gitkeep
# Taskfile build files
Taskfile.unified.yml
SPEC_CONSOLIDATION.md

View File

@ -9,6 +9,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [ ] - [ ]
## [1.10.0] - 2026-03-26
### Added
- Automated builds for Releases. This is much preferred over manual releases because users can be sure the files are built directly from the source code.
- Azure theme added to Releases.
- vban.toml files can now use key `host` intead of `ip`.
- `ip` is still usable for backwards compatibility.
### Changed
- Attempting a VBAN connection now uses a PING/PONG handshake to verify connection, this makes connections more reliable.
- Navigation frame is disabled by default. You can easily enable it from the menu or with an app.toml config.
### Fixed
- Comp, Gate sliders now receive feedback when changes are made on the Voicemeeter GUI.
- Bus CONFIG mode button rotates through the correct modes for Basic Kind.
- Bus CONFIG mono now rotates through *off, on, stereo reverse*.
- Bus CONFIG mode/mono buttons are now a fixed width.
## [1.9.8] - 2025-01-22
### Changed
- vm-compact config dirs now override _internal/configs (if using build from releases). See [TOML Files](https://github.com/onyx-and-iris/voicemeeter-compact?tab=readme-ov-file#toml-files) section in README.
- after disconnecting from a vban connection, vban menus are re-enabled after 500ms.
## [1.9.5] - 2024-07-03 ## [1.9.5] - 2024-07-03
### Changed ### Changed

View File

@ -65,15 +65,18 @@ Set the kind of Voicemeeter, KIND_ID may be:
## TOML Files ## TOML Files
This is how your files should be organised. Wherever your `__main__.py` file is located (after install this can be any location), `configs` should be in the same location. If you've downloaded the binary from [Releases][releases] you can find configs included in the `_internal/configs` directory.
Directly inside of configs directory you may place an app.toml, vban.toml and a directory for each kind.
Inside each kind directory you may place as many custom toml configurations as you wish. You may override these configs by placing a directory `vm-compact` in one of the following locations:
- `user home directory / .config`
- `user home directory / Documents / Voicemeeter`
The contents should match the following directory structure:
. .
├── `__main__.py` ├── vm-compact
├── configs
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── app.toml &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;├── app.toml
@ -111,7 +114,7 @@ Configure certain startup states for the app.
Configure a user config to load on app startup. Don't include the .toml extension in the config name. Configure a user config to load on app startup. Don't include the .toml extension in the config name.
- `theme` - `theme`
By default the app loads up the [Sun Valley light or dark theme][def] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect. By default the app loads up the [Sun Valley light or dark theme][releases] by @rdbende. You have the option to load up the app without any theme loaded. Simply set `enabled` to false and `mode` will take no effect.
- `extends` - `extends`
Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly. Extending the app will show both strips and buses. In reduced mode only one or the other. This app will extend both horizontally and vertically, simply set `extends_horizontal` true or false accordingly.
@ -136,13 +139,13 @@ A valid `vban.toml` might look like this:
```toml ```toml
[connection-1] [connection-1]
kind = 'banana' kind = 'banana'
ip = '192.168.1.2' host = '192.168.1.2'
streamname = 'worklaptop' streamname = 'worklaptop'
port = 6980 port = 6980
[connection-2] [connection-2]
kind = 'potato' kind = 'potato'
ip = '192.168.1.3' host = '192.168.1.3'
streamname = 'streampc' streamname = 'streampc'
port = 6990 port = 6990
``` ```
@ -164,4 +167,5 @@ User configs may be loaded at any time via the menu.
[Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme]. [Rdbende](https://github.com/rdbende) for creating the beautiful [Sun Valley theme][sv-theme].
[sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme [sv-theme]: https://github.com/rdbende/Sun-Valley-ttk-theme
[releases]: https://github.com/onyx-and-iris/voicemeeter-compact/releases

38
Taskfile.azure.yml Normal file
View File

@ -0,0 +1,38 @@
version: '3'
tasks:
build:
desc: Build Azure artifacts
deps: [rewrite]
cmds:
- defer: { task: restore }
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [azure-light, azure-dark]
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/azure/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
rewrite:
desc: Run the source code rewriter
cmds:
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
restore:
desc: Restore the backup files
cmds:
- poetry run python tools/rewriter.py --restore
compress:
desc: Compress Azure artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [azure-light, azure-dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
clean:
desc: Clean build and dist directories
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/azure-*,dist/azure-* -Recurse -Force"

84
Taskfile.dynamic.yml Normal file
View File

@ -0,0 +1,84 @@
version: '3'
# Dynamic build system - no spec files needed!
# Examples:
# - task -t Taskfile.dynamic.yml build THEMES="azure forest"
# - task -t Taskfile.dynamic.yml build-all
# THEMES can be specified as a space-separated list or "all" to build everything.
#
# Compression tasks are also dynamic and can be used like:
# Usage examples:
# - task -t Taskfile.dynamic.yml compress THEME=azure
# - task -t Taskfile.dynamic.yml compress-all
vars:
THEMES: '{{.THEMES | default "all"}}'
SHELL: pwsh
tasks:
build:
desc: Build specified themes dynamically (no spec files needed)
cmds:
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py {{.THEMES}}
build-all:
desc: Build all themes
cmds:
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py all
build-azure:
desc: Build only azure theme
cmds:
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py azure
build-forest:
desc: Build only forest theme
cmds:
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py forest
build-sunvalley:
desc: Build only sunvalley theme
cmds:
- ${POETRY_BIN:-poetry} run python tools/dynamic_builder.py sunvalley
compress:
desc: Compress artifacts for specified theme
cmds:
- task: compress-{{.THEME}}
compress-all:
desc: Compress artifacts for all themes
cmds:
- for:
matrix:
THEME: [azure, forest, sunvalley]
task: compress-{{.ITEM.THEME}}
compress-azure:
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
VARIANT: [azure-light, azure-dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
compress-forest:
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
VARIANT: [forest-light, forest-dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.VARIANT}}-{{.ITEM.KIND}}.zip -Force"'
compress-sunvalley:
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
clean:
desc: Clean all build artifacts
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"

38
Taskfile.forest.yml Normal file
View File

@ -0,0 +1,38 @@
version: '3'
tasks:
build:
desc: Build Forest artifacts
deps: [rewrite]
cmds:
- defer: { task: restore }
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [forest-light, forest-dark]
cmd: poetry run pyinstaller --noconfirm --distpath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} spec/forest/{{.ITEM.THEME}}-{{.ITEM.KIND}}.spec
rewrite:
desc: Run the source code rewriter
cmds:
- poetry run python tools/rewriter.py --rewrite --theme {{.THEME}}
restore:
desc: Restore the backup files
cmds:
- poetry run python tools/rewriter.py --restore
compress:
desc: Compress Forest artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [forest-light, forest-dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/{{.ITEM.THEME}}-{{.ITEM.KIND}} -DestinationPath dist/{{.ITEM.THEME}}-{{.ITEM.KIND}}.zip -Force"'
clean:
desc: Clean build and dist directories
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/forest-*,dist/forest-* -Recurse -Force"

24
Taskfile.sunvalley.yml Normal file
View File

@ -0,0 +1,24 @@
version: '3'
tasks:
build:
desc: Build Sunvalley artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: poetry run pyinstaller --noconfirm --distpath dist/sunvalley-{{.ITEM.KIND}} spec/sunvalley/sunvalley-{{.ITEM.KIND}}.spec
compress:
desc: Compress Sunvalley artifacts
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/sunvalley-{{.ITEM.KIND}} -DestinationPath dist/sunvalley-{{.ITEM.KIND}}.zip -Force"'
clean:
desc: Clean build and dist directories
cmds:
- |
{{.SHELL}} -Command "Remove-Item -Path build/sunvalley-*,dist/sunvalley-* -Recurse -Force"

61
Taskfile.yml Normal file
View File

@ -0,0 +1,61 @@
version: '3'
includes:
sunvalley:
taskfile: ./Taskfile.sunvalley.yml
vars:
THEME: sunvalley
forest:
taskfile: ./Taskfile.forest.yml
vars:
THEME: forest
azure:
taskfile: ./Taskfile.azure.yml
vars:
THEME: azure
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
- echo "Release complete"
generate-specs:
desc: Generate all spec files from templates
cmds:
- poetry run python tools/spec_generator.py --clean
build:
desc: Build all artifacts
deps: [generate-specs]
cmds:
- for:
matrix:
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:build'
compress:
desc: Compress all artifacts
cmds:
- for:
matrix:
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:compress'
clean:
desc: Clean up build and dist directories
cmds:
- for:
matrix:
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:clean'

View File

@ -1,41 +0,0 @@
param(
[Parameter(Mandatory = $true)]
[string]$prefix,
[string]$theme
)
function Format-SpecName {
param(
[string]$Kind
)
return @(
$prefix,
(& { if ($theme) { $theme } else { "" } }),
$Kind
).Where({ $_ -ne "" }) -Join "_"
}
function Compress-Builds {
$target = Join-Path -Path $PSScriptRoot -ChildPath "dist"
@("basic", "banana", "potato") | ForEach-Object {
$compressPath = Format-SpecName -Kind $_
Compress-Archive -Path (Join-Path -Path $target -ChildPath $compressPath) -DestinationPath (Join-Path -Path $target -ChildPath "${compressPath}.zip") -Force
}
}
function Get-Builds {
@("basic", "banana", "potato") | ForEach-Object {
$specName = Format-SpecName -Kind $_
Write-Host "building $specName"
poetry run pyinstaller --noconfirm --distpath (Join-Path -Path "dist" -ChildPath $specName) (Join-Path -Path "spec" -ChildPath "${specName}.spec")
}
}
function main {
Get-Builds
Compress-Builds
}
if ($MyInvocation.InvocationName -ne '.') { main }

View File

@ -1,10 +1,9 @@
# load a specific profile on start (file name without .toml ext) # load a specific profile on start (file name without .toml ext)
# [configs] # [configs]
# config="example" # config="example"
# load with themes enabled? set the default mode # load with themes enabled?
[theme] [theme]
enabled = true enabled = true
mode = "light"
# load in extended mode? if so which orientation # load in extended mode? if so which orientation
[extends] [extends]
extended = true extended = true
@ -22,4 +21,4 @@ size = 3
default = 0 default = 0
# show the navigation frame? # show the navigation frame?
[navigation] [navigation]
show = true show = false

View File

@ -2,12 +2,12 @@
### set the ip then uncomment ### set the ip then uncomment
# [connection-1] # [connection-1]
# kind = 'banana' # kind = 'banana'
# ip = '<ip address 1>' # ip = 'localhost'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6980
# [connection-2] # [connection-2]
# kind = 'potato' # kind = 'potato'
# ip = '<ip address 2>' # ip = 'gamepc.local'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6980

52
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. # This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]] [[package]]
name = "altgraph" name = "altgraph"
@ -147,24 +147,24 @@ files = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "75.8.0" version = "78.1.1"
description = "Easily download, build, install, upgrade, and uninstall Python packages" description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["build"] groups = ["build"]
files = [ files = [
{file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, {file = "setuptools-78.1.1-py3-none-any.whl", hash = "sha256:c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561"},
{file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, {file = "setuptools-78.1.1.tar.gz", hash = "sha256:fcc17fd9cd898242f6b4adfaca46137a9edef687f43e6f78469692a5e70d851d"},
] ]
[package.extras] [package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"] cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"] enabler = ["pytest-enabler (>=2.2)"]
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]] [[package]]
name = "sv-ttk" name = "sv-ttk"
@ -184,8 +184,8 @@ version = "2.2.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["main"] groups = ["main", "dev"]
markers = "python_version < \"3.11\"" markers = "python_version == \"3.10\""
files = [ files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@ -223,35 +223,39 @@ files = [
[[package]] [[package]]
name = "vban-cmd" name = "vban-cmd"
version = "2.5.0" version = "2.10.3"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main", "dev"]
files = [ files = []
{file = "vban_cmd-2.5.0-py3-none-any.whl", hash = "sha256:22a19037066487d464a61941a3b85a0331b498a9efb1bcacdc932e9d06c5bf87"}, develop = true
{file = "vban_cmd-2.5.0.tar.gz", hash = "sha256:691a852e5052e50103839b06a0a9d0746b90df3346545c2cf4f10b099d9666e4"},
]
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
[package.source]
type = "directory"
url = "../vban-cmd-python"
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.6.1" version = "2.7.2"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
optional = false optional = false
python-versions = "<4.0,>=3.10" python-versions = ">=3.10"
groups = ["main"] groups = ["main", "dev"]
files = [ files = []
{file = "voicemeeter_api-2.6.1-py3-none-any.whl", hash = "sha256:8ae3bce0f9ad6bbad78f2f69f522b6fb2e229d314918a075ad83d4009aff7020"}, develop = true
{file = "voicemeeter_api-2.6.1.tar.gz", hash = "sha256:34d8672603ec66197f2d61fd8f038f46d8451759c81fbe222b00e7d3ccccd1f5"},
]
[package.dependencies] [package.dependencies]
tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""} tomli = {version = ">=2.0.1,<3.0", markers = "python_version < \"3.11\""}
[package.source]
type = "directory"
url = "../voicemeeter-api-python"
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.10,<3.14" python-versions = ">=3.10,<3.14"
content-hash = "19c384acd36868a5bfdc3f3173f444858136603694c3f1134c0d30cd17157651" content-hash = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71"

View File

@ -1,34 +1,34 @@
[project] [project]
name = "voicemeeter-compact" name = "voicemeeter-compact"
version = "1.9.7" version = "1.10.0"
description = "A Compact Voicemeeter Remote App" description = "A Compact Voicemeeter Remote App"
authors = [ authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
{name = "Onyx and Iris",email = "code@onyxandiris.online"} license = { text = "MIT" }
]
license = {text = "MIT"}
readme = "README.md" readme = "README.md"
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"
dependencies = [ dependencies = [
"voicemeeter-api (>=2.6.1,<3.0.0)", "voicemeeter-api (>=2.7.2,<3.0.0)",
"vban-cmd (>=2.5.0,<3.0.0)", "vban-cmd (>=2.10.2,<3.0.0)",
"sv-ttk (>=2.6.0,<3.0.0)", "sv-ttk (>=2.6.0,<3.0.0)",
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'", "tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
] ]
[project.scripts] [project.scripts]
gui-basic = "vmcompact.gui.basic:run" voicemeeter-compact-basic = "vmcompact.gui.basic:run"
gui-banana = "vmcompact.gui.banana:run" voicemeeter-compact-banana = "vmcompact.gui.banana:run"
gui-potato = "vmcompact.gui.potato:run" voicemeeter-compact-potato = "vmcompact.gui.potato:run"
[tool.poetry] [tool.poetry]
packages = [{ include = "vmcompact" }] packages = [{ include = "vmcompact" }]
include = ["vmcompact/img/cat.ico"] include = ["vmcompact/img/cat.ico"]
[tool.poetry.requires-plugins] [tool.poetry.requires-plugins]
poethepoet = "^0.32.1" poethepoet = ">=0.42.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "^0.9.1" ruff = "^0.9.1"
voicemeeter-api = { path = "../voicemeeter-api-python/", develop = true }
vban-cmd = { path = "../vban-cmd-python/", develop = true }
[tool.poetry.group.build.dependencies] [tool.poetry.group.build.dependencies]
pyinstaller = "^6.11.1" pyinstaller = "^6.11.1"
@ -38,9 +38,14 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poe.tasks] [tool.poe.tasks]
build_sunvalley.script = "scripts:build_sunvalley" build-sunvalley = "task build-sunvalley"
build_forest.script = "scripts:build_forest" build-forest = "task build-forest"
build_all.script = "scripts:build_all" release = [
{ ref = "build-sunvalley" },
{ ref = "build-forest" },
{ cmd = "task compress-sunvalley" },
{ cmd = "task compress-forest" },
]
[tool.ruff] [tool.ruff]
exclude = [ exclude = [
@ -120,7 +125,4 @@ docstring-code-line-length = "dynamic"
max-complexity = 10 max-complexity = 10
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = [ "__init__.py" = ["E402", "F401"]
"E402",
"F401",
]

View File

@ -1,24 +0,0 @@
import subprocess
import sys
from pathlib import Path
def build_sunvalley():
buildscript = Path.cwd() / "build.ps1"
subprocess.run(["powershell", str(buildscript), "sv"])
def build_forest():
rewriter = Path.cwd() / "tools" / "rewriter.py"
subprocess.run([sys.executable, str(rewriter), "-r"])
buildscript = Path.cwd() / "build.ps1"
for theme in ("light", "dark"):
subprocess.run(["powershell", str(buildscript), "fst", theme])
subprocess.run([sys.executable, str(rewriter), "-c"])
def build_all():
steps = (build_sunvalley, build_forest)
[step() for step in steps]

0
spec/azure/.gitkeep Normal file
View File

0
spec/forest/.gitkeep Normal file
View File

0
spec/sunvalley/.gitkeep Normal file
View File

0
theme/azure/.gitkeep Normal file
View File

0
theme/forest/.gitkeep Normal file
View File

322
tools/dynamic_builder.py Normal file
View File

@ -0,0 +1,322 @@
#!/usr/bin/env python3
"""
Dynamic build system for voicemeeter-compact.
Generates spec files on-the-fly and builds executables without storing intermediate files.
"""
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import Dict
# Build configuration
THEMES = {
'azure': ['azure-light', 'azure-dark'],
'forest': ['forest-light', 'forest-dark'],
'sunvalley': ['sunvalley'],
}
KINDS = ['basic', 'banana', 'potato']
# Templates (same as spec_generator.py)
PYTHON_TEMPLATE = """import voicemeeterlib
import vmcompact
def main():
KIND_ID = '{kind}'
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
app = vmcompact.connect(KIND_ID, vmr{theme_param})
app.mainloop()
if __name__ == '__main__':
main()
"""
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
added_files = [
( '{img_path}', 'img' ),{theme_files}
( '{config_path}', 'configs' ),
]
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='{theme_variant}-{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='{theme_variant}-{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 __enter__(self):
self.temp_dir = Path(tempfile.mkdtemp(prefix='vmcompact_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, theme_variant: str, kind: str) -> Path:
"""Create a temporary Python launcher file."""
if theme_variant == 'sunvalley':
theme_arg = ''
theme_param = ''
else:
theme_arg = f"\n theme = '{theme_variant}'"
theme_param = ', theme=theme'
content = PYTHON_TEMPLATE.format(
kind=kind, theme_arg=theme_arg, theme_param=theme_param
)
py_file = self.temp_dir / f'{theme_variant}-{kind}.py'
with open(py_file, 'w') as f:
f.write(content)
return py_file
def create_spec_file(self, theme_variant: str, kind: str, py_file: Path) -> Path:
"""Create a temporary PyInstaller spec file."""
if theme_variant == 'sunvalley':
theme_files = ''
else:
theme_base = theme_variant.split('-')[0]
theme_path = (self.base_dir / 'theme' / theme_base).as_posix()
theme_files = f"\n ( '{theme_path}', 'theme' ),"
content = SPEC_TEMPLATE.format(
script_path=py_file.as_posix(),
img_path=(self.base_dir / 'vmcompact' / 'img').as_posix(),
config_path=(self.base_dir / 'configs').as_posix(),
theme_files=theme_files,
kind=kind,
theme_variant=theme_variant,
)
spec_file = self.temp_dir / f'{theme_variant}-{kind}.spec'
with open(spec_file, 'w') as f:
f.write(content)
return spec_file
def build_variant(self, theme_variant: str, kind: str) -> bool:
"""Build a single theme/kind variant."""
print(f'Building {theme_variant}-{kind}...')
# Create temporary files
py_file = self.create_python_file(theme_variant, kind)
spec_file = self.create_spec_file(theme_variant, kind, py_file)
# Build with PyInstaller
dist_path = self.dist_dir / f'{theme_variant}-{kind}'
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
cmd = [
poetry_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 {theme_variant}-{kind}')
return True
else:
print(f'[FAIL] Failed to build {theme_variant}-{kind}')
print(f'Error: {result.stderr}')
return False
except Exception as e:
print(f'[ERROR] Exception building {theme_variant}-{kind}: {e}')
return False
def build_theme(self, theme_family: str) -> Dict[str, bool]:
"""Build all variants for a theme family."""
results = {}
if theme_family not in THEMES:
print(f'Unknown theme: {theme_family}')
return results
variants = THEMES[theme_family]
for variant in variants:
for kind in KINDS:
success = self.build_variant(variant, kind)
results[f'{variant}-{kind}'] = success
return results
def run_rewriter(theme_family: str, base_dir: Path) -> bool:
"""Run the theme rewriter if needed."""
if theme_family in ['azure', 'forest']:
print(f'Running rewriter for {theme_family} theme...')
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
cmd = [
poetry_bin,
'run',
'python',
'tools/rewriter.py',
'--rewrite',
'--theme',
theme_family,
]
try:
result = subprocess.run(cmd, cwd=base_dir)
return result.returncode == 0
except Exception as e:
print(f'Rewriter failed: {e}')
return False
return True
def restore_rewriter(base_dir: Path) -> bool:
"""Restore files after rewriter."""
print('Restoring rewriter changes...')
poetry_bin = os.getenv('POETRY_BIN', 'poetry')
cmd = [poetry_bin, 'run', 'python', 'tools/rewriter.py', '--restore']
try:
result = subprocess.run(cmd, cwd=base_dir)
return result.returncode == 0
except Exception as e:
print(f'Restore failed: {e}')
return False
def main():
parser = argparse.ArgumentParser(
description='Dynamic build system for voicemeeter-compact'
)
parser.add_argument(
'themes',
nargs='*',
choices=list(THEMES.keys()) + ['all'],
help='Themes 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.themes or 'all' in args.themes:
themes_to_build = list(THEMES.keys())
else:
themes_to_build = args.themes
base_dir = Path.cwd()
args.dist_dir.mkdir(exist_ok=True)
print(f'Building themes: {", ".join(themes_to_build)}')
all_results = {}
with DynamicBuilder(base_dir, args.dist_dir) as builder:
for theme_family in themes_to_build:
# Run rewriter if needed
if not run_rewriter(theme_family, base_dir):
print(f'Skipping {theme_family} due to rewriter failure')
continue
try:
# Build theme
results = builder.build_theme(theme_family)
all_results.update(results)
finally:
# Always restore rewriter changes
if theme_family in ['azure', 'forest']:
restore_rewriter(base_dir)
# 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()

View File

@ -1,14 +1,19 @@
#!/usr/bin/env python3
"""Rewrites app.py, builders.py, menu.py, and navigation.py to remove sv_ttk dependencies and apply theme changes for the build process.
Also provides a cleanup function to restore the original files after building.
"""
import argparse import argparse
import logging import logging
from pathlib import Path from pathlib import Path
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger("vm-compact-rewriter") logger = logging.getLogger('vm-compact-rewriter')
PACKAGE_DIR = Path(__file__).parent.parent / "vmcompact" PACKAGE_DIR = Path(__file__).parent.parent / 'vmcompact'
SRC_DIR = Path(__file__).parent / "src" SRC_DIR = Path(__file__).parent / 'src'
def write_outs(output, outs: tuple): def write_outs(output, outs: tuple):
@ -16,235 +21,265 @@ def write_outs(output, outs: tuple):
output.write(out) output.write(out)
def rewrite_app(): def rewrite_app(theme):
app_logger = logger.getChild("app") app_logger = logger.getChild('app')
app_logger.info("rewriting app.py") app_logger.info('rewriting app.py')
infile = Path(SRC_DIR) / "app.bk" infile = Path(SRC_DIR) / 'app.bk'
outfile = Path(PACKAGE_DIR) / "app.py" outfile = Path(PACKAGE_DIR) / 'app.py'
with open(infile, "r") as input: with open(infile, 'r') as input:
with open(outfile, "w") as output: with open(outfile, 'w') as output:
for line in input: for line in input:
match line: match line:
# App init() case ' self._vmr = vmr\n':
case " def __init__(self, vmr):\n":
output.write(" def __init__(self, vmr, theme):\n")
case " self._vmr = vmr\n":
write_outs( write_outs(
output, output,
( (
" self._vmr = vmr\n", ' self._vmr = vmr\n',
" self._theme = theme\n", ' self._theme = theme\n',
' tcldir = Path.cwd() / "theme"\n', ' self._theme_name = theme.split("-")[0]\n',
" if not tcldir.is_dir():\n", ' self._theme_type = theme.split("-")[-1]\n',
' tcldir = Path.cwd() / "theme" / self._theme_name\n',
' if not tcldir.is_dir():\n',
' tcldir = Path.cwd() / "_internal" / "theme"\n', ' tcldir = Path.cwd() / "_internal" / "theme"\n',
' self.tk.call("source", tcldir.resolve() / f"forest-{self._theme}.tcl")\n', ' match self._theme_name:\n',
' case "forest":\n',
' self.tk.call("source", tcldir.resolve() / f"{self._theme}.tcl")\n',
' case "azure":\n',
' self.tk.call("source", tcldir.resolve() / f"{self._theme_name}.tcl")\n',
), ),
) )
# def connect()
case "def connect(kind_id: str, vmr) -> App:\n":
output.write(
'def connect(kind_id: str, vmr, theme="light") -> App:\n'
)
case " return VMMIN_cls(vmr)\n":
output.write(" return VMMIN_cls(vmr, theme)\n")
case _: case _:
output.write(line) output.write(line)
def rewrite_builders(): def rewrite_builders(theme):
builders_logger = logger.getChild("builders") builders_logger = logger.getChild('builders')
builders_logger.info("rewriting builders.py") builders_logger.info('rewriting builders.py')
infile = Path(SRC_DIR) / "builders.bk" infile = Path(SRC_DIR) / 'builders.bk'
outfile = Path(PACKAGE_DIR) / "builders.py" outfile = Path(PACKAGE_DIR) / 'builders.py'
with open(infile, "r") as input: with open(infile, 'r') as input:
with open(outfile, "w") as output: with open(outfile, 'w') as output:
ignore_next_lines = 0 ignore_next_lines = 0
for line in input: for line in input:
if ignore_next_lines > 0: if ignore_next_lines > 0:
builders_logger.info(f"ignoring: {line}") builders_logger.info(f'ignoring: {line}')
ignore_next_lines -= 1 ignore_next_lines -= 1
continue continue
match line: match line:
# loading themes # loading themes
case "import sv_ttk\n": case 'import sv_ttk\n':
output.write("#import sv_ttk\n") output.write('#import sv_ttk\n')
case " self.app.resizable(False, False)\n": case ' self.app.resizable(False, False)\n':
write_outs( if theme.startswith('forest'):
output, write_outs(
( output,
" self.app.resizable(False, False)\n" (
" if _configuration.themes_enabled:\n", ' self.app.resizable(False, False)\n'
' ttk.Style().theme_use(f"forest-{self.app._theme}")\n', ' if _configuration.themes_enabled:\n',
' self.logger.info(f"Forest Theme applied")\n', ' ttk.Style().theme_use(self.app._theme)\n',
), ' self.logger.info(f"{self.app._theme} Theme applied")\n',
) ),
)
elif theme.startswith('azure'):
write_outs(
output,
(
' self.app.resizable(False, False)\n'
' if _configuration.themes_enabled:\n',
' self.app.tk.call("set_theme", self.app._theme_type)\n',
' self.logger.info(f"Azure {self.app._theme_type} Theme applied")\n',
),
)
ignore_next_lines = 6 ignore_next_lines = 6
# setting navframe button widths # setting navframe button widths
case " variable=self.navframe.submix,\n": case ' variable=self.navframe.submix,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.navframe.submix,\n" ' variable=self.navframe.submix,\n'
" width=8,\n", ' width=8,\n',
), ),
) )
case " variable=self.navframe.channel,\n": case ' variable=self.navframe.channel,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.navframe.channel,\n" ' variable=self.navframe.channel,\n'
" width=8,\n", ' width=8,\n',
), ),
) )
case " variable=self.navframe.extend,\n": case ' variable=self.navframe.extend,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.navframe.extend,\n" ' variable=self.navframe.extend,\n'
" width=8,\n", ' width=8,\n',
), ),
) )
case " variable=self.navframe.info,\n": case ' variable=self.navframe.info,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.navframe.info,\n" ' variable=self.navframe.info,\n'
" width=8,\n", ' width=8,\n',
), ),
) )
# set channelframe button widths # set channelframe button widths
case " variable=self.labelframe.mute,\n": case ' variable=self.labelframe.mute,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.labelframe.mute,\n" ' variable=self.labelframe.mute,\n'
" width=7,\n", ' width=7,\n',
), ),
) )
case " variable=self.labelframe.conf,\n": case ' variable=self.labelframe.conf,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.labelframe.conf,\n" ' variable=self.labelframe.conf,\n'
" width=7,\n", ' width=7,\n',
), ),
) )
case " variable=self.labelframe.on,\n": case ' variable=self.labelframe.on,\n':
write_outs( write_outs(
output, output,
( (
" variable=self.labelframe.on,\n" ' variable=self.labelframe.on,\n'
" width=7,\n", ' width=7,\n',
), ),
) )
# set stripconfigframe button widths # set stripconfigframe button widths
case " self.configframe.phys_out_params.index(param)\n": case ' self.configframe.phys_out_params.index(param)\n':
write_outs( write_outs(
output, output,
( (
" self.configframe.phys_out_params.index(param)\n", ' self.configframe.phys_out_params.index(param)\n',
" ],\n", ' ],\n',
" width=6,\n", ' width=6,\n',
), ),
) )
ignore_next_lines = 1 ignore_next_lines = 1
case " self.configframe.virt_out_params.index(param)\n": case ' self.configframe.virt_out_params.index(param)\n':
write_outs( write_outs(
output, output,
( (
" self.configframe.virt_out_params.index(param)\n", ' self.configframe.virt_out_params.index(param)\n',
" ],\n", ' ],\n',
" width=6,\n", ' width=6,\n',
), ),
) )
ignore_next_lines = 1 ignore_next_lines = 1
# This does both strip and bus param vars buttons # This does both strip and bus param vars buttons
case " variable=self.configframe.param_vars[i],\n": case ' variable=self.configframe.param_vars[i],\n':
write_outs( write_outs(
output, output,
( (
" variable=self.configframe.param_vars[i],\n", ' variable=self.configframe.param_vars[i],\n',
" width=6,\n", ' width=6,\n',
), ),
) )
case _: case _:
if "Toggle.TButton" in line: if 'Toggle.TButton' in line:
output.write(line.replace("Toggle.TButton", "ToggleButton")) if theme.startswith('forest'):
output.write(
line.replace('Toggle.TButton', 'ToggleButton')
)
elif theme.startswith('azure'):
output.write(
line.replace(
'Toggle.TButton', 'Switch.TCheckbutton'
)
)
else: else:
output.write(line) output.write(line)
def rewrite_menu(): def rewrite_menu(theme):
menu_logger = logger.getChild("menu") menu_logger = logger.getChild('menu')
menu_logger.info("rewriting menu.py") menu_logger.info('rewriting menu.py')
infile = Path(SRC_DIR) / "menu.bk" infile = Path(SRC_DIR) / 'menu.bk'
outfile = Path(PACKAGE_DIR) / "menu.py" outfile = Path(PACKAGE_DIR) / 'menu.py'
with open(infile, "r") as input: with open(infile, 'r') as input:
with open(outfile, "w") as output: with open(outfile, 'w') as output:
ignore_next_lines = 0 ignore_next_lines = 0
for line in input: for line in input:
if ignore_next_lines > 0: if ignore_next_lines > 0:
menu_logger.info(f"ignoring: {line}") menu_logger.info(f'ignoring: {line}')
ignore_next_lines -= 1 ignore_next_lines -= 1
continue continue
match line: match line:
case "import sv_ttk\n": case 'import sv_ttk\n':
output.write("#import sv_ttk\n") output.write('#import sv_ttk\n')
case " # layout/themes\n": case ' # layout/themes\n':
ignore_next_lines = 14 ignore_next_lines = 14
case _: case _:
output.write(line) output.write(line)
def prepare_for_build(): def rewrite_navigation(theme):
navigation_logger = logger.getChild('navigation')
navigation_logger.info('rewriting navigation.py')
infile = Path(SRC_DIR) / 'navigation.bk'
outfile = Path(PACKAGE_DIR) / 'navigation.py'
with open(infile, 'r') as input:
with open(outfile, 'w') as output:
for line in input:
match line:
case ' self.builder.create_info_button()\n':
if theme.startswith('azure'):
output.write(
' # self.builder.create_info_button()\n'
)
else:
output.write(line)
case _:
output.write(line)
def prepare_for_build(theme):
################# MOVE FILES FROM PACKAGE DIR INTO SRC DIR ######################### ################# MOVE FILES FROM PACKAGE DIR INTO SRC DIR #########################
for file in ( for file in (
PACKAGE_DIR / "app.py", PACKAGE_DIR / 'app.py',
PACKAGE_DIR / "builders.py", PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / "menu.py", PACKAGE_DIR / 'menu.py',
PACKAGE_DIR / 'navigation.py',
): ):
if file.exists(): if file.exists():
logger.debug(f"moving {str(file)}") logger.debug(f'moving {str(file)}')
file.rename(SRC_DIR / f"{file.stem}.bk") file.rename(SRC_DIR / f'{file.stem}.bk')
###################### RUN THE FILE REWRITER FOR EACH *.BK ######################### ###################### RUN THE FILE REWRITER FOR EACH *.BK #########################
steps = ( for step in (rewrite_app, rewrite_builders, rewrite_menu, rewrite_navigation):
rewrite_app, step(theme)
rewrite_builders,
rewrite_menu,
)
[step() for step in steps]
def cleanup(): def cleanup():
########################## RESTORE *.BK FILES ##################################### ########################## RESTORE *.BK FILES #####################################
for file in ( for file in (
PACKAGE_DIR / "app.py", SRC_DIR / 'app.bk',
PACKAGE_DIR / "builders.py", SRC_DIR / 'builders.bk',
PACKAGE_DIR / "menu.py", SRC_DIR / 'menu.bk',
SRC_DIR / 'navigation.bk',
): ):
file.unlink() if file.exists():
logger.debug(f'moving {str(file)}')
for file in ( file.replace(PACKAGE_DIR / f'{file.stem}.py')
SRC_DIR / "app.bk",
SRC_DIR / "builders.bk",
SRC_DIR / "menu.bk",
):
file.rename(PACKAGE_DIR / f"{file.stem}.py")
if __name__ == "__main__": if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-r", "--rewrite", action="store_true") parser.add_argument('--rewrite', action='store_true')
parser.add_argument("-c", "--cleanup", action="store_true") parser.add_argument('--theme', type=str, default='forest')
parser.add_argument('--restore', action='store_true')
args = parser.parse_args() args = parser.parse_args()
if args.rewrite: if args.rewrite:
logger.info("preparing files for build") logger.info('preparing files for build')
prepare_for_build() prepare_for_build(args.theme)
elif args.cleanup: elif args.restore:
logger.info("cleaning up files") logger.info('cleaning up files')
cleanup() cleanup()

192
tools/spec_generator.py Normal file
View File

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""
Spec file generator for voicemeeter-compact builds.
Generates Python launcher files and PyInstaller spec files from templates.
"""
import argparse
from pathlib import Path
# Build configuration
THEMES = {
'azure': ['azure-light', 'azure-dark'],
'forest': ['forest-light', 'forest-dark'],
'sunvalley': ['sunvalley'], # Single variant, no light/dark
}
KINDS = ['basic', 'banana', 'potato']
# Templates
PYTHON_TEMPLATE = """import voicemeeterlib
import vmcompact
def main():
KIND_ID = '{kind}'
with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg}
app = vmcompact.connect(KIND_ID, vmr{theme_param})
app.mainloop()
if __name__ == '__main__':
main()
"""
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
added_files = [
( '../../vmcompact/img', 'img' ),{theme_files}
( '../../configs', 'configs' ),
]
a = Analysis(
['{script_name}'],
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}',
)
"""
def generate_python_file(theme_variant: str, kind: str, output_dir: Path) -> None:
"""Generate a Python launcher file."""
if theme_variant == 'sunvalley':
# Sunvalley doesn't use theme parameter
theme_arg = ''
theme_param = ''
else:
theme_arg = f"\n theme = '{theme_variant}'"
theme_param = ', theme=theme'
content = PYTHON_TEMPLATE.format(
kind=kind, theme_arg=theme_arg, theme_param=theme_param
)
filename = f'{theme_variant}-{kind}.py'
output_path = output_dir / filename
with open(output_path, 'w') as f:
f.write(content)
print(f'Generated: {output_path}')
def generate_spec_file(theme_variant: str, kind: str, output_dir: Path) -> None:
"""Generate a PyInstaller spec file."""
script_name = f'{theme_variant}-{kind}.py'
if theme_variant == 'sunvalley':
# Sunvalley doesn't include theme files
theme_files = ''
else:
theme_base = theme_variant.split('-')[0] # 'azure' from 'azure-dark'
theme_files = f"\n ( '../../theme/{theme_base}', 'theme' ),"
content = SPEC_TEMPLATE.format(
script_name=script_name, theme_files=theme_files, kind=kind
)
filename = f'{theme_variant}-{kind}.spec'
output_path = output_dir / filename
with open(output_path, 'w') as f:
f.write(content)
print(f'Generated: {output_path}')
def generate_all_files(output_base_dir: Path) -> None:
"""Generate all Python and spec files for all theme/kind combinations."""
for theme_family, theme_variants in THEMES.items():
theme_dir = output_base_dir / theme_family
theme_dir.mkdir(parents=True, exist_ok=True)
for theme_variant in theme_variants:
for kind in KINDS:
generate_python_file(theme_variant, kind, theme_dir)
generate_spec_file(theme_variant, kind, theme_dir)
def clean_existing_files(output_base_dir: Path) -> None:
"""Remove all existing generated files."""
for theme_family in THEMES.keys():
theme_dir = output_base_dir / theme_family
if theme_dir.exists():
for file in theme_dir.glob('*.py'):
file.unlink()
print(f'Removed: {file}')
for file in theme_dir.glob('*.spec'):
file.unlink()
print(f'Removed: {file}')
def main():
parser = argparse.ArgumentParser(
description='Generate spec files for voicemeeter-compact'
)
parser.add_argument(
'--clean', action='store_true', help='Clean existing files before generating'
)
parser.add_argument(
'--output-dir',
type=Path,
default=Path('spec'),
help='Output directory for spec files (default: spec)',
)
args = parser.parse_args()
if args.clean:
print('Cleaning existing files...')
clean_existing_files(args.output_dir)
print('Generating spec files...')
generate_all_files(args.output_dir)
print('Done!')
if __name__ == '__main__':
main()

View File

@ -6,10 +6,11 @@ from tkinter import messagebox, ttk
from typing import NamedTuple from typing import NamedTuple
import voicemeeterlib import voicemeeterlib
from voicemeeterlib import kinds
from .builders import MainFrameBuilder from .builders import MainFrameBuilder
from .configurations import loader from .configurations import loader
from .data import _base_values, _configuration, _kinds_all, get_configuration from .data import _base_values, _configuration, get_configuration
from .errors import VMCompactError from .errors import VMCompactError
from .menu import Menus from .menu import Menus
from .subject import Subject from .subject import Subject
@ -37,7 +38,7 @@ class App(tk.Tk):
) )
return APP_cls return APP_cls
def __init__(self, vmr): def __init__(self, vmr, theme):
super().__init__() super().__init__()
self.logger = logger.getChild(self.__class__.__name__) self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr self._vmr = vmr
@ -198,14 +199,14 @@ class App(tk.Tk):
self.destroy() self.destroy()
_apps = {kind.name: App.make(kind) for kind in _kinds_all} _apps = {kind.name: App.make(kind) for kind in kinds.all}
def connect(kind_id: str, vmr) -> App: def connect(kind_id: str, vmr, theme=None) -> App:
"""return App of the kind requested""" """return App of the kind requested"""
try: try:
VMMIN_cls = _apps[kind_id] VMMIN_cls = _apps[kind_id]
except KeyError: except KeyError:
raise VMCompactError(f'Invalid kind: {kind_id}') raise VMCompactError(f'Invalid kind: {kind_id}')
return VMMIN_cls(vmr) return VMMIN_cls(vmr, theme)

View File

@ -6,6 +6,7 @@ from tkinter import ttk
import sv_ttk import sv_ttk
from . import util
from .banner import Banner from .banner import Banner
from .channels import _make_channelframe from .channels import _make_channelframe
from .config import BusConfig, StripConfig from .config import BusConfig, StripConfig
@ -227,7 +228,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a progress bar widget to a single label frame""" """Adds a progress bar widget to a single label frame"""
self.labelframe.pb = ttk.Progressbar( self.labelframe.pb = ttk.Progressbar(
self.labelframe, self.labelframe,
maximum=72, maximum=72, # Range: 0 = -60dB, 72 = +12dB (72dB total range)
orient='vertical', orient='vertical',
mode='determinate', mode='determinate',
variable=self.labelframe.level, variable=self.labelframe.level,
@ -362,15 +363,15 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
tk.BooleanVar() for _ in self.configframe.virt_out_params tk.BooleanVar() for _ in self.configframe.virt_out_params
] ]
self.configframe.params = ('mono', 'solo') self.configframe.bool_params = ('mono', 'solo')
self.configframe.param_vars = list( self.configframe.bool_param_vars = list(
tk.BooleanVar() for _ in self.configframe.params tk.BooleanVar() for _ in self.configframe.bool_params
) )
if self.configframe.parent.kind.name in ('banana', 'potato'): if self.configframe.parent.kind.name in ('banana', 'potato'):
if self.configframe.index == self.configframe.phys_in: if self.configframe.index == self.configframe.phys_in:
self.configframe.params = list( self.configframe.params = list(
map(lambda x: x.replace('mono', 'mc'), self.configframe.params) map(lambda x: x.replace('mono', 'mc'), self.configframe.bool_params)
) )
if self.configframe.parent.kind.name == 'banana': if self.configframe.parent.kind.name == 'banana':
pass pass
@ -388,7 +389,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
== self.configframe.phys_in + self.configframe.virt_in - 1 == self.configframe.phys_in + self.configframe.virt_in - 1
): ):
self.configframe.params = list( self.configframe.params = list(
map(lambda x: x.replace('mono', 'mc'), self.configframe.params) map(
lambda x: x.replace('mono', 'mc'),
self.configframe.bool_params,
)
) )
def create_comp_slider(self): def create_comp_slider(self):
@ -542,9 +546,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.pause_updates, self.configframe.toggle_p, param self.configframe.pause_updates, self.configframe.toggle_p, param
), ),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}', style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.param_vars[i], variable=self.configframe.bool_param_vars[i],
) )
for i, param in enumerate(self.configframe.params) for i, param in enumerate(self.configframe.bool_params)
] ]
[ [
button.grid( button.grid(
@ -558,36 +562,41 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
class BusConfigFrameBuilder(ChannelConfigFrameBuilder): class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets""" """Responsible for building channel configframe widgets"""
def __init__(self, configframe, app):
super().__init__(configframe)
self.app = app
def setup(self): def setup(self):
# fmt: off self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind)
self.configframe.bus_mode_map = { self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed(
"normal": "Normal", self.app.kind
"amix": "Mix Down A", )
"bmix": "Mix Down B", self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind)
"repeat": "Stereo Repeat", self.configframe.int_params = ('mono',)
"composite": "Composite", self.configframe.int_param_vars = [
"tvmix": "Up Mix TV", tk.IntVar(value=getattr(self.configframe.target, param))
"upmix21": "Up Mix 2.1", for param in self.configframe.int_params
"upmix41": "Up Mix 4.1", ]
"upmix61": "Up Mix 6.1", self.configframe.mono_modes = util.get_busmono_modes()
"centeronly": "Center Only", self.configframe.bus_mono_label_text = tk.StringVar(
"lfeonly": "LFE Only", value=self.configframe.mono_modes[self.configframe.target.mono]
"rearonly": "Rear Only", )
} self.configframe.bool_params = ('eq.on', 'eq.ab')
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys()) self.configframe.bool_param_vars = [
# fmt: on tk.BooleanVar() for _ in self.configframe.bool_params
self.configframe.params = ('mono', 'eq.on', 'eq.ab') ]
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_label_text = tk.StringVar( self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()] value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
) )
def create_bus_mode_button(self): def create_bus_mode_button(self):
self.configframe.busmode_button = ttk.Button( self.configframe.busmode_button = ttk.Button(
self.configframe, textvariable=self.configframe.bus_mode_label_text self.configframe,
textvariable=self.configframe.bus_mode_label_text,
width=15,
) )
self.configframe.busmode_button.grid( self.configframe.busmode_button.grid(
column=0, row=0, columnspan=2, sticky=(tk.W) column=0, row=0, columnspan=2, sticky=(tk.W), padx=1, pady=1
) )
self.configframe.busmode_button.bind( self.configframe.busmode_button.bind(
'<Button-1>', '<Button-1>',
@ -602,6 +611,24 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
), ),
) )
def create_bus_mono_button(self):
self.configframe.mono_button = ttk.Button(
self.configframe,
textvariable=self.configframe.bus_mono_label_text,
width=15,
)
self.configframe.mono_button.bind(
'<Button-1>',
partial(self.configframe.pause_updates, self.configframe.rotate_mono_right),
)
self.configframe.mono_button.bind(
'<Button-3>',
partial(self.configframe.pause_updates, self.configframe.rotate_mono_left),
)
self.configframe.mono_button.grid(
column=0, row=1, sticky=(tk.W), padx=1, pady=1
)
def create_param_buttons(self): def create_param_buttons(self):
param_buttons = [ param_buttons = [
ttk.Checkbutton( ttk.Checkbutton(
@ -611,13 +638,13 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.pause_updates, self.configframe.toggle_p, param self.configframe.pause_updates, self.configframe.toggle_p, param
), ),
style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}', style=f'{"Toggle.TButton" if _configuration.themes_enabled else f"{param}.TButton"}',
variable=self.configframe.param_vars[i], variable=self.configframe.bool_param_vars[i],
) )
for i, param in enumerate(self.configframe.params) for i, param in enumerate(self.configframe.bool_params)
] ]
[ [
button.grid( button.grid(
column=i, column=i + 1,
row=1, row=1,
) )
for i, button in enumerate(param_buttons) for i, button in enumerate(param_buttons)

View File

@ -197,14 +197,22 @@ class Strip(ChannelLabelFrame):
def upd_levels(self): def upd_levels(self):
""" """
Updates level values. Updates level values using direct dB values.
""" """
if self.index < self.parent.parent.kind.num_strip: if self.index < self.parent.parent.kind.num_strip:
if self.target.levels.is_updated: if self.target.levels.is_updated:
val = max(self.target.levels.prefader) val = max(self.target.levels.prefader)
self.level.set( if val < -72:
(0 if self.mute.get() else 72 + val - 12 + self.gain.get()) if self.level.get() != 0:
) self.level.set(0)
return
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
if self.mute.get():
level_display = 0
else:
level_db = val + self.gain.get()
level_display = max(0, min(72, level_db + 60))
self.level.set(level_display)
class Bus(ChannelLabelFrame): class Bus(ChannelLabelFrame):
@ -223,9 +231,18 @@ class Bus(ChannelLabelFrame):
def upd_levels(self): def upd_levels(self):
if self.index < self.parent.parent.kind.num_bus: if self.index < self.parent.parent.kind.num_bus:
if self.target.levels.is_updated or self.level.get() != -118: if self.target.levels.is_updated:
val = max(self.target.levels.all) val = max(self.target.levels.all)
self.level.set((0 if self.mute.get() else 72 + val - 12)) if val < -72:
if self.level.get() != 0:
self.level.set(0)
return
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
if self.mute.get():
level_display = 0
else:
level_display = max(0, min(72, val + 60))
self.level.set(level_display)
class ChannelFrame(ttk.Frame): class ChannelFrame(ttk.Frame):

View File

@ -98,7 +98,7 @@ class Config(ttk.Frame):
self.slider_vars[self.slider_params.index(param)].set(val) self.slider_vars[self.slider_params.index(param)].set(val)
def toggle_p(self, param): def toggle_p(self, param):
val = self.param_vars[self.params.index(param)].get() val = self.bool_param_vars[self.bool_params.index(param)].get()
self.setter(param, val) self.setter(param, val)
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
self.styletable.configure( self.styletable.configure(
@ -177,15 +177,14 @@ class StripConfig(Config):
for i, param in enumerate(self.virt_out_params) for i, param in enumerate(self.virt_out_params)
] ]
[ [
self.param_vars[i].set(self.getter(param)) self.bool_param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params) for i, param in enumerate(self.bool_params)
]
[
self.slider_vars[i].set(self.getter(param))
for i, param in enumerate(self.slider_params)
if self.index < self.phys_in
] ]
if not _base_values.vban_connected: # slider vars not defined in RT Packet
[
self.slider_vars[i].set(self.getter(param))
for i, param in enumerate(self.slider_params)
if self.index < self.phys_in
]
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
[ [
@ -207,7 +206,7 @@ class StripConfig(Config):
f'{param}.TButton', f'{param}.TButton',
background=f'{"green" if self.param_vars[i].get() else "white"}', background=f'{"green" if self.param_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.params) for i, param in enumerate(self.bool_params)
] ]
@ -218,7 +217,7 @@ class BusConfig(Config):
self.grid(column=0, row=1, columnspan=4, padx=(2,)) self.grid(column=0, row=1, columnspan=4, padx=(2,))
else: else:
self.grid(column=0, row=3, columnspan=4, padx=(2,)) self.grid(column=0, row=3, columnspan=4, padx=(2,))
self.builder = builders.BusConfigFrameBuilder(self) self.builder = builders.BusConfigFrameBuilder(self, parent)
self.builder.setup() self.builder.setup()
self.make_row_0() self.make_row_0()
self.make_row_1() self.make_row_1()
@ -238,53 +237,56 @@ class BusConfig(Config):
self.builder.create_bus_mode_button() self.builder.create_bus_mode_button()
def make_row_1(self): def make_row_1(self):
self.builder.create_bus_mono_button()
self.builder.create_param_buttons() self.builder.create_param_buttons()
def current_bus_mode(self): def current_bus_mode(self):
return self.target.mode.get() return self.target.mode.get()
def rotate_bus_modes_right(self, *args): def rotate_bus_modes_right(self, *args):
current_mode = self.current_bus_mode() current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
next = self.bus_modes.index(current_mode) + 1 current_index = self.bus_modes.index(current_mode)
if next < len(self.bus_modes): next_index = (current_index + 1) % len(self.bus_modes)
setattr( next_mode = self.bus_modes[next_index]
self.target.mode,
self.bus_modes[next], setattr(self.target.mode, next_mode, True)
True, self.bus_mode_label_text.set(self.bus_mode_map[next_mode])
)
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[next]])
else:
self.target.mode.normal = True
self.bus_mode_label_text.set('Normal')
def rotate_bus_modes_left(self, *args): def rotate_bus_modes_left(self, *args):
current_mode = self.current_bus_mode() current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
prev = self.bus_modes.index(current_mode) - 1 current_index = self.bus_modes.index(current_mode)
if prev < 0: prev_index = (current_index - 1) % len(self.bus_modes)
self.target.mode.rearonly = True prev_mode = self.bus_modes[prev_index]
self.bus_mode_label_text.set('Rear Only')
else: setattr(self.target.mode, prev_mode, True)
setattr( self.bus_mode_label_text.set(self.bus_mode_map[prev_mode])
self.target.mode,
self.bus_modes[prev], def rotate_mono_right(self, *args):
True, current_val = self.mono_modes.index(self.bus_mono_label_text.get())
) next_val = (current_val + 1) % 3
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]]) self.bus_mono_label_text.set(self.mono_modes[next_val])
self.setter('mono', next_val)
def rotate_mono_left(self, *args):
current_val = self.mono_modes.index(self.bus_mono_label_text.get())
next_val = (current_val - 1) % 3
self.bus_mono_label_text.set(self.mono_modes[next_val])
self.setter('mono', next_val)
def teardown(self): def teardown(self):
self.builder.teardown() self.builder.teardown()
def sync(self): def sync(self):
[ [
self.param_vars[i].set(self.getter(param)) self.bool_param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params) for i, param in enumerate(self.bool_params)
] ]
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()]) self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
if not _configuration.themes_enabled: if not _configuration.themes_enabled:
[ [
self.styletable.configure( self.styletable.configure(
f'{param}.TButton', f'{param}.TButton',
background=f'{"green" if self.param_vars[i].get() else "white"}', background=f'{"green" if self.bool_param_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.params) for i, param in enumerate(self.bool_params)
] ]

View File

@ -12,14 +12,14 @@ configuration = {}
def get_configpath(): def get_configpath():
configpaths = [ for pn in (
Path.home() / '.config' / 'vm-compact',
Path.home() / 'Documents' / 'Voicemeeter' / 'vm-compact',
Path.cwd() / '_internal' / 'configs',
Path.cwd() / 'configs', Path.cwd() / 'configs',
Path.home() / '.config' / 'vm-compact' / 'configs', ):
Path.home() / 'Documents' / 'Voicemeeter' / 'configs', if pn.exists():
] return pn
for configpath in configpaths:
if configpath.exists():
return configpath
if configpath := get_configpath(): if configpath := get_configpath():
@ -60,7 +60,7 @@ _defaults = {
'submixes': { 'submixes': {
'default': 0, 'default': 0,
}, },
'navigation': {'show': True}, 'navigation': {'show': False},
} }

View File

@ -61,10 +61,6 @@ class BaseValues(metaclass=SingletonMeta):
_base_values = BaseValues() _base_values = BaseValues()
_configuration = Configurations() _configuration = Configurations()
_kinds = {kind.name: kind for kind in kinds.kinds_all}
_kinds_all = _kinds.values()
def kind_get(kind_id): def kind_get(kind_id):
return _kinds[kind_id] return kinds.request_kind_map(kind_id)

View File

@ -161,17 +161,18 @@ class GainLayer(ttk.LabelFrame):
""" """
Updates level values. Updates level values.
""" """
if self.parent.target.strip[self.index].levels.is_updated: if self.parent.target.strip[self.index].levels.is_updated:
val = max(self.parent.target.strip[self.index].levels.prefader) val = max(self.parent.target.strip[self.index].levels.prefader)
self.level.set( # Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
( if (
0 self.parent.parent.strip_frame.strips[self.index].mute.get()
if self.parent.parent.strip_frame.strips[self.index].mute.get() or not self.on.get()
or not self.on.get() ):
else 72 + val - 12 + self.gain.get() level_display = 0
) else:
) level_db = val + self.gain.get()
level_display = max(0, min(72, level_db + 60))
self.level.set(level_display)
def grid_configure(self): def grid_configure(self):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S)) self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))

View File

@ -357,15 +357,17 @@ class Menus(tk.Menu):
opts = {} opts = {}
opts |= self.vban_config[f'connection-{i + 1}'] opts |= self.vban_config[f'connection-{i + 1}']
kind_id = opts.pop('kind') kind_id = opts.pop('kind')
if 'ip' in opts:
opts['host'] = opts.pop('ip')
self.vban = vban_cmd.api(kind_id, **opts) self.vban = vban_cmd.api(kind_id, **opts)
# login to vban interface # login to vban interface
try: try:
self.logger.info(f'Attempting vban connection to {opts.get("ip")}') self.logger.info(f'Attempting vban connection to {opts.get("host")}')
self.vban.login() self.vban.login()
except VBANCMDConnectionError as e: except VBANCMDConnectionError as e:
self.vban.logout() self.vban.logout()
msg = ( msg = (
f'Timeout attempting to establish connection to {opts.get("ip")}', f'Timeout attempting to establish connection to {opts.get("host")}',
'Please check your connection settings', 'Please check your connection settings',
) )
messagebox.showerror('Connection Error', '\n'.join(msg)) messagebox.showerror('Connection Error', '\n'.join(msg))
@ -421,7 +423,7 @@ class Menus(tk.Menu):
del self.parent.__dict__['userconfigs'] del self.parent.__dict__['userconfigs']
self.menu_setup() self.menu_setup()
self.after(15000, self.enable_vban_menus) self.after(50, self.enable_vban_menus)
def documentation(self): def documentation(self):
webbrowser.open_new(r'https://voicemeeter.com/') webbrowser.open_new(r'https://voicemeeter.com/')

34
vmcompact/util.py Normal file
View File

@ -0,0 +1,34 @@
def get_busmode_fullnames(kind) -> dict:
if kind.name == 'basic':
return {
'normal': 'Normal',
'amix': 'Mix Down A',
'repeat': 'Stereo Repeat',
'composite': 'Composite',
}
return {
'normal': 'Normal',
'amix': 'Mix Down A',
'bmix': 'Mix Down B',
'repeat': 'Stereo Repeat',
'composite': 'Composite',
'tvmix': 'Up Mix TV',
'upmix21': 'Up Mix 2.1',
'upmix41': 'Up Mix 4.1',
'upmix61': 'Up Mix 6.1',
'centeronly': 'Center Only',
'lfeonly': 'LFE Only',
'rearonly': 'Rear Only',
}
def get_busmode_fullnames_reversed(kind) -> dict:
return {v: k for k, v in get_busmode_fullnames(kind).items()}
def get_busmode_shortnames(kind) -> list:
return list(get_busmode_fullnames(kind).keys())
def get_busmono_modes() -> list:
return ['Mono: off', 'Mono: on', 'Stereo Reverse']