Skip to main content

What are Exporters?

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.

Parallel Output

Run alongside video serialization

External Systems

Send data to MIDI, files, timecode

Read-Only

Access but don’t modify DMX data

Frame-Synced

Process data once per frame

The IExporter Interface

All exporters implement the IExporter interface:
IExporter.cs:6-35
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);
}

Lifecycle Methods

1

InitFrame

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.

Available Exporters

HNode includes several powerful exporters for different use cases:

MIDIDMX

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
Setup Process:
1

Install loopMIDI

Install loopMIDI to create virtual MIDI ports.
2

Create MIDI Port

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.

TextFileExporter

Purpose: Export channel data to text files for debugging and analysis
TextFileExporter.cs:14-21
public void CompleteFrame(ref List<byte> channelValues)
{
    data = channelValues; // Store latest frame
}
Configuration:
  • onlyNonZeroChannels: Only export channels with values > 0
Features:
  • Manual Export: Click “Export channels to text file” button to save
  • Channel Filtering: Optionally exclude zero-value channels
  • Human-Readable Format: Each line shows channel: value
Output Format:
0.1: 255
0.2: 128
1.5: 64
3.10: 200
Use Cases:
  • Debug DMX routing issues
  • Document channel assignments
  • Verify generator output
  • Create fixture profiles
  • Analyze show data
Enable “Only export non-zero channels” when working with large DMX setups to create more readable output files.

TimeCodeExporter

Purpose: Send timecode data to external systems Exports SMPTE or MIDI timecode synchronized with the DMX data stream, enabling time-synchronized playback across multiple systems. Use Cases:
  • Synchronize with video playback
  • Coordinate multiple lighting controllers
  • Maintain frame-accurate timing
  • Record/replay shows with timecode

Exporter Execution Flow

Exporters run in parallel with the serializer:
TextureWriter.cs:107-154
// Initialize all exporters
Loader.showconf.Serializer.InitFrame(ref mergedDmxValues);
foreach (var exporter in Loader.showconf.Exporters)
{
    exporter.InitFrame(ref mergedDmxValues);
}

// Process each channel
for (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 frame
Loader.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.

Creating Custom Exporters

Creating an exporter is straightforward:
1

Implement IExporter

Create a class implementing IExporter:
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));
}
5

Add Configuration UI

Create user-configurable properties:
public void ConstructUserInterface(RectTransform rect)
{
    Util.AddInputField(rect, "Server Address")
        .WithText(serverAddress)
        .WithCallback((v) => { serverAddress = v; });
}
Never block the main thread in an exporter. Use async/await or background threads for network I/O, file operations, or heavy processing.

Exporter Patterns

Buffering Pattern

Store data and export periodically:
private Queue<byte[]> frameBuffer = new Queue<byte[]>();

public void CompleteFrame(ref List<byte> channelValues)
{
    // Buffer frame
    frameBuffer.Enqueue(channelValues.ToArray());
    
    // Export in batches
    if (frameBuffer.Count >= 60) // Every 60 frames
    {
        ExportBatch(frameBuffer.ToArray());
        frameBuffer.Clear();
    }
}

Change Detection Pattern

Only export when data changes:
private byte[] lastFrame;

public void CompleteFrame(ref List<byte> channelValues)
{
    if (lastFrame == null || !channelValues.SequenceEqual(lastFrame))
    {
        lastFrame = channelValues.ToArray();
        ExportData(lastFrame);
    }
}

Throttling Pattern

Limit export frequency:
private DateTime lastExport = DateTime.MinValue;
private TimeSpan throttleInterval = TimeSpan.FromMilliseconds(100);

public void CompleteFrame(ref List<byte> channelValues)
{
    if (DateTime.Now - lastExport > throttleInterval)
    {
        ExportData(channelValues);
        lastExport = DateTime.Now;
    }
}

Multi-Exporter Scenarios

You can run multiple exporters simultaneously: Example Configuration:
  1. MIDIDMX: Send to VRChat world
  2. TextFileExporter: Log data for debugging
  3. TimeCodeExporter: Sync with video playback
All three receive the same DMX data and operate independently.
Exporters execute in configuration order, but since they don’t modify data, order typically doesn’t matter.

Performance Considerations

  • Avoid allocating large objects every frame
  • Reuse buffers and arrays where possible
  • Use object pooling for frequently created objects
  • Profile memory usage with Unity Profiler
  • Use Task.Run() for heavy operations
  • Don’t access Unity objects from background threads
  • Copy data before passing to async methods
  • Handle thread synchronization carefully
  • Use UDP for low-latency protocols
  • Implement connection pooling for TCP
  • Handle network errors gracefully
  • Add timeout mechanisms
  • Catch exceptions in exporters to prevent crashes
  • Log errors for debugging
  • Implement retry logic for transient failures
  • Provide user feedback via UI

Exporter Configuration

Exporters are configured in show files:
exporters:
- !MIDIDMX
  useEditorLog: false
  midiDevice: "loopMIDI Port"
  channelsPerUpdate: 100
  idleScanChannels: 10

- !TextFileExporter
  onlyNonZeroChannels: true
Like generators and serializers, exporters are discovered automatically via reflection. Simply implement IExporter and HNode will find it.

Debugging Exporters

Tips for debugging exporter issues:
  1. Use Debug.Log(): Unity’s logging appears in the console
  2. Check Construct(): Verify initialization succeeds
  3. Monitor Frame Rate: Heavy exporters reduce FPS
  4. Test Independently: Disable other exporters to isolate issues
  5. Validate External Systems: Ensure receiving systems are ready
public void CompleteFrame(ref List<byte> channelValues)
{
    Debug.Log($"Exporting {channelValues.Count} channels");
    // Export logic...
}
The TextFileExporter is useful for verifying that other exporters receive correct data. Enable it alongside your custom exporter during development.

Build docs developers (and LLMs) love