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>
This commit is contained in:
@@ -13,6 +13,12 @@ clang++ \
|
||||
-Ivendor/reaper-sdk/sdk \
|
||||
-Ivendor/reaper-sdk/WDL \
|
||||
-o "$OUT" \
|
||||
src/main.cpp
|
||||
src/main.cpp \
|
||||
src/tracking.cpp \
|
||||
src/socket.cpp
|
||||
|
||||
echo "Built $OUT"
|
||||
|
||||
DEST="$HOME/Library/Application Support/REAPER/UserPlugins/$OUT"
|
||||
cp "$OUT" "$DEST"
|
||||
echo "Copied to $DEST (restart REAPER to reload)"
|
||||
|
||||
@@ -1,50 +1,112 @@
|
||||
// extension-reaper-macos — bare-minimum REAPER extension.
|
||||
//
|
||||
// Verifies the extension loads and is recognized by REAPER: prints a
|
||||
// confirmation line to the REAPER console on load, and registers one test
|
||||
// action ("extension-reaper-macos: Hello") in the Actions list.
|
||||
// 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"
|
||||
|
||||
static int g_hello_cmd_id = 0;
|
||||
#include <cstdio>
|
||||
|
||||
static bool HookCommand2(KbdSectionInfo *sec, int command, int val, int val2, int relmode, HWND hwnd)
|
||||
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)
|
||||
{
|
||||
if (command == g_hello_cmd_id)
|
||||
// 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; // REAPER is unloading us
|
||||
|
||||
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; // a required API function was missing
|
||||
return 0;
|
||||
|
||||
custom_action_register_t action = {
|
||||
0, // main section
|
||||
"EXTENSION_REAPER_MACOS_HELLO",
|
||||
"extension-reaper-macos: Hello",
|
||||
// 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,
|
||||
};
|
||||
g_hello_cmd_id = rec->Register("custom_action", &action);
|
||||
action1_id = rec->Register("custom_action", &actionDescription);
|
||||
|
||||
rec->Register("hookcommand2", (void *)HookCommand2);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// A basic UDP socket listener. Opens a local port and polls it
|
||||
// non-blockingly via REAPER's "timer" callback (same mechanism
|
||||
// tracking.cpp uses — each Register("timer", ...) call adds its own
|
||||
// independent subscriber, so this runs alongside tracking.cpp's OnTimer,
|
||||
// not instead of it). Non-blocking means this never stalls REAPER's main
|
||||
// thread waiting for network data that may never arrive.
|
||||
//
|
||||
// Incoming messages are parsed as a plain number (e.g. "0.05" or "-0.05")
|
||||
// and applied as a delta to the currently-selected automation item's
|
||||
// baseline, via the same NudgeSelectedBaseline tracking.cpp's manual
|
||||
// actions already use.
|
||||
|
||||
// No REAPERAPI_IMPLEMENT here — main.cpp owns the real storage.
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_ShowConsoleMsg
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
#include "reaper_plugin_functions.h"
|
||||
#include "socket.h"
|
||||
#include "tracking.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
static const int kListenPort = 9124;
|
||||
static int listen_fd = -1;
|
||||
|
||||
static void OnSocketTimer()
|
||||
{
|
||||
if (listen_fd < 0)
|
||||
return;
|
||||
|
||||
// Drain every pending packet each tick, instead of one recvfrom() per
|
||||
// tick. REAPER's timer only fires ~30x/second-ish, so if messages arrive
|
||||
// faster than that (a fast fader drag easily does), a one-shot read
|
||||
// falls further and further behind as packets queue up in the OS buffer.
|
||||
// Since our protocol is deltas, summing everything pending into one net
|
||||
// value and applying it once is equivalent to applying each
|
||||
// individually — just one REAPER API call instead of many.
|
||||
double total_delta = 0.0;
|
||||
char data[256];
|
||||
ssize_t n;
|
||||
while ((n = recvfrom(listen_fd, data, sizeof(data) - 1, 0, NULL, NULL)) > 0)
|
||||
{
|
||||
data[n] = '\0';
|
||||
|
||||
// Note: atof() returns 0.0 both for "actually parsed as zero" and for
|
||||
// "couldn't parse this at all" — can't tell those apart from the
|
||||
// return value alone. Fine for now since we control what sends here.
|
||||
total_delta += atof(data);
|
||||
}
|
||||
|
||||
if (total_delta != 0.0)
|
||||
NudgeSelectedBaseline(total_delta);
|
||||
}
|
||||
|
||||
void RegisterSocket(reaper_plugin_info_t *rec)
|
||||
{
|
||||
listen_fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (listen_fd < 0)
|
||||
{
|
||||
ShowConsoleMsg("[extension-reaper-macos] socket() failed\n");
|
||||
return;
|
||||
}
|
||||
|
||||
// Non-blocking — recvfrom() returns immediately if nothing's arrived,
|
||||
// instead of stalling REAPER's main thread waiting for data.
|
||||
fcntl(listen_fd, F_SETFL, O_NONBLOCK);
|
||||
|
||||
sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only, for now
|
||||
addr.sin_port = htons(kListenPort);
|
||||
|
||||
if (bind(listen_fd, (sockaddr *)&addr, sizeof(addr)) < 0)
|
||||
{
|
||||
ShowConsoleMsg("[extension-reaper-macos] socket bind() failed\n");
|
||||
close(listen_fd);
|
||||
listen_fd = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "[extension-reaper-macos] socket listening on 127.0.0.1:%d\n", kListenPort);
|
||||
ShowConsoleMsg(buf);
|
||||
|
||||
rec->Register("timer", (void *)OnSocketTimer);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// A basic UDP socket listener for extension-reaper-macos — the eventual
|
||||
// entry point for the desktop app to spray fader/control data at REAPER.
|
||||
|
||||
#ifndef EXTENSION_REAPER_MACOS_SOCKET_H
|
||||
#define EXTENSION_REAPER_MACOS_SOCKET_H
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
// Opens a UDP socket and registers a REAPER timer to poll it
|
||||
// non-blockingly. Call once from the entrypoint, after REAPERAPI_LoadAPI
|
||||
// has succeeded.
|
||||
void RegisterSocket(reaper_plugin_info_t *rec);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,156 @@
|
||||
// Tracks REAPER's currently selected automation item(s), purely by
|
||||
// polling. REAPER doesn't tell us when selection changes, so we check
|
||||
// repeatedly via a "timer" callback and only print when something's new.
|
||||
//
|
||||
// Also registers two test actions (nudge baseline up/down) to prove we can
|
||||
// WRITE to an automation item, not just read it — same pattern as Hello in
|
||||
// main.cpp: custom_action to register, hookcommand2 to react when it runs.
|
||||
|
||||
// No REAPERAPI_IMPLEMENT here — main.cpp owns the real storage for these
|
||||
// function boxes (it's the one .cpp file that defines IMPLEMENT). This file
|
||||
// just borrows them via extern declarations, same names, same addresses.
|
||||
#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
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
#include "reaper_plugin_functions.h"
|
||||
#include "tracking.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <vector>
|
||||
|
||||
static int nudge_up_id = 0;
|
||||
static int nudge_down_id = 0;
|
||||
static const double kNudgeAmount = 0.05;
|
||||
|
||||
struct SelectedItem
|
||||
{
|
||||
TrackEnvelope *env;
|
||||
int idx;
|
||||
bool operator==(const SelectedItem &other) const
|
||||
{
|
||||
return env == other.env && idx == other.idx;
|
||||
}
|
||||
};
|
||||
|
||||
// Scans every envelope on every track in the project, collecting every
|
||||
// automation item currently marked selected (D_UISEL) — regardless of
|
||||
// which track/envelope currently has UI focus. That's what lets this work
|
||||
// across multiple tracks at once, and also what makes a clip keep being
|
||||
// targeted after you've clicked onto a different track: we're not asking
|
||||
// REAPER "what's focused right now," we're checking the actual selection
|
||||
// flag on every item, everywhere, every time.
|
||||
static std::vector<SelectedItem> FindAllSelectedItems()
|
||||
{
|
||||
std::vector<SelectedItem> result;
|
||||
|
||||
int track_count = CountTracks(NULL);
|
||||
for (int t = 0; t < track_count; t++)
|
||||
{
|
||||
MediaTrack *track = GetTrack(NULL, t);
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
int env_count = CountTrackEnvelopes(track);
|
||||
for (int e = 0; e < env_count; e++)
|
||||
{
|
||||
TrackEnvelope *env = GetTrackEnvelope(track, e);
|
||||
if (!env)
|
||||
continue;
|
||||
|
||||
int item_count = CountAutomationItems(env);
|
||||
for (int i = 0; i < item_count; i++)
|
||||
{
|
||||
if (GetSetAutomationItemInfo(env, i, "D_UISEL", 0, false) != 0)
|
||||
result.push_back({env, i});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::vector<SelectedItem> last_printed_items;
|
||||
|
||||
// REAPER calls this automatically, many times per second, no matter what
|
||||
// the user is doing. Prints only when the selected set actually changes.
|
||||
static void OnTimer()
|
||||
{
|
||||
std::vector<SelectedItem> items = FindAllSelectedItems();
|
||||
|
||||
if (items != last_printed_items)
|
||||
{
|
||||
last_printed_items = items;
|
||||
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), "[extension-reaper-macos] %zu item(s) selected\n", items.size());
|
||||
ShowConsoleMsg(buf);
|
||||
}
|
||||
}
|
||||
|
||||
// Shared by the manual nudge actions (below) and socket.cpp's listener —
|
||||
// one path, two ways to trigger it. Applies the same delta to every
|
||||
// currently-selected item, anywhere in the project, each clamped
|
||||
// independently.
|
||||
void NudgeSelectedBaseline(double delta)
|
||||
{
|
||||
std::vector<SelectedItem> items = FindAllSelectedItems();
|
||||
if (items.empty())
|
||||
{
|
||||
ShowConsoleMsg("[extension-reaper-macos] nudge: nothing selected\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const SelectedItem &item : items)
|
||||
{
|
||||
double baseline = GetSetAutomationItemInfo(item.env, item.idx, "D_BASELINE", 0, false);
|
||||
double newBaseline = baseline + delta;
|
||||
if (newBaseline < 0.0) newBaseline = 0.0; // D_BASELINE's valid range is [0,1]
|
||||
if (newBaseline > 1.0) newBaseline = 1.0;
|
||||
GetSetAutomationItemInfo(item.env, item.idx, "D_BASELINE", newBaseline, true);
|
||||
}
|
||||
// No console log here — this runs on every fader move, and a GUI
|
||||
// text-widget update per call was itself a real latency cost.
|
||||
}
|
||||
|
||||
// Called for every action, any trigger source. Checks if it was one of our
|
||||
// two nudge actions; if so, calls the shared NudgeSelectedBaseline above.
|
||||
static bool NudgeAction(KbdSectionInfo *sec, int command, int val, int val2, int relmode, HWND hwnd)
|
||||
{
|
||||
if (command == nudge_up_id)
|
||||
NudgeSelectedBaseline(kNudgeAmount);
|
||||
else if (command == nudge_down_id)
|
||||
NudgeSelectedBaseline(-kNudgeAmount);
|
||||
else
|
||||
return false; // not ours
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterTracking(reaper_plugin_info_t *rec)
|
||||
{
|
||||
rec->Register("timer", (void *)OnTimer);
|
||||
|
||||
custom_action_register_t nudge_up_desc = {
|
||||
0,
|
||||
"EXTENSION_REAPER_MACOS_NUDGE_UP",
|
||||
"extension-reaper-macos: Nudge baseline up",
|
||||
NULL,
|
||||
};
|
||||
nudge_up_id = rec->Register("custom_action", &nudge_up_desc);
|
||||
|
||||
custom_action_register_t nudge_down_desc = {
|
||||
0,
|
||||
"EXTENSION_REAPER_MACOS_NUDGE_DOWN",
|
||||
"extension-reaper-macos: Nudge baseline down",
|
||||
NULL,
|
||||
};
|
||||
nudge_down_id = rec->Register("custom_action", &nudge_down_desc);
|
||||
|
||||
rec->Register("hookcommand2", (void *)NudgeAction);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Polls REAPER for the currently selected envelope + automation item, since
|
||||
// REAPER has no "selection changed" notification to hook directly.
|
||||
|
||||
#ifndef EXTENSION_REAPER_MACOS_TRACKING_H
|
||||
#define EXTENSION_REAPER_MACOS_TRACKING_H
|
||||
|
||||
#include "reaper_plugin.h"
|
||||
|
||||
// Call once from the entrypoint, after REAPERAPI_LoadAPI has succeeded.
|
||||
// Registers our polling function with REAPER's "timer" callback.
|
||||
void RegisterTracking(reaper_plugin_info_t *rec);
|
||||
|
||||
// Applies delta to D_BASELINE on the currently-selected automation item (if
|
||||
// any). Shared by the manual nudge actions and socket.cpp's listener.
|
||||
void NudgeSelectedBaseline(double delta);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user