Skip to content

Commit 9dd2bf0

Browse files
authored
Merge pull request #5 from alphonsom/mcu-translator
Translate Mackie Control in the dongle: a freestanding C module, shared with the firmware
2 parents a0fc22f + 880f80f commit 9dd2bf0

8 files changed

Lines changed: 1217 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/build/
2+
/build-*/
23
*.o
34
*.a
45
compile_commands.json

CMakeLists.txt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# SPDX-License-Identifier: GPL-3.0-or-later
22
cmake_minimum_required(VERSION 3.16)
3-
project(command8 LANGUAGES CXX)
3+
project(command8 LANGUAGES C CXX)
44

55
set(CMAKE_CXX_STANDARD 17)
66
set(CMAKE_CXX_STANDARD_REQUIRED ON)
7+
set(CMAKE_C_STANDARD 99)
8+
set(CMAKE_C_STANDARD_REQUIRED ON)
79
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
810
set(CMAKE_BUILD_TYPE Release)
911
endif()
@@ -28,6 +30,15 @@ add_library(command8 STATIC
2830
target_include_directories(command8 PUBLIC src)
2931
target_compile_options(command8 PRIVATE ${C8_WARNINGS})
3032

33+
# Freestanding C99 Command|8 <-> Mackie Control translator, shared verbatim with
34+
# the dongle firmware, where it runs on bare metal with no allocator and no C++
35+
# runtime. It is a separate target with no link dependencies on purpose: the
36+
# build then enforces that property instead of leaving it to convention, so an
37+
# accidental #include of a host header fails here rather than on the RP2040.
38+
add_library(command8-mcu STATIC src/mcu/c8_mcu.c)
39+
target_include_directories(command8-mcu PUBLIC src)
40+
target_compile_options(command8-mcu PRIVATE ${C8_WARNINGS})
41+
3142
if(WIN32)
3243
find_package(RtMidi CONFIG REQUIRED)
3344
target_sources(command8 PRIVATE
@@ -162,6 +173,11 @@ target_link_libraries(test_protocol PRIVATE command8)
162173
target_compile_options(test_protocol PRIVATE ${C8_WARNINGS})
163174
add_test(NAME protocol COMMAND test_protocol)
164175

176+
add_executable(test_mcu tests/test_mcu.cpp)
177+
target_link_libraries(test_mcu PRIVATE command8 command8-mcu)
178+
target_compile_options(test_mcu PRIVATE ${C8_WARNINGS})
179+
add_test(NAME mcu COMMAND test_mcu)
180+
165181
add_executable(test_feedback tests/test_feedback.cpp)
166182
target_link_libraries(test_feedback PRIVATE command8)
167183
target_compile_options(test_feedback PRIVATE ${C8_WARNINGS})

README.md

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,19 +204,44 @@ talks to the engine (over OSC, or over the MCU loopback pair), never to the
204204
surface directly: the Command|8 speaks a proprietary protocol, so a DAW sending
205205
it generic MIDI just makes the faders twitch and leaves the display Offline.
206206

207+
### No bridge at all: the dongle's MCU cable
208+
209+
A dongle built with on-board translation exposes a fourth cable that *is* a
210+
Mackie Control endpoint, so none of the above is needed — no engine process, no
211+
loopback pair, no OSC. Point the DAW's Mackie Control support straight at it,
212+
input and output, and leave the surface's own port disabled.
213+
214+
The cable is named `Command8 MCU` on Linux and macOS. On Windows, WinMM names
215+
cables positionally and ignores the jack strings, so it appears as
216+
`MIDIIN4 (Command|8 Bridge)` / `MIDIOUT4 (Command|8 Bridge)` — the fourth port.
217+
218+
Verified on hardware 2026-08-11: Reaper driving a Command|8 through the dongle
219+
on Windows with no host software running.
220+
221+
The translation is the same `src/mcu/c8_mcu.c` this repo unit-tests on the
222+
desktop; the firmware compiles that file directly rather than a copy.
223+
207224
### Mackie bridge on Windows
208225

209226
Windows has no app-created virtual MIDI ports, so create a loopback pair once
210227
with [Windows MIDI Services](https://aka.ms/midi) (or two loopMIDI cables and
211228
`--mcu-recv`/`--mcu-send`):
212229

213230
```bat
214-
midi loopback create --name-a "Command8 MCU A" --name-b "Command8 MCU B"
231+
midi loopback create --name-a "Command8 MCU Bridge" --name-b "Command8 MCU DAW"
215232
```
216233

217-
`command8-mackie` uses side **A** by default; point the DAW's Mackie Control
218-
input *and* output at side **B**. The pair is crossed, so neither end hears its
219-
own output.
234+
The ends are named for who owns them. `command8-mackie` takes **Command8 MCU
235+
Bridge** by default; point the DAW's Mackie Control input *and* output at
236+
**Command8 MCU DAW**. The pair is crossed, so neither end hears its own output.
237+
238+
Note there is no bar in these names, deliberately. `Command|8` is how the tools
239+
find the surface itself, by prefix, so a loopback named `Command|8 MCU …` could
240+
be matched as the device. Keep the loopback pair on the unbarred `Command8`.
241+
242+
Verified end to end on Windows: with the three `Command|8 Bridge` device ports
243+
left **disabled** in Reaper's MIDI Devices, `Command8 MCU DAW` enabled, and a
244+
Mackie Control Universal surface pointed at it for both input and output.
220245

221246

222247
## License

src/mackie/mackie_backend.hpp

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,26 @@
2121
namespace command8 {
2222

2323
// Default MCU port-name matches. recv = DAW-to-bridge, send = bridge-to-DAW.
24-
// Linux: one duplex virmidi port carries both directions. Windows: side A of a
25-
// Windows MIDI Services loopback pair (create once with
26-
// midi loopback create --name-a "Command8 MCU A" --name-b "Command8 MCU B"
27-
// ); the bridge opens A both ways and the DAW's Mackie Control uses B, so
28-
// neither hears its own output. macOS: these are the names of the virtual
29-
// ports the bridge CREATES rather than ones to search for, so no loopback is
30-
// needed and the DAW points at this name for both directions.
24+
// Linux: one duplex virmidi port carries both directions. Windows: the bridge
25+
// end of a Windows MIDI Services loopback pair (create once with
26+
// midi loopback create --name-a "Command8 MCU Bridge" --name-b "Command8 MCU DAW"
27+
// ); the bridge opens its own end both ways and the DAW's Mackie Control uses
28+
// the other, so neither hears its own output. macOS: these are the names of the
29+
// virtual ports the bridge CREATES rather than ones to search for, so no
30+
// loopback is needed and the DAW points at this name for both directions.
31+
//
32+
// The ends are named for who owns them because "A" and "B" gave no clue which
33+
// was which, and picking the wrong one in the DAW produces a silent failure
34+
// that looks exactly like broken hardware.
35+
//
36+
// Do NOT put a bar in these names. "Command|8" is the surface port matcher (see
37+
// kDefaultPortMatch in surface.hpp), and it takes the first prefix match it
38+
// finds -- a loopback called "Command|8 MCU ..." could win that match ahead of
39+
// the real device depending on enumeration order. The unbarred "Command8" is
40+
// what keeps the two families of port distinguishable.
3141
#if defined(_WIN32)
32-
inline constexpr const char* kDefaultMcuRecvMatch = "Command8 MCU A";
33-
inline constexpr const char* kDefaultMcuSendMatch = "Command8 MCU A";
42+
inline constexpr const char* kDefaultMcuRecvMatch = "Command8 MCU Bridge";
43+
inline constexpr const char* kDefaultMcuSendMatch = "Command8 MCU Bridge";
3444
#elif defined(__APPLE__)
3545
inline constexpr const char* kDefaultMcuRecvMatch = "Command|8";
3646
inline constexpr const char* kDefaultMcuSendMatch = "Command|8";

src/mackie/mackie_main.cpp

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
// Linux: use a snd-virmidi kernel port so DAWs (e.g. Bitwig) can see it:
66
// sudo modprobe snd-virmidi -> "Virtual Raw MIDI 4-0..4-3"
77
// Windows: create a Windows MIDI Services loopback pair once:
8-
// midi loopback create --name-a "Command8 MCU A" --name-b "Command8 MCU B"
9-
// The bridge uses side A; point the DAW's Mackie Control at side B (in + out).
8+
// midi loopback create --name-a "Command8 MCU Bridge" --name-b "Command8 MCU DAW"
9+
// The bridge takes the "Bridge" end; point the DAW's Mackie Control at the
10+
// "DAW" end (in + out). Leave the "Command|8 Bridge" device ports themselves
11+
// disabled in the DAW -- those belong to this process, and a DAW holding them
12+
// stops it opening the surface at all.
1013
//
1114
// ./command8-mackie [--mcu-port <match>] [--mcu-recv <match>]
1215
// [--mcu-send <match>] [--port <device-match>]
@@ -83,8 +86,8 @@ int main(int argc, char** argv) {
8386
#ifdef _WIN32
8487
std::fprintf(stderr, "Could not open the MCU ports ('%s' / '%s'). Create "
8588
"the loopback pair first:\n midi loopback create --name-a "
86-
"\"Command8 MCU A\" --name-b \"Command8 MCU B\"\n(or create "
87-
"cables in loopMIDI) and retry.\n",
89+
"\"Command8 MCU Bridge\" --name-b \"Command8 MCU DAW\"\n"
90+
"(or create cables in loopMIDI) and retry.\n",
8891
recv_match.c_str(), send_match.c_str());
8992
#else
9093
std::fprintf(stderr, "Could not open an MCU port matching '%s'. Load "

0 commit comments

Comments
 (0)