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.
Files changed (37) hide show
  1. totalmix_osc-0.0.1/.github/workflows/ci.yaml +32 -0
  2. totalmix_osc-0.0.1/.github/workflows/docs.yaml +47 -0
  3. totalmix_osc-0.0.1/.github/workflows/publish-to-pypi.yml +38 -0
  4. totalmix_osc-0.0.1/.gitignore +77 -0
  5. totalmix_osc-0.0.1/CLAUDE.md +61 -0
  6. totalmix_osc-0.0.1/PKG-INFO +70 -0
  7. totalmix_osc-0.0.1/docs/api.md +9 -0
  8. totalmix_osc-0.0.1/docs/guide.md +300 -0
  9. totalmix_osc-0.0.1/docs/index.md +21 -0
  10. totalmix_osc-0.0.1/examples/reset_routing.py +24 -0
  11. totalmix_osc-0.0.1/license.txt +11 -0
  12. totalmix_osc-0.0.1/mkdocs.yml +44 -0
  13. totalmix_osc-0.0.1/pyproject.toml +43 -0
  14. totalmix_osc-0.0.1/readme.md +41 -0
  15. totalmix_osc-0.0.1/setup.cfg +4 -0
  16. totalmix_osc-0.0.1/tests/conftest.py +52 -0
  17. totalmix_osc-0.0.1/tests/fake_totalmix.py +208 -0
  18. totalmix_osc-0.0.1/tests/test_cli.py +126 -0
  19. totalmix_osc-0.0.1/tests/test_control.py +179 -0
  20. totalmix_osc-0.0.1/tests/test_fader_curve.py +27 -0
  21. totalmix_osc-0.0.1/tests/test_hardware.py +102 -0
  22. totalmix_osc-0.0.1/totalmix_osc/__init__.py +8 -0
  23. totalmix_osc-0.0.1/totalmix_osc/__main__.py +6 -0
  24. totalmix_osc-0.0.1/totalmix_osc/cli.py +308 -0
  25. totalmix_osc-0.0.1/totalmix_osc/control.py +558 -0
  26. totalmix_osc-0.0.1/totalmix_osc/fader_curve.py +51 -0
  27. totalmix_osc-0.0.1/totalmix_osc/osc_client.py +118 -0
  28. totalmix_osc-0.0.1/totalmix_osc/totalmix_fx_volume_map.json +1003 -0
  29. totalmix_osc-0.0.1/totalmix_osc/version.py +24 -0
  30. totalmix_osc-0.0.1/totalmix_osc.egg-info/PKG-INFO +70 -0
  31. totalmix_osc-0.0.1/totalmix_osc.egg-info/SOURCES.txt +35 -0
  32. totalmix_osc-0.0.1/totalmix_osc.egg-info/dependency_links.txt +1 -0
  33. totalmix_osc-0.0.1/totalmix_osc.egg-info/entry_points.txt +2 -0
  34. totalmix_osc-0.0.1/totalmix_osc.egg-info/requires.txt +10 -0
  35. totalmix_osc-0.0.1/totalmix_osc.egg-info/scm_file_list.json +30 -0
  36. totalmix_osc-0.0.1/totalmix_osc.egg-info/scm_version.json +8 -0
  37. 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,9 @@
1
+ # API reference
2
+
3
+ ::: totalmix_osc.TotalMixFX
4
+
5
+ ::: totalmix_osc.TotalMixError
6
+
7
+ ::: totalmix_osc.OSCClient
8
+
9
+ ::: totalmix_osc.load_volume_map
@@ -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()