realearn icon indicating copy to clipboard operation
realearn copied to clipboard

Make it possible to import data via ReaScript in a fine-granular way

Open jamesd256 opened this issue 2 years ago • 9 comments

When passing session data to Realearn via the Reascript API, the controller preset selected option reverts from what it was set to to .

This means while external control is working well for controlling mappings, this rules out using it when the companion app features are required. After the data is sent, mappings in the main compartment are updated as expected, but in the companion app, it now prompts the user to select a controller preset.

It would help if the controller preset could be left untouched when setting the value of set-state.

Unless there's a way to specify the controller preset in the session data? That would also do the trick.

Sample Reascript LUA code:

`local state = [[{"version":"2.13.1","name":"reaset-state","controlDeviceId": "12","feedbackDeviceId": "12","activeControllerPresetId":"1"," ":{},"mappings":[{"id":"JEDZzyfvhppQrCS2xW91V","name":"Test","source":{"type":1,"channel":0,"number":0,"isRegistered":false,"is14Bit":false,"oscArgIndex":0},"mode":{"maxStepSize":0.05,"minStepFactor":1,"maxStepFactor":5},"target":{"type":0,"commandName":"40041","invocationType":0,"fxAnchor":"id","useProject":true,"moveView":true,"seekPlay":true,"oscArgIndex":0}}]}]]

reaper.TrackFX_SetNamedConfigParm(reaper.GetSelectedTrack(0 , 0),0,"set-state",state)``

jamesd256 avatar Sep 01 '22 16:09 jamesd256

"set-data" sets the complete plug-in state (also called session state). This includes everything by definition, also the device settings etc. So yes, you can also set the controller compartment with that.

I never encountered anyone so far who uses "set-data". I use it only for integration testing purposes. What's your use case?

If I would make an official ReaScript API for ReaLearn (not something as raw as "set-data"), I would not make it JSON based. Instead I would make it accept the same thing that you can pass when doing a Lua export. The JSON API is mainly for internal persistence and is super ugly in terms of naming, so you might want to stay away from it.

helgoboss avatar Sep 01 '22 16:09 helgoboss

Fair enough. I'm happy to hack around with set-data until the API comes along, if it is in your plans. I made progress now I worked out the camel conversion in the code, and was able to set the activeControllerID value. Of course this only sets the select value but doesn't load the preset's mappings or layout, so I am also passing in those now, and now the companion projection is keeping it's view, so great.

It's nice to be able to start doing some fun scripting stuff controlling the mappings dynamically from script code. I can think of a few use cases for sound installations I've worked on for instance. How about playing a complex melody on one key for kids? I might also try do something in the area of auto mapping VST params onto a standard banking scheme. I'm sure the built in functionality can do nearly everything for most people - I'm having great success making my X-Touch Mini into a powerhouse device with built in options.

What I was trying to do here in fact was hijack the main mapping label in the companion so I could cheat and populate it with track name while I wait for companion to get LCD support. Helps a lot while banking through 8 tracks at a time. Now I have a case working, I will probably try some other stuff. I'm not saying it's normal to work this way but I enjoy the hacking.

jamesd256 avatar Sep 01 '22 18:09 jamesd256

I guess there's no equivalent method to retrieve the session data from Realearn?

When trying this the boolean comes back false:

ok, current_state = reaper.TrackFX_GetNamedConfigParm(reaper.GetSelectedTrack(0 , 0),0,"set-state")

jamesd256 avatar Sep 01 '22 21:09 jamesd256

Probably not. Yea, maybe would have been better if I call this just "state" and then implement both set and get.

helgoboss avatar Sep 06 '22 06:09 helgoboss

Proposal

Basics

When talking about import and export of Helgobox (ReaLearn) configuration, it's important to know that Helgobox knows two schemas for serializing/deserializing its data:

The old schema

  • This is a very messy data structure which carries historical burden
  • It's optimized for backward compatibility with even the oldest ReaLearn versions. I go to great efforts to support this schema in current versions, with the goal to still be able to load projects that have been saved with ReaLearn v1.
  • It's complete (it covers all parts of Helgobox).
  • This is what Helgobox currently uses to save the plug-in state (as VST chunk). However, in future, Helgobox might use the new schema for that purpose!
  • In addition, this schema is used when exporting/importing as JSON. At the moment, whenever you use the JSON import/export, you know you are dealing with the old schema. An "Export ... as JSON with new schema" function is not available currently.

The new schema

  • This is a much more polished data structure
  • It also has a focus on backward compatibility, but only back until 2.11.0
  • It's currently incomplete (it covers compartments and Playtime, but it still misses state related to the instance and e.g. input/output).
  • This is what Helgobox currently uses when exporting/importing as Lua.

1. Add a config parameter helgobox.state.json

This works similarly to the named config parameter vst_chunk, which REAPER supports for all VST plug-ins, but it has the following advantages:

  • The content is not base64-encoded. It will always be plain JSON.
  • It's plugin-standard-agnostic. So if Helgobox will change to CLAP one day, this will still work.

Get

  • Returns the complete state of the Helgobox plug-in instance as JSON.
  • Currently, the structure of the returned JSON conforms to the old schema.
  • Important notice: The structure of this JSON may change as Helgobox evolves! At some point, I probably want to save the state of Helgobox using the new schema, in which case this getter will return the new schema as well!
  • As a consequence, parsing that JSON and making decisions based on its content is possible, but it might break in future Helgobox versions!

Set

  • Expects the complete instance state as JSON and applies it to that instance (= works exactly like set-state).
  • Currently, the structure of the passed JSON must adhere to the old schema.
  • Unlike "get", the old schema will probably always be supported for "set". Simply because ReaLearn must ensure that old projects are loadable anyway, so this doesn't require any additional effort on my side.
  • In future, this might accept even the new schema (in addition to the old one).

2. Add a config parameter helgobox.unit.X.compartment.Y.state.Z

  • X is the unit index (starting at 0)
  • Y is either controller or main
  • Z is either json or lua

Get

  • Returns the compartment state in the given format (JSON or Lua) using the new schema.
  • Important notice: The structure of the returned data may change as Helgobox evolves!
  • As a consequence, parsing that Lua or JSON and making decisions based on its content is possible, but it might break in future Helgobox versions!

Set

  • Expects the compartment state in the given format using the new schema and applies it to that compartment.

helgoboss avatar May 15 '24 06:05 helgoboss

Hello,

Thank you for getting to this so quickly!

For helgobox.unit.X.compartment.Y.state.Z: once you’re past your experimental phase with this endpoint and it feel like it’s worth releasing, I wouldn’t be too worried about breaking compatibility from time to time - provided it doesn’t happen every week. If anything, people in reaper environment generally welcome the change.

ReaImGui just went through a change of API, and everything went well: we knew about it in advance, there was a pre-release on github, cfillion provided some shims for projects to keep functioning while transitioning the code - and nobody complained…

Typing the retval of helgobox.unit.X.compartment.Y.state.Z:

Obviously I’m partial to passing and retrieving Lua tables. Sorry to be kicking that dead horse, but: If this is going to be returning Lua tables, then coders will need a type annotations file to navigate them. I understand those types already exist in Luau, but coders could use something similar for lua reascripts.

Again, my offer still stands: if you can have a script to generate a types annotations file, you could have an automation outputting the new version of the type-defs in your release, I could pick it up from there.

Option 1: output the type-defs in the release, include it in the reapack download, and let reascript extension find it and link it to user’s workspaces. That’s probably the lowest-effort approach.

Option 2 : include it in the extension itself, and add an extension-setting saying «which realearn version are you using, here’s the right types for your workspace».

I did this for ReaImGui, and it’s helped smoothe the transition to its new api.

If you’d rather be in control of the whole process, I’d be open to adding you as contributor to the repo - that way you can create PRs with your own changes, and you get to keep your freedom. It’s more work for you, though.

If you’re worried about the generator functions that your Luau workspaces provide: I’m open to including generator functions or snippets as part of the extension as well. It doesn’t have the «Tada!» effect of an auto-generated workspace, but there’s the big upside of having the reascript extension as a one-stop-shop for coders - it provides type-check, documentation, formatting, a debugger, and Sexan’s working on including a lua profiler and flame graph to measure performance.

I’m looking forward to this, thanks for putting together this proposal!

AntoineBalaine avatar May 15 '24 15:05 AntoineBalaine

@AntoineBalaine

Getters/setters deal with strings, not tables

Not sure if you are aware of this. If not, it's important to know: The getter will not return a Lua table directly but a string, which is either formatted as JSON or (hopefully) valid Lua/Luau code (the generated code is compatible with both). It's the task of the ReaScript to then parse this into actual Lua tables. I have not tried it but I think load() would do the job for evaluating a snippet of Lua? Can you confirm that this works in ReaScripts?

Likewise, the setter will expect a Luau/JSON string, not a real Lua table, so it's up to the ReaScript to come up with that string. If it's Luau, this must be valid Luau. There are some subtle differences and my Luau runtime in Helgobox is intentionally much more restricted than the Lua runtime in REAPER. But probably you would just build the final table in ReaScript (Lua) and then serialize this table using a simple table serialization function. So the resulting code would be just a plain table, very comparable to JSON, not containing any logic. When used like this, there are AFAIK no risks having a mismatch between Lua and Luau.

Concerning the types

In general, I'm open to generate LuaLS type definitions (though I don't have time for it right now).

When I evaluated LuaLS some months ago (alongside Teal and Luau), I decided against it for a few reasons. One of the main reasons was that it didn't seem to support tagged unions. And this classified it as instant showstopper for Helgobox. Because ReaLearn and Playtime make extremely heavy use of tagged unions (very different from the REAPER API which is mostly just a list of flat functions with a few primitive parameters).

Have a look at this:

-- A "bla" target has prop1 and prop2
local target = {
    kind = "bla",
    prop1 = 1,
    prop2 = "blablabla",
}

-- A "foo" target has prop3 and prop4
local target = {
    kind = "foo",
    prop3 = 5,
    prop4 = "foofoo",
}

In above example, how do I tell the type system that the kind property acts as discriminator between those 2 target types? I didn't find any way to express this with LuaLS type definitions.

I'm not sure ... maybe I overlooked something. Do you know a solution? If you do, then I might add LuaLS to my type generation "engine".

Anyway, Luau provided this and a few other goodies as well. And since in Helgobox I have the freedom to choose whichever engine I like, I decided for Luau.

Addition

There's maybe a workaround for the missing tagged unions: Provide helper functions directly within the ReaLearn Luau engine and write type definitions for those functions. Then the functions themselves would act as discriminator for LuaLS. This could be a way to go.

To be honest, the main reason why I'm thinking about putting some effort into this at some point is Playtime. Not sure whether it would be worth the effort for just ReaLearn alone. Do you have a number how many people are potentially interested in programming ReaLearn from ReaScript?

helgoboss avatar May 15 '24 16:05 helgoboss

Getters and Setters serialization:

I’m not too worried about that. I’m sure it’ll be easy - there’s plenty of libraries for that. Users have to serialize to store to ext states already, so I reckon it’s pretty common.

Who would be interested

I’m totally in the blind. I’m checking on the reascript discord, since that’s where I’m in touch with people. I’ve asked anyone interested to drop a line over here. I can ask about it in the forum, too - though if nobody manifests, I’d say it’s not worth implementing the proposal.

Plus, there’s already a lot that can be done with the set-state that’s already in there. I’m glad you told me about this.

About the tagged union types

LuaLS’ type system can’t discriminate. It just jumbles up all the properties together. If I’m to declare some types like this:

---@class Target1
---@field kind "bla"
---@field prop1 number
---@field prop2 string

---@class Target2
---@field kind "foo"
---@field prop3 number
---@field prop4 string

---@class Target1
local target1 = {
    kind = "bla",
    prop1 = 1,
    prop2 = "blablabla",
}

---@class Target2
local target2 = {
    kind = "foo",
    prop3 = 5,
    prop4 = "foofoo",
}

---@return Target1 | Target2
local function getTarget()
    return target1
end

then getTarget()’s retval comes checked as

    target: Target1|Target2 {
	    kind: string,
	    prop1: number,
	    prop2: string,
	    prop3: number,
	    prop4: string,
	}

If the coder wants to discriminate between the two types, then he’d have to implement his own sort of type-predicate function:

---@return Target1 | Target2
local function getTarget()
    return target1
end

---@param target Target1 | Target2
---@return "Target1"|"Target2"
local function targetType(target)
    if target.kind == "bla" then
        return "Target1"
    else
        return "Target2"
    end
end

local function useTarget()
    local target = getTarget()
    if targetType(target) == "Target1" then
        doTarget1Stuff(target --[[@as Target1]])
    else
        doTarget2Stuff(target --[[@as Target2]])
    end
end

That’s obviously a band-aid, though.

in conclusion?

My gut feeling tells me this might not be worth it (unless a bunch more people ask for it) and that’s perfectly fine. As it is, I’m very happy with passing JSON data. Please don’t take that entry point away, though!

If you have the old JSON schema laying around, I’d be a glad taker for it.

Some projects of mine that are already within reach with set-data:

  • a Console1 (from softube) integration for reaper. I can have logic that dynamically swaps the plugins for each module of the console, and I can implement other use-cases for the controller: track controls, item editing à la Tycho, etc.
  • an FX chain swapper on top of the MFT - same thing, basically, but more sound-design oriented
  • using synths for controlling synth plugins - one desktop synth could be used for multiple plugins (like in omnisphere 2.6), and swapping the plugins from the synth. This obviously sounds like the planned use-case for pot-browser, but then again - what’s nice about this is that I can start coding through ideas today, rather than have to nag you about implementing x, y and z features until it happens.

AntoineBalaine avatar May 15 '24 18:05 AntoineBalaine

Well, the actual proposal is fairly easy to implement. Just the LuaLS type definitions would be some effort (to make them work nicely).

Let's see.

helgoboss avatar May 16 '24 07:05 helgoboss