DEV Community

Maxim Radugin
Maxim Radugin

Posted on • Originally published at radugin.com

Extensible Control of Reaper via OSC and Scripts

Introduction

REAPER is a powerful Digital Audio Workstation (DAW) with enormous customization possibilities. Its scripting support, external control capabilities, support for many DAW plugin formats, and compatibility with MacOS and Windows make it an obvious choice for building all sorts of integrations and automation. At Sonarworks, we use REAPER as a plugin host as part of our DAW plugin test automation framework.

Despite its great functionality, REAPER features minimal documentation, which makes it challenging to unleash its full potential.

In this article, I'll provide a complete guide on how to extend REAPER's control possibilities when interacting with it via the Open Sound Control (OSC) interface.

Out-of-the-Box Functionality

Out of the box, REAPER supports a limited set of functions via the OSC interface, which are defined in .ReaperOSC files located in ~/Library/Application\ Support/REAPER/OSC/ on MacOS. You can modify the address patterns of OSC messages REAPER will react to and call pre-defined functions, but you can't define your own functions directly.

Defining and Calling Custom Actions

Fortunately, REAPER supports calling custom actions via /action/_COMMAND_ID OSC messages.

Below, I'll demonstrate the setup. Please note that I'll be modifying some of REAPER's configuration files directly, as certain modifications via REAPER itself are not possible or are very unhandy. Always backup your files before making any modifications to avoid losing your work or breaking your existing setup.

Custom actions are stored in ~/Library/Application\ Support/REAPER/reaper-kb.ini on MacOS. The file format description can be found here.

For my use case, I only need my custom actions, so I started with an empty file and added the following line:

SCR 4 0 MY_COMPANY_TEST_SIMPLE_OSC "MyCompany: test simple OSC trigger" "MyCompany/test_simple_osc.lua"
Enter fullscreen mode Exit fullscreen mode

This defines a custom action. For the meaning of SCR 4 0, see the reaper-kb.ini documentation. _MY_COMPANY_TEST_SIMPLE_OSC (note the underscore) is the Command ID of the test_simple_osc.lua script, which should be placed in the ~/Library/Application\ Support/REAPER/Scripts/MyCompany/ directory.

Content of test_simple_osc.lua:

reaper.ShowMessageBox("This is a simple script that can be invoked via OSC", "Info", 0)
Enter fullscreen mode Exit fullscreen mode

After manually editing files, REAPER must be restarted for changes to take effect!

To verify the script and reaper-kb.ini are correct, run REAPER, and from the Actions dialog (Shift + ?), find the MyCompany: test simple OSC trigger action and run it. You should see a message box with the text "This is a simple script that can be invoked via OSC".

Message Box that was triggered from the script

If not done already, from REAPER Preferences (Command + ,), navigate to the Control/OSC/web section and add a new OSC (Open Sound Control) control surface. Leave the Default pattern config, select Local port mode, and note the Local listen port. Other settings can remain at their default values for now, as shown in the screenshot.

REAPER OSC Config

Now you can send an OSC message to trigger the custom action. For testing purposes, you can use sendosc (which can be installed on MacOS using brew install yoggy/tap/sendosc).

From the terminal, run: sendosc localhost 8000 /action/_MY_COMPANY_TEST_SIMPLE_OSC

If everything worked correctly, you should see the same message box with the text "This is a simple script that can be invoked via OSC".

Troubleshooting OSC Communication

To ensure OSC messages are reaching REAPER, navigate to Preferences (Command + ,), then the Control/OSC/web section. Select the existing OSC control from the list and press Edit.

From the Control Surface Settings window, press Listen..., and observe incoming OSC messages. Each line represents a single received OSC message, for example: /test_message_with_multi_arg [sf] "test string" 123.449997, where:

  • /test_message_with_multi_arg is the message address;
  • [sf] describes argument types, s - string, f - float number, i - integer number. In this example, two arguments were passed: the first of type string, the second a float;
  • followed by argument values "test string" 123.449997.

Incoming OSC Messages

Passing Arguments to Custom Actions

The above method works well for many use cases, but it has one downside: it's impossible to pass any arguments when calling actions. If the variance of possible argument values is low, one can create multiple actions. For example, to enable or disable a certain option: /action/_ENABLE_OPTION, /action/_DISABLE_OPTION. This is fine, but such an approach is not suitable for passing numeric or string arguments to an action.

Is there a way to pass an argument to an action? Yes, as REAPER supports binding OSC messages to trigger actions. When an action is invoked this way, a single (first) OSC message argument of string or float type can be retrieved from the script.

To start, make sure binding OSC messages to actions is allowed: In REAPER, navigate to Preferences (Command + ,), then the Control/OSC/web section. Select the existing OSC control from the list and press Edit.
Check the Allow binding messages to REAPER actions and FX learn option and press Ok.

Create osc.lua in ~/Library/Application\ Support/REAPER/Scripts/MyCompany/ with the following content:

-- osc.lua

-- Utility functions to get and parse OSC message and argument from REAPER action context

local osc = {}

function osc.parse(context)
    local msg = {}

    -- Extract the message
    msg.address = context:match("^osc:/([^:[]+)")

    if msg.address == nil then
        return nil
    end

    -- Extract float or string value
    local value_type, value = context:match(":([fs])=([^%]]+)")

    if value_type == "f" then
        msg.arg = tonumber(value)
    elseif value_type == "s" then
        msg.arg = value
    end

    return msg
end

function osc.get()
    local is_new, name, sec, cmd, rel, res, val, ctx = reaper.get_action_context()
    if ctx == nil or ctx == '' then
        return nil
    end

    return osc.parse(ctx)
end

return osc
Enter fullscreen mode Exit fullscreen mode

Next to it, create test_osc_with_arg.lua:

-- Retrieve the directory of the current script.
local script_path = debug.getinfo(1, "S").source:match("@?(.*/)")
-- Set the package path to include the other scripts in the directory
package.path = package.path .. ';' .. script_path .. '?.lua'
-- Require the osc module
local osc = require('osc')

local msg = osc.get()
if msg then
    reaper.ShowMessageBox("OSC address: " .. msg.address .. ", argument: " .. (msg.arg and msg.arg or "(nil)"), "Info", 0)
else
    reaper.ShowMessageBox("Invalid or no OSC message", "Error", 0)
end
Enter fullscreen mode Exit fullscreen mode

Add the following line to ~/Library/Application\ Support/REAPER/reaper-kb.ini:

SCR 4 0 MY_COMPANY_TEST_OSC_WITH_ARG "MyCompany: test OSC trigger with argument" "MyCompany/test_osc_with_arg.lua"
Enter fullscreen mode Exit fullscreen mode

Finally, the newly created action can be mapped to OSC messages. This can be done in two ways:

  • By editing the ~/Library/Application\ Support/REAPER/reaper-osc-actions.ini file;
  • Or by creating a shortcut from the Actions dialog by listening for an incoming OSC message.

I prefer the former method. Add the following line to ~/Library/Application\ Support/REAPER/reaper-osc-actions.ini:

"/test_message_with_arg" 0 0 _MY_COMPANY_TEST_OSC_WITH_ARG
Enter fullscreen mode Exit fullscreen mode

This instructs REAPER to invoke the _MY_COMPANY_TEST_OSC_WITH_ARG action when a "/test_message_with_arg" OSC message is received.

Restart REAPER for changes to take effect.

To test that everything works, from the Terminal run: sendosc localhost 8000 /test_message_with_arg s "test string". This should invoke the test_osc_with_arg.lua script in REAPER and display a message box with the text "OSC address: test_message_with_arg, argument: test string".

Running sendosc localhost 8000 /test_message_with_arg f 123.456 will display the message "OSC address: test_message_with_arg, argument: 123.456001".

Running sendosc localhost 8000 /test_message_with_arg will display the message "OSC address: test_message_with_arg, argument: (nil)".

Limitations

REAPER has the following limitations when it comes to working with OSC messages:

  • Scripts can only receive the first argument of the OSC message. As a workaround, a string argument can be used to encapsulate any number of arguments.
  • Only string and float argument types are supported. Again, a string can be used to hold any required datatype that can be represented as a string.
  • Sending OSC messages from scripts is not supported. As a workaround, sendosc or a similar utility can be invoked using os.execute(), or third-party REAPER extensions can be used to accomplish this task.

Conclusion

REAPER is a powerful DAW offering scripting and extensible external control capabilities, which makes it suitable for use as part of various automation applications, including DAW plugin test automation frameworks. Unfortunately, its minimalistic documentation makes feature discovery problematic and requires additional effort to make things work. Some basic functionality is still missing and requires workarounds.

Related Materials

Top comments (0)