totalmix-osc 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- totalmix_osc-0.0.1/.github/workflows/ci.yaml +32 -0
- totalmix_osc-0.0.1/.github/workflows/docs.yaml +47 -0
- totalmix_osc-0.0.1/.github/workflows/publish-to-pypi.yml +38 -0
- totalmix_osc-0.0.1/.gitignore +77 -0
- totalmix_osc-0.0.1/CLAUDE.md +61 -0
- totalmix_osc-0.0.1/PKG-INFO +70 -0
- totalmix_osc-0.0.1/docs/api.md +9 -0
- totalmix_osc-0.0.1/docs/guide.md +300 -0
- totalmix_osc-0.0.1/docs/index.md +21 -0
- totalmix_osc-0.0.1/examples/reset_routing.py +24 -0
- totalmix_osc-0.0.1/license.txt +11 -0
- totalmix_osc-0.0.1/mkdocs.yml +44 -0
- totalmix_osc-0.0.1/pyproject.toml +43 -0
- totalmix_osc-0.0.1/readme.md +41 -0
- totalmix_osc-0.0.1/setup.cfg +4 -0
- totalmix_osc-0.0.1/tests/conftest.py +52 -0
- totalmix_osc-0.0.1/tests/fake_totalmix.py +208 -0
- totalmix_osc-0.0.1/tests/test_cli.py +126 -0
- totalmix_osc-0.0.1/tests/test_control.py +179 -0
- totalmix_osc-0.0.1/tests/test_fader_curve.py +27 -0
- totalmix_osc-0.0.1/tests/test_hardware.py +102 -0
- totalmix_osc-0.0.1/totalmix_osc/__init__.py +8 -0
- totalmix_osc-0.0.1/totalmix_osc/__main__.py +6 -0
- totalmix_osc-0.0.1/totalmix_osc/cli.py +308 -0
- totalmix_osc-0.0.1/totalmix_osc/control.py +558 -0
- totalmix_osc-0.0.1/totalmix_osc/fader_curve.py +51 -0
- totalmix_osc-0.0.1/totalmix_osc/osc_client.py +118 -0
- totalmix_osc-0.0.1/totalmix_osc/totalmix_fx_volume_map.json +1003 -0
- totalmix_osc-0.0.1/totalmix_osc/version.py +24 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/PKG-INFO +70 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/SOURCES.txt +35 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/dependency_links.txt +1 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/entry_points.txt +2 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/requires.txt +10 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/scm_file_list.json +30 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/scm_version.json +8 -0
- totalmix_osc-0.0.1/totalmix_osc.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Continuous Integration
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
tests:
|
|
13
|
+
name: Unit tests
|
|
14
|
+
runs-on: ${{ matrix.os }}
|
|
15
|
+
strategy:
|
|
16
|
+
matrix:
|
|
17
|
+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
|
18
|
+
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v4
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
- name: Install dependencies
|
|
26
|
+
run: |
|
|
27
|
+
python -m pip install --upgrade pip
|
|
28
|
+
pip install pytest
|
|
29
|
+
pip install -e .[test]
|
|
30
|
+
- name: Test with pytest
|
|
31
|
+
run: |
|
|
32
|
+
pytest
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Deploy documentation
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
pages: write
|
|
12
|
+
id-token: write
|
|
13
|
+
|
|
14
|
+
concurrency:
|
|
15
|
+
group: pages
|
|
16
|
+
cancel-in-progress: false
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
build:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
- name: Set up Python
|
|
24
|
+
uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: "3.11"
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -e .[docs]
|
|
31
|
+
- name: Build documentation
|
|
32
|
+
run: mkdocs build --strict
|
|
33
|
+
- name: Upload Pages artifact
|
|
34
|
+
uses: actions/upload-pages-artifact@v3
|
|
35
|
+
with:
|
|
36
|
+
path: site
|
|
37
|
+
|
|
38
|
+
deploy:
|
|
39
|
+
needs: build
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
environment:
|
|
42
|
+
name: github-pages
|
|
43
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
44
|
+
steps:
|
|
45
|
+
- name: Deploy to GitHub Pages
|
|
46
|
+
id: deployment
|
|
47
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish Python distribution to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- '[0-9].[0-9]+.[0-9]+'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pypi-publish:
|
|
10
|
+
name: Build and publish Python distribution to PyPI
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment:
|
|
13
|
+
name: pypi
|
|
14
|
+
url: https://pypi.org/p/totalmix-osc
|
|
15
|
+
permissions:
|
|
16
|
+
id-token: write
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@main
|
|
19
|
+
- name: Setup Python 3.10
|
|
20
|
+
uses: actions/setup-python@v3
|
|
21
|
+
with:
|
|
22
|
+
python-version: "3.10"
|
|
23
|
+
- name: Install pypa/build
|
|
24
|
+
run: >-
|
|
25
|
+
python -m
|
|
26
|
+
pip install
|
|
27
|
+
build
|
|
28
|
+
--user
|
|
29
|
+
- name: Build a binary wheel and a source tarball
|
|
30
|
+
run: >-
|
|
31
|
+
python -m
|
|
32
|
+
build
|
|
33
|
+
--sdist
|
|
34
|
+
--wheel
|
|
35
|
+
--outdir dist/
|
|
36
|
+
- name: Publish distribution to PyPI
|
|
37
|
+
if: startsWith(github.ref, 'refs/tags')
|
|
38
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
totalmix_osc/version.py
|
|
2
|
+
|
|
3
|
+
# Byte-compiled / optimized / DLL files
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
|
|
8
|
+
# C extensions
|
|
9
|
+
*.so
|
|
10
|
+
|
|
11
|
+
# Distribution / packaging
|
|
12
|
+
.Python
|
|
13
|
+
env/
|
|
14
|
+
build/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.coverage
|
|
43
|
+
.coverage.*
|
|
44
|
+
.cache
|
|
45
|
+
nosetests.xml
|
|
46
|
+
coverage.xml
|
|
47
|
+
*,cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
.pytest_cache/
|
|
50
|
+
|
|
51
|
+
# Translations
|
|
52
|
+
*.mo
|
|
53
|
+
*.pot
|
|
54
|
+
|
|
55
|
+
# Django stuff:
|
|
56
|
+
*.log
|
|
57
|
+
|
|
58
|
+
# MkDocs
|
|
59
|
+
site/
|
|
60
|
+
|
|
61
|
+
# PyBuilder
|
|
62
|
+
target/
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Vim global
|
|
66
|
+
[._]*.s[a-w][a-z]
|
|
67
|
+
[._]s[a-w][a-z]
|
|
68
|
+
*.un~
|
|
69
|
+
Session.vim
|
|
70
|
+
.netrwhist
|
|
71
|
+
*~
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__enamlcache__/
|
|
75
|
+
sandbox/
|
|
76
|
+
.ropeproject/
|
|
77
|
+
.ipynb_checkpoints/
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What this repo is
|
|
6
|
+
|
|
7
|
+
`totalmix-osc` controls RME TotalMix FX (the mixer software for RME audio
|
|
8
|
+
interfaces) over its legacy OSC protocol: routing-matrix crosspoints,
|
|
9
|
+
output gains, and output loopback, with every write verified against
|
|
10
|
+
TotalMix's feedback. It was split out of `ncrar-audio` so psiexperiment can
|
|
11
|
+
use it without depending on that package; `ncrar-audio` now depends on it.
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .[test,docs]
|
|
17
|
+
pytest # offline, against tests/fake_totalmix.py
|
|
18
|
+
pytest tests/test_control.py -k loopback
|
|
19
|
+
pytest --hardware # also runs tests/test_hardware.py
|
|
20
|
+
mkdocs build --strict
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Versioning comes from git tags via `setuptools_scm`
|
|
24
|
+
(`totalmix_osc/version.py` is generated). Pushing a tag like `0.1.0`
|
|
25
|
+
publishes to PyPI via `.github/workflows/publish-to-pypi.yml`.
|
|
26
|
+
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
- `osc_client.py` (`OSCClient`): UDP send/receive. Uses a single-threaded
|
|
30
|
+
`BlockingOSCUDPServer` so feedback is handled in arrival order, and
|
|
31
|
+
converts every number to float before sending (TotalMix misreads OSC
|
|
32
|
+
ints).
|
|
33
|
+
- `control.py` (`TotalMixFX`): reads the channel layout at construction
|
|
34
|
+
(walking banks with `/1/bank+`), then sends selector messages
|
|
35
|
+
(`/setSubmix`, `/setBankStart`, `/1/busX`, `/setOffsetInBank`) followed by
|
|
36
|
+
writes. `_on_feedback` records every relevant message with the context
|
|
37
|
+
(bus, submix label, page-2 channel) TotalMix last reported;
|
|
38
|
+
`_scan_fader`/`_scan_loopback` only trust a value once the same burst has
|
|
39
|
+
confirmed the expected channel name. `_transact` sends and waits on that
|
|
40
|
+
feedback.
|
|
41
|
+
- `cli.py`: the `totalmix-osc` command (also `python -m totalmix_osc`).
|
|
42
|
+
Subcommands are get-or-set (omit the value to read); `shell` reuses one
|
|
43
|
+
`TotalMixFX` and parses each line with the same argparse subparsers.
|
|
44
|
+
Tested offline in `tests/test_cli.py`.
|
|
45
|
+
- `fader_curve.py`: dB <-> raw fader value mapping from
|
|
46
|
+
`totalmix_fx_volume_map.json`.
|
|
47
|
+
- `tests/fake_totalmix.py`: simulates the TotalMix behaviors the code
|
|
48
|
+
depends on (float misreads, banks, stale bursts, toggles, crashing on an
|
|
49
|
+
out-of-range selection). When you discover a new TotalMix behavior on
|
|
50
|
+
hardware, model it here and add a test.
|
|
51
|
+
|
|
52
|
+
## Hardware cautions
|
|
53
|
+
|
|
54
|
+
- Never send `/setBankStart` or `/setSubmix` past the end of a row: it can
|
|
55
|
+
crash TotalMix FX. Channel numbers are validated against the layout read
|
|
56
|
+
at startup; keep it that way.
|
|
57
|
+
- The hardware tests change and restore the live mixer. Ask before running
|
|
58
|
+
them, and before sending any OSC writes to a real device.
|
|
59
|
+
- `docs/guide.md` ("Legacy OSC protocol quirks") is the record of observed
|
|
60
|
+
protocol behavior; update it when you learn something new, and say
|
|
61
|
+
whether it was verified on hardware.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: totalmix-osc
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Verified control of RME TotalMix FX routing, gains, and loopback over OSC
|
|
5
|
+
Author-email: "Buran Consulting, LLC" <info@bradburan.com>
|
|
6
|
+
Maintainer-email: "Buran Consulting, LLC" <info@bradburan.com>
|
|
7
|
+
License: Copyright 2026 totalmix-osc development team
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
10
|
+
|
|
11
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
18
|
+
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: python-osc
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs; extra == "docs"
|
|
25
|
+
Requires-Dist: mkdocs-material; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocstrings[python]; extra == "docs"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest; extra == "test"
|
|
29
|
+
|
|
30
|
+
# totalmix-osc
|
|
31
|
+
|
|
32
|
+
Verified control of RME TotalMix FX routing, gains, and loopback over OSC.
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from totalmix_osc import TotalMixFX
|
|
36
|
+
|
|
37
|
+
with TotalMixFX() as tmfx:
|
|
38
|
+
tmfx.set_mono_straight_playback() # Playback N -> Output N, nothing else
|
|
39
|
+
tmfx.set_gain(1, -6.0)
|
|
40
|
+
tmfx.set_loopback(9, True)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Every change is confirmed by TotalMix's own OSC feedback before returning,
|
|
44
|
+
and a `TotalMixError` is raised if it isn't. Requires OSC Control and
|
|
45
|
+
"Submix linked to OSC Control" to be enabled in TotalMix FX under
|
|
46
|
+
Settings > OSC.
|
|
47
|
+
|
|
48
|
+
A `totalmix-osc` command does the same from the command line, for
|
|
49
|
+
checking changes by eye in the TotalMix window:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
totalmix-osc gain "Phones L" -6
|
|
53
|
+
totalmix-osc xp 2 playback 1 off
|
|
54
|
+
totalmix-osc shell # interactive, one connection
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See the documentation for the full guide, including the quirks of
|
|
58
|
+
TotalMix's legacy OSC protocol that this package works around.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e .[test,docs]
|
|
64
|
+
pytest # offline tests against a simulated TotalMix
|
|
65
|
+
pytest --hardware # also run against TotalMix FX and a connected device
|
|
66
|
+
mkdocs serve
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The hardware tests change the mixer's routing, gains, and loopback, then
|
|
70
|
+
restore the original state.
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Guide
|
|
2
|
+
|
|
3
|
+
`totalmix-osc` provides programmatic control of RME TotalMix FX -- the
|
|
4
|
+
routing/mixing software behind RME audio interfaces such as the Fireface
|
|
5
|
+
and Babyface -- via its OSC (Open Sound Control) interface. It exists to
|
|
6
|
+
solve a specific problem: TotalMix's workspace can be changed by a stray
|
|
7
|
+
click (wrong routing, wrong gain, a channel silently muted), which then
|
|
8
|
+
breaks whatever experiment assumes a known-good mixer state. Rather than
|
|
9
|
+
trusting a saved snapshot or settings file -- which is just as easy to
|
|
10
|
+
overwrite by accident -- every function here fully reconstructs the state
|
|
11
|
+
it touches from first principles, and verifies the result by waiting for
|
|
12
|
+
TotalMix's own OSC feedback before returning.
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
This package targets TotalMix FX's legacy, stateful OSC protocol (not the
|
|
17
|
+
"Global OSC" mode added in TotalMix FX 2.1). Two settings must be enabled
|
|
18
|
+
once, by hand, in TotalMix FX under **Settings > OSC**:
|
|
19
|
+
|
|
20
|
+
* **OSC Control** must be enabled.
|
|
21
|
+
* **Submix linked to OSC Control** must be enabled. Without it,
|
|
22
|
+
`/setSubmix` -- which is used to address individual cells of the routing
|
|
23
|
+
matrix -- is silently ignored.
|
|
24
|
+
|
|
25
|
+
Only one program can receive TotalMix's OSC feedback at a time, so close
|
|
26
|
+
any other OSC tools (e.g., Bitfocus Companion) first.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from totalmix_osc import TotalMixFX
|
|
32
|
+
|
|
33
|
+
with TotalMixFX() as tmfx:
|
|
34
|
+
# Wipe out whatever routing/gain state TotalMix is currently in and
|
|
35
|
+
# reconstruct straight 1:1 mono playback from scratch: Playback channel
|
|
36
|
+
# N feeds Output channel N at unity gain, and nothing else (no other
|
|
37
|
+
# playback channel, no live hardware input) feeds that output.
|
|
38
|
+
tmfx.set_mono_straight_playback()
|
|
39
|
+
|
|
40
|
+
# Per-experiment gain, applied on top of the straight routing above.
|
|
41
|
+
tmfx.set_gain(channel=1, db=-6.0)
|
|
42
|
+
tmfx.set_gain(channel=2, db=-6.0)
|
|
43
|
+
|
|
44
|
+
# Return output 9's signal to the computer as a recording input.
|
|
45
|
+
tmfx.set_loopback(channel=9, enabled=True)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All channel numbers are 1-indexed, matching the TotalMix GUI.
|
|
49
|
+
|
|
50
|
+
## Command line
|
|
51
|
+
|
|
52
|
+
Installing the package adds a `totalmix-osc` command for checking and
|
|
53
|
+
changing settings by hand, for example while watching the TotalMix window
|
|
54
|
+
to confirm a change lands where you expect. Each command reads the
|
|
55
|
+
current value if you leave the value off, and sets it otherwise. Changes
|
|
56
|
+
are confirmed by TotalMix before the command returns. Channels can be
|
|
57
|
+
given by number or by name.
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
totalmix-osc layout # list channels in each row
|
|
61
|
+
totalmix-osc gain 1 # read Output 1's fader
|
|
62
|
+
totalmix-osc gain "Phones L" -6 # set it to -6 dB
|
|
63
|
+
totalmix-osc gain 9 off # fader fully down (-oo)
|
|
64
|
+
totalmix-osc xp 2 playback 1 -12 # Playback 1 -> Output 2 at -12 dB
|
|
65
|
+
totalmix-osc xp "Phones R" input "Analog 3" off
|
|
66
|
+
totalmix-osc loopback 9 on
|
|
67
|
+
totalmix-osc straight # straight 1:1 playback, all outputs
|
|
68
|
+
totalmix-osc show 1 2 # gain, loopback, and sources
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use `off` rather than `-inf` for silence; a value starting with `-` that
|
|
72
|
+
isn't a number is read as an option.
|
|
73
|
+
|
|
74
|
+
Starting the command reads the channel layout, which takes a second or
|
|
75
|
+
two. To avoid repeating that, `totalmix-osc shell` keeps one connection
|
|
76
|
+
open and accepts the same commands interactively (`help` lists them).
|
|
77
|
+
Add `-v` to log progress, or `-vv` to log every OSC message sent. Run
|
|
78
|
+
`totalmix-osc --help` for the connection options (`--ip`, ports,
|
|
79
|
+
`--timeout`).
|
|
80
|
+
|
|
81
|
+
## Channel layout
|
|
82
|
+
|
|
83
|
+
When `TotalMixFX` is created, it reads the channel layout from TotalMix:
|
|
84
|
+
the names of the channels in each row (Input, Playback, and Output) and the
|
|
85
|
+
number of faders per bank. These are available as `channel_names` and
|
|
86
|
+
`bank_size`:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
>>> tmfx.channel_names['Output']
|
|
90
|
+
['Analog 1', 'Analog 2', ..., 'ADAT 3', 'ADAT 4']
|
|
91
|
+
>>> tmfx.bank_size
|
|
92
|
+
8
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Reading the layout rather than configuring it has two purposes. It checks
|
|
96
|
+
that TotalMix is reachable, raising `TotalMixError` straight away if it
|
|
97
|
+
isn't. It also gives the exact channel count, which differs between
|
|
98
|
+
devices and, for some, between sample rates (see
|
|
99
|
+
[The channel count can depend on sample rate](#the-channel-count-can-depend-on-sample-rate)).
|
|
100
|
+
Selecting a channel past the end of a row can crash TotalMix, so every
|
|
101
|
+
channel number is checked against this layout before anything is sent,
|
|
102
|
+
raising `ValueError` if it is out of range.
|
|
103
|
+
|
|
104
|
+
Feedback is matched to channels by name, so channel names must be unique
|
|
105
|
+
within each row. `TotalMixFX` raises `TotalMixError` if they aren't. This
|
|
106
|
+
only matters if you've renamed channels in TotalMix.
|
|
107
|
+
|
|
108
|
+
## Reading the mixer state
|
|
109
|
+
|
|
110
|
+
`get_gain`, `get_crosspoint`, and `get_loopback` read the current state
|
|
111
|
+
from TotalMix, for example to log the mixer configuration alongside
|
|
112
|
+
acquired data:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
tmfx.get_gain(1) # -6.0
|
|
116
|
+
tmfx.get_crosspoint(1, 'Playback', 1) # 0.0
|
|
117
|
+
tmfx.get_crosspoint(1, 'Input', 3) # -inf
|
|
118
|
+
tmfx.get_loopback(9) # True
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Loopback
|
|
122
|
+
|
|
123
|
+
`set_loopback` turns loopback on or off for a hardware output, so the
|
|
124
|
+
signal sent to that output is also returned to the computer as a recording
|
|
125
|
+
input, replacing the matching hardware input. This is useful for recording
|
|
126
|
+
the stimulus that was actually played. `get_loopback` reads back the
|
|
127
|
+
current state.
|
|
128
|
+
|
|
129
|
+
TotalMix's `/2/loopback` is a toggle button, not a setter, so
|
|
130
|
+
`set_loopback` reads the current state first and only presses the
|
|
131
|
+
button if needed, which makes repeated calls safe.
|
|
132
|
+
`set_mono_straight_playback` does not change loopback.
|
|
133
|
+
|
|
134
|
+
## Key concepts
|
|
135
|
+
|
|
136
|
+
**Full reconstruction, not incremental changes**
|
|
137
|
+
|
|
138
|
+
`set_mono_straight_playback` does not check what TotalMix's routing matrix
|
|
139
|
+
currently looks like -- it overwrites every crosspoint it's responsible
|
|
140
|
+
for on every call. This makes it self-healing: it doesn't matter whether
|
|
141
|
+
the previous state came from a clean run, a manual experiment in the GUI,
|
|
142
|
+
or a half-finished OSC script -- the result after calling it is always the
|
|
143
|
+
same.
|
|
144
|
+
|
|
145
|
+
**Verified writes**
|
|
146
|
+
|
|
147
|
+
OSC runs over UDP, which is fire-and-forget: a dropped packet, a disabled
|
|
148
|
+
"OSC Control" setting, or a wrong IP/port all look identical to Python --
|
|
149
|
+
the send call still returns normally. To avoid silently trusting a command
|
|
150
|
+
that had no effect, every change blocks until TotalMix's OSC feedback
|
|
151
|
+
confirms the new value, and raises `TotalMixError` if that confirmation
|
|
152
|
+
doesn't arrive within `timeout` seconds or doesn't match what was
|
|
153
|
+
requested. Feedback only counts as confirmation once TotalMix has also
|
|
154
|
+
confirmed which channel it describes (see
|
|
155
|
+
[Changing a selector triggers a burst of feedback](#changing-a-selector-triggers-a-burst-of-feedback)).
|
|
156
|
+
|
|
157
|
+
**Routing matrix vs. master gain**
|
|
158
|
+
|
|
159
|
+
`set_crosspoint` controls an individual cell of the routing matrix (how
|
|
160
|
+
much of one input/playback channel is fed into one output channel).
|
|
161
|
+
`set_gain` controls an output channel's master fader -- the gain applied
|
|
162
|
+
after routing/mixing. Under a straight 1:1 routing, `set_gain` is the
|
|
163
|
+
natural place for per-experiment calibration offsets, since each output
|
|
164
|
+
only receives one playback channel.
|
|
165
|
+
|
|
166
|
+
## Legacy OSC protocol quirks
|
|
167
|
+
|
|
168
|
+
The legacy protocol has no complete official spec. These notes are what
|
|
169
|
+
we found testing against a Fireface UCX II running TotalMix FX 1.98 at
|
|
170
|
+
96 kHz (16 channels per row: Analog 1-6, Phones L/R, SPDIF L/R, AES L/R,
|
|
171
|
+
ADAT 1-4). Each entry says whether it was verified on hardware, and
|
|
172
|
+
credits the
|
|
173
|
+
[Bitfocus Companion TotalMix module](https://github.com/bitfocus/companion-module-rme-totalmix),
|
|
174
|
+
the most thorough open implementation of this protocol, where it
|
|
175
|
+
documents the same behavior.
|
|
176
|
+
|
|
177
|
+
### Numeric arguments must be float32
|
|
178
|
+
|
|
179
|
+
TotalMix reads every numeric argument as a 32-bit float, including
|
|
180
|
+
selectors and buttons whose values are whole numbers. An OSC int is
|
|
181
|
+
misread rather than rejected: `/setSubmix 2` (int) selected the first
|
|
182
|
+
submix, while `/setSubmix 2.0` (float) selected the third. Python's
|
|
183
|
+
`python-osc` sends a Python `int` as an OSC int, so `OSCClient` converts
|
|
184
|
+
every number to a float before sending. *Verified; Companion encodes all
|
|
185
|
+
numbers as float32 and notes that TotalMix documents this.*
|
|
186
|
+
|
|
187
|
+
### Faders are addressed by bank, not by channel
|
|
188
|
+
|
|
189
|
+
`/1/volumeN` only reaches strips 1 to N of the current bank, where N is
|
|
190
|
+
"Number of faders per bank" in Settings > OSC (default 8). To reach other
|
|
191
|
+
channels, select the bank with `/setBankStart`, which takes the
|
|
192
|
+
**0-indexed** first channel of the bank and rounds down to a bank
|
|
193
|
+
boundary (`/setBankStart 12.0` with 8-wide banks selects channel 9
|
|
194
|
+
onward). `/setSubmix` is also 0-indexed, but `/1/volumeN` strip numbers
|
|
195
|
+
start at 1. `TotalMixFX` reads the bank size from TotalMix and converts
|
|
196
|
+
between these and its own 1-indexed channel numbers. *Verified.*
|
|
197
|
+
|
|
198
|
+
### Selecting a bank past the last channel can crash TotalMix
|
|
199
|
+
|
|
200
|
+
TotalMix FX 1.98 crashed (access violation `0xc0000005`) while we were
|
|
201
|
+
enumerating channels by sending `/setBankStart` with increasing values up
|
|
202
|
+
to 64, well past the 16 channels it exposed. We did not isolate exactly
|
|
203
|
+
which message caused it. An out-of-range `/setSubmix` may also be unsafe.
|
|
204
|
+
`TotalMixFX` reads the layout with `/1/bank+` instead, which stops at the
|
|
205
|
+
last bank rather than overrunning, and refuses any channel outside that
|
|
206
|
+
layout before anything is sent. *Observed once; trigger not isolated.*
|
|
207
|
+
|
|
208
|
+
### The channel count can depend on sample rate
|
|
209
|
+
|
|
210
|
+
Companion's profile for the UCX II lists 20 channels per row (ADAT 1-8),
|
|
211
|
+
but at 96 kHz TotalMix exposed only 16 (ADAT 1-4). This is consistent
|
|
212
|
+
with ADAT switching to S/MUX at double sample rates, which halves its
|
|
213
|
+
channel count. A fixed per-device channel count is therefore not enough
|
|
214
|
+
to guard against the crash above, which is why `TotalMixFX` reads the
|
|
215
|
+
layout from TotalMix at startup. Slots past the end of a row are named
|
|
216
|
+
`n.a.`; Companion discards them the same way. *Inferred; we have not yet
|
|
217
|
+
compared sample rates directly.*
|
|
218
|
+
|
|
219
|
+
### Changing a selector triggers a burst of feedback
|
|
220
|
+
|
|
221
|
+
Selecting a bus (`/1/busInput`, `/1/busPlayback`, `/1/busOutput`), bank
|
|
222
|
+
or submix makes TotalMix send back the full state of the newly selected
|
|
223
|
+
bank: about 110 messages on page 1, about 85 on page 2. Some changes
|
|
224
|
+
produce two bursts, and the first can still reflect the previous bus.
|
|
225
|
+
Bursts from an earlier command can also still be arriving after the next
|
|
226
|
+
command is sent. A burst lists every strip's value first and the strip
|
|
227
|
+
names last, so a value alone doesn't say which channel it belongs to.
|
|
228
|
+
|
|
229
|
+
`TotalMixFX` therefore only trusts a strip's value once the same burst
|
|
230
|
+
has also reported the expected bus, submix label (for crosspoints), and
|
|
231
|
+
channel name for that strip. After that, further values for the strip
|
|
232
|
+
(the reply to a write) belong to the same selection. *Verified; Companion
|
|
233
|
+
documents the same bursts and also ignores stale values.*
|
|
234
|
+
|
|
235
|
+
### Message order within a burst matters
|
|
236
|
+
|
|
237
|
+
A burst is a series of separate UDP messages, and their order carries
|
|
238
|
+
meaning. For example, a page-2 burst sends a channel's `/2/loopback`
|
|
239
|
+
before its `/2/trackname`, so the name tells you which channel the
|
|
240
|
+
loopback value belonged to. `python-osc`'s `ThreadingOSCUDPServer`
|
|
241
|
+
handles each message on its own thread, so handlers can run out of
|
|
242
|
+
order. This only showed up once logging slowed some handlers down.
|
|
243
|
+
`OSCClient` therefore uses `BlockingOSCUDPServer`, which handles messages
|
|
244
|
+
one at a time in arrival order. *Verified.*
|
|
245
|
+
|
|
246
|
+
### Page-2 buttons toggle instead of setting
|
|
247
|
+
|
|
248
|
+
On page 2 (per-channel settings), buttons such as `/2/loopback` toggle
|
|
249
|
+
when sent `1.0`, and `0.0` does nothing. Sending `1.0` twice turns
|
|
250
|
+
loopback on and then off again. To set a state rather than flip it, read
|
|
251
|
+
the current value first and only send `1.0` if it differs.
|
|
252
|
+
`set_loopback` does this. *Verified for loopback; Companion treats all
|
|
253
|
+
legacy toggle-style parameters this way.*
|
|
254
|
+
|
|
255
|
+
### Selecting a channel on page 2
|
|
256
|
+
|
|
257
|
+
Page 2 shows one channel at a time: the strip at `/setOffsetInBank`
|
|
258
|
+
(0-indexed) within the current bank. For example, `/setBankStart 8.0`,
|
|
259
|
+
`/setOffsetInBank 7.0`, `/2/busOutput 1.0` selects channel 16. Selecting a
|
|
260
|
+
bank resets the offset to 0, and `/2/track+` and `/2/track-` step one
|
|
261
|
+
channel at a time. *Verified; `/setOffsetInBank` learned from Companion.*
|
|
262
|
+
|
|
263
|
+
### Where feedback is sent
|
|
264
|
+
|
|
265
|
+
TotalMix sends feedback to the "Remote Controller" IP set in Settings >
|
|
266
|
+
OSC, on the outgoing port (9001 by default). On our machine that was the
|
|
267
|
+
host's network address, not `127.0.0.1`, so a listener bound only to
|
|
268
|
+
loopback received nothing. `OSCClient` binds to the host's address by
|
|
269
|
+
default, which matches. Only one process can listen on the feedback port.
|
|
270
|
+
*Address verified; the port conflict is noted by Companion.*
|
|
271
|
+
|
|
272
|
+
### Minor details
|
|
273
|
+
|
|
274
|
+
* TotalMix reports unity gain as `-0.0 dB` after some writes and
|
|
275
|
+
`0.0 dB` after others. They parse to the same float, but direct string
|
|
276
|
+
comparisons will treat them as different.
|
|
277
|
+
* "Send Level Data" in Settings > OSC adds a steady stream of meter
|
|
278
|
+
messages. `TotalMixFX` doesn't need them; turn it off to reduce traffic.
|
|
279
|
+
* A verified fader write takes about 10-50 ms locally.
|
|
280
|
+
`set_mono_straight_playback` on 16 channels (528 writes) takes about
|
|
281
|
+
10 s.
|
|
282
|
+
|
|
283
|
+
## Design notes
|
|
284
|
+
|
|
285
|
+
TotalMix FX 2.1 adds "Global OSC", which addresses every fader and setting
|
|
286
|
+
directly (e.g., `/mix/in/3/8/faderlin`) instead of through bus, bank, and
|
|
287
|
+
submix selection. It avoids most of the quirks above, but is still in beta
|
|
288
|
+
and the devices we use run older TotalMix versions. The public API (bus
|
|
289
|
+
names, 1-indexed channel numbers, dB values, explicit `get_*`/`set_*`
|
|
290
|
+
methods) is deliberately independent of the legacy addressing scheme, so
|
|
291
|
+
that a Global OSC backend can be added later without changing it.
|
|
292
|
+
|
|
293
|
+
## Known limitations
|
|
294
|
+
|
|
295
|
+
* Matrix crosspoint control relies on TotalMix's `/setSubmix` command,
|
|
296
|
+
which is documented mainly through the RME user forum rather than an
|
|
297
|
+
official spec.
|
|
298
|
+
* Stereo-paired channels have not been tested; the devices we tested on
|
|
299
|
+
had all channels set to mono.
|
|
300
|
+
* Tested only on a Fireface UCX II with TotalMix FX 1.98.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# totalmix-osc
|
|
2
|
+
|
|
3
|
+
Verified control of RME TotalMix FX routing, gains, and loopback over OSC.
|
|
4
|
+
|
|
5
|
+
TotalMix FX is the mixer software for RME audio interfaces. Its state can
|
|
6
|
+
be changed by a stray click, which breaks experiments that assume a
|
|
7
|
+
known-good mixer configuration. `totalmix-osc` sets the routing matrix,
|
|
8
|
+
output gains, and loopback from Python, and confirms every change with
|
|
9
|
+
TotalMix before returning.
|
|
10
|
+
|
|
11
|
+
## Installing
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install totalmix-osc
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
* The [Guide](guide.md) covers setup, usage, and the quirks of TotalMix's
|
|
20
|
+
OSC protocol that this package works around.
|
|
21
|
+
* The [API reference](api.md) is generated from docstrings.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from totalmix_osc import TotalMixFX
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
logging.basicConfig(level='INFO')
|
|
8
|
+
|
|
9
|
+
# Uses the default ports (7001/9001) and this machine's IP. Pass
|
|
10
|
+
# ip_address explicitly if TotalMix is running on a different machine.
|
|
11
|
+
tmfx = TotalMixFX()
|
|
12
|
+
|
|
13
|
+
# Primary use case: wipe out whatever routing/gain state TotalMix is
|
|
14
|
+
# currently in and reconstruct straight 1:1 mono playback from
|
|
15
|
+
# scratch, regardless of what a stray click may have changed.
|
|
16
|
+
tmfx.set_mono_straight_playback()
|
|
17
|
+
|
|
18
|
+
# Per-experiment gain, applied on top of the straight routing above.
|
|
19
|
+
tmfx.set_gain(1, -6.0)
|
|
20
|
+
tmfx.set_gain(2, -6.0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == '__main__':
|
|
24
|
+
main()
|