Extensible Control of Reaper via OSC and Scripts
Tags: reaper, daw, osc, lua
Published: 2024-07-07 21:33:33

## Introduction

[REAPER](https://reaper.fm) 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)](https://opensoundcontrol.stanford.edu/) 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](https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html#Reaper-kb.ini).

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"
```

This defines a custom action. For the meaning of `SCR 4 0`, see the [reaper-kb.ini](https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html#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`:

```lua
reaper.ShowMessageBox("This is a simple script that can be invoked via OSC", "Info", 0)
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1l9oo1w03mvswtiabsso.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ojt81tukr0s6eos97ccg.png)

Now you can send an OSC message to trigger the custom action. For testing purposes, you can use [sendosc](https://github.com/yoggy/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c6ch3h53uquspcc2n8uq.png)

## 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:

```lua
-- 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
```

Next to it, create `test_osc_with_arg.lua`:

```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
```

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"
```

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
```

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

- [REAPER file type description](https://mespotin.uber.space/Ultraschall/Reaper-Filetype-Descriptions.html)
- [REAPER API functions](https://www.reaper.fm/sdk/reascript/reascripthelp.html)
- [REAPER ReaScript](https://www.reaper.fm/sdk/reascript/reascript.php)
- [REAPER OSC](https://www.reaper.fm/sdk/osc/osc.php)