Files
virtual-controller/extension-reaper-macos/src/main.cpp
T
Paul Lipscomb 7d4f8a4cd2 Extend REAPER extension to read/write automation items, wire desktop app to it over UDP
extension-reaper-macos: split main.cpp into tracking.cpp (polls selected
envelope/automation items, reads/writes D_BASELINE) and socket.cpp (basic
UDP listener, non-blocking, drains all pending packets per tick). Nudging
now applies to every selected automation item across every track in the
project, not just the first one found.

app-desktop-macos: new "Extension" column (mirrors the OSC/Remote columns)
to enable/disable the UDP connection and point it at a host/port, plus a
log window. FocusFaderWidget gets an OSC/Ext output-mode toggle (mirrors
TransportWidget's MIDI/OSC toggle) so a fader can drive the extension
directly instead of/alongside OSC. Also renamed the Remote/Tablet buttons
for clarity and split their status indicator into its own column.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 19:13:34 -04:00

113 lines
4.2 KiB
C++

// extension-reaper-macos — bare-minimum REAPER extension.
//
// Loads, registers one test action ("extension-reaper-macos: Hello"), and
// prints a confirmation to the REAPER console.
//
// Top-level story:
// - We tell the compiler which REAPER functions we need a box for.
// - REAPER loads our .dylib and calls our one required function, once.
// - We check we're actually loading, not unloading, and the version matches.
// - We fill our function box(es) with their real address.
// - We describe a new action and hand it to REAPER — it creates it, gives
// us back a number.
// - We subscribe our own function to REAPER's "every action, any trigger"
// stream.
// - We print "loaded successfully" — proof of everything up to that point,
// but NOT proof the action/subscription actually works.
// - Setup's done. We now just sit in memory, doing nothing.
// - Later, any action anywhere in REAPER (click, key, MIDI, OSC) calls our
// subscribed function.
// - It checks if the action was ours. If yes, react. If no, ignore.
// We tell the compiler which REAPER functions we need a box for. This file
// owns the real storage (REAPERAPI_IMPLEMENT), so this list has to cover
// everything used anywhere in the project, including tracking.cpp.
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_CountAutomationItems
#define REAPERAPI_WANT_GetSetAutomationItemInfo
#define REAPERAPI_WANT_CountTracks
#define REAPERAPI_WANT_GetTrack
#define REAPERAPI_WANT_CountTrackEnvelopes
#define REAPERAPI_WANT_GetTrackEnvelope
#define REAPERAPI_IMPLEMENT
#include "reaper_plugin.h"
#include "reaper_plugin_functions.h"
#include "tracking.h"
#include "socket.h"
#include <cstdio>
static int action1_id = 0;
static const char *kAction1IdStr = "EXTENSION_REAPER_MACOS_HELLO";
static const char *kAction1Name = "Hello Action List Display Name";
// Testing hookcommand2 again (REAPER's docs specifically recommend it for
// custom_action-registered actions), this time with debug prints kept in.
static bool Action1(KbdSectionInfo *sec, int command, int val, int val2, int relmode, HWND hwnd)
{
// Unconditional — proves whether this callback is being reached at all,
// and for which command IDs, regardless of whether it's ours.
char buf[128];
snprintf(buf, sizeof(buf), "[extension-reaper-macos] Action1 called: command=%d action1_id=%d\n", command, action1_id);
ShowConsoleMsg(buf);
if (command == action1_id)
{
ShowConsoleMsg("[extension-reaper-macos] Hello action triggered\n");
return true;
}
ShowConsoleMsg("[extension-reaper-macos] ignored — not ours\n");
return false;
}
// REAPER loads our .dylib and calls this once.
extern "C" REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(REAPER_PLUGIN_HINSTANCE hInstance, reaper_plugin_info_t *rec)
{
// Check we're actually loading, not unloading, and the version matches.
if (!rec)
return 0;
if (rec->caller_version != REAPER_PLUGIN_VERSION)
return 0;
// Fill our function box(es) with their real address.
if (REAPERAPI_LoadAPI(rec->GetFunc) != 0)
return 0;
// Describe a new action and hand it to REAPER — it creates it, gives us
// back a number, which we save.
custom_action_register_t actionDescription = {
0,
kAction1IdStr,
kAction1Name,
NULL,
};
action1_id = rec->Register("custom_action", &actionDescription);
// Verify registration actually succeeded — Register returns 0 on failure,
// and we've never checked that until now.
char buf[128];
snprintf(buf, sizeof(buf), "[extension-reaper-macos] action1_id = %d\n", action1_id);
ShowConsoleMsg(buf);
// Subscribe Action1 via hookcommand2 — REAPER's documented pairing for
// custom_action-registered actions specifically.
rec->Register("hookcommand2", (void *)Action1);
// Envelope/automation-item polling now lives in tracking.cpp.
RegisterTracking(rec);
// Basic UDP socket listener lives in socket.cpp.
RegisterSocket(rec);
// Print "loaded successfully" — proof of everything above, but NOT proof
// the action/subscription actually works.
ShowConsoleMsg("[extension-reaper-macos] loaded successfully\n");
// Setup's done. Return 1 = "keep me loaded." We now just sit in memory,
// doing nothing, until Action1 gets called later.
return 1;
}