Compare commits

..

33 Commits

Author SHA1 Message Date
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
31 changed files with 1381 additions and 219 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

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

@ -0,0 +1,216 @@
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
run: poetry install --with build
shell: bash
- name: Build artifacts with dynamic taskfile
run: task --taskfile Taskfile.dynamic.yml build-all
shell: bash
env:
POETRY_BIN: /c/Users/runneradmin/.local/bin/poetry
- name: Create release archives
run: task --taskfile Taskfile.dynamic.yml compress-all
shell: bash
env:
POETRY_BIN: /c/Users/runneradmin/.local/bin/poetry
# 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
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 "Release $TAG_NAME" --generate-notes
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'

112
.gitignore vendored
View File

@ -1,9 +1,9 @@
# quick test
quick.py
# Generated by ignr: github.com/onyx-and-iris/ignr
## Python ##
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*.py[codz]
*$py.class
# C extensions
@ -23,7 +23,6 @@ parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
@ -50,9 +49,10 @@ htmlcov/
nosetests.xml
coverage.xml
*.cover
*.py,cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
@ -75,6 +75,7 @@ instance/
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
@ -85,7 +86,9 @@ profile_default/
ipython_config.py
# 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
# 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.
#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__/
# Celery stuff
@ -106,13 +139,13 @@ celerybeat.pid
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
venv_vmcompact/
# Spyder project settings
.spyderproject
@ -132,8 +165,65 @@ dmypy.json
# Pyre type checker
.pyre/
# build
theme/
spec/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# 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

@ -139,13 +139,13 @@ A valid `vban.toml` might look like this:
```toml
[connection-1]
kind = 'banana'
ip = '192.168.1.2'
host = '192.168.1.2'
streamname = 'worklaptop'
port = 6980
[connection-2]
kind = 'potato'
ip = '192.168.1.3'
host = '192.168.1.3'
streamname = 'streampc'
port = 6990
```

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"

76
Taskfile.dynamic.yml Normal file
View File

@ -0,0 +1,76 @@
version: '3'
# Dynamic build system - no spec files needed!
# Usage: task build THEMES="azure forest" or task build-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"

View File

@ -1,5 +1,19 @@
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
@ -16,66 +30,32 @@ tasks:
- 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
cmds:
- task: build-sunvalley
- echo "Sunvalley build complete"
- task: build-forest
- echo "Forest build complete"
build-sunvalley:
desc: Build Sunvalley artifacts
deps: [generate-specs]
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
cmd: poetry run pyinstaller --noconfirm --distpath dist/sunvalley-{{.ITEM.KIND}} spec/sunvalley-{{.ITEM.KIND}}.spec
build-forest:
desc: Build Forest artifacts
deps: [rewrite]
cmds:
- defer: { task: restore }
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [light, dark]
cmd: poetry run pyinstaller --noconfirm --distpath dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}} spec/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}}.spec
rewrite:
internal: true
desc: Run the source code rewriter
cmds:
- poetry run python tools/rewriter.py --rewrite
restore:
internal: true
desc: Restore the backup files
cmds:
- poetry run python tools/rewriter.py --restore
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:build'
compress:
deps: [compress-sunvalley, compress-forest]
compress-sunvalley:
desc: Compress all 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"'
compress-forest:
cmds:
- for:
matrix:
KIND: [basic, banana, potato]
THEME: [light, dark]
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}} -DestinationPath dist/forest-{{.ITEM.KIND}}-{{.ITEM.THEME}}.zip -Force"'
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:compress'
clean:
desc: Clean up build and dist directories
cmds:
- |
{{.SHELL}} -Command "
Remove-Item -Path build/forest-*,build/sunvalley-*,dist/forest-*,dist/sunvalley-* -Recurse -Force"
- for:
matrix:
THEME: [sunvalley, forest, azure]
task: '{{.ITEM.THEME}}:clean'

View File

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

View File

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

8
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.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]]
name = "altgraph"
@ -223,7 +223,7 @@ files = [
[[package]]
name = "vban-cmd"
version = "2.5.2"
version = "2.10.2"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
optional = false
python-versions = ">=3.10"
@ -240,7 +240,7 @@ url = "../vban-cmd-python"
[[package]]
name = "voicemeeter-api"
version = "2.7.1"
version = "2.7.2"
description = "A Python wrapper for the Voiceemeter API"
optional = false
python-versions = ">=3.10"
@ -258,4 +258,4 @@ url = "../voicemeeter-api-python"
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<3.14"
content-hash = "d457ab1aaa0beaad130dc9a2a04e5e1f8c1b29ed1d0e7d43183e2648fd9ed47c"
content-hash = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71"

View File

@ -1,22 +1,22 @@
[project]
name = "voicemeeter-compact"
version = "1.9.8"
version = "1.10.0"
description = "A Compact Voicemeeter Remote App"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"voicemeeter-api (>=2.6.1,<3.0.0)",
"vban-cmd (>=2.5.0,<3.0.0)",
"voicemeeter-api (>=2.7.2,<3.0.0)",
"vban-cmd (>=2.10.2,<3.0.0)",
"sv-ttk (>=2.6.0,<3.0.0)",
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
]
[project.scripts]
gui-basic-compact = "vmcompact.gui.basic:run"
gui-banana-compact = "vmcompact.gui.banana:run"
gui-potato-compact = "vmcompact.gui.potato:run"
voicemeeter-compact-basic = "vmcompact.gui.basic:run"
voicemeeter-compact-banana = "vmcompact.gui.banana:run"
voicemeeter-compact-potato = "vmcompact.gui.potato:run"
[tool.poetry]
packages = [{ include = "vmcompact" }]

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,3 +1,8 @@
#!/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 logging
from pathlib import Path
@ -16,7 +21,7 @@ def write_outs(output, outs: tuple):
output.write(out)
def rewrite_app():
def rewrite_app(theme):
app_logger = logger.getChild('app')
app_logger.info('rewriting app.py')
infile = Path(SRC_DIR) / 'app.bk'
@ -25,33 +30,29 @@ def rewrite_app():
with open(outfile, 'w') as output:
for line in input:
match line:
# App init()
case ' def __init__(self, vmr):\n':
output.write(' def __init__(self, vmr, theme):\n')
case ' self._vmr = vmr\n':
write_outs(
output,
(
' self._vmr = vmr\n',
' self._theme = theme\n',
' tcldir = Path.cwd() / "theme"\n',
' self._theme_name = theme.split("-")[0]\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',
' 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 _:
output.write(line)
def rewrite_builders():
def rewrite_builders(theme):
builders_logger = logger.getChild('builders')
builders_logger.info('rewriting builders.py')
infile = Path(SRC_DIR) / 'builders.bk'
@ -71,13 +72,24 @@ def rewrite_builders():
case 'import sv_ttk\n':
output.write('#import sv_ttk\n')
case ' self.app.resizable(False, False)\n':
if theme.startswith('forest'):
write_outs(
output,
(
' self.app.resizable(False, False)\n'
' if _configuration.themes_enabled:\n',
' ttk.Style().theme_use(f"forest-{self.app._theme}")\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
@ -171,12 +183,21 @@ def rewrite_builders():
)
case _:
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:
output.write(line)
def rewrite_menu():
def rewrite_menu(theme):
menu_logger = logger.getChild('menu')
menu_logger.info('rewriting menu.py')
infile = Path(SRC_DIR) / 'menu.bk'
@ -199,52 +220,66 @@ def rewrite_menu():
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 #########################
for file in (
PACKAGE_DIR / 'app.py',
PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / 'menu.py',
PACKAGE_DIR / 'navigation.py',
):
if file.exists():
logger.debug(f'moving {str(file)}')
file.rename(SRC_DIR / f'{file.stem}.bk')
###################### RUN THE FILE REWRITER FOR EACH *.BK #########################
steps = (
rewrite_app,
rewrite_builders,
rewrite_menu,
)
[step() for step in steps]
for step in (rewrite_app, rewrite_builders, rewrite_menu, rewrite_navigation):
step(theme)
def cleanup():
########################## RESTORE *.BK FILES #####################################
for file in (
PACKAGE_DIR / 'app.py',
PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / 'menu.py',
):
file.unlink()
for file in (
SRC_DIR / 'app.bk',
SRC_DIR / 'builders.bk',
SRC_DIR / 'menu.bk',
SRC_DIR / 'navigation.bk',
):
file.rename(PACKAGE_DIR / f'{file.stem}.py')
if file.exists():
logger.debug(f'moving {str(file)}')
file.replace(PACKAGE_DIR / f'{file.stem}.py')
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('--rewrite', action='store_true')
parser.add_argument('--theme', type=str, default='forest')
parser.add_argument('--restore', action='store_true')
args = parser.parse_args()
if args.rewrite:
logger.info('preparing files for build')
prepare_for_build()
prepare_for_build(args.theme)
elif args.restore:
logger.info('cleaning up files')
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
import voicemeeterlib
from voicemeeterlib import kinds
from .builders import MainFrameBuilder
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 .menu import Menus
from .subject import Subject
@ -37,7 +38,7 @@ class App(tk.Tk):
)
return APP_cls
def __init__(self, vmr):
def __init__(self, vmr, theme):
super().__init__()
self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr
@ -198,14 +199,14 @@ class App(tk.Tk):
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"""
try:
VMMIN_cls = _apps[kind_id]
except KeyError:
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
from . import util
from .banner import Banner
from .channels import _make_channelframe
from .config import BusConfig, StripConfig
@ -227,7 +228,7 @@ class ChannelLabelFrameBuilder(AbstractBuilder):
"""Adds a progress bar widget to a single label frame"""
self.labelframe.pb = ttk.Progressbar(
self.labelframe,
maximum=72,
maximum=72, # Range: 0 = -60dB, 72 = +12dB (72dB total range)
orient='vertical',
mode='determinate',
variable=self.labelframe.level,
@ -362,15 +363,15 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
tk.BooleanVar() for _ in self.configframe.virt_out_params
]
self.configframe.params = ('mono', 'solo')
self.configframe.param_vars = list(
tk.BooleanVar() for _ in self.configframe.params
self.configframe.bool_params = ('mono', 'solo')
self.configframe.bool_param_vars = list(
tk.BooleanVar() for _ in self.configframe.bool_params
)
if self.configframe.parent.kind.name in ('banana', 'potato'):
if self.configframe.index == self.configframe.phys_in:
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':
pass
@ -388,7 +389,10 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
== self.configframe.phys_in + self.configframe.virt_in - 1
):
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):
@ -542,9 +546,9 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.pause_updates, self.configframe.toggle_p, param
),
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(
@ -558,36 +562,41 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets"""
def __init__(self, configframe, app):
super().__init__(configframe)
self.app = app
def setup(self):
# fmt: off
self.configframe.bus_mode_map = {
"normal": "Normal",
"amix": "Mix Down A",
"bmix": "Mix Down B",
"repeat": "Stereo Repeat",
"composite": "Composite",
"tvmix": "Up Mix TV",
"upmix21": "Up Mix 2.1",
"upmix41": "Up Mix 4.1",
"upmix61": "Up Mix 6.1",
"centeronly": "Center Only",
"lfeonly": "LFE Only",
"rearonly": "Rear Only",
}
self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
# fmt: on
self.configframe.params = ('mono', 'eq.on', 'eq.ab')
self.configframe.param_vars = [tk.BooleanVar() for _ in self.configframe.params]
self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind)
self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed(
self.app.kind
)
self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind)
self.configframe.int_params = ('mono',)
self.configframe.int_param_vars = [
tk.IntVar(value=getattr(self.configframe.target, param))
for param in self.configframe.int_params
]
self.configframe.mono_modes = util.get_busmono_modes()
self.configframe.bus_mono_label_text = tk.StringVar(
value=self.configframe.mono_modes[self.configframe.target.mono]
)
self.configframe.bool_params = ('eq.on', 'eq.ab')
self.configframe.bool_param_vars = [
tk.BooleanVar() for _ in self.configframe.bool_params
]
self.configframe.bus_mode_label_text = tk.StringVar(
value=self.configframe.bus_mode_map[self.configframe.current_bus_mode()]
)
def create_bus_mode_button(self):
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(
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(
'<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):
param_buttons = [
ttk.Checkbutton(
@ -611,13 +638,13 @@ class BusConfigFrameBuilder(ChannelConfigFrameBuilder):
self.configframe.pause_updates, self.configframe.toggle_p, param
),
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(
column=i,
column=i + 1,
row=1,
)
for i, button in enumerate(param_buttons)

View File

@ -197,14 +197,22 @@ class Strip(ChannelLabelFrame):
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.target.levels.is_updated:
val = max(self.target.levels.prefader)
self.level.set(
(0 if self.mute.get() else 72 + val - 12 + self.gain.get())
)
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_db = val + self.gain.get()
level_display = max(0, min(72, level_db + 60))
self.level.set(level_display)
class Bus(ChannelLabelFrame):
@ -223,9 +231,18 @@ class Bus(ChannelLabelFrame):
def upd_levels(self):
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)
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):

View File

@ -98,7 +98,7 @@ class Config(ttk.Frame):
self.slider_vars[self.slider_params.index(param)].set(val)
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)
if not _configuration.themes_enabled:
self.styletable.configure(
@ -177,10 +177,9 @@ class StripConfig(Config):
for i, param in enumerate(self.virt_out_params)
]
[
self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params)
self.bool_param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params)
]
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)
@ -207,7 +206,7 @@ class StripConfig(Config):
f'{param}.TButton',
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,))
else:
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.make_row_0()
self.make_row_1()
@ -238,53 +237,56 @@ class BusConfig(Config):
self.builder.create_bus_mode_button()
def make_row_1(self):
self.builder.create_bus_mono_button()
self.builder.create_param_buttons()
def current_bus_mode(self):
return self.target.mode.get()
def rotate_bus_modes_right(self, *args):
current_mode = self.current_bus_mode()
next = self.bus_modes.index(current_mode) + 1
if next < len(self.bus_modes):
setattr(
self.target.mode,
self.bus_modes[next],
True,
)
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')
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
current_index = self.bus_modes.index(current_mode)
next_index = (current_index + 1) % len(self.bus_modes)
next_mode = self.bus_modes[next_index]
setattr(self.target.mode, next_mode, True)
self.bus_mode_label_text.set(self.bus_mode_map[next_mode])
def rotate_bus_modes_left(self, *args):
current_mode = self.current_bus_mode()
prev = self.bus_modes.index(current_mode) - 1
if prev < 0:
self.target.mode.rearonly = True
self.bus_mode_label_text.set('Rear Only')
else:
setattr(
self.target.mode,
self.bus_modes[prev],
True,
)
self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()]
current_index = self.bus_modes.index(current_mode)
prev_index = (current_index - 1) % len(self.bus_modes)
prev_mode = self.bus_modes[prev_index]
setattr(self.target.mode, prev_mode, True)
self.bus_mode_label_text.set(self.bus_mode_map[prev_mode])
def rotate_mono_right(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 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):
self.builder.teardown()
def sync(self):
[
self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.params)
self.bool_param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params)
]
self.bus_mode_label_text.set(self.bus_mode_map[self.current_bus_mode()])
if not _configuration.themes_enabled:
[
self.styletable.configure(
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

@ -60,7 +60,7 @@ _defaults = {
'submixes': {
'default': 0,
},
'navigation': {'show': True},
'navigation': {'show': False},
}

View File

@ -61,10 +61,6 @@ class BaseValues(metaclass=SingletonMeta):
_base_values = BaseValues()
_configuration = Configurations()
_kinds = {kind.name: kind for kind in kinds.kinds_all}
_kinds_all = _kinds.values()
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.
"""
if self.parent.target.strip[self.index].levels.is_updated:
val = max(self.parent.target.strip[self.index].levels.prefader)
self.level.set(
(
0
if self.parent.parent.strip_frame.strips[self.index].mute.get()
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72
if (
self.parent.parent.strip_frame.strips[self.index].mute.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):
self.grid(padx=_configuration.channel_xpadding, sticky=(tk.N, tk.S))

View File

@ -357,15 +357,17 @@ class Menus(tk.Menu):
opts = {}
opts |= self.vban_config[f'connection-{i + 1}']
kind_id = opts.pop('kind')
if 'ip' in opts:
opts['host'] = opts.pop('ip')
self.vban = vban_cmd.api(kind_id, **opts)
# login to vban interface
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()
except VBANCMDConnectionError as e:
self.vban.logout()
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',
)
messagebox.showerror('Connection Error', '\n'.join(msg))
@ -421,7 +423,7 @@ class Menus(tk.Menu):
del self.parent.__dict__['userconfigs']
self.menu_setup()
self.after(500, self.enable_vban_menus)
self.after(50, self.enable_vban_menus)
def documentation(self):
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']