Compare commits

..

No commits in common. "cf3205a86fa8a19d4286da84d816b5fe3be68d15" and "edc76db88e990465fb1dc878ca017837d3ae2ab1" have entirely different histories.

31 changed files with 219 additions and 1381 deletions

View File

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

View File

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

View File

@ -1,19 +0,0 @@
name: Ruff
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/ruff-action@v3
with:
args: 'format --check --diff'

114
.gitignore vendored
View File

@ -1,9 +1,9 @@
# Generated by ignr: github.com/onyx-and-iris/ignr # quick test
quick.py
## Python ##
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[codz] *.py[cod]
*$py.class *$py.class
# C extensions # C extensions
@ -23,6 +23,7 @@ parts/
sdist/ sdist/
var/ var/
wheels/ wheels/
pip-wheel-metadata/
share/python-wheels/ share/python-wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
@ -49,10 +50,9 @@ 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,7 +75,6 @@ instance/
docs/_build/ docs/_build/
# PyBuilder # PyBuilder
.pybuilder/
target/ target/
# Jupyter Notebook # Jupyter Notebook
@ -86,9 +85,7 @@ profile_default/
ipython_config.py ipython_config.py
# pyenv # pyenv
# For a library or package, you might want to ignore these files since the code is .python-version
# 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.
@ -97,37 +94,7 @@ ipython_config.py
# install all needed dependencies. # install all needed dependencies.
#Pipfile.lock #Pipfile.lock
# UV # PEP 582; used by e.g. github.com/David-OConnor/pyflow
# 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
@ -139,13 +106,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
@ -165,65 +132,8 @@ dmypy.json
# Pyre type checker # Pyre type checker
.pyre/ .pyre/
# pytype static type analyzer # build
.pytype/ theme/
spec/
# Cython debug symbols .vscode/
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 ```toml
[connection-1] [connection-1]
kind = 'banana' kind = 'banana'
host = '192.168.1.2' ip = '192.168.1.2'
streamname = 'worklaptop' streamname = 'worklaptop'
port = 6980 port = 6980
[connection-2] [connection-2]
kind = 'potato' kind = 'potato'
host = '192.168.1.3' ip = '192.168.1.3'
streamname = 'streampc' streamname = 'streampc'
port = 6990 port = 6990
``` ```

View File

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

View File

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

View File

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

View File

@ -1,24 +0,0 @@
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,19 +1,5 @@
version: '3' 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: vars:
SHELL: pwsh SHELL: pwsh
@ -30,32 +16,66 @@ tasks:
- task: compress - task: compress
- echo "Release complete" - echo "Release complete"
generate-specs:
desc: Generate all spec files from templates
cmds:
- poetry run python tools/spec_generator.py --clean
build: build:
desc: Build all artifacts desc: Build all artifacts
deps: [generate-specs] cmds:
- task: build-sunvalley
- echo "Sunvalley build complete"
- task: build-forest
- echo "Forest build complete"
build-sunvalley:
desc: Build Sunvalley artifacts
cmds: cmds:
- for: - for:
matrix: matrix:
THEME: [sunvalley, forest, azure] KIND: [basic, banana, potato]
task: '{{.ITEM.THEME}}:build' 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
compress: compress:
desc: Compress all artifacts deps: [compress-sunvalley, compress-forest]
compress-sunvalley:
cmds: cmds:
- for: - for:
matrix: matrix:
THEME: [sunvalley, forest, azure] KIND: [basic, banana, potato]
task: '{{.ITEM.THEME}}:compress' 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"'
clean: clean:
desc: Clean up build and dist directories desc: Clean up build and dist directories
cmds: cmds:
- for: - |
matrix: {{.SHELL}} -Command "
THEME: [sunvalley, forest, azure] Remove-Item -Path build/forest-*,build/sunvalley-*,dist/forest-*,dist/sunvalley-* -Recurse -Force"
task: '{{.ITEM.THEME}}:clean'

View File

@ -1,9 +1,10 @@
# 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? # load with themes enabled? set the default mode
[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
@ -21,4 +22,4 @@ size = 3
default = 0 default = 0
# show the navigation frame? # show the navigation frame?
[navigation] [navigation]
show = false show = true

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 = 'localhost' # ip = '<ip address 1>'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6980
# [connection-2] # [connection-2]
# kind = 'potato' # kind = 'potato'
# ip = 'gamepc.local' # ip = '<ip address 2>'
# streamname = 'Command1' # streamname = 'Command1'
# port = 6980 # port = 6980

8
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. # This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand.
[[package]] [[package]]
name = "altgraph" name = "altgraph"
@ -223,7 +223,7 @@ files = [
[[package]] [[package]]
name = "vban-cmd" name = "vban-cmd"
version = "2.10.2" version = "2.5.2"
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"
@ -240,7 +240,7 @@ url = "../vban-cmd-python"
[[package]] [[package]]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.7.2" version = "2.7.1"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
optional = false optional = false
python-versions = ">=3.10" python-versions = ">=3.10"
@ -258,4 +258,4 @@ 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 = "f1e1782280c5e165fef043ca2695ea5f5c93fd00a66ace809266e0196fef6b71" content-hash = "d457ab1aaa0beaad130dc9a2a04e5e1f8c1b29ed1d0e7d43183e2648fd9ed47c"

View File

@ -1,22 +1,22 @@
[project] [project]
name = "voicemeeter-compact" name = "voicemeeter-compact"
version = "1.10.0" version = "1.9.8"
description = "A Compact Voicemeeter Remote App" description = "A Compact Voicemeeter Remote App"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }] authors = [{ 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.7.2,<3.0.0)", "voicemeeter-api (>=2.6.1,<3.0.0)",
"vban-cmd (>=2.10.2,<3.0.0)", "vban-cmd (>=2.5.0,<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]
voicemeeter-compact-basic = "vmcompact.gui.basic:run" gui-basic-compact = "vmcompact.gui.basic:run"
voicemeeter-compact-banana = "vmcompact.gui.banana:run" gui-banana-compact = "vmcompact.gui.banana:run"
voicemeeter-compact-potato = "vmcompact.gui.potato:run" gui-potato-compact = "vmcompact.gui.potato:run"
[tool.poetry] [tool.poetry]
packages = [{ include = "vmcompact" }] packages = [{ include = "vmcompact" }]

View File

View File

View File

View File

View File

@ -1,322 +0,0 @@
#!/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,8 +1,3 @@
#!/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
@ -21,7 +16,7 @@ def write_outs(output, outs: tuple):
output.write(out) output.write(out)
def rewrite_app(theme): def rewrite_app():
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'
@ -30,29 +25,33 @@ def rewrite_app(theme):
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 ' def __init__(self, vmr):\n':
output.write(' def __init__(self, vmr, theme):\n')
case ' self._vmr = vmr\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',
' self._theme_name = theme.split("-")[0]\n', ' tcldir = Path.cwd() / "theme"\n',
' self._theme_type = theme.split("-")[-1]\n',
' tcldir = Path.cwd() / "theme" / self._theme_name\n',
' if not tcldir.is_dir():\n', ' if not tcldir.is_dir():\n',
' tcldir = Path.cwd() / "_internal" / "theme"\n', ' tcldir = Path.cwd() / "_internal" / "theme"\n',
' match self._theme_name:\n', ' self.tk.call("source", tcldir.resolve() / f"forest-{self._theme}.tcl")\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(theme): def rewrite_builders():
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'
@ -72,26 +71,15 @@ def rewrite_builders(theme):
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':
if theme.startswith('forest'): write_outs(
write_outs( output,
output, (
( ' self.app.resizable(False, False)\n'
' self.app.resizable(False, False)\n' ' if _configuration.themes_enabled:\n',
' if _configuration.themes_enabled:\n', ' ttk.Style().theme_use(f"forest-{self.app._theme}")\n',
' ttk.Style().theme_use(self.app._theme)\n', ' self.logger.info(f"Forest Theme applied")\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':
@ -183,21 +171,12 @@ def rewrite_builders(theme):
) )
case _: case _:
if 'Toggle.TButton' in line: if 'Toggle.TButton' in line:
if theme.startswith('forest'): output.write(line.replace('Toggle.TButton', 'ToggleButton'))
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(theme): def rewrite_menu():
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'
@ -220,66 +199,52 @@ def rewrite_menu(theme):
output.write(line) output.write(line)
def rewrite_navigation(theme): def prepare_for_build():
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 #########################
for step in (rewrite_app, rewrite_builders, rewrite_menu, rewrite_navigation): steps = (
step(theme) rewrite_app,
rewrite_builders,
rewrite_menu,
)
[step() for step in steps]
def cleanup(): def cleanup():
########################## RESTORE *.BK FILES ##################################### ########################## RESTORE *.BK FILES #####################################
for file in (
PACKAGE_DIR / 'app.py',
PACKAGE_DIR / 'builders.py',
PACKAGE_DIR / 'menu.py',
):
file.unlink()
for file in ( for file in (
SRC_DIR / 'app.bk', SRC_DIR / 'app.bk',
SRC_DIR / 'builders.bk', SRC_DIR / 'builders.bk',
SRC_DIR / 'menu.bk', SRC_DIR / 'menu.bk',
SRC_DIR / 'navigation.bk',
): ):
if file.exists(): file.rename(PACKAGE_DIR / f'{file.stem}.py')
logger.debug(f'moving {str(file)}')
file.replace(PACKAGE_DIR / f'{file.stem}.py')
if __name__ == '__main__': if __name__ == '__main__':
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--rewrite', action='store_true') parser.add_argument('--rewrite', action='store_true')
parser.add_argument('--theme', type=str, default='forest')
parser.add_argument('--restore', action='store_true') 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(args.theme) prepare_for_build()
elif args.restore: elif args.restore:
logger.info('cleaning up files') logger.info('cleaning up files')
cleanup() cleanup()

View File

@ -1,192 +0,0 @@
#!/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,11 +6,10 @@ 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, get_configuration from .data import _base_values, _configuration, _kinds_all, 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
@ -38,7 +37,7 @@ class App(tk.Tk):
) )
return APP_cls return APP_cls
def __init__(self, vmr, theme): def __init__(self, vmr):
super().__init__() super().__init__()
self.logger = logger.getChild(self.__class__.__name__) self.logger = logger.getChild(self.__class__.__name__)
self._vmr = vmr self._vmr = vmr
@ -199,14 +198,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, theme=None) -> App: def connect(kind_id: str, vmr) -> 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, theme) return VMMIN_cls(vmr)

View File

@ -6,7 +6,6 @@ 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
@ -228,7 +227,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, # Range: 0 = -60dB, 72 = +12dB (72dB total range) maximum=72,
orient='vertical', orient='vertical',
mode='determinate', mode='determinate',
variable=self.labelframe.level, variable=self.labelframe.level,
@ -363,15 +362,15 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
tk.BooleanVar() for _ in self.configframe.virt_out_params tk.BooleanVar() for _ in self.configframe.virt_out_params
] ]
self.configframe.bool_params = ('mono', 'solo') self.configframe.params = ('mono', 'solo')
self.configframe.bool_param_vars = list( self.configframe.param_vars = list(
tk.BooleanVar() for _ in self.configframe.bool_params tk.BooleanVar() for _ in self.configframe.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.bool_params) map(lambda x: x.replace('mono', 'mc'), self.configframe.params)
) )
if self.configframe.parent.kind.name == 'banana': if self.configframe.parent.kind.name == 'banana':
pass pass
@ -389,10 +388,7 @@ 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( map(lambda x: x.replace('mono', 'mc'), self.configframe.params)
lambda x: x.replace('mono', 'mc'),
self.configframe.bool_params,
)
) )
def create_comp_slider(self): def create_comp_slider(self):
@ -546,9 +542,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.bool_param_vars[i], variable=self.configframe.param_vars[i],
) )
for i, param in enumerate(self.configframe.bool_params) for i, param in enumerate(self.configframe.params)
] ]
[ [
button.grid( button.grid(
@ -562,41 +558,36 @@ 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):
self.configframe.bus_mode_map = util.get_busmode_fullnames(self.app.kind) # fmt: off
self.configframe.bus_mode_map_reverse = util.get_busmode_fullnames_reversed( self.configframe.bus_mode_map = {
self.app.kind "normal": "Normal",
) "amix": "Mix Down A",
self.configframe.bus_modes = util.get_busmode_shortnames(self.app.kind) "bmix": "Mix Down B",
self.configframe.int_params = ('mono',) "repeat": "Stereo Repeat",
self.configframe.int_param_vars = [ "composite": "Composite",
tk.IntVar(value=getattr(self.configframe.target, param)) "tvmix": "Up Mix TV",
for param in self.configframe.int_params "upmix21": "Up Mix 2.1",
] "upmix41": "Up Mix 4.1",
self.configframe.mono_modes = util.get_busmono_modes() "upmix61": "Up Mix 6.1",
self.configframe.bus_mono_label_text = tk.StringVar( "centeronly": "Center Only",
value=self.configframe.mono_modes[self.configframe.target.mono] "lfeonly": "LFE Only",
) "rearonly": "Rear Only",
self.configframe.bool_params = ('eq.on', 'eq.ab') }
self.configframe.bool_param_vars = [ self.configframe.bus_modes = list(self.configframe.bus_mode_map.keys())
tk.BooleanVar() for _ in self.configframe.bool_params # 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_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, self.configframe, textvariable=self.configframe.bus_mode_label_text
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), padx=1, pady=1 column=0, row=0, columnspan=2, sticky=(tk.W)
) )
self.configframe.busmode_button.bind( self.configframe.busmode_button.bind(
'<Button-1>', '<Button-1>',
@ -611,24 +602,6 @@ 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(
@ -638,13 +611,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.bool_param_vars[i], variable=self.configframe.param_vars[i],
) )
for i, param in enumerate(self.configframe.bool_params) for i, param in enumerate(self.configframe.params)
] ]
[ [
button.grid( button.grid(
column=i + 1, column=i,
row=1, row=1,
) )
for i, button in enumerate(param_buttons) for i, button in enumerate(param_buttons)

View File

@ -197,22 +197,14 @@ class Strip(ChannelLabelFrame):
def upd_levels(self): def upd_levels(self):
""" """
Updates level values using direct dB values. Updates level 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)
if val < -72: self.level.set(
if self.level.get() != 0: (0 if self.mute.get() else 72 + val - 12 + self.gain.get())
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):
@ -231,18 +223,9 @@ 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: if self.target.levels.is_updated or self.level.get() != -118:
val = max(self.target.levels.all) val = max(self.target.levels.all)
if val < -72: self.level.set((0 if self.mute.get() else 72 + val - 12))
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.bool_param_vars[self.bool_params.index(param)].get() val = self.param_vars[self.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,14 +177,15 @@ class StripConfig(Config):
for i, param in enumerate(self.virt_out_params) for i, param in enumerate(self.virt_out_params)
] ]
[ [
self.bool_param_vars[i].set(self.getter(param)) self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.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:
[ [
@ -206,7 +207,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.bool_params) for i, param in enumerate(self.params)
] ]
@ -217,7 +218,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, parent) self.builder = builders.BusConfigFrameBuilder(self)
self.builder.setup() self.builder.setup()
self.make_row_0() self.make_row_0()
self.make_row_1() self.make_row_1()
@ -237,56 +238,53 @@ 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.bus_mode_map_reverse[self.bus_mode_label_text.get()] current_mode = self.current_bus_mode()
current_index = self.bus_modes.index(current_mode) next = self.bus_modes.index(current_mode) + 1
next_index = (current_index + 1) % len(self.bus_modes) if next < len(self.bus_modes):
next_mode = self.bus_modes[next_index] setattr(
self.target.mode,
setattr(self.target.mode, next_mode, True) self.bus_modes[next],
self.bus_mode_label_text.set(self.bus_mode_map[next_mode]) 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')
def rotate_bus_modes_left(self, *args): def rotate_bus_modes_left(self, *args):
current_mode = self.bus_mode_map_reverse[self.bus_mode_label_text.get()] current_mode = self.current_bus_mode()
current_index = self.bus_modes.index(current_mode) prev = self.bus_modes.index(current_mode) - 1
prev_index = (current_index - 1) % len(self.bus_modes) if prev < 0:
prev_mode = self.bus_modes[prev_index] self.target.mode.rearonly = True
self.bus_mode_label_text.set('Rear Only')
setattr(self.target.mode, prev_mode, True) else:
self.bus_mode_label_text.set(self.bus_mode_map[prev_mode]) setattr(
self.target.mode,
def rotate_mono_right(self, *args): self.bus_modes[prev],
current_val = self.mono_modes.index(self.bus_mono_label_text.get()) True,
next_val = (current_val + 1) % 3 )
self.bus_mono_label_text.set(self.mono_modes[next_val]) self.bus_mode_label_text.set(self.bus_mode_map[self.bus_modes[prev]])
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.bool_param_vars[i].set(self.getter(param)) self.param_vars[i].set(self.getter(param))
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.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.bool_param_vars[i].get() else "white"}', background=f'{"green" if self.param_vars[i].get() else "white"}',
) )
for i, param in enumerate(self.bool_params) for i, param in enumerate(self.params)
] ]

View File

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

View File

@ -61,6 +61,10 @@ 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.request_kind_map(kind_id) return _kinds[kind_id]

View File

@ -161,18 +161,17 @@ 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)
# Convert dB to progressbar: -60dB=0, 0dB=60, +12dB=72 self.level.set(
if ( (
self.parent.parent.strip_frame.strips[self.index].mute.get() 0
or not self.on.get() if self.parent.parent.strip_frame.strips[self.index].mute.get()
): or not self.on.get()
level_display = 0 else 72 + val - 12 + self.gain.get()
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,17 +357,15 @@ 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("host")}') self.logger.info(f'Attempting vban connection to {opts.get("ip")}')
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("host")}', f'Timeout attempting to establish connection to {opts.get("ip")}',
'Please check your connection settings', 'Please check your connection settings',
) )
messagebox.showerror('Connection Error', '\n'.join(msg)) messagebox.showerror('Connection Error', '\n'.join(msg))
@ -423,7 +421,7 @@ class Menus(tk.Menu):
del self.parent.__dict__['userconfigs'] del self.parent.__dict__['userconfigs']
self.menu_setup() self.menu_setup()
self.after(50, self.enable_vban_menus) self.after(500, self.enable_vban_menus)
def documentation(self): def documentation(self):
webbrowser.open_new(r'https://voicemeeter.com/') webbrowser.open_new(r'https://voicemeeter.com/')

View File

@ -1,34 +0,0 @@
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']