This document describes the complete IPC (Inter-Process Communication) system for communicating between a C# application and the C++ projectM visualizer. The system uses JSON messages over stdin/stdout for easy parsing and extensibility.
- Core IPC handler that manages stdin/stdout communication
- Runs message listening in a separate thread
- Provides thread-safe message sending
- Manages preset scheduling with timestamps
- Keeps presets sorted by start time
- Provides queries for preset at specific timestamp
- Manages audio playback state and timing
- Tracks current playback position
- Handles seek operations
- Coordinates all IPC functionality
- Routes incoming messages to appropriate handlers
- Sends state updates back to C#
- Complete C# client for communicating with C++
- Handles message serialization/deserialization
- Provides event-based message handling
All messages are single-line JSON followed by newline:
{"type": 0, "data": {"field": "value"}}1. TIMESTAMP (type: 0)
{
"type": 0,
"data": {
"timestampMs": 5000
}
}C# sends the current audio timestamp in milliseconds.
2. LOAD_PRESET (type: 1)
{
"type": 1,
"data": {
"presetName": "mypreset.milk",
"startTimestampMs": 10000
}
}C# requests to load a preset at a specific timestamp.
3. DELETE_PRESET (type: 2)
{
"type": 2,
"data": {
"presetName": "mypreset.milk",
"timestampMs": 10000
}
}C# requests to delete a preset from the queue.
4. START_PREVIEW (type: 3)
{
"type": 3,
"data": {
"fromTimestampMs": 0
}
}C# requests to start audio preview from a specific timestamp.
5. STOP_PREVIEW (type: 4)
{
"type": 4,
"data": {}
}C# requests to stop audio preview.
1. PRESET_LOADED (type: 5)
{
"type": 5,
"data": {
"presetName": "mypreset.milk",
"startTimestampMs": 10000,
"lastReceivedTimestampMs": 5000
}
}C++ confirms that a preset was successfully loaded.
2. CURRENT_STATE (type: 6)
{
"type": 6,
"data": {
"lastReceivedTimestampMs": 5000,
"presets": [
{
"presetName": "preset1.milk",
"timestampMs": 0
},
{
"presetName": "preset2.milk",
"timestampMs": 10000
}
]
}
}C++ sends the current state of all queued presets (sorted by timestamp).
3. PREVIEW_STATUS (type: 7)
{
"type": 7,
"data": {
"isPlaying": true,
"currentTimestampMs": 5234
}
}C++ sends the current preview playback status and position.
4. ERROR_RESPONSE (type: 8)
{
"type": 8,
"data": {
"error": "Preset not found"
}
}C++ sends error information about failed operations.
Add the new source files to your CMakeLists.txt:
# In src/sdl-test-ui/CMakeLists.txt
target_sources(LvsAudioReactiveVisualizer PRIVATE
ipc_communication.hpp
ipc_communication.cpp
preset_queue_manager.hpp
preset_queue_manager.cpp
audio_preview_manager.hpp
audio_preview_manager.cpp
ipc_manager.hpp
ipc_manager.cpp
)
# Add jsoncpp dependency
target_link_libraries(LvsAudioReactiveVisualizer PRIVATE jsoncpp)In pmSDL.hpp, add member variable:
private:
std::unique_ptr<IPCManager> ipcManager;In pmSDL.cpp constructor:
projectMSDL::projectMSDL(...)
: ...
{
// ... existing code ...
ipcManager = std::make_unique<IPCManager>();
ipcManager->initialize();
}In pmSDL.cpp destructor:
projectMSDL::~projectMSDL()
{
if (ipcManager) {
ipcManager->shutdown();
}
// ... existing cleanup code ...
}In the main render loop (in mainLoop or renderFrame):
// Update preview based on current audio position
if (ipcManager && ipcManager->getAudioPreview().isPlaying()) {
uint64_t currentAudioTimestampMs = app->getCurrentAudioTimestamp();
app->ipcManager->getAudioPreview().updateCurrentTimestamp(currentAudioTimestampMs);
// Get preset that should be playing now
std::string currentPreset = app->ipcManager->getPresetQueue()
.getPresetAtTimestamp(currentAudioTimestampMs);
if (!currentPreset.empty()) {
// Load this preset (implement this in your existing code)
app->loadPreset(currentPreset);
}
}
// Periodically send state updates to C#
static uint32_t updateCounter = 0;
if (updateCounter++ % 30 == 0) { // Every ~500ms at 60fps
if (app->ipcManager && app->ipcManager->hasPendingStateUpdate()) {
app->ipcManager->sendCurrentState();
}
}
// Send preview status updates
static uint32_t statusCounter = 0;
if (statusCounter++ % 60 == 0) { // Every ~1000ms at 60fps
if (app->ipcManager) {
app->ipcManager->sendPreviewStatusUpdate();
}
}Install-Package Newtonsoft.Jsonusing ProjectMIPC;
// Start the C++ process with IPC
var ipcClient = new ProjectMIPCClient(
"path/to/LvsAudioReactiveVisualizer.exe",
"--preset-dir C:\\path\\to\\presets"
);
// Subscribe to messages
ipcClient.MessageReceived += (sender, args) =>
{
Console.WriteLine($"Received message: {args.MessageType}");
};// Update current timestamp (typically in audio position callback)
ipcClient.SendTimestamp(currentAudioPositionMs);
// Load a preset at specific timestamp
ipcClient.LoadPreset("mypreset.milk", 5000);
// Delete a preset
ipcClient.DeletePreset("mypreset.milk", 5000);
// Start audio preview
ipcClient.StartPreview(0);
// Stop audio preview
ipcClient.StopPreview();// Access current preset queue
foreach (var preset in ipcClient.PresetQueue)
{
Console.WriteLine($"{preset.PresetName} at {preset.TimestampMs}ms");
}
// Check if preview is playing
if (ipcClient.IsPreviewPlaying)
{
Console.WriteLine("Preview is playing");
}- C# UI: User selects preset "cool.milk" and sets it to start at 5000ms
- C# → C++: Sends
LOAD_PRESETmessage - C++: Adds preset to queue, sends
PRESET_LOADEDconfirmation - C# UI: Updates UI to show queued preset
- C# → C++: Sends
START_PREVIEWfrom 0ms - C++: Starts audio playback
- C++ → C#: Periodically sends
PREVIEW_STATUSwith current timestamp - C++: At 5000ms, loads "cool.milk" preset (via
getPresetAtTimestamp()) - C++ → C#: Sends
CURRENT_STATEwith updated queue
- C# UI: User clicks delete on "cool.milk" (5000ms)
- C# → C++: Sends
DELETE_PRESETmessage - C++: Removes preset from queue
- C++ → C#: Sends
CURRENT_STATEconfirmation - C# UI: Updates UI, preset no longer shown
- IPC Handler: Listens on separate thread, thread-safe message sending via mutex
- Preset Queue Manager: All operations are thread-safe with mutex
- Audio Preview Manager: Uses atomic variables for state management
// In render thread (safe):
ipcManager->getPresetQueue().getPresetAtTimestamp(timestamp);
ipcManager->getAudioPreview().updateCurrentTimestamp(timestamp);
// From IPC thread (safe):
ipcManager->getPresetQueue().addPreset(name, timestamp);
ipcManager->getAudioPreview().startPreview(timestamp);If C++ encounters an error, it sends an ERROR_RESPONSE:
{
"type": 8,
"data": {
"error": "Preset file not found"
}
}ipcClient.MessageReceived += (sender, args) =>
{
if (args.MessageType == MessageType.ERROR_RESPONSE)
{
string error = args.Data["error"]?.Value<string>();
Console.WriteLine($"Error from C++: {error}");
// Handle error in UI
ShowErrorDialog(error);
}
};- Update Frequency: Send timestamps at audio update rate (44.1kHz / 512 samples ≈ 86 times/sec)
- State Updates: Send full state every 500ms (not on every timestamp)
- Message Queue: Preset queue is sorted, lookup is O(n) but typically small (<100 presets)
- Memory: Each message is ~100-500 bytes, negligible impact
- Binary Protocol: For higher performance, implement binary format instead of JSON
- Compression: Compress state updates if queue becomes very large
- Network IPC: Switch from stdin/stdout to network sockets for remote control
- State Persistence: Save/load preset queues to disk
- Undo/Redo: Track command history for UI undo functionality
// Test preset queue ordering
PresetQueueManager queue;
queue.addPreset("a.milk", 1000);
queue.addPreset("b.milk", 500);
queue.addPreset("c.milk", 2000);
auto all = queue.getAllPresets();
assert(all[0].startTimestampMs == 500);
assert(all[1].startTimestampMs == 1000);
assert(all[2].startTimestampMs == 2000);var client = new ProjectMIPCClient(exePath);
// Load preset
client.LoadPreset("test.milk", 5000);
// Wait for confirmation
Thread.Sleep(100);
// Check state
assert(client.PresetQueue.Count == 1);
assert(client.PresetQueue[0].TimestampMs == 5000);- Check that C++ process is still running:
if (cppProcess.HasExited) - Check stdout is redirected:
RedirectStandardOutput = true - Check for exceptions in IPC handler
- Verify JSON format: Use online JSON validator
- Check message type enum values match
- Enable debug logging in both C# and C++
- Ensure timestamp updates are frequent enough
- Use audio driver timestamp, not application clock
- Account for audio latency in calculations
See CSharpIPCClient_Example.cs for full working example including:
- Process startup
- Message sending/receiving
- State management
- Error handling
- Resource cleanup