Lua arpeggiator plugin anyone?

Hi everybody,

I’ve recently started dabbling with editor actions, so I have some familiarity with Lua scripting in Ardour. Works great, the Lua integration is really nice, kudos!

Now I’d like to cut my teeth on some real-time stuff and write a Lua arpeggiator plugin, porting existing Lua code I’ve written for Pd using pd-lua.

Has anyone tried this before? I couldn’t find an example in the example scripts, the one that comes closest probably is _midi_lfo.lua, but this doesn’t deal with meter and BBT information and outputting MIDI data in sync with the current BBT.

Now I know how to get that musical time information using Temporal.timepos_t(Session:transport_sample()), Temporal.TempoMap:bbt_at(), and Temporal.TempoMap:meter_at().

But the problem I see there is that I’d actually have to extract that information at each sample in the processed block to figure out exactly when the next beat is due (i.e., when the arpeggiator needs to output the next bunch of MIDI notes), using some kind of loop a la for ts = 1, n_samples do ... end, as is done in the _midi_lfo.lua example.

That might seem a little inefficient, since I need to execute some code for each and every sample. Is there a better way? It would be great to have a little helper function that would return the exact offset in the current block where a beat-bar change occurs, or some subdivision of that (e.g., to get eighth pulses in a 4/4 meter, etc.).

Also, is there a way to make print work in “dsp” type Lua scripts? That would be handy for debugging purposes. Currently those prints seem to get lost, but maybe they could just be printed in the Lua scripting console if the window is open?

3 Likes

You provide motivation to finally expose time information to Lua DSP scripts. I have been holding that off until the new Tempo-Map (v7.x) was in-place, and now seems a good time.

The information to be passed was inspired by LV2 and VST3. Since Ardour 7.4-287-g34789ff22f, the following information is available as Lua Table (values are examples):

time["sampleTime"] = 0      -- time in samples at the start of the current process-cycle
time["sampleTimeEnd"] = 16  -- time in samples at the end of the current cycle
time["musicTime"] = 0.0     -- quarter note at the start of the current cycle
time["musicTimeEnd"] = 0.00052083333333333 -- quarter note at the end 
time["tempo"] = 120.0            -- tempo in quarter notes per minute
time["tempoEnd"] = 120.0         -- ditto at the end of the current cycle
time["barPositionMusic"] = 0     -- last bar start position in quarter notes
time["timeSigNumerator"] = 4     -- time signature (cycle start)
time["timeSigDenominator"] = 4   -- time signature (cycle start)
time["TCframesPerSecond"] = 30.0 -- timecode FPS
time["TCdropFrames"] = false     -- timecode uses drop-frame counting
time["looping"] = false          -- currently looping

When looping, the following additional information is available

time["looping"] = true
time["loopStart"] = 192000
time["loopEnd"] = 288000
time["loopStartMusic"] = 8.0
time["loopEndMusic"] = 12.0

To enable this information, the following option has to be set in the DSP script

function dsp_options ()
  return { time_info = true }
end

For an example, please see ardour/share/scripts/_time_info.lua at master · Ardour/ardour · GitHub

Please let me know if that works for you and if you have further suggestions before we finalize this API with the 7.5 release.

Not quite. The transport_sample is the audible sample at the speakers. Plugins have a local latency-compensated time.

Say the transport is at 0, and there is plaback latency of 10. Then the plugin starts processing at 10, so that by the time the global transport reaches 10, the produced sample is already audible.

It works, and by is shown in Window > Log. If you run a debug-build the output is also printed to stdout.

2 Likes

You provide motivation to finally expose time information to Lua DSP scripts.

Nice! That’s exactly what I need, thanks a lot.

Plugins have a local latency-compensated time.

Ok, I see. That’s probably why my BBT changes detected with that method were always a bit off. :slight_smile:

It works, and by is shown in Window > Log.

Ah yes, that’s good to know.

Please let me know if that works for you and if you have further suggestions before we finalize this API with the 7.5 release.

Looks good to me. But would it be possible to have actual BBT and the length of the current cycle in ticks as well? Having that kind of information readily available would make things a little easier for my purposes, although that information can be computed relatively easily from the data that’s already there, I guess.

But would it be possible to have actual BBT and the length of the current cycle in ticks as well?

Scratch that. Ticks won’t actually be all that useful in this context. If you want to do sample-accurate playback then something like a sample count since the last quarter beat might be more useful. In any case, it should be possible to get all these numbers from what’s already there.

From an API POV the sample-position of the last beat would be more consistent. Then you can get sample count since the last quarter note by subtraction :
time["sampleTime"] - time["beatPosition"]

just git pushed as

1 Like

Agreed.

BTW, what’s up with all the foo["bar"] indexing in the sample Lua code, is that a stylistic thing, or personal preference? I’m sure you know that in a Lua table, you can always just write the equivalent foo.bar instead (of course, this only works if the symbol is a proper Lua identifier). I also find it more convenient and readable to write something like

midiout[cnt] = { time = time, data = data }

which is equivalent to

midiout[cnt] = {}
midiout[cnt]["time"] = time;
midiout[cnt]["data"] = data;

I’d even say that the former is more idiomatic Lua, but of course everyone has their own coding style. I’m just curious. :wink:

One more thing, though: The sampleTime from the time_info table is local latency-compensated time, right? So I can just pass that to Temporal.TempoMap:bbt_at() to get accurate and up-to-date BBT information?

The reason I still need the BBT info from the tempo map is that I need to be able to properly deal with more esoteric time signatures such as 11/8 or 13/16 that don’t evenly divide into quarters. Now one could derive that data from what time_info provides, but that would be cumbersome – Ardour already knows how to compute those values, so I’d rather use those. I can still put time_info to good use to detect meter changes and base pulses, so that I only need to invoke the BBT calculation when a new beat is due.

It’s based on code copied from C/C++. Also, when I add Lua bindings and write a quick example script I still have a C mindset.

Yes it is.

I think I also was too quick last night, exposing “sample-position of the last beat”. The value is not useful if there are tempo-ramps, or if the tempo-changed since the last beat.

I’m also not a big fan of using camelCase here (in C++ we only use it for classes and enums), so far I’ve just based it off VST 3 Interfaces: ProcessContext Struct Reference (and steinberg apparently likes camels).

I am thinking of changing it to underscores. e.g. “sample_time”, which is more consistent. Even more Lua-like would be nested tables: time.sample.start, time.sig.numerator etc. except there is the problem that end is reserved keyword we we cannot use time.sample.end, but time.sample_time_end work.

Any thoughts?

I think I also was too quick last night, exposing “sample-position of the last beat”. The value is not useful if there are tempo-ramps, or if the tempo-changed since the last beat.

I agree that it’s better to remove it again. It’s of limited use anyway, and this kind of information can be determined through the tempo map if needed.

And what about musicTimeEnd? That one is actually much more useful, since it lets you detect imminent beat increments, in order to schedule MIDI notes at the right sample time. For that to work, it should always equal the next cycle’s musicTime. Is that always true, regardless of any tempo changes?

I’m also not a big fan of using camelCase here

I’m firmly with you on that one. :slight_smile: Lowercase + underscores FTW!

1 Like

Yes, that should be the case.

Also note the end is inclusive. sampleTimeEnd = next cycle’s sampleTime, same for musicTime and musicTimeEnd.

It is reminiscent of the fence-post problem. e.g. play up to beat 2, and in the next cycle: start playing from beat 2.

Great. It should be an easy search/replace in your script. I’m sorry for the inconvenience.

I’m sorry for the inconvenience.

Better change it now than never.

Done.

I’m tempted to remove time["bar"], which is somewhat ill defined (and assumes 4/4). Are you using that?

I’m tempted to remove time["bar"], which is somewhat ill defined (and assumes 4/4). Are you using that?

Not really. I’m using beat and ts_denominator to detect beats, so that I don’t have to access the tempo map in each cycle, and then sample to get proper BBT information from the tempo map. I think that for sample-accurate triggering I’ll also need beat_end and sample_end, and maybe tempo and tempo_end, but that’s about it.

This is looking good. I have a basic arpeggiator working now, which already does the job fairly well. I still need to figure out sample-accurate triggering, though, where things are likely to get complicated. I’ll look into that tomorrow.

1 Like

Ok, there we go: simple_arp.lua - Google Drive

This does the job reasonably well for me, also works great for playing drums. :wink:

The sample-accurate triggering wasn’t trivial, but not rocket science either. So the time_info API, as it stands, seems to work reasonably well for this kind of thing.

Please let me know if you spot any bugs or other mishaps. I wasn’t sure how to categorize this kind of MIDI effect, so an effect it is for now, which also makes it easy to find.

I’ll eventually upload this example, along with some other Ardour Lua scripts I’ve done for the students, to GitHub. But for the time being feel free to add this to the Ardour source, as you see fit.

1 Like

It works nicely in a quick test. Amazing!

At first I was confused that nothing played.

Perhaps, when the transport is stopped MIDI messages can be passed though as-is?
make uncomment to pass through input notes transport dependent (Session:transport_rolling()).

Also transport start/stop should perhaps send all-note-off messages.

PS. It is super fun to play around with this!

1 Like

Perhaps, when the transport is stopped MIDI messages can be passed though as-is?
Also transport start/stop should perhaps send all-note-off messages.

Good ideas, will do.

Yeah, it’s a little thing, but I also found it to be stupidly fun, especially when I hook it up to the avldrums. :slight_smile: Hope the students will like it, too. (I’m doing a DAW course this semester, mostly about Ardour. It’s a lot of fun, especially after I rediscovered its Lua interface. You’ve done a great job with this, it’s come a long way since I last dabbled around with it.)

I’d still like to do an alternative version of the plugin with better auto-generated velocities, using Barlow indispensabilities. I already have that as Lua code, so it will be easy to port over.

1 Like

P.S.: Do you know of any other DAW which offers such a scripting interface? I mean real scripting, not just controller interface stuff.

Reaper has a fairly extensive scripting system, which includes being able to create GUIs (ours doesn’t allow this).

Hi Paul, you’re right, that’s ReaScript, I should have known this. Thanks for reminding me.