updates to gui to match changes to interfaces

updates to gui to match changes to interfaces

now packaged with poetry and on pypi
This commit is contained in:
onyx-and-iris 2022-06-16 23:53:28 +01:00
parent 0688a36a76
commit 2c39b9d215
15 changed files with 273 additions and 78 deletions

View File

@ -13,17 +13,14 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Prerequisites ## Prerequisites
- [Voicemeeter](https://voicemeeter.com/) (Basic v1.0.8.2), (Banana v2.0.6.2) or (Potato v3.0.2.2) - [Voicemeeter](https://voicemeeter.com/) (Basic v1.0.8.2), (Banana v2.0.6.2) or (Potato v3.0.2.2)
- [Git for Windows](https://gitforwindows.org/) - Python 3.11+
- Python 3.9+
## Installation ## Installation
For a step-by-step guide [click here](INSTALLATION.md) For a step-by-step guide [click here](INSTALLATION.md)
``` ```
git clone https://github.com/onyx-and-iris/voicemeeter-compact pip install voicemeeter-compact
cd voicemeeter-compact
pip install .
``` ```
## Usage ## Usage
@ -31,13 +28,13 @@ pip install .
Example `__main__.py` file: Example `__main__.py` file:
```python ```python
import voicemeeter import voicemeeterlib
import vmcompact import vmcompact
def main(): def main():
# pass the kind_id and the vmr object to the app # pass the kind_id and the vmr object to the app
with voicemeeter.remote(kind_id) as vmr: with voicemeeterlib.api(kind_id) as vmr:
app = vmcompact.connect(kind_id, vmr) app = vmcompact.connect(kind_id, vmr)
app.mainloop() app.mainloop()
@ -46,8 +43,6 @@ if __name__ == "__main__":
# choose the kind of Voicemeeter (Local connection) # choose the kind of Voicemeeter (Local connection)
kind_id = "banana" kind_id = "banana"
voicemeeter.launch(kind_id, hide=False)
main() main()
``` ```
@ -141,13 +136,13 @@ A valid `vban.toml` might look like this:
[connection-1] [connection-1]
kind = 'banana' kind = 'banana'
ip = '192.168.1.2' ip = '192.168.1.2'
streamname = 'streampc' streamname = 'worklaptop'
port = 6990 port = 6980
[connection-2] [connection-2]
kind = 'potato' kind = 'potato'
ip = '192.168.1.3' ip = '192.168.1.3'
streamname = 'worklaptop' streamname = 'streampc'
port = 6990 port = 6990
``` ```
@ -165,7 +160,4 @@ Profiles may be loaded at any time via the menu.
[Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app! [Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app!
[Christian Volkmann](https://github.com/chvolkmann) for the detailed work that went into creating the underlying Remote API Python Interface.
Unfortunately, the Remote API Python Interface has `NOT` been open source licensed. I have [raised an issue](https://github.com/chvolkmann/voicemeeter-remote-python/issues/13) and asked directly and politely but so far no response. If a license is added in future I will update this section. Without an open source license there is no guarantee that in future this package may not be pulled down, without any notice.
[Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi! [Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi!

View File

@ -1,9 +1,10 @@
import voicemeeter import voicemeeterlib
import vmcompact import vmcompact
def main(): def main():
with voicemeeter.remote(kind_id) as vmr: with voicemeeterlib.api(kind_id) as vmr:
app = vmcompact.connect(kind_id, vmr) app = vmcompact.connect(kind_id, vmr)
app.mainloop() app.mainloop()
@ -11,6 +12,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
kind_id = "banana" kind_id = "banana"
voicemeeter.launch(kind_id, hide=False)
main() main()

View File

@ -0,0 +1,45 @@
[strip-0]
label = "PhysStrip0"
A1 = true
gain = -8.8
comp = 3.2
[strip-1]
label = "PhysStrip1"
B1 = true
gate = 4.1
[strip-2]
label = "PhysStrip2"
gain = 1.1
limit = -15
[strip-3]
label = "VirtStrip0"
bass = -3.2
mid = 1.5
treble = 2.1
[strip-4]
label = "VirtStrip1"
limit = -12
[bus-0]
label = "PhysBus0"
mute = true
[bus-1]
label = "PhysBus1"
mono = true
[bus-2]
label = "PhysBus2"
eq = true
[bus-3]
label = "VirtBus0"
eq_ab = true
[bus-4]
label = "VirtBus1"
gain = -22.8

View File

@ -0,0 +1,23 @@
[strip-0]
label = "PhysStrip0"
A1 = true
gain = -8.8
[strip-1]
label = "PhysStrip1"
B1 = true
audibility = 3.2
[strip-2]
label = "VirtStrip0"
bass = -3.2
mid = 1.5
treble = 2.1
[bus-0]
label = "PhysBus0"
mute = true
[bus-1]
label = "PhysBus1"
mono = true

View File

@ -0,0 +1,70 @@
[strip-0]
label = "PhysStrip0"
A1 = true
gain = -8.8
comp = 3.2
[strip-1]
label = "PhysStrip1"
B1 = true
gate = 4.1
[strip-2]
label = "PhysStrip2"
gain = 1.1
limit = -15
[strip-3]
label = "PhysStrip3"
B2 = false
[strip-4]
label = "PhysStrip4"
B3 = true
gain = -8.8
[strip-5]
label = "VirtStrip0"
A3 = true
A5 = true
bass = -3.2
mid = 1.5
treble = 2.1
[strip-6]
label = "VirtStrip1"
limit = -12
k = 3
[strip-7]
label = "VirtStrip2"
mc = true
[bus-0]
label = "PhysBus0"
mute = true
[bus-1]
label = "PhysBus1"
mono = true
[bus-2]
label = "PhysBus2"
eq = true
[bus-3]
label = "PhysBus3"
[bus-4]
label = "PhysBus4"
[bus-5]
label = "VirtBus0"
eq_ab = true
[bus-6]
label = "VirtBus1"
gain = -22.8
[bus-7]
label = "VirtBus2"

42
poetry.lock generated Normal file
View File

@ -0,0 +1,42 @@
[[package]]
name = "sv-ttk"
version = "0.1"
description = "Python module to easily use the Sun Valley ttk theme"
category = "main"
optional = false
python-versions = ">=3.4"
[[package]]
name = "vban-cmd"
version = "1.0.5"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
category = "main"
optional = false
python-versions = ">=3.11,<4.0"
[[package]]
name = "voicemeeter-api"
version = "0.1.6"
description = "A Python wrapper for the Voiceemeter API"
category = "main"
optional = false
python-versions = ">=3.11,<4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.11"
content-hash = "3b50c0fa4701fbf590b50609cf3d56a8f97b274ebb9e259ba52ce6771b106d0c"
[metadata.files]
sv-ttk = [
{file = "sv_ttk-0.1-py3-none-any.whl", hash = "sha256:c47ab1c70aad0333bc7f5063ab55e8a16b562e700e89e7853e577a1e2fdaa091"},
{file = "sv_ttk-0.1.tar.gz", hash = "sha256:342754618292b6d224060307eccaa35b6f6c284b34b4da1d0cf0484b652ffb0f"},
]
vban-cmd = [
{file = "vban-cmd-1.0.5.tar.gz", hash = "sha256:e50efd373816c01fb7e62f9ad90c33113b26cc12fbb46866b952b1946030282b"},
{file = "vban_cmd-1.0.5-py3-none-any.whl", hash = "sha256:c51008049652fec61a94fb1b8ccd173cb0296684eee6249e9865f395340d32c5"},
]
voicemeeter-api = [
{file = "voicemeeter-api-0.1.6.tar.gz", hash = "sha256:354fe37aea680f2431d428c9bd489d7d9c9e143cbd84a769c567dd657e63370d"},
{file = "voicemeeter_api-0.1.6-py3-none-any.whl", hash = "sha256:20a3072b0379862254d5ee3cf4ec96f6c839291faa4ecbd29d3c1ed66a5492cb"},
]

25
pyproject.toml Normal file
View File

@ -0,0 +1,25 @@
[tool.poetry]
name = "voicemeeter-compact"
version = "1.0.1"
description = "A Compact Voicemeeter Remote App"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
readme = "README.md"
repository = "https://github.com/onyx-and-iris/voicemeeter-compact"
packages = [
{ include = "vmcompact" },
]
include = ["vmcompact/img/cat.ico"]
[tool.poetry.dependencies]
python = "^3.11"
sv-ttk = "^0.1"
voicemeeter-api = { version = "^0.1.6", python = "^3.10" }
vban-cmd = { version = "^1.0.5", python = "^3.10" }
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,13 +1,13 @@
import tkinter as tk import tkinter as tk
from pathlib import Path
from tkinter import ttk from tkinter import ttk
from typing import NamedTuple from typing import NamedTuple
from pathlib import Path
from .errors import VMCompactErrors
from .data import _kinds_all, _configuration, _base_values
from .subject import Subject
from .builders import MainFrameBuilder from .builders import MainFrameBuilder
from .data import _base_values, _configuration, _kinds_all
from .errors import VMCompactErrors
from .menu import Menus from .menu import Menus
from .subject import Subject
class App(tk.Tk): class App(tk.Tk):
@ -89,7 +89,7 @@ class App(tk.Tk):
if _configuration.extended: if _configuration.extended:
self.nav_frame.extend.set(True) self.nav_frame.extend.set(True)
self.nav_frame.extend_frame() self.nav_frame.extend_frame()
if self.kind.name == "Potato": if self.kind.name == "potato":
self.builder.create_banner() self.builder.create_banner()
def on_update(self, subject, data): def on_update(self, subject, data):
@ -145,7 +145,7 @@ class App(tk.Tk):
self.drag_id = "" self.drag_id = ""
_apps = {kind.id: 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) -> App:

View File

@ -1,15 +1,15 @@
import tkinter as tk
from tkinter import ttk
from functools import partial
import abc import abc
import tkinter as tk
from functools import partial
from tkinter import ttk
import sv_ttk import sv_ttk
from .data import _base_values, _configuration
from .channels import _make_channelframe
from .navigation import Navigation
from .config import StripConfig, BusConfig
from .banner import Banner from .banner import Banner
from .channels import _make_channelframe
from .config import BusConfig, StripConfig
from .data import _base_values, _configuration
from .navigation import Navigation
class AbstractBuilder(abc.ABC): class AbstractBuilder(abc.ABC):
@ -144,7 +144,7 @@ class NavigationFrameBuilder(AbstractBuilder):
variable=self.navframe.submix, variable=self.navframe.submix,
) )
self.navframe.submix_button.grid(column=0, row=0) self.navframe.submix_button.grid(column=0, row=0)
if self.navframe.parent.kind.name != "Potato": if self.navframe.parent.kind.name != "potato":
self.navframe.submix_button["state"] = "disabled" self.navframe.submix_button["state"] = "disabled"
def create_channel_button(self): def create_channel_button(self):
@ -321,7 +321,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
"""Responsible for building channel configframe widgets""" """Responsible for building channel configframe widgets"""
def setup(self): def setup(self):
if self.configframe.parent.kind.name == "Basic": if self.configframe.parent.kind.name == "basic":
self.configframe.slider_params = ("audibility",) self.configframe.slider_params = ("audibility",)
self.configframe.slider_vars = (tk.DoubleVar(),) self.configframe.slider_vars = (tk.DoubleVar(),)
else: else:
@ -349,12 +349,12 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder):
tk.BooleanVar() for _ in self.configframe.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.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
# karaoke modes not in RT Packet yet. May implement in future # karaoke modes not in RT Packet yet. May implement in future
""" """

View File

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk
from math import log from math import log
from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration

View File

@ -1,9 +1,9 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk
from functools import partial from functools import partial
from tkinter import ttk
from . import builders from . import builders
from .data import _configuration, _base_values from .data import _base_values, _configuration
class Config(ttk.Frame): class Config(ttk.Frame):
@ -96,7 +96,7 @@ class StripConfig(Config):
def make_row_0(self): def make_row_0(self):
if self.index < self.phys_in: if self.index < self.phys_in:
if self.parent.kind.name == "Basic": if self.parent.kind.name == "basic":
self.builder.create_audibility_slider() self.builder.create_audibility_slider()
else: else:
self.builder.create_comp_slider() self.builder.create_comp_slider()

View File

@ -1,6 +1,7 @@
import toml
from pathlib import Path from pathlib import Path
import tomllib
configuration = {} configuration = {}
config_path = [Path.cwd() / "configs"] config_path = [Path.cwd() / "configs"]
@ -11,8 +12,9 @@ for path in config_path:
for filename in filenames: for filename in filenames:
name = filename.with_suffix("").stem name = filename.with_suffix("").stem
try: try:
configs[name] = toml.load(filename) with open(filename, "rb") as f:
except toml.TomlDecodeError: configs[name] = tomllib.load(f)
except tomllib.TOMLDecodeError:
print(f"Invalid TOML profile: configs/{filename.stem}") print(f"Invalid TOML profile: configs/{filename.stem}")
for name, cfg in configs.items(): for name, cfg in configs.items():

View File

@ -1,5 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from voicemeeter import kinds
from voicemeeterlib import kinds
from .configurations import get_configuration from .configurations import get_configuration
@ -51,7 +52,7 @@ class BaseValues(metaclass=SingletonMeta):
# a vban connection established # a vban connection established
vban_connected: bool = False vban_connected: bool = False
# pdirty delay # pdirty delay
pdelay: int = 5 pdelay: int = 1
# ldirty delay # ldirty delay
ldelay: int = 5 ldelay: int = 5
@ -59,7 +60,7 @@ class BaseValues(metaclass=SingletonMeta):
_base_values = BaseValues() _base_values = BaseValues()
_configuration = Configurations() _configuration = Configurations()
_kinds = {kind.id: kind for kind in kinds.all} _kinds = {kind.name: kind for kind in kinds.kinds_all}
_kinds_all = _kinds.values() _kinds_all = _kinds.values()

View File

@ -1,6 +1,6 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk
from math import log from math import log
from tkinter import ttk
from . import builders from . import builders
from .data import _base_values, _configuration from .data import _base_values, _configuration

View File

@ -1,16 +1,12 @@
import tkinter as tk import tkinter as tk
from tkinter import ttk, messagebox
from functools import partial
import webbrowser import webbrowser
import sv_ttk from functools import partial
import vbancmd from tkinter import messagebox, ttk
from .data import ( import sv_ttk
get_configuration, import vban_cmd
_base_values,
_configuration, from .data import _base_values, _configuration, get_configuration, kind_get
kind_get,
)
class Menus(tk.Menu): class Menus(tk.Menu):
@ -76,31 +72,31 @@ class Menus(tk.Menu):
command=partial(self.action_set_voicemeeter, "lock", False), command=partial(self.action_set_voicemeeter, "lock", False),
) )
# profiles menu # configs menu
menu_profiles = tk.Menu(self, tearoff=0) menu_configs = tk.Menu(self, tearoff=0)
self.add_cascade(menu=menu_profiles, label="Profiles") self.add_cascade(menu=menu_configs, label="Configs")
self.menu_profiles_load = tk.Menu(menu_profiles, tearoff=0) self.menu_configs_load = tk.Menu(menu_configs, tearoff=0)
menu_profiles.add_cascade(menu=self.menu_profiles_load, label="Load profile") menu_configs.add_cascade(menu=self.menu_configs_load, label="Load profile")
defaults = {"base", "blank"} defaults = {"reset"}
if len(self.target.profiles) > len(defaults) and all( if len(self.target.configs) > len(defaults) and all(
key in self.target.profiles for key in defaults key in self.target.configs for key in defaults
): ):
[ [
self.menu_profiles_load.add_command( self.menu_configs_load.add_command(
label=profile, command=partial(self.load_profile, profile) label=profile, command=partial(self.load_profile, profile)
) )
for profile in self.target.profiles.keys() for profile in self.target.configs.keys()
if profile not in defaults if profile not in defaults
] ]
else: else:
menu_profiles.entryconfig(0, state="disabled") menu_configs.entryconfig(0, state="disabled")
menu_profiles.add_command(label="Reset to defaults", command=self.load_defaults) menu_configs.add_command(label="Reset to defaults", command=self.load_defaults)
# layout menu # layout menu
self.menu_layout = tk.Menu(self, tearoff=0) self.menu_layout = tk.Menu(self, tearoff=0)
self.add_cascade(menu=self.menu_layout, label="Layout") self.add_cascade(menu=self.menu_layout, label="Layout")
# layout/submixes # layout/submixes
# here we build menu regardless of kind but disable if not Potato # here we build menu regardless of kind but disable if not potato
buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3)) buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3))
self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0) self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0)
self.menu_layout.add_cascade(menu=self.menu_submixes, label="Submixes") self.menu_layout.add_cascade(menu=self.menu_submixes, label="Submixes")
@ -116,7 +112,7 @@ class Menus(tk.Menu):
for i in range(8) for i in range(8)
] ]
self._selected_bus[_configuration.submixes].set(True) self._selected_bus[_configuration.submixes].set(True)
if self.parent.kind.name != "Potato": if self.parent.kind.name != "potato":
self.menu_layout.entryconfig(0, state="disabled") self.menu_layout.entryconfig(0, state="disabled")
# layout/extends # layout/extends
self.menu_extends = tk.Menu(self.menu_layout, tearoff=0) self.menu_extends = tk.Menu(self.menu_layout, tearoff=0)
@ -211,14 +207,14 @@ class Menus(tk.Menu):
setattr(self.target.command, cmd, val) setattr(self.target.command, cmd, val)
def load_profile(self, profile): def load_profile(self, profile):
self.target.apply_profile(profile) self.target.apply_config(profile)
def load_defaults(self): def load_defaults(self):
resp = messagebox.askyesno( resp = messagebox.askyesno(
message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF" message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF"
) )
if resp: if resp:
self.target.apply_profile("base") self.target.apply_config("reset")
def always_on_top(self): def always_on_top(self):
self.parent.attributes("-topmost", self._is_topmost.get()) self.parent.attributes("-topmost", self._is_topmost.get())
@ -258,7 +254,7 @@ class Menus(tk.Menu):
if isinstance(menu, tk.Menu) if isinstance(menu, tk.Menu)
] ]
self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}") self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}")
self.menu_profiles_load.config(bg=f"{'black' if theme == 'dark' else 'white'}") self.menu_configs_load.config(bg=f"{'black' if theme == 'dark' else 'white'}")
[ [
menu.config(bg=f"{'black' if theme == 'dark' else 'white'}") menu.config(bg=f"{'black' if theme == 'dark' else 'white'}")
for menu in self.menu_vban.winfo_children() for menu in self.menu_vban.winfo_children()
@ -280,7 +276,7 @@ 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")
self.vban = vbancmd.connect(kind_id, **opts) self.vban = vban_cmd.api(kind_id, **opts)
# login to vban interface # login to vban interface
self.vban.login() self.vban.login()
# destroy the current App frames # destroy the current App frames
@ -294,7 +290,7 @@ class Menus(tk.Menu):
target_menu.entryconfig(0, state="disabled") target_menu.entryconfig(0, state="disabled")
target_menu.entryconfig(1, state="normal") target_menu.entryconfig(1, state="normal")
self.menu_layout.entryconfig( self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}" 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
) )
def vban_disconnect(self, i): def vban_disconnect(self, i):
@ -311,7 +307,7 @@ class Menus(tk.Menu):
target_menu.entryconfig(0, state="normal") target_menu.entryconfig(0, state="normal")
target_menu.entryconfig(1, state="disabled") target_menu.entryconfig(1, state="disabled")
self.menu_layout.entryconfig( self.menu_layout.entryconfig(
0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}" 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}"
) )
self.after(15000, self.enable_vban_menus) self.after(15000, self.enable_vban_menus)