Exporters are plugins that send processed DMX data to external systems and protocols. They run in parallel with serializers, receiving the same final DMX channel data and outputting it to MIDI devices, files, timecode systems, and other destinations.
public interface IExporter : IUserInterface<IExporter>, IConstructable{ /// <summary> /// Serializes a channel from a raw byte representation /// </summary> void SerializeChannel(byte channelValue, int channel); /// <summary> /// Called at the start of each frame to reset any state. /// </summary> void InitFrame(ref List<byte> channelValues); /// <summary> /// Called after all channels have been serialized for the current frame. /// Can be used to for example generate a CRC block area, or operate on /// multiple channels at once. /// </summary> void CompleteFrame(ref List<byte> channelValues);}
Called once per frame before any channels are processed. Use to reset counters or prepare state.
2
SerializeChannel
Called for each active DMX channel in sequence. Note: many exporters ignore this and only use CompleteFrame.
3
CompleteFrame
Called after all channels are processed with the complete channel array. Most export logic happens here.
Unlike generators, exporters receive a read-only view of the DMX data (though the signature uses ref for performance). They should not modify channel values.
Purpose: Export DMX data to VRChat worlds via MIDI
Protocol
Uses MIDI Note On/Off messages
Capacity
Up to 16,384 channels (8 banks × 2,048)
Platform
VRChat worlds with MIDIDMX system
Bidirectional
Handshake protocol for synchronization
How it works: MIDIDMX encodes DMX channel values into MIDI note events using an 18-bit addressing scheme. It’s designed to work with the VRC-MIDIDMX system by Micca.
MIDIDMX.cs:238-260
private void SendMidi(int channel, byte value){ if (midiOutput == null) return; int bank = (int)(channel / 2048); if (bankStatus != bank) { ChangeBanks(bank); } channel -= bank * 2048; if (channel < 1024) { Send18BitMessage<NoteOnEvent>(channel, value, 0); } else { Send18BitMessage<NoteOffEvent>(channel, value, 1024); } midiUpdates++;}
Configuration:
midiDevice: Name of MIDI output device (e.g., “loopMIDI Port”)
channelsPerUpdate: Max channels sent per frame (default: 100)
idleScanChannels: Channels to refresh during idle (default: 10)
useEditorLog: Monitor Unity Editor log instead of VRChat log
Features:
Automatic Discovery: Lists all available MIDI devices
Bank Switching: Handles 8 banks of 2,048 channels each
Watchdog Protocol: Ensures world is responsive before sending data
Log Monitoring: Reads VRChat log file to confirm data reception
Selective Updates: Only sends changed channels to minimize bandwidth
Create a port named “loopMIDI Port” (or customize the name).
3
Configure HNode
Add MIDIDMX exporter and set midiDevice to your port name.
4
Enter VRChat World
Load a VRChat world with the VRC-MIDIDMX system installed.
5
Wait for Handshake
MIDIDMX will “knock” and wait for the world to respond via log file.
6
Data Flows
Once connected, DMX data streams to the VRChat world in real-time.
MIDIDMX monitors the VRChat log file for handshake responses. Make sure VRChat has write permissions to %AppData%\..\LocalLow\VRChat\VRChat\.
Status Codes:
MIDIDMX.cs:31-36
public enum Status : int{ Disconnected, // Not connected to MIDI ConnectedWait, // Connected to MIDI, not connected to world ConnectedSendingData // Connected to world and sending data}
Check status with MidiStatus() to verify connection state.Performance Optimization:
MIDIDMX.cs:152-167
midiUpdates = 0;for (int i = midiCatchup; i < channelValues.Count; i++){ // Only send if changed or in scan range if ((channelValues[i] != midiData[i] || (i >= midiScanPosition && i < midiScanPosition + idleScanChannels)) && i < channelValues.Count) { if (midiUpdates >= channelsPerUpdate) { midiCatchup = i; // Resume next frame break; } midiData[i] = channelValues[i]; SendMidi(i, channelValues[i]); }}
The exporter tracks previous values and only sends changes, plus a slow background scan to ensure synchronization.
Set channelsPerUpdate to 100 (default) for best compatibility. Higher values may exceed VRChat’s input buffer capacity.
Purpose: Send timecode data to external systemsExports SMPTE or MIDI timecode synchronized with the DMX data stream, enabling time-synchronized playback across multiple systems.Use Cases:
// Initialize all exportersLoader.showconf.Serializer.InitFrame(ref mergedDmxValues);foreach (var exporter in Loader.showconf.Exporters){ exporter.InitFrame(ref mergedDmxValues);}// Process each channelfor (int i = 0; i < ChannelsToSerialize; i++){ // Serialize to video Loader.showconf.Serializer.SerializeChannel( ref pixels, mergedDmxValues[i], i, TextureWidth, TextureHeight); // Export to external systems foreach (var exporter in Loader.showconf.Exporters) { exporter.SerializeChannel(mergedDmxValues[i], i); }}// Finalize frameLoader.showconf.Serializer.CompleteFrame( ref pixels, ref mergedDmxValues, TextureWidth, TextureHeight);foreach (var exporter in Loader.showconf.Exporters){ exporter.CompleteFrame(ref mergedDmxValues);}
Exporters run every frame, so performance is critical. Heavy operations should be async or throttled.
public class MyExporter : IExporter{ public void Construct() { /* Initialize */ } public void Deconstruct() { /* Cleanup */ } public void SerializeChannel(byte channelValue, int channel) { } public void InitFrame(ref List<byte> channelValues) { } public void CompleteFrame(ref List<byte> channelValues) { // Export logic here } // ... UI methods ...}
2
Choose Processing Mode
Decide if you need:
Per-Channel: Implement SerializeChannel (rare)
Per-Frame: Implement CompleteFrame (most common)
Both: Process in stages
3
Implement Export Logic
Most exporters do all work in CompleteFrame:
public void CompleteFrame(ref List<byte> channelValues){ // Access all channels for (int i = 0; i < channelValues.Count; i++) { byte value = channelValues[i]; // Send to external system }}
4
Handle Async Operations
If exporting to network/disk, use async to avoid blocking:
public void CompleteFrame(ref List<byte> channelValues){ // Copy data var dataCopy = channelValues.ToArray(); // Export async (don't wait) Task.Run(() => SendToNetwork(dataCopy));}