helgoboss / helgobox

Helgobox: ReaLearn & Playtime
https://www.helgoboss.org/projects/helgobox
GNU General Public License v3.0
204 stars 20 forks source link

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

Open jamesd256 opened 2 years ago

jamesd256 commented 2 years ago

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

helgoboss commented 2 years ago

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

jamesd256 commented 2 years ago

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 commented 2 years ago

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

helgoboss commented 2 years ago

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

helgoboss commented 4 months ago

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

The new schema

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:

Get

Set

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

Get

Set

AntoineBalaine commented 4 months ago

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!

helgoboss commented 4 months ago

@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?

AntoineBalaine commented 4 months ago

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:

helgoboss commented 4 months ago

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.