Cross-plugin data transport via shared memory.
discoLink enables real-time audio streaming and bidirectional MIDI/parameter control via shared memory IPC — designed as an intra-plugin bridge for modular operation. Use it to route full synth paths, individual oscillators, envelopes, filters, or external effects into a host plugin.
discoLink uses two plugin instances on the same DAW channel:
DAW Channel
|
| [Host Plugin] <-- instrument (host)
| |
| |-- sends MIDI via IPC commands ------+
| |-- receives audio from shared memory -+---> shared memory
| | | ring buffers
| [discoLink Effect] <-- effect (device) |
| |-- receives MIDI via IPC commands ----+
| |-- writes audio to shared memory -----+
| |
| DAW Output
Or with an external process:
Process A (DAW) Process B (standalone)
[Host Plugin] <-- IPC --> [Device Plugin]
shared memory ring buffers
opaque 256-byte messages
separate OS processes
- Shared memory transport — lock-free SPSC ring buffers, near-zero latency
- 100% IPC protocol — no function calls cross the process boundary
- Registry-based discovery — devices register in a shared memory bulletin board, hosts scan and auto-connect
- Buffer negotiation — handshake exchanges sampleRate, blockSize, numChannels bidirectionally
- Multi-instance — unique device IDs auto-assigned, saved/restored with plugin state for project reload
- Host claim tracking —
hostPid+linkTagin transport header for exclusive access and DAW session recall - MIDI 1.0 + MIDI 2.0 UMP + MPE — NoteOn/Off, CC, PitchBend, SysEx, raw MIDI bytes
- Multichannel — mono through 12-channel multi-out
- Link routing — tap into oscillator, filter, LFO, envelope, gate, or any output bus
- Transport modes — audio+midi (default), audio-only, midi-only
- Protocol versioned — handshake negotiation, capability flags, message fragmentation
- Cross-platform — macOS, Linux, Windows. POSIX
shm_open/ Win32CreateFileMapping - Zero dependencies — pure C++17, no JUCE required for the core library
- Self-contained JUCE module — drop-in
juce_discoLinkmodule for Projucer/CMake projects
# Device (sine synth)
clang++ -std=c++17 -O2 -o cli_device examples/cli_device.cpp discoLink/DiscoLinkSharedMemory.cpp -I. -lpthread
# Host
clang++ -std=c++17 -O2 -o cli_host examples/cli_host.cpp discoLink/DiscoLinkSharedMemory.cpp -I. -lpthread
# Master (device with status display)
clang++ -std=c++17 -O2 -o cli_master examples/cli_master.cpp discoLink/DiscoLinkSharedMemory.cpp -I. -lpthreadcmake -B build
cmake --build build
# -> libdiscoLink.a, cli_device, cli_host, cli_masterOpen testApp/discoLinkPlugin.jucer in Projucer. Builds as:
- AU (Audio Unit) — effect plugin
- VST3 — effect plugin
- Standalone — standalone app
The plugin acts as a device that generates a polyphonic sine synth responding to IPC MIDI commands. Use it to test the full IPC chain with any host (Discovery Pro, cli_host, etc.).
Copy or symlink juce_discoLink/ into your project's Modules folder.
The module is self-contained — all headers and the implementation file
are included. No external dependencies.
YourProject/
Modules/
juce_discoLink/ <-- symlink or copy
juce_audio_basics/
...
Add juce_discoLink to your .jucer file or CMake juce_add_module().
#include <juce_discoLink/juce_discoLink.h>
// or: #include <discoLink/discoLink.h> (without JUCE)
discolink::DiscoLinkDevice device;
// Handle incoming MIDI from host
device.onCommand([](const discolink::Message& cmd, discolink::Transport& t) -> bool {
auto type = static_cast<discolink::CommandType>(cmd.type);
if (type == discolink::CommandType::MidiRaw) {
uint32_t len = cmd.readU32(0);
uint8_t status = cmd.payload[4] & 0xF0;
if (status == 0x90 && len >= 3 && cmd.payload[6] > 0)
noteOn(cmd.payload[5], cmd.payload[6]);
else if (status == 0x80)
noteOff(cmd.payload[5]);
return true;
}
return false; // let default handler process handshake, ping, etc.
});
// Start — creates shared memory, registers in bulletin board
device.startDevice("MySynth:0", 2, 48000.0f, 512);
// In your audio callback:
const float* outputs[2] = { leftBuffer, rightBuffer };
device.sendAudio(outputs, blockSize);
device.processCommands(); // handle incoming IPC
// Cleanup
device.stopDevice();discolink::DiscoLinkHost host;
// Set local buffer info for handshake
host.setLocalBufferInfo(sampleRate, blockSize);
// Discover running devices
auto devices = host.getAvailableDevices();
// -> [{ "MySynth:0", pid=1234, 2ch, 48kHz }, ...]
// Connect (auto-sends handshake with buffer negotiation)
host.connectToDevice(devices[0]);
// In your audio callback:
float* inputs[2] = { leftBuffer, rightBuffer };
host.receiveAudio(inputs, blockSize);
// Send MIDI to device
host.sendCommand(discolink::cmd::midiNoteOn(0, 60, 100));
host.sendCommand(discolink::cmd::midiRaw(rawBytes, length));
// Process responses
host.processResponses();
// Cleanup
host.disconnect();// On connect — assign a tag and save it with plugin state
host.setLinkTag("DiscoPro:LayerA:session_42");
host.connectToDevice(devices[0]);
// Save: { linkTag: "DiscoPro:LayerA:session_42", deviceId: "OBXd:0" }
// On restore — find previously-paired device by tag
auto devices = host.getAvailableDevices();
for (auto& dev : devices) {
if (dev.linkTag == savedLinkTag && !dev.claimed) // stale claim = available
host.connectToDevice(dev);
else if (dev.linkTag == savedLinkTag && dev.hostPid == myPid)
host.connectToDevice(dev); // already ours
}
// Devices report: claimed=false (stale hostPid from previous session)
// claimed=true, hostPid=other (busy, another host has it)// In prepareToPlay:
m_device.startDevice("discoLink:0", 2, sampleRate, blockSize);
// In processBlock — write audio to shared memory:
const float* ptrs[2] = { buffer.getReadPointer(0), buffer.getReadPointer(1) };
m_device.sendAudio(ptrs, numSamples);
m_device.processCommands();
// Audio passes through unchanged to DAW output
// External hosts read from shared memory# Terminal 1: start device
./cli_master --samplerate 48000 --blocksize 512 --id ExampleSine:0
# Terminal 2: start host
./cli_host --id ExampleSine:0- Load your instrument plugin (e.g. Discovery Pro) on a channel
- Insert discoLink as an effect on the same channel
- The effect auto-registers as a device
- Enable host mode in your instrument (e.g. "discoLink Mode")
- Play MIDI notes — audio flows via IPC shared memory
- Save as DAW template for instant setup on reload
discoLink uses silent fallback in the real-time path (zero-fill on disconnect, false returns on failure) and optional DebugMonitor callbacks for diagnostics. No exceptions, no blocking, no allocations in the audio path.
// Install a debug monitor to see all errors
auto monitor = discolink::makeConsoleMonitor();
device.setDebugMonitor(&monitor);
// -> [discoLink ERROR] startDevice: failed to create shared memory transportDead device processes are automatically cleaned from the registry. See docs/architecture.md for the full error handling model.
- Shared memory ring buffers — lock-free SPSC, 16384 samples per channel
- Command IPC — 256-byte messages via separate command/response ring buffers
- Registry — shared memory bulletin board with 16 slots, dead process cleanup
- Handshake — protocol version, capabilities, buffer size negotiation
- Device ID persistence — saved in plugin state, auto-reconnect on project reload
- Host claim tracking —
hostPid+linkTagfor exclusive device access and DAW session recall - Debug monitoring — zero-overhead tap on all traffic (commands, audio, errors, connections)
See docs/architecture.md for the full architecture guide.
See docs/api-reference.md for the complete API documentation.
See docs/protocol.md for the IPC message format and command reference.
The IPC architecture uses separate processes with opaque shared memory, following the same pattern as JACK, PipeWire, and ReWire.
MIT License. See LICENSE.
Copyright (c) 2026 discoDSP