# Eurorack and Unity Audio tutorial 7 of the [[Audio]] series. Previous: [[FMOD vs Built-in Mixer and Procedural Synth]] (a software synth *inside* Unity). This note is the hardware cousin: Unity drives your **Moog Subharmonicon** and **Mother-32** over MIDI, listens to them through an audio interface, and can talk to other software over OSC. Next: [[Music Visualization]], which consumes the audio return built here. **Target:** Unity **6000.5+** on **Windows (x86_64)**. Nothing here is Quest-specific. If you later want the same ideas on a Quest 2, that is a separate project in a **6000.3.27f2+** stack ([[0.Choosing a Unity Version]]); note the Android caveats in 1.2 and 5.4. > [!note] Verification status > Checked 2026-11-09: > - **Subharmonicon** MIDI behaviour (MIDI IN on a 3.5 mm jack via the supplied DIN-to-TRS Type A adapter; note data is an offset from C4; CC numbers and ranges; MIDI clock overrides the internal and analog clock; default channel 1; channel changed by sending a SysEx file) from Moog's official manual PDF (https://cdn.inmusicbrands.com/Moog/Subharmonicon/Subharmonicon_Manual%20AMZ.pdf), Moog's help article "How do I change the MIDI CH on Subharmonicon" (https://moogmusic-help.freshdesk.com/en/support/solutions/articles/69000840565-moog-subharmonicon-how-do-i-change-midi-channel-on-the-subharmonicon-) and the community table at https://midi.guide/d/moog/subharmonicon/. > - **Mother-32** MIDI behaviour (5-pin DIN MIDI IN, default channel 1, Setup mode page 2 for channel, note 0 = -5 V to note 120 = +5 V, pitch bend +/-1 octave default and the RPN to change it, CC 5/64/65, CC 1/2/4/7 to the ASSIGN output, program changes) from Moog's manual (https://cdn.inmusicbrands.com/Moog/Mother32/Mother_32_Users_Manual.pdf) and the ManualsLib copy of its "MIDI Functions and Implementation" page. > - **Packages:** the npm registry `https://registry.npmjs.com` reports latest `jp.keijiro.minis` **1.3.2**, `jp.keijiro.rtmidi` **2.2.0**, `jp.keijiro.osc-jack` **2.0.0**. Minis and RtMidi for Unity list Windows x86_64 as supported and require Unity 2022.3+; OscJack requires 2021.3+ (READMEs on GitHub: keijiro/Minis, keijiro/jp.keijiro.rtmidi, keijiro/OscJack). Minis' README states it has **no MIDI out** and points to RtMidi for output. > - **RtMidi for Unity API** (`RtMidi.MidiOut.Create()`, `PortCount`, `GetPortName(i)`, `OpenPort(i)`, `SendMessage(ReadOnlySpan<byte>)`, `ErrorReceived`, `Dispose`) read from the package source on GitHub (`MidiOut.cs`, `Assets/MidiOutTest.cs`). **Minis callbacks** (`Minis.MidiDevice`, `onWillNoteOn`, `onWillControlChange`, `MidiNoteControl.noteNumber/channel`, channels numbered from 0) from its README and `CallbackTest.cs`. **OscJack** API (`OscClient`, `OscServer`, `MessageDispatcher.AddCallback`, `OscDataHandle.GetElementAsFloat`, callbacks run on the *server thread*) from its README. > - **Unity docs (6000.5):** `Microphone.Start` (null/empty name = default device; use `AudioSettings.outputSampleRate` for the frequency), `Microphone.GetPosition`, `AudioClip.GetData` (interleaved; reads wrap around the clip) pages fetched. > - **ES-9 example interface** specs (DC-coupled 3.5 mm inputs and outputs, about +/-10 V, optional input DC-blocking filters that must be OFF for CV, 3.5 mm outputs default to channels 9-16, MIDI I/O on a breakout) from Expert Sleepers' product page and user manual (https://www.expert-sleepers.co.uk/es9.html). It is used purely as an *example* of a DC-coupled interface; I do not know what interface or rack MIDI module you own. > > **Not verified:** how the Unity `Microphone` API presents a multichannel interface (channel count/which channels), Unity's maximum output channel count on Windows, whether RtMidi output is safe from multiple threads (the code below serialises every send behind one lock), whether the Subharmonicon reacts to MIDI Start/Stop realtime messages (Moog documents transport as SysEx), and the Subharmonicon's default 14-bit CC value. Treat each as a bench test. **None of the code has been compiled or play-tested in Unity.** Do not connect any CV between a computer interface and the rack until you have read the voltage warnings in section 5. ## Parts of this note 1. [Signal map and what each box can do](#1-signal-map-and-what-each-box-can-do) 2. [Packages and project setup](#2-packages-and-project-setup) 3. [MIDI out: a thread-safe service](#3-midi-out-a-thread-safe-service) 4. [Instrument controllers: Subharmonicon and Mother-32](#4-instrument-controllers-subharmonicon-and-mother-32) 5. [Audio return: interface, Microphone API and monitoring](#5-audio-return-interface-microphone-api-and-monitoring) 6. [DC-coupled CV: what works, what hurts](#6-dc-coupled-cv-what-works-what-hurts) 7. [MIDI in with Minis, and game state to synth](#7-midi-in-with-minis-and-game-state-to-synth) 8. [OSC with OscJack](#8-osc-with-oscjack) 9. [Bench test order, pitfalls, Level Up! and patterns](#9-bench-test-order-pitfalls-level-up-and-patterns) ## 1. Signal map and what each box can do ### 1.1 The chain ``` Unity (RtMidi MidiOut) --USB--> USB-MIDI interface --5-pin DIN--> Mother-32 MIDI IN (ch 1) | +--5-pin DIN --> (Type A DIN-to-3.5mm adapter) --> Subharmonicon MIDI IN (ch 2) Mother-32 / Subharmonicon audio outs --> mixer or rack mix --> audio interface input --USB--> Unity (Microphone API) Optional: DC-coupled interface <--> CV out from Unity / CV in to Unity (section 6) Optional: OSC over UDP <--> Max, VCV Rack, TouchDesigner, a phone app (section 8) ``` - The **Subharmonicon** takes MIDI on a **3.5 mm jack** (MIDI Type A, with the DIN-to-mini adapter that ships with it). The **Mother-32** takes a normal **5-pin DIN**. Neither page I read describes a MIDI THRU, so plan a split: either an interface with two MIDI outs, a MIDI thru box, or two separate ports on the interface. - You said you also have a rack MIDI module. If that module is a MIDI-to-CV/gate converter, it feeds *other* rack voices and is orthogonal to the two Moogs. If it exposes MIDI over USB (some DC-coupled interfaces do, for example the ES-9 breakout), it can *replace* the separate USB-MIDI interface. Either way Unity just sees "a MIDI output port by name." - **Channels.** Both Moogs default to **channel 1**. Put them on different channels so one cable (or one thru chain) can address both: set the Mother-32 in Setup mode page 2 (step buttons select channel 1-8; Shift selects 9-16, per the manual) and send the Subharmonicon one of Moog's channel SysEx files (for example `subharmonicon_midi_ch2.syx`) using a SysEx tool such as SysEx Transfer Utility on Windows, as Moog's help article recommends. The code below assumes **Mother-32 = channel 1, Subharmonicon = channel 2**. ### 1.2 What MIDI can and cannot do on each instrument | Instrument | Notes | Controls | Clock | Unity should... | |---|---|---|---|---| | **Mother-32** | Note 0 = -5 V, note 120 = +5 V on 1 V/oct KB CV; notes 121-127 repeat 109-115; a Gate is made for the note's duration; a running sequencer is *transposed* by incoming notes | Pitch bend (default +/-1 octave, changeable via RPN CC101=0, CC100=0, CC6=0..12); CC5 portamento time, CC65 portamento on/off, CC64 sustain; **CC1/2/4/7 become -5..+5 V on the ASSIGN jack** if you set Setup page 1 to modes 13-16; program changes 1-64 select pattern, 71-86 select ASSIGN mode | MIDI clock sync is listed as a feature | Play notes, bend, and turn game variables into CV via the ASSIGN output | | **Subharmonicon** | Note data is an **offset from C4 (60)**: with VCO 1/2 FREQ knobs fully counter-clockwise, MIDI notes arrive unchanged | CC 4/36 = VCO1 freq (14-bit, +/-2.5 oct added to the knob); CC 12/44 = VCO2 freq; CC 103-106 = sub divisions (value ranges map to 16...1); CC 113 = rhythm generator logic (0-63 OR, 64-127 XOR) | **MIDI clock overrides the internal clock and any analog clock** | Send tempo and a few "macro" CCs; let its own sequencer make the rhythm | Design consequence: both are *semi-modular instruments with their own sequencers.* Unity should usually be the **conductor** (tempo, tension, transposition, timbre moves), not a step-by-step note sequencer fighting main-thread jitter (see 3.3). ## 2. Packages and project setup 1. **Scoped registry.** Project Settings > Package Manager > Scoped Registries: Name `Keijiro`, URL `https://registry.npmjs.com`, Scope `jp.keijiro` (this is the registry OscJack's README documents; Minis and RtMidi use the same one). 2. In Package Manager > **My Registries**, install **RtMidi** (`jp.keijiro.rtmidi`, registry latest 2.2.0 on 2026-11-09), **Minis** (`jp.keijiro.minis`, 1.3.2) and **OSC Jack** (`jp.keijiro.osc-jack`, 2.0.0). Install **Input System** from the Unity Registry first if it is not already there (see [[Input System]]); Minis is an extension of it. Set Active Input Handling to *Input System Package (New)* or *Both*. 3. **Assembly definitions.** RtMidi's runtime assembly is `RtMidi.Runtime` with `autoReferenced: true`, so scripts in `Assembly-CSharp` see it. If you put your scripts in an asmdef, add a reference to `RtMidi.Runtime`, and (for Minis) `Minis` plus `Unity.InputSystem` (verify the assembly name Minis shipped in the Package Manager > Samples/Packages tree; I did not read it). 4. Create `Assets/Scripts/Eurorack/` and add the scripts below. Create an empty GameObject `Synth Rig` and add `MidiOutService`, `MidiClockThread`, `SubharmoniconController`, `Mother32Controller` to it. 5. **Player Settings (Windows):** *Run in Background* ON (otherwise MIDI and the Microphone stop feeding when the Game view loses focus), and keep the Editor *Enter Play Mode Options* in mind: with Domain Reload disabled, static fields survive; the service below cleans up in `OnDisable`/`OnDestroy` regardless. ## 3. MIDI out: a thread-safe service ### 3.1 Design - One `MidiOutService` owns the port and exposes **0-based channels** internally (matching Minis, which numbers channels from 0). Controllers take human channel numbers (1-16) and subtract one. - All sends go through one `lock`, so the clock thread and the main thread cannot interleave bytes. (RtMidi's thread-safety is not something I verified; serialising is the safe default.) - It **tracks held notes** and releases them in `OnDisable`/`OnDestroy`/`OnApplicationQuit`. A stuck gate on a drone synth is the single most annoying failure here. It does *not* rely on CC 123 "All Notes Off" because Moog's pages do not list it as supported. - **Rate limit.** Classic MIDI DIN runs at 31,250 baud (MIDI spec), so a 3-byte message takes about 1 ms on the wire. Do not send a CC every frame at 144 fps. The bridge in section 7 sends only when the value moves by a whole CC step and caps the rate. ### 3.2 `MidiOutService.cs` ```csharp using System; using System.Collections.Generic; using UnityEngine; using RtMidi; /// <summary> /// Opens one MIDI output port by (part of) its name and sends channel messages. /// Channels are 0-15 here (0 = MIDI channel 1). Main-thread API; also safe to call /// from the clock thread because every send is serialised by a lock. /// </summary> [DefaultExecutionOrder(-100)] public sealed class MidiOutService : MonoBehaviour { public static MidiOutService Instance { get; private set; } [Tooltip("Case-insensitive part of the port name, e.g. 'USB MIDI' or 'ES-9'. Empty = first usable port.")] [SerializeField] string portNameContains = ""; [SerializeField] bool logPorts = true; MidiOut _out; string _openName = "(none)"; readonly object _lock = new object(); readonly byte[] _b1 = new byte[1]; readonly byte[] _b2 = new byte[2]; readonly byte[] _b3 = new byte[3]; readonly HashSet<int> _held = new HashSet<int>(); // key = channel * 128 + note public bool IsOpen => _out != null; public string OpenPortName => _openName; void Awake() { if (Instance != null && Instance != this) { Destroy(this); return; } Instance = this; } void OnEnable() => Open(); void OnDisable() { ReleaseAll(); Close(); } void OnApplicationQuit() => ReleaseAll(); void OnDestroy() { if (Instance == this) Instance = null; } // ---------- port handling ---------- void Open() { lock (_lock) { if (_out != null) return; using var probe = MidiOut.Create(); int count = probe.PortCount; if (logPorts) for (int i = 0; i < count; i++) Debug.Log(quot;[MidiOut] port {i}: {probe.GetPortName(i)}"); for (int i = 0; i < count; i++) { string name = probe.GetPortName(i); bool match = string.IsNullOrEmpty(portNameContains) || name.IndexOf(portNameContains, StringComparison.OrdinalIgnoreCase) >= 0; if (!match) continue; var dev = MidiOut.Create(); dev.ErrorReceived = (type, msg) => Debug.LogWarning(quot;[MidiOut:{name}] {type}: {msg}"); dev.OpenPort(i); _out = dev; _openName = name; Debug.Log(quot;[MidiOut] opened: {name}"); return; } Debug.LogWarning(quot;[MidiOut] no port matched '{portNameContains}'. Is the interface plugged in and not owned by another app?"); } } void Close() { lock (_lock) { _out?.Dispose(); _out = null; } } // ---------- low-level sends ---------- void Send(byte[] buf, int len) { // caller holds _lock if (_out == null) return; _out.SendMessage(new ReadOnlySpan<byte>(buf, 0, len)); } public void SendRealtime(byte status) { lock (_lock) { _b1[0] = status; Send(_b1, 1); } } public void Send2(byte status, int d1) { lock (_lock) { _b2[0] = status; _b2[1] = (byte)(d1 & 0x7F); Send(_b2, 2); } } public void Send3(byte status, int d1, int d2) { lock (_lock) { _b3[0] = status; _b3[1] = (byte)(d1 & 0x7F); _b3[2] = (byte)(d2 & 0x7F); Send(_b3, 3); } } // ---------- channel messages (channel 0-15) ---------- public void NoteOn(int ch, int note, int velocity) { note = Mathf.Clamp(note, 0, 127); velocity = Mathf.Clamp(velocity, 1, 127); // velocity 0 would mean note-off lock (_lock) _held.Add(ch * 128 + note); Send3((byte)(0x90 | (ch & 0x0F)), note, velocity); } public void NoteOff(int ch, int note) { note = Mathf.Clamp(note, 0, 127); lock (_lock) _held.Remove(ch * 128 + note); Send3((byte)(0x80 | (ch & 0x0F)), note, 64); } public void ControlChange(int ch, int cc, int value) => Send3((byte)(0xB0 | (ch & 0x0F)), cc, Mathf.Clamp(value, 0, 127)); /// <summary>14-bit controller: MSB on cc (0-31), LSB on cc + 32. value is 0..16383.</summary> public void ControlChange14(int ch, int ccMsb, int value14) { value14 = Mathf.Clamp(value14, 0, 16383); ControlChange(ch, ccMsb, value14 >> 7); ControlChange(ch, ccMsb + 32, value14 & 0x7F); } /// <summary>Pitch bend, 0..16383, centre 8192.</summary> public void PitchBend(int ch, int value14) { value14 = Mathf.Clamp(value14, 0, 16383); Send3((byte)(0xE0 | (ch & 0x0F)), value14 & 0x7F, value14 >> 7); } public void ProgramChange(int ch, int program) => Send2((byte)(0xC0 | (ch & 0x0F)), Mathf.Clamp(program, 0, 127)); // ---------- realtime ---------- public void Clock() => SendRealtime(0xF8); public void Start_() => SendRealtime(0xFA); public void Continue_() => SendRealtime(0xFB); public void Stop_() => SendRealtime(0xFC); // ---------- safety ---------- /// <summary>Sends an explicit note-off for every note we turned on and have not turned off.</summary> public void ReleaseAll() { int[] snapshot; lock (_lock) { snapshot = new int[_held.Count]; _held.CopyTo(snapshot); _held.Clear(); } foreach (int key in snapshot) Send3((byte)(0x80 | ((key / 128) & 0x0F)), key % 128, 64); } } ``` **What to notice** - `Send3` masks data bytes with `0x7F`: a data byte with the high bit set would be read as a *status* byte and corrupt the stream. - `NoteOn` clamps velocity to at least 1 because velocity 0 is a note-off by MIDI convention. - `using var probe` disposes the probe port right after enumeration; the real port is a separate `MidiOut` (the same pattern as RtMidi for Unity's own `MidiOutTest.cs`). - `ErrorReceived` is assigned per device, as in the package's test script. If the log says the port is busy, another program (a DAW, a SysEx tool) owns it. Close it: classic Windows MIDI drivers commonly allow a single owner per port. ### 3.3 `MidiClockThread.cs` (tempo for the Subharmonicon) The Subharmonicon follows an incoming MIDI clock over its own and any analog clock (manual). MIDI clock is **24 pulses per quarter note**. A `Update`-driven clock would wobble with frame time, so this uses a dedicated thread with a `Stopwatch`. ```csharp using System.Diagnostics; using System.Threading; using UnityEngine; /// <summary> /// Sends MIDI clock (0xF8, 24 per quarter note) from a background thread. /// Start/Stop messages are optional: the Subharmonicon documents transport as SysEx, /// so press PLAY on the panel and use this for tempo. Test what your units do with 0xFA/0xFC. /// </summary> public sealed class MidiClockThread : MonoBehaviour { [SerializeField, Range(30f, 240f)] double bpm = 100; [SerializeField] bool sendStartStop = false; Thread _thread; volatile bool _running; readonly object _bpmLock = new object(); public double Bpm { get { lock (_bpmLock) return bpm; } set { lock (_bpmLock) bpm = System.Math.Clamp(value, 30.0, 300.0); } } public bool IsRunning => _running; public void StartClock() { if (_running) return; var svc = MidiOutService.Instance; if (svc == null) { UnityEngine.Debug.LogWarning("[MidiClock] no MidiOutService"); return; } if (sendStartStop) svc.Start_(); _running = true; _thread = new Thread(() => Run(svc)) { IsBackground = true, Name = "MidiClock", Priority = System.Threading.ThreadPriority.AboveNormal }; _thread.Start(); } public void StopClock() { if (!_running) return; _running = false; _thread?.Join(200); _thread = null; if (sendStartStop) MidiOutService.Instance?.Stop_(); } void Run(MidiOutService svc) { var sw = Stopwatch.StartNew(); double nextTick = 0.0; // seconds while (_running) { double intervalSec = 60.0 / (Bpm * 24.0); double now = sw.Elapsed.TotalSeconds; if (now >= nextTick) { svc.Clock(); nextTick += intervalSec; if (now - nextTick > 0.1) nextTick = now + intervalSec; // fell far behind, resync } else { double wait = nextTick - now; if (wait > 0.002) Thread.Sleep(1); else Thread.SpinWait(200); } } } void OnDisable() => StopClock(); } ``` `bpm` is read through a lock so the Inspector value and `Bpm` setter are consistent. The fully-qualified `System.Threading.ThreadPriority` avoids an ambiguity with `UnityEngine.ThreadPriority`. ## 4. Instrument controllers: Subharmonicon and Mother-32 ### 4.1 `Mother32Controller.cs` ```csharp using UnityEngine; /// <summary>Moog Mother-32 over MIDI. Default channel 1 (change in Setup mode page 2).</summary> public sealed class Mother32Controller : MonoBehaviour { [SerializeField, Range(1, 16)] int midiChannel = 1; // CCs from the Mother-32 manual's "MIDI Functions and Implementation". public const int CcPortamentoTime = 5; public const int CcPortamentoOnOff = 65; public const int CcSustain = 64; // CC 1, 2, 4, 7 feed the ASSIGN output when Setup page 1 is set to modes 13-16. public const int CcAssign1 = 1, CcAssign2 = 2, CcAssign3 = 4, CcAssign4 = 7; int Ch => midiChannel - 1; MidiOutService Out => MidiOutService.Instance; /// <summary>Note 0 = -5 V, note 120 = +5 V (1 V/oct). 121-127 repeat 109-115.</summary> public void NoteOn(int note, int velocity = 100) => Out?.NoteOn(Ch, Mathf.Clamp(note, 0, 120), velocity); public void NoteOff(int note) => Out?.NoteOff(Ch, Mathf.Clamp(note, 0, 120)); /// <summary>bend01 in -1..+1, scaled by the current bend range (default +/-1 octave).</summary> public void PitchBend(float bend01) { int v = Mathf.Clamp(Mathf.RoundToInt(8192 + bend01 * 8191f), 0, 16383); Out?.PitchBend(Ch, v); } /// <summary>Registered parameter 0 (pitch bend depth): CC101=0, CC100=0, CC6=semitones (0-12).</summary> public void SetPitchBendRangeSemitones(int semitones) { if (Out == null) return; Out.ControlChange(Ch, 101, 0); Out.ControlChange(Ch, 100, 0); Out.ControlChange(Ch, 6, Mathf.Clamp(semitones, 0, 12)); // Optional "RPN null" so later CC6 messages cannot change it by accident. Out.ControlChange(Ch, 101, 127); Out.ControlChange(Ch, 100, 127); } public void SetPortamento(bool on, float time01) { if (Out == null) return; Out.ControlChange(Ch, CcPortamentoTime, Mathf.RoundToInt(Mathf.Clamp01(time01) * 127f)); Out.ControlChange(Ch, CcPortamentoOnOff, on ? 127 : 0); } public void SetSustain(bool on) => Out?.ControlChange(Ch, CcSustain, on ? 127 : 0); /// <summary>Drives the ASSIGN jack (0..127 maps to -5..+5 V) once Setup page 1 selects the matching CC mode.</summary> public void SetAssignCc(int ccNumber, float value01) => Out?.ControlChange(Ch, ccNumber, Mathf.RoundToInt(Mathf.Clamp01(value01) * 127f)); /// <summary>Program changes 1-64 select a sequencer pattern (check zero- vs one-based on the bench).</summary> public void SelectPattern(int program) => Out?.ProgramChange(Ch, program); } ``` Practical Mother-32 setup: in **Setup page 1** choose the ASSIGN output source (for example mode 13 = MIDI CC 1). Then patch ASSIGN to the filter cutoff or VCO mod input, and Unity's `SetAssignCc(1, value)` becomes a **-5 V to +5 V** control voltage. Because there is only one ASSIGN jack, you get one live mapped CV at a time; program changes 71-86 select the ASSIGN mode over MIDI, which lets you re-purpose it from Unity (bench-test whether the mode change is instant). ### 4.2 `SubharmoniconController.cs` ```csharp using UnityEngine; /// <summary>Moog Subharmonicon over MIDI. Set to channel 2 here by sending Moog's SysEx channel file.</summary> public sealed class SubharmoniconController : MonoBehaviour { [SerializeField, Range(1, 16)] int midiChannel = 2; // CC map from the Subharmonicon manual (MIDI Operations). public const int CcVco1FreqMsb = 4, CcVco1FreqLsb = 36; public const int CcVco2FreqMsb = 12, CcVco2FreqLsb = 44; public const int CcVco1Sub1 = 103, CcVco1Sub2 = 104, CcVco2Sub1 = 105, CcVco2Sub2 = 106; public const int CcRhythmLogic = 113; // 0-63 OR, 64-127 XOR int Ch => midiChannel - 1; MidiOutService Out => MidiOutService.Instance; /// <summary> /// The unit treats note data as an OFFSET from C4 (MIDI 60). semitonesFromC4 = 0 plays "unchanged" /// when the VCO FREQ knobs are fully counter-clockwise. /// </summary> public void NoteOn(int semitonesFromC4, int velocity = 100) => Out?.NoteOn(Ch, 60 + semitonesFromC4, velocity); public void NoteOff(int semitonesFromC4) => Out?.NoteOff(Ch, 60 + semitonesFromC4); /// <summary> /// 14-bit VCO frequency CC added to the knob (+/-2.5 octaves total range per Moog). /// amount01: 0..1 across the CC range. I did not verify the power-on default; check by ear. /// </summary> public void SetVco1Freq(float amount01) => Out?.ControlChange14(Ch, CcVco1FreqMsb, To14(amount01)); public void SetVco2Freq(float amount01) => Out?.ControlChange14(Ch, CcVco2FreqMsb, To14(amount01)); /// <summary>vco 1 or 2, sub 1 or 2, divisor 1..16. The CC replaces the knob until you move the knob.</summary> public void SetSubDivision(int vco, int sub, int divisor) { divisor = Mathf.Clamp(divisor, 1, 16); // Moog: 0-7 = 16, 8-15 = 15, ... 120-127 = 1. Aim for the middle of each 8-wide band. int value = (16 - divisor) * 8 + 4; int cc = (vco == 1) ? (sub == 1 ? CcVco1Sub1 : CcVco1Sub2) : (sub == 1 ? CcVco2Sub1 : CcVco2Sub2); Out?.ControlChange(Ch, cc, value); } public void SetRhythmLogicXor(bool xor) => Out?.ControlChange(Ch, CcRhythmLogic, xor ? 127 : 0); static int To14(float a) => Mathf.Clamp(Mathf.RoundToInt(Mathf.Clamp01(a) * 16383f), 0, 16383); } ``` **Why the sub-division maths looks odd.** The manual maps value bands of eight (0-7 -> 16, 8-15 -> 15, ... 120-127 -> 1), so `(16 - divisor) * 8 + 4` lands in the middle of the right band for any divisor 1-16. Moog also notes that moving the physical SUB knob overrides the CC; when you demo from Unity, do not touch the panel. **Why transposition feels strange.** Because the Subharmonicon treats incoming note numbers as offsets from C4, a keyboard-style `NoteOn(60+n)` and `NoteOn(n)` are *different calls*. This controller uses semitone offsets so the API cannot get it wrong. ### 4.3 A first smoke test ```csharp using System.Collections; using UnityEngine; /// <summary>Plays a short phrase on both Moogs when you press Play. Remove after the bench test.</summary> public sealed class EurorackSmokeTest : MonoBehaviour { [SerializeField] Mother32Controller mother; [SerializeField] SubharmoniconController sub; IEnumerator Start() { yield return new WaitForSeconds(1f); int[] mother32Notes = { 36, 43, 48, 43 }; foreach (int n in mother32Notes) { mother.NoteOn(n, 110); yield return new WaitForSeconds(0.25f); mother.NoteOff(n); yield return new WaitForSeconds(0.05f); } sub.NoteOn(0); yield return new WaitForSeconds(1f); sub.NoteOff(0); } } ``` ## 5. Audio return: interface, Microphone API and monitoring ### 5.1 Why the Microphone API Unity's `Microphone` class records from any *input device the OS exposes*, including a class-compliant audio interface. `Microphone.devices` lists names; `Microphone.Start(deviceName, loop, lengthSec, frequency)` returns an `AudioClip` that is filled live; `Microphone.GetPosition(deviceName)` returns the current write position in samples. Per the 6000.5 docs: a null or empty device name selects the default microphone, and the docs recommend passing `AudioSettings.outputSampleRate` as the frequency to match the project. `GetDeviceCaps` gives the supported sample-rate range. ### 5.2 `AudioInputTap.cs` This is the foundation for [[Music Visualization]]: a looping one-second ring clip plus a method that copies the **most recent N samples** of one channel into your array. It reads straight from the clip with `AudioClip.GetData` (interleaved samples; offsets in frames; reads wrap around the clip, as documented), so it needs **no AudioSource** and therefore no feedback risk. ```csharp using System; using UnityEngine; /// <summary> /// Records an input device into a looping clip and exposes the latest samples of one channel. /// Main-thread only (call Read from Update or LateUpdate). /// </summary> public sealed class AudioInputTap : MonoBehaviour { [Tooltip("Case-insensitive part of the device name (e.g. 'ES-9', 'Scarlett', 'USB Audio'). Empty = default device.")] [SerializeField] string deviceContains = ""; [SerializeField, Range(1, 4)] int bufferSeconds = 1; [SerializeField] int channelToRead = 0; [SerializeField] bool logDevices = true; AudioClip _clip; string _device; float[] _scratch = new float[8192]; public bool IsRecording => _clip != null && Microphone.IsRecording(_device); public int SampleRate => _clip != null ? _clip.frequency : AudioSettings.outputSampleRate; public int Channels => _clip != null ? _clip.channels : 0; public string DeviceName => _device; void OnEnable() => StartRecording(); void OnDisable() => StopRecording(); void StartRecording() { if (logDevices) foreach (var d in Microphone.devices) Debug.Log(quot;[AudioInputTap] device: {d}"); _device = null; // null/empty = default microphone if (!string.IsNullOrEmpty(deviceContains)) { foreach (var d in Microphone.devices) if (d.IndexOf(deviceContains, StringComparison.OrdinalIgnoreCase) >= 0) { _device = d; break; } if (_device == null) Debug.LogWarning(quot;[AudioInputTap] no device contains '{deviceContains}', using default."); } int rate = AudioSettings.outputSampleRate; Microphone.GetDeviceCaps(_device, out int minF, out int maxF); if (maxF > 0) rate = Mathf.Clamp(rate, minF, maxF); // (0,0) means "any rate" _clip = Microphone.Start(_device, true, bufferSeconds, rate); if (_clip == null) Debug.LogError("[AudioInputTap] Microphone.Start returned null."); else Debug.Log(quot;[AudioInputTap] started '{_device ?? "default"}' at {_clip.frequency} Hz, {_clip.channels} channel(s)"); } void StopRecording() { if (_clip != null && Microphone.IsRecording(_device)) Microphone.End(_device); _clip = null; } /// <summary> /// Copies the most recent dest.Length mono samples of one channel into dest (channel -1 = the Inspector's channelToRead). /// Returns false until the device has delivered enough audio. /// </summary> public bool ReadLatest(float[] dest, int channel = -1) { if (_clip == null) return false; int pos = Microphone.GetPosition(_device); // frames written so far (wraps) int n = dest.Length; int ch = Mathf.Clamp(channel < 0 ? channelToRead : channel, 0, _clip.channels - 1); if (pos <= 0 || n > _clip.samples) return false; int start = pos - n; if (start < 0) start += _clip.samples; int needed = n * _clip.channels; // GetData fills the whole array, so keep the scratch buffer exactly sized. if (_scratch.Length != needed) _scratch = new float[needed]; if (!_clip.GetData(_scratch, start)) return false; for (int i = 0; i < n; i++) dest[i] = _scratch[i * _clip.channels + ch]; return true; } } ``` **Notes** - **Windows microphone permission.** Windows 10/11 has a *Privacy > Microphone* switch that also gates audio-interface inputs for desktop apps in some configurations. If `Microphone.devices` is empty or the clip is silent, check it first. (OS behaviour; not from the Unity docs.) - **Channels.** I did not verify how Unity numbers channels from a multichannel interface or whether it exposes more than two. The script logs `clip.channels` at start and clamps `channelToRead`. If you only get channels 1-2, set the interface's own routing/mixer app so the Moogs' mix lands on inputs 1-2. - **Latency.** The docs' own advice for latency control is to poll `GetPosition` until it has advanced by the number of samples you want and only then start playback. For *analysis* (visualization, envelope following) a few tens of milliseconds is fine. For *monitoring through Unity* it is too slow to play over; use the interface's **direct monitoring** or your mixer for the live sound and treat Unity's copy as data. - **Sample-rate mismatch.** Match the interface's driver rate with `AudioSettings.outputSampleRate` (Project Settings > Audio) so both ends agree; otherwise Windows may resample. ### 5.3 Optional: hear it through Unity If you must monitor in-engine (for example to run the live signal through a Unity mixer effect or spatializer, see [[Spatial Audio and Reverb Zones]]), the recipe from the Unity docs is: expose the recording clip from `AudioInputTap` (add a public `Clip` property returning `_clip`), assign it to an `AudioSource` with `loop = true`, wait until `Microphone.GetPosition` has advanced by your target latency in samples, then call `Play()`. I deliberately do not give a full script: monitoring through Unity adds tens of milliseconds and a feedback hazard (speakers feeding the same mics), and hardware **direct monitoring** on the interface is the right production answer. Whether `GetSpectrumData` on such a source sees the signal before or after a muted mixer group is not verified, which is one more reason [[Music Visualization]] analyses the clip directly instead. ## 6. DC-coupled CV: what works, what hurts Eurorack control voltages are **slowly changing or constant** voltages. Most consumer audio interfaces are **AC-coupled**: a high-pass filter in the input and output stages removes DC, so a steady 3 V CV decays to 0 V on the way in, and a constant "CV" you generate in Unity never reaches the rack as a level. Audio-rate signals work fine; slow LFOs, gates and held pitches do not. ### 6.1 What you need - A **DC-coupled** interface. Example: the Expert Sleepers **ES-9** advertises 14 DC-coupled 3.5 mm inputs and 8 DC-coupled 3.5 mm outputs of about +/-10 V, 16 in / 16 out USB, class-compliant. Its manual adds three details that apply to any such device: the input ADC has optional **DC-blocking filters that must be turned off for CV**, the 1/4" balanced main outs are *not* DC-coupled, and the default channel map puts the 3.5 mm outputs on channels **9-16**. - **Voltage safety.** Eurorack signals can swing +/-10 V or more. A normal line input expects about +/-2 V maximum. **Never** patch rack CV or audio into a non-modular interface input without attenuating, and never feed a DC-coupled output into something that expects line level. Mother-32 outputs include -5..+5 V signals (for example the ASSIGN output); the Subharmonicon's CLOCK/CV outputs are 0 to +10 V per its manual. ### 6.2 `CvOutput.cs`: a DC level out of an AudioSource This is the only reasonable way to produce CV from Unity *without* MIDI: write a constant (slewed) sample value into one output channel. Use it **only** with a DC-coupled interface, calibrated with a multimeter. ```csharp using UnityEngine; /// <summary> /// Writes a slewed DC level into one output channel via OnAudioFilterRead. /// Needs: a DC-coupled DAC path, calibration, and an AudioSource playing on the master output. /// Volts are approximate: set fullScaleVolts after measuring with a meter. /// </summary> [RequireComponent(typeof(AudioSource))] public sealed class CvOutput : MonoBehaviour { [Tooltip("Zero-based channel in the interleaved stream Unity is outputting.")] [SerializeField] int channel = 0; [Tooltip("Volts that correspond to a sample value of 1.0. Measure it; ~10 on an ES-9-class device.")] [SerializeField] float fullScaleVolts = 10f; [SerializeField] float maxVolts = 5f; [Tooltip("Slew limit in volts per second (protects your VCO from steps).")] [SerializeField] float slewVoltsPerSecond = 20f; volatile float _targetVolts; float _currentVolts; // audio thread only double _sampleRate; void Awake() { _sampleRate = AudioSettings.outputSampleRate; var src = GetComponent<AudioSource>(); src.spatialBlend = 0f; src.volume = 1f; src.loop = true; src.playOnAwake = true; } /// <summary>Main thread.</summary> public void SetVolts(float v) => _targetVolts = Mathf.Clamp(v, -maxVolts, maxVolts); /// <summary>1 V/oct helper: MIDI note 60 = 0 V (Mother-32 maps note 0 to -5 V, note 120 to +5 V).</summary> public void SetPitchNote(float midiNote) => SetVolts((midiNote - 60f) / 12f); void OnAudioFilterRead(float[] data, int channels) { if (channel >= channels) return; float step = slewVoltsPerSecond / (float)_sampleRate; float target = _targetVolts; int frames = data.Length / channels; for (int f = 0; f < frames; f++) { float d = target - _currentVolts; _currentVolts += Mathf.Clamp(d, -step, step); data[f * channels + channel] = _currentVolts / fullScaleVolts; } } } ``` **Caveats** - `OnAudioFilterRead` rules (audio thread, no allocation, interleaved data) are in [[2. OnAudioFilterRead Procedural Synth]]. This script follows them: a `volatile float` target, a cached sample rate. - With **no clip assigned** and this as the first filter, the filter acts as the generator (per the Unity docs quoted in that note), but `AudioSource.Play` must have been called; `playOnAwake` handles that. - **Unity's maximum output channel count on Windows** and its speaker-mode options are not something I verified. If the CV channel you need is beyond what Unity can address, remap channels inside the interface's own routing tool (the ES-9 manual says channel numbering can be reconfigured) so a CV output lands on channel 1 or 2. - Do not add Audio Mixer effects, limiters or spatializers to this source: they can filter DC. Keep it on the master output with the listener volume at 1. - For **pitch**, prefer MIDI. A MIDI note is exact; DAC-generated pitch CV depends on calibration and the VCO's tracking. ### 6.3 CV *into* Unity For an envelope or LFO from the rack, turn the interface's input DC-blocking **off** for that pair, attenuate to line-safe levels, then treat the input as audio samples, but **smooth and average**: the `AudioInputTap` buffer gives raw samples; take the mean of the last N and scale by your calibration. See the envelope follower in [[Music Visualization]]. ## 7. MIDI in with Minis, and game state to synth ### 7.1 `SynthMidiInput.cs` (Observer over Minis) Minis exposes each MIDI channel as an Input System device and raises events *before* control state updates, so use the event arguments, not control state (README). It only creates a device after a message arrives on that channel. ```csharp using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; /// <summary> /// Listens to every Minis MIDI device and republishes simple static events for the rest of the game /// (visualizers, stats, quest triggers). Minis channels are 0-based. /// </summary> public sealed class SynthMidiInput : MonoBehaviour { public static event Action<int, int, float> NoteOn; // channel0, note, velocity01 public static event Action<int, int> NoteOff; // channel0, note public static event Action<int, int, float> Control; // channel0, cc, value01 readonly List<Minis.MidiDevice> _devices = new(); void OnEnable() { InputSystem.onDeviceChange += OnDeviceChange; Connect(); } void OnDisable() { InputSystem.onDeviceChange -= OnDeviceChange; Disconnect(); } void OnDeviceChange(InputDevice device, InputDeviceChange change) { if (device is not Minis.MidiDevice) return; Disconnect(); Connect(); } void Connect() { foreach (var d in InputSystem.devices) { if (d is not Minis.MidiDevice md) continue; md.onWillNoteOn += OnNoteOn; md.onWillNoteOff += OnNoteOff; md.onWillControlChange += OnControl; _devices.Add(md); } } void Disconnect() { foreach (var md in _devices) { md.onWillNoteOn -= OnNoteOn; md.onWillNoteOff -= OnNoteOff; md.onWillControlChange -= OnControl; } _devices.Clear(); } static void OnNoteOn(Minis.MidiNoteControl note, float velocity) => NoteOn?.Invoke(note.channel, note.noteNumber, velocity); static void OnNoteOff(Minis.MidiNoteControl note) => NoteOff?.Invoke(note.channel, note.noteNumber); static void OnControl(Minis.MidiValueControl cc, float value) => Control?.Invoke(cc.channel, cc.controlNumber, value); } ``` **What this needs on your side.** A *MIDI input* that Unity can see: a keyboard or controller into the same interface, or the Moogs' MIDI output where they have one (check; Mother-32 has a MIDI IN per its manual, no output is mentioned in the pages I read). Without a source, no device appears. Hence the README's note: prompt the user to press a key. ### 7.2 `GameToSynthBridge.cs`: game state drives the rig Map a **tension** value (from the stats and mood systems in [[Music Mood Mixer]]) to controller moves, with a rate limit so we never flood a 31,250-baud cable. ```csharp using UnityEngine; /// <summary> /// Turns a 0..1 tension value into Mother-32 ASSIGN CC and Subharmonicon VCO/sub changes. /// Sends only when the MIDI value changes, at most sendHz times per second. /// </summary> public sealed class GameToSynthBridge : MonoBehaviour { [SerializeField] Mother32Controller mother; [SerializeField] SubharmoniconController sub; [SerializeField, Range(0f, 1f)] float tension; [SerializeField] float sendHz = 30f; [SerializeField] AnimationCurve motherCutoff = AnimationCurve.EaseInOut(0, 0.15f, 1, 0.9f); [SerializeField] AnimationCurve subVco2 = AnimationCurve.Linear(0, 0.5f, 1, 0.7f); [SerializeField] int subDivisionCalm = 8, subDivisionTense = 3; float _nextSend; int _lastMotherCc = -1, _lastVco2 = -1, _lastDiv = -1; public void SetTension(float t) => tension = Mathf.Clamp01(t); void Update() { if (Time.unscaledTime < _nextSend) return; _nextSend = Time.unscaledTime + 1f / Mathf.Max(1f, sendHz); int motherCc = Mathf.RoundToInt(motherCutoff.Evaluate(tension) * 127f); if (motherCc != _lastMotherCc) { mother?.SetAssignCc(Mother32Controller.CcAssign1, motherCc / 127f); _lastMotherCc = motherCc; } int vco2 = Mathf.RoundToInt(subVco2.Evaluate(tension) * 16383f); if (Mathf.Abs(vco2 - _lastVco2) > 64) // ~0.4% of the 14-bit range { sub?.SetVco2Freq(vco2 / 16383f); _lastVco2 = vco2; } int div = Mathf.RoundToInt(Mathf.Lerp(subDivisionCalm, subDivisionTense, tension)); if (div != _lastDiv) { sub?.SetSubDivision(1, 1, div); _lastDiv = div; } } } ``` Hook `SetTension` to whatever event your game already raises. If you use ScriptableObject event channels from [[ScriptableObject Architecture]], a `FloatVariable` listener can call it. For a **designer-facing** version, store the curves in a ScriptableObject `SynthMapping` asset and let each level reference its own. ## 8. OSC with OscJack OSC (Open Sound Control) is a UDP message format used by Max, VCV Rack, TouchDesigner, many phone apps and plugin hosts. Use it when the instrument is *software* or lives on another machine; hardware Moogs do not speak it. ### 8.1 `OscBridge.cs` OscJack's server invokes callbacks **on the server thread**, per its README, so we queue and drain on the main thread. We use the documented two-float forms for `Send` and `GetElementAsFloat(0/1)`. ```csharp using System.Collections.Concurrent; using OscJack; using UnityEngine; public sealed class OscBridge : MonoBehaviour { [SerializeField] int listenPort = 9000; [SerializeField] string remoteHost = "127.0.0.1"; [SerializeField] int remotePort = 9001; OscServer _server; OscClient _client; readonly ConcurrentQueue<(string address, float a, float b)> _inbox = new(); void OnEnable() { _client = new OscClient(remoteHost, remotePort); _server = new OscServer(listenPort); // Empty string = receive every address (per the OscJack README). _server.MessageDispatcher.AddCallback("", (address, data) => { // Runs on the server thread: copy values only, do not touch Unity objects. _inbox.Enqueue((address, data.GetElementAsFloat(0), data.GetElementAsFloat(1))); }); } void OnDisable() { _server?.Dispose(); _client?.Dispose(); _server = null; _client = null; } void Update() { while (_inbox.TryDequeue(out var m)) { switch (m.address) { case "/synth/tension": FindFirstObjectByType<GameToSynthBridge>()?.SetTension(m.a); break; default: Debug.Log(quot;[OSC] {m.address} {m.a} {m.b}"); break; } } } /// <summary>Send a pair of floats (second value can be 0).</summary> public void Send(string address, float a, float b = 0f) => _client?.Send(address, a, b); } ``` Use **OSC Monitor** (OscJack: Window > OSC Monitor) to see what the other app actually sends. `FindFirstObjectByType` per message is only acceptable for a demo; cache the reference in `OnEnable` for real use. OSC over UDP has no delivery guarantee, so use it for continuous values, not for "start the song now" commands. Firewall: allow Unity/Editor on the chosen UDP ports. ## 9. Bench test order, pitfalls, Level Up! and patterns ### 9.1 Bench test order (do these in sequence) 1. **Monitor MIDI out first.** Open a MIDI monitor app on another machine or use the interface's MIDI LED. In Unity press Play with `EurorackSmokeTest`; confirm the port opens (Console) and the Mother-32's **MIDI IN LED** flashes red (manual). 2. **Mother-32 on channel 1.** Notes sound on its VCO; pitch is musical. Lower the sequencer first: if the sequencer is running, incoming notes **transpose** it. 3. **Subharmonicon channel.** Confirm it responds to channel 2 only after you send the SysEx channel file; the MIDI light on its panel blinks on receipt. Set VCO FREQ knobs fully counter-clockwise to play "unchanged" note numbers. 4. **Clock.** Start `MidiClockThread`, press PLAY on the Subharmonicon, listen to the tempo follow your `Bpm`. Try `sendStartStop` and note what happens. 5. **CCs.** Turn the ASSIGN output mode to a CC mode on the Mother-32, patch it to a filter, move `tension`. 6. **Audio return.** Run `AudioInputTap`, log the channel count, confirm non-zero samples while playing a note. 7. **Last, CV.** Voltage check with a meter, then `CvOutput`. ### 9.2 Pitfalls - **Stuck notes** when you stop Play in the Editor: the service releases held notes in `OnDisable`; also verify after a script recompile or a crash by hitting the instruments' panel or sending `ReleaseAll` from a button. - **Port owned by another app.** Close DAWs and SysEx utilities before pressing Play. - **Channel base mismatch.** Minis counts channels from 0; humans count from 1. Services here are 0-based; controllers are 1-based. - **Subharmonicon transposition.** Notes are offsets from C4, not absolute pitches (1.2). - **CC flooding.** A knob sweep at frame rate saturates DIN MIDI. Rate-limit. - **Editor focus.** Run in Background ON. - **Unity thread rules.** MIDI-in handlers run through the Input System; OSC callbacks run on the OscJack server thread; neither may touch Unity objects off the main thread without queuing. - **Quest / Android.** Minis' README says Android needs the "Activity" entry point (not GameActivity) and notes a multi-port issue, and that platform's `Microphone` needs the runtime microphone permission. A USB audio interface on a Quest 2 is a project on its own; verify before promising it in class. - **Do not leave CV patched** when the Unity scene is not running: a floating or zero output may be a legal-but-unexpected voltage. Disconnect when done. ### 9.3 Level Up! tie-ins - **Level 16 - music with style.** Rogers argues that music should serve and heighten the experience. A **real analogue voice** answering to the player's tension is a very literal version: the Mother-32's filter opens as the Level Up! "tension" curve rises. Have students sketch the intensity curve first (as in [[Music Mood Mixer]]) and *then* map it to CCs. - **Level 16 - sound as feedback.** A hit gets a short gated note on the Mother-32 and a sub-division change on the Subharmonicon, immediately audible, physically in the room. See [[Interactable Audio]] for the software equivalent. - **Teaching angle.** MIDI is a tiny, brutally rate-limited protocol, which makes it a good way to teach *budgets*: bytes per frame, messages per second, what the player can perceive. ### 9.4 Game Programming Patterns - **Command** - each MIDI message is a small immutable command object (status, d1, d2); `MidiOutService` is an invoker. See [[Game Programming Patterns (Review)]]. - **Observer** - `SynthMidiInput` republishes device events as static events for the rest of the game. - **Event Queue** - the OSC inbox is a producer-consumer queue between the server thread and `Update`. - **Service Locator vs Singleton** - `MidiOutService.Instance` is a deliberately simple locator; the book argues against singletons because of global state. Here the hardware really is a single global resource, but keep the controllers decoupled (inject `MidiOutService` via a field if you test with a fake). ### 9.5 Exercises 1. Swap the smoke test for a **Mother-32 arpeggiator** that outputs a pattern chosen by the player's current stat build ([[Stat-Driven RPG Systems]]). 2. Make the Subharmonicon's sub-divisions follow enemy count in a room ([[Enemy AI State Machine]]). 3. Put the same tension variable on an **OSC** channel to a VCV Rack patch and compare how it feels against the hardware. 4. Add `SynthMidiInput` as a *controller input* for a puzzle: the player plays a four-note motif on a keyboard to open a door ([[Unity 2D Point-and-Click System]] for the interaction pattern). ## Related (existing) - [[Audio]] - series index - [[FMOD vs Built-in Mixer and Procedural Synth]] / [[2. OnAudioFilterRead Procedural Synth]] - the in-engine synth and the audio-thread rules `CvOutput` follows - [[Music Mood Mixer]] - the tension/mood signal this bridge consumes - [[Interactable Audio]] and [[Spatial Audio and Reverb Zones]] - software SFX and spatial treatment - [[Input System]] - Minis is an extension of it - [[ScriptableObject Architecture]] - event channels and mapping assets - [[Music Visualization]] - the next note; uses `AudioInputTap` - [[Game Programming Patterns (Review)]], [[Level Up! (Review)]] - [[0.Choosing a Unity Version]] - for any Quest 2 version of this project ## Official docs and sources (index) - Unity `Microphone.Start` (6000.5): https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Microphone.Start.html - Unity `Microphone.GetPosition`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Microphone.GetPosition.html - Unity `AudioClip.GetData`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/AudioClip.GetData.html - Minis: https://github.com/keijiro/Minis - RtMidi for Unity: https://github.com/keijiro/jp.keijiro.rtmidi - OscJack: https://github.com/keijiro/OscJack - Subharmonicon manual: https://cdn.inmusicbrands.com/Moog/Subharmonicon/Subharmonicon_Manual%20AMZ.pdf - Subharmonicon MIDI channel (SysEx): https://moogmusic-help.freshdesk.com/en/support/solutions/articles/69000840565-moog-subharmonicon-how-do-i-change-midi-channel-on-the-subharmonicon- - Subharmonicon CC table (community): https://midi.guide/d/moog/subharmonicon/ - Mother-32 manual: https://cdn.inmusicbrands.com/Moog/Mother32/Mother_32_Users_Manual.pdf - Expert Sleepers ES-9 (DC-coupled example) : https://www.expert-sleepers.co.uk/es9.html