The design goal of the plugin bundle was to create a tool to easily add realtime programmable logic glue in LV2 plugin graphs.
To have plugins which do a specific task efficiently is great, especially for audio plugins. LV2 stands apart from other audio plugin specifications with its extentable event system based on Atoms. As events can be much more varied in nature and represent pretty much anything (NOT ONLY MIDI), it would be useful to have a tool to create arbitrary event filters for a given setup on-the-fly.
For a given setup, one may need a special event filter only once and it seems to be overkill to write a native LV2 event filter in C/C++ just for that. It would also be nice to have a tool for fast prototyping of new event filters.
A scripting language seems to be ideal for these cases, where the user can write an event filter on a higher level of abstraction on-the-fly. The scripting language needs to be realtime safe, though, which restricts the choices dramatically.
One such scripting language is Lua. It is small, fast, easily embeddable and realtime-safe if coupled to a realtime-safe memory allocator like TLSF.
The Moony plugins can handle LV2 control and atom event ports, only. They do not handle LV2 audio ports. Control port values are internally handled as simple floating point numbers, whereas the atom event ports build on top of the LV2 atom and atom forge C headers.
The Moony plugin bundle ships with multiple plugin variants, whereas all of them share the same API but with different configurations of input and output ports. Depending on the users's usage scenario, one or the other variant may be preferable.
Plugin variants only differ in number of control and atom event input and output ports. Apart from this difference, they can often be used interchangeably.
This plugin variant features a single control port input and a single control port output.
-- C1 x C1 prototype 'run' function
function run(n, control, notify, c)
return c
end
This plugin variant features two control port inputs and two control port outputs.
-- C2 x C2 prototype 'run' function
function run(n, control, notify, c1, c2)
return c1, c2
end
This plugin variant features four control port inputs and four control port outputs.
-- C4 x C4 prototype 'run' function
function run(n, control, notify, c1, c2, c3, c4)
return c1, c2, c3, c4
end
This plugin variant features a single atom port input and a single atom port output.
-- A1 x A1 prototype 'run' function
function run(n, control, notify, seq, forge)
end
This plugin variant features two atom port inputs and two atom port outputs.
-- A2 x A2 prototype 'run' function
function run(n, control, notify, seq1, forge1, seq2, forge2)
end
This plugin variant features four atom port inputs and four atom port outputs.
-- A4 x A4 prototype 'run' function
function run(n, control, notify, seq1, forge1, seq2, forge2, seq3, forge3, seq4, forge4)
end
This plugin variant features a single control port input and a single control port output with a single atom port input and a single atom port output.
-- C1+A1 x C1+A1 prototype 'run' function
function run(n, control, notify, seq, forge, c)
return c
end
This plugin variant features two control port inputs and two control port outputs with a single atom port input and a single atom port output.
-- C2+A1 x C2+A1 prototype 'run' function
function run(n, control, notify, seq, forge, c1, c2)
return c1, c2
end
This plugin variant features four control port inputs and four control port outputs with a single atom port input and a single atom port output.
-- C4+A1 x C4+A1 prototype 'run' function
function run(n, control, notify, seq, forge, c1, c2, c3, c4)
return c1, c2, c3, c4
end
Moony runs its Lua interpreter in sandboxed mode, e.g. because of its real-time requirements, Moony cannot provide any access to blocking functions like file I/O and pipes. Only the following standard Lua libraries are enabled:
The following standard Lua libraries are disabled:
The following base functions are disabled:
The following math functions are disabled (use the dedicated random utility library instead):
Additionally to the standard libraries, Moony ships the following commonly used utility libraries:
Whenever you want to log or debug something while developing your scripts, you can easily dump any value via Lua's print function. The print's output will be shown on the UI and also be sent to the host's log backend, e.g. to a log window or console.
-- sends 'hello world' to the UI and the host's log backend
print('hello world')
Moony can run user defined callbacks at different positions in its processing graph. If the user provides an implementation for a specific callback, Moony will run the latter. If the plugin does not need a specific callback, the latter can simply be omitted. The implementations of all callbacks are thus optional.
The run callback function is the main callback function and thus the most important one. It is called once per period by the plugin host. The period size, e.g. the number of audio samples the function is called for may be chosen arbitrary by the host. Do not expect it to be constant.
Depending on the plugin variant, the run function takes variable amount of arguments. All variants have the first and last two arguments in common, though.
If a plugin has input event ports, sequence objects (Lua userdata) will be given to the run function whose events can be iterated over.
If a plugin has output event ports, forge objects (Lua userdata) will be given to the run function which can be filled with arbitrary atom event structures.
If a plugin has input control ports, those values will be given to the run function as native Lua numbers.
If a plugin has output control ports, the run function is expected to return native Lua numbers, missing return values will be set to 0.0.
-- 'run' callback prototype for moony#a1xa1
function run(n, control, notify, seq, forge)
-- here we will process events
end
The once function exists for pure convenience only. It has exactly the same form as the run function, but is called only once after the whole script code is loaded or updated.
The once function will run directly before the run function, it therefore is no replacement for the latter in a given update period, but and extension to it.
Put logic here that needs to run once after script code update, e.g. registering a new StateResponder.
-- 'once' callback prototype for moony#a1xa1
function once(n, control, notify, seq, forge)
-- here we will run logic once right after a code update
-- send all-notes-off MIDI message on all channels
for channel = 0x0, 0xf do
forge:time(0):midi(MIDI.Controller | channel, MIDI.AllNotesOff)
end
end
When script code is reloaded, plugin state inside Lua virtual machine potentially is lost. In order to preserve state across a script code reload, arbitrary state can be serialized and parked in temporary memory. Such a temporary serialized state can later be deserialized via apply.
The stash function is directly called before switching to the new script code.
-- 'stash' callback prototype
-- a simple flag
local myflag = true
function stash(forge)
-- serialize single boolean flag
forge:bool(myflag)
end
function apply(atom)
assert(atom.type == Atom.Bool)
-- deserialize single boolean flag
myflag = atom.body
assert(myflag == true)
end
When script code is reloaded, plugin state inside Lua virtual machine potentially is lost. In order to preserve state across a script code reload, arbitrary state previously serialized by stash can be deserialized from temporary memory.
The apply function is directly called after switching to the new script code.
-- 'apply' callback prototype
-- table that holds active notes
local active_notes = {
60, 64, 67, 72
}
function stash(forge)
local tup = forge:tuple()
-- serialize active notes to atom tuple
for i, v in ipairs(active_notes) do
tup:int(v)
end
tup:pop()
end
function apply(atom)
assert(atom.type == Atom.Tuple)
-- clear active notes table
active_notes = {}
-- deserialize active notes from atom tuple
for i, v in atom:foreach() do
assert(v.type == Atom.Int)
active_notes[i] = v.body
end
assert(#active_notes == 4)
end
The save function works analogously to stash, but instead of serializing to temporary memory before a script code reload, save serializes to disk upon a state save request issued by the host.
State serialized to disk by save is preserved across plugin instantiations and truly is the same as a preset.
-- 'save' callback prototype
local urn = Mapper('urn:uuid:6f7e2445-ae5f-4e2e-ae11-9a5cfb1b5d1e#')
-- state
local state = {
[urn.foo] = {
[RDFS.range] = Atom.Int,
[RDF.value] = 12
},
[urn.bar] = {
[RDFS.range] = Atom.Chunk,
[RDF.value] = string.char(0x0, 0x1, 0x2)
}
}
function save(forge)
local obj = forge:object()
-- serialize state to atom object
for k, v in pairs(state) do
obj:key(k):typed(v[RDFS.range], v[RDF.value])
end
obj:pop()
end
function restore(atom)
assert(atom.type == Atom.Object)
-- deserialize state from atom object
for k, v in pairs(state) do
local itm = atom[k]
if itm then
assert(itm.type == v[RDFS.range])
assert(itm.body == v[RDF.value])
v[RDF.value] = itm.body
end
end
end
The restore function works analogously to apply, but instead of deserializing from temporary memory after a script code reload, restore deserializes from disk upon a state reload request issued by the host.
If defined, the function is called upon each code reload with the most recently saved state directly after apply and before once or run.
-- 'restore' callback prototype
local urn = Mapper('urn:uuid:8fdd61cd-9c12-4366-b208-8306b37cb981#')
-- 3D positional state
local state = {
[urn.position] = { 0.2, 0.3, 0.9 },
[urn.rotation] = { -math.pi, 0.0, math.pi/2 }
}
function save(forge)
local obj = forge:object()
-- serialize state to atom object
for k, v in pairs(state) do
for vec in obj:key(k):vector(Atom.Float):autopop() do
for i, w in ipairs(v) do
vec:float(w)
end
end
end
obj:pop()
end
function restore(atom)
assert(atom.type == Atom.Object)
-- deserialize state from atom object
for k, v in pairs(state) do
local itm = atom[k]
if itm then
assert(itm.type == Atom.Vector)
assert(itm.childType == Atom.Float)
state[k] = { itm:unpack() }
assert(#state[k] == 3)
end
end
end
LV2 references objects and their properties by URIs, usually rather long, unique string identifiers. When comparing URIs, whole strings need to be compared, which is inefficient. In LV2, an URI thus can be mapped to a simple integer URID, greatly increasing efficiency of comparisons. Unmapping from URID to original URI is supported, too.
Moony directly mirrors mapping and unmapping of the underlying low-level implementation, but additionally caches every newly queried URI or URID for faster lookup inside script code. Cached URIs and URIDs are not preserved across script code updates, though.
Map an URI to its corresponding URID. Map is a first-class Lua table acting as a cache for all queried URIDs and can also be called as function.
-- Map can be indexed or called as function
local prefix = 'http://example.com#'
local foo_urid = Map(prefix .. 'foo')
local bar_urid = Map[prefix .. 'bar']
Unmap an URID to its original URI. Unmap is a first-class Lua table acting as a cache for all queried URIs and can also be called as function.
-- Unmap can be indexed or called as function
local prefix = 'http://example.com#'
local foo_urid= Map(prefix .. 'foo')
local bar_urid = Map[prefix .. 'bar']
local foo_uri = Unmap(foo_urid)
local bar_uri = Unmap[bar_urid]
assert(prefix .. 'foo' == foo_uri)
assert(prefix .. 'bar' == bar_uri)
A convenience wrapper and syntactic sugar around Map. Simplifies creating multiple mappings which share a common URI prefix.
Returns a table which can be queried by indexing with the URI postfix. The query is equivalent to a mapping of concatenated URI prefix and postfix.
This is especially useful when dealing with custom properties and the StateResponder.
-- Mapper is a convenience wrapper over Map
local prefix = 'urn:uuid:d178f397-3179-4c38-9d25-31d60ffc5788#'
local mapper = Mapper(prefix)
local bar_urid = Map[prefix .. 'bar']
assert(mapper.bar == bar_urid)
Returns a unique, non-colliding integer ID to be used in event messaging. Especially useful for Patch messages or custom event systems where there is a need to track event properties over space and time.
-- Blank returns unique integer IDs
local cache = {}
for i = 1, 100 do
local id = Blank()
assert(not cache[id])
cache[id] = true
end
A forge object is used to serialize arbitrary complex atoms, both simple primitives and nested containers.
Atom types that contain single data as part of their body are referred to as primitives.
Forge a boolean atom, e.g. of type Atom.Bool.
-- Forge Bool
function stash_tuple(forge)
forge:bool(true)
forge:bool(false)
end
Forge an integer atom, e.g. of type Atom.Int.
-- Forge Int
function stash(forge)
forge:int(12)
end
Forge long integer atom, e.g. of type Atom.Long.
-- Forge Long
function stash(forge)
forge:long(12)
end
Forge single-precision float atom, e.g. of type Atom.Float.
-- Forge Float
function stash(forge)
forge:float(12.5)
end
Forge double-precision float atom, e.g. of type Atom.Double.
-- Forge Double
function stash(forge)
forge:double(12.5)
end
Forge URID atom, e.g. of type Atom.URID.
-- Forge URID
local uri = 'urn:uuid:887c5b2e-89f9-4f1d-aa7c-0ac240ea11b5#foo'
local urid = Map[uri]
function stash(forge)
forge:urid(urid)
end
Forge URI atom, e.g. of type Atom.URI.
-- Forge URI
local uri = 'urn:uuid:1f2eb75a-29dc-446b-a8eb-c22d20144a85#foo'
function stash(forge)
forge:uri(uri)
end
Forge string atom, e.g. of type Atom.String.
-- Forge String
function stash(forge)
forge:string('hello world')
end
Forge path atom, e.g. of type Atom.Path.
-- Forge Path
function stash(forge)
forge:path('/tmp/xyz')
end
Forge literal atom, e.g. of type Atom.Literal.
-- Forge Literal
local rus = Map['http://lexvo.org/id/iso639-3/rus'] -- Russian language
function stash_tuple(forge)
forge:literal('902A7F', MIDI.MidiEvent) -- MIDI encoded as hexidecimal text
forge:literal('Приве́т!', nil, rus) -- 'hello!'
end
Forge chunk atom, e.g. of type Atom.Chunk.
-- Forge Chunk
function stash_tuple(forge)
forge:chunk(string.char(0x1, 0x2, 0x3))
forge:chunk({0x1, 0x2, 0x3})
forge:chunk(0x1, 0x2, 0x3)
end
Forge MIDI atom, e.g. of type MIDI.MidiEvent.
-- Forge MIDI
function stash_tuple(forge)
forge:midi(string.char(MIDI.NoteOn, 69, 0x7f))
forge:midi({MIDI.NoteOn, 69, 0x7f})
forge:midi(MIDI.NoteOn, 69, 0x7f)
end
Forge a custom atom of arbitrary type.
-- Forge Raw
local byt = { -- stores individual bytes of zero-terminated string 'LV2'
string.byte('L'),
string.byte('V'),
string.byte('2'),
string.byte('\0')
}
function stash_tuple(forge)
forge:raw(Atom.String, string.char(table.unpack(byt)))
forge:raw(Atom.String, byt)
forge:raw(Atom.String, table.unpack(byt))
end
Forge an atom of arbitrary type.
-- Forge Typed
function stash_tuple(forge)
forge:typed(Atom.Int, 12) -- equivalent to forge:int(...)
forge:typed(Atom.Float, 12.5) -- equivalent to forge:float(...)
forge:typed(Atom.String, 'LV2 rocks!') -- equivalent to forge:string(...)
end
Forge an unchanged atom. Useful for cloning whole atoms.
-- Forge Atom
-- forge integer atom
function stash_tuple(forge)
forge:int(12)
end
-- forge prepared integer atom in whole
function apply_tuple(tup, forge)
assert(tup[1].body == 12)
forge:atom(tup[1])
end
-- check if integer atom has been forged in whole
function check(tup)
assert(tup[1].body == 12)
end
Atom types that contain nested atoms as part of their body are referred to as containers.
Forge a sequence atom, e.g. an atom of type Atom.Sequence.
-- Forge Sequence
function stash_tuple(forge)
local seq = forge:sequence() -- create derived container forge object
seq:time(0):int(12)
seq:frameTime(1):int(13)
seq:pop() -- finalize container
seq = forge:sequence(Atom.frameTime) -- create derived container forge object
seq:time(0):int(12)
seq:frameTime(1):int(13)
seq:pop() -- finalize container
for seq in forge:sequence(Atom.beatTime):autopop() do -- create iterator for derived container forge object
seq:time(0.0):int(12)
seq:beatTime(1.0):int(13)
end -- finalize container
end
Forge frame time of event. Use this on sequences with unit 0 or Atom.frameTime.
Forge beat time of event. Use this on sequences with unit Atom.beatTime.
Forge frame or beat time of event. Can be used as syntactic sugar instead of Frame Time or Beat Time.
Forge an object atom, e.g. an atom of type Atom.Object.
-- Forge Object
local urn = Mapper('urn:uuid:efc85bf7-0246-486f-948b-0cfbcae4e1053#')
function stash_tuple(forge)
local obj = forge:object(urn.FooBar) -- create derived container forge object
obj:key(urn.foo):int(12) -- without context
obj:key(urn.bar, urn.ctx):long(13) -- with context
obj:pop() -- finalize container
for obj in forge:object(urn.FooBar):autopop() do -- create iterator for derived container forge object
obj:key(urn.foo):int(12) -- without context
obj:key(urn.bar, urn.ctx):long(13) -- with context
end -- finalize container
end
Forge key of object property.
Forge a tuple atom, e.g. an atom of type Atom.Tuple.
-- Forge Tuple
function stash_tuple(forge)
local tup = forge:tuple() -- create derived container forge object
tup:int(12)
tup:long(13)
tup:float(13.5)
tup:double(14.5)
tup:string('this is an element of an atom tuple')
tup:bool(true)
tup:pop() -- finalize container
for tup in forge:tuple():autopop() do -- create iterator for derived container forge object
tup:int(12)
tup:long(13)
tup:float(13.5)
tup:double(14.5)
tup:string('this is an element of an atom tuple')
tup:bool(true)
end -- finalize container
end
Forge a vector atom, e.g. an atom of type Atom.Vector.
-- Forge Vector
local vBool = {
true, false, true
}
function stash_tuple(forge)
forge:vector(Atom.Int):int(1):int(2):int(3):int(4):int(5):pop()
forge:vector(Atom.Int, {1, 2, 3, 4, 5})
forge:vector(Atom.Int, 1, 2, 3, 4, 5)
for vec in forge:vector(Atom.Bool):autopop() do
for i, v in ipairs(vBool) do
vec:bool(v)
end
end
forge:vector(Atom.Bool, vBool)
forge:vector(Atom.Bool, table.unpack(vBool))
end
Finalize any derived container forge object.
-- Pop
function stash(forge)
local tup = forge:tuple()
tup:int(1)
tup:int(2)
tup:pop()
end
function apply(atom)
assert(#atom == 2)
end
Finalize any derived container forge object automatically via a for-iterator-construction.
-- Autopop
function stash(forge)
for tup in forge:tuple():autopop() do
tup:int(1)
tup:int(2)
end
end
function apply(atom)
assert(#atom == 2)
end
Atom types that contain Open Sound Control bundles or messages as part of their body.
Forge a OSC bundle atom, e.g. an atom object of type OSC.Bundle.
-- Forge Bundle
function stash_tuple(forge)
-- schedule bundle for immediate dispatch
local bndl = forge:bundle() -- create derived container forge object
bndl:message('/hello', 's', 'world')
bndl:pop() -- finalize container
-- schedule bundle for dispatch @ Sep 19, 2016 02.09.16.015811000 UTC
bndl = forge:bundle(0xdb89c74c040c3199)
bndl:message('/hello', 's', 'world')
bndl:pop()
-- schedule bundle for dispatch in 0.1s
bndl = forge:bundle(0.1)
bndl:message('/hello', 's', 'world')
bndl:pop()
end
Forge a OSC message atom, e.g. an atom object of type OSC.Message. Supported OSC argument types are: 'i', 'f', 's', 'b', 'h', 'd', 't', 'm', 'S', 'c', 'r', 'T', 'F', 'N', 'I'
-- Forge Message
function stash(forge)
-- schedule bundle for immediate dispatch
local bndl = forge:bundle() -- create derived container forge object
bndl:message('/hello', 'ifs', 2016, 12.5, 'hello')
bndl:message('/hello', 'b', string.char(0x1, 0x2, 0x3))
bndl:message('/hello', 'hdS', 2017, 13.5, Param.sampleRate)
bndl:message('/hello', 't', 0xdb89c74c040c3199)
bndl:message('/hello', 'TFNI')
bndl:message('/hello', 'm', string.char(MIDI.NoteOn, 69, 0x7f))
bndl:message('/hello', 'cr', string.byte('s'), 0xff00ff00)
bndl:pop() -- finalize container)
end
Forge Impulse argument of OSC message.
Forge Char argument of OSC message.
Forge RGBA color argument of OSC message.
Forge Timetag argument of OSC message.
-- Forge custom OSC arguments
function stash(forge)
for obj in forge:object(OSC.Message):autopop() do
obj:key(OSC.messagePath):string('/custom')
for tup in obj:key(OSC.messageArguments):tuple():autopop() do
-- forge 'i', 'f', 's', 'b' OSC arguments
tup:int(12) -- 'i'nteger
tup:float(1.5) -- 'f'loat
tup:string('foo') -- 's'tring
tup:chunk(string.char(0x1, 0x2)) -- 'b'lob
-- forge 'h', 'd', 't' OSC arguments
tup:long(13) -- 'h'
tup:double(1.6) -- 'd'ouble
tup:timetag(0.1) -- 't', 100ms from now
-- forge 'N', 'I', 'T', 'F' OSC arguments
tup:raw(0) -- empty atom, 'N'il
tup:impulse() -- 'I'mpulse
tup:bool(true) -- 'T'rue
tup:bool(false) -- 'F'alse
-- forge 'c', 'r', 'm', 'S' OSC arguments
tup:char(string.byte('c')) -- 'c'char
tup:rgba(0xbb0000ff) -- reddish, 'r'gba
tup:midi(MIDI.NoteOn, Note['C-0'], 64) -- 'm'idi
tup:urid(Param.sampleRate) -- 'S'ymbol
end
end
end
Atom types that contain Patch messages as part of their body.
Forge a patch patch atom, e.g. an atom object of type Patch.Patch.
-- Forge Patch
local urn = Mapper('urn:uuid:29f87e4f-fa45-4f91-aa88-767053006a0d#')
function stash(forge)
local patch= forge:patch(urn.subj, 1002) -- with subject and sequence number
local rem = patch:remove() -- equivalent to patch:object(Patch.remove)
rem:key(urn.foo):urid(Patch.wildcard)
rem:key(urn.bar):urid(Patch.wildcard)
rem:pop()
local add = patch:add() -- equivalent to patch:object(Patch.add)
add:key(urn.foo):int(12)
add:key(urn.bar):float(12.5)
add:pop()
patch:pop()
end
Forge add property of patch property.
Forge remove property of patch property.
Forge a patch get atom, e.g. an atom object of type Patch.Get.
-- Forge Get
local urn = Mapper('urn:uuid:5b172c1f-9152-4d4e-ad68-84965792b931#')
function stash_tuple(forge)
forge:get(urn.prop) -- without subject and sequence number
forge:get(urn.prop, nil, 1001) -- without subject
forge:get(urn.prop, urn.subj, 1002) -- with subject and sequence number
end
Forge a patch set atom, e.g. an atom object of type Patch.Set.
-- Forge Set
local urn = Mapper('urn:uuid:29c714c1-0c7b-4434-81d6-82ee8a4d64b8#')
function stash_tuple(forge)
local set = forge:set(urn.prop) -- without subject and sequence number
set:int(12)
set:pop()
set = forge:set(urn.prop, urn.subj, 1002) -- with subject and sequence number
set:float(12)
set:pop()
end
Forge a patch put atom, e.g. an atom object of type Patch.Put.
-- Forge Put
local urn = Mapper('urn:uuid:dabe2235-2405-46e6-a26c-90dbe34d9bf3#')
function stash_tuple(forge)
local put = forge:put() -- without subject and sequence number
put:key(urn.foo):int(12)
put:key(urn.bar):float(12.5)
put:pop()
put= forge:put(urn.subj, 1002) -- with subject and sequence number
put:key(urn.foo):int(12)
put:key(urn.bar):float(12.5)
put:pop()
end
Forge an ack patch atom, e.g. an atom object of type Patch.Ack.
-- Forge Ack
local urn = Mapper('urn:uuid:f35c0f85-5b7f-4434-912d-8bf982711b30#')
function stash(forge)
forge:ack(urn.subj, 1002) -- with subject and sequence number
end
Forge an error patch atom, e.g. an atom object of type Patch.Error.
-- Forge Error
local urn = Mapper('urn:uuid:93ea103c-75ff-47e2-b89b-fc79173bedee#')
function stash(forge)
forge:error(urn.subj, 1002) -- with subject and sequence number
end
Forge a delete patch atom, e.g. an atom object of type Patch.Delete.
-- Forge Delete
local urn = Mapper('urn:uuid:cea077fc-b822-4143-b5c5-34c0f7d9f016#')
function stash(forge)
forge:delete(urn.subj, 1002) -- with sequence number
end
Forge a copy patch atom, e.g. an atom object of type Patch.Copy.
-- Forge Copy
local urn = Mapper('urn:uuid:3d4de1a6-a5fa-463d-a59c-c7778a6b18e9#')
function stash(forge)
forge:copy(urn.subj, urn.dest, 1002) -- with sequence number
end
Forge a move patch atom, e.g. an atom object of type Patch.Move.
-- Forge Move
local urn = Mapper('urn:uuid:6370dea9-a41a-4347-80f0-75d9fcccc28a#')
function stash(forge)
forge:move(urn.subj, urn.dest, 1002) -- with sequence number
end
Forge a patch insert atom, e.g. an atom object of type Patch.Insert.
-- Forge Insert
local urn = Mapper('urn:uuid:16e7ed0d-20ee-45ef-8268-ed931a998a4f#')
function stash_tuple(forge)
local insert = forge:insert() -- without subject and sequence number
insert:key(urn.foo):int(12)
insert:key(urn.bar):float(12.5)
insert:pop()
insert= forge:insert(urn.subj, 1002) -- with subject and sequence number
insert:key(urn.foo):int(12)
insert:key(urn.bar):float(12.5)
insert:pop()
end
Atom types that contain Canvas messages as part of their body. Modeled after the HTML5 Canvas API. But the vector drawing instructions into an atom:Tuple and store it as property Canvas:graph.
Forge an atom object of type Canvas.BeginPath. Manually begin a new path. Not needed for instructions that inherently begin a new path by themeselves: Canvas.MoveTo, Canvas.Arc, Canvas.Rectangle.
-- Forge BeginPath
function stash(ctx)
ctx:beginPath()
end
Forge an atom object of type Canvas.ClosePath. Close a previously begun path.
-- Forge ClosePath
function stash(ctx)
ctx:closePath()
end
Forge an atom object of type Canvas.Arc. Create an Arc structure.
-- Forge Arc
function stash(ctx)
ctx:arc(0.5, 0.5, 0.1):stroke()
ctx:arc(0.5, 0.5, 0.1, math.pi/4, math.pi/2):stroke()
end
Forge an atom object of type Canvas.CurveTo. Append a spline curve segment to path.
-- Forge CurveTo
function stash(ctx)
ctx:beginPath():moveTo(0.0, 0.0):curveTo(0.1, 0.1, 0.2, 0.4, 0.3, 0.9):closePath():stroke()
end
Forge an atom object of type Canvas.LineTo. Append line segment to path.
-- Forge LineTo
function stash(ctx)
ctx:beginPath():moveTo(0.0, 0.0):lineTo(1.0, 1.0):closePath():stroke()
end
Forge an atom object of type Canvas.MoveTo. Move to given point and begin new path.
-- Forge MoveTo
function stash(ctx)
ctx:moveTo(0.0, 0.0)
end
Forge an atom object of type Canvas.Rectangle. Create rectangle.
-- Forge Rectangle
function stash(ctx)
ctx:rectangle(0.1, 0.1, 0.8, 0.8):fill()
end
Forge an atom object of type Canvas.PolyLine. Create polyline.
-- Forge PolyLine
function stash(ctx)
ctx:beginPath():polyLine(0.1, 0.1, 0.9, 0.1, 0.5, 0.9):closePath():fill()
end
Forge an atom object of type Canvas.Style. Set current stroke and fill color.
-- Forge Style
function stash(ctx)
ctx:rectangle(0.1, 0.1, 0.2, 0.2):style(0xffffffff):fill() -- opaque white
ctx:rectangle(0.2, 0.2, 0.2, 0.2):style(0xff0000ff):fill() -- opaque red
ctx:rectangle(0.3, 0.3, 0.2, 0.2):style(0x00ff0080):fill() -- half-transparent green
end
Forge an atom object of type Canvas.LineWidth. Set current line width.
-- Forge LineWidth
function stash(ctx)
ctx:rectangle(0.1, 0.1, 0.2, 0.2):lineWidth(0.01):stroke()
ctx:rectangle(0.2, 0.2, 0.2, 0.2):lineWidth(0.04):stroke()
end
Forge an atom object of type Canvas.LineDash. Set current line dash style.
-- Forge LineDash
function stash(ctx)
ctx:rectangle(0.1, 0.1, 0.2, 0.2):lineDash(0.01, 0.01):stroke()
ctx:rectangle(0.2, 0.2, 0.2, 0.2):lineDash(0.01, 0.04):stroke()
end
Forge an atom object of type Canvas.LineCap. Set current line cap style.
-- Forge LineCap
local function tri(ctx, x, y, s, cap)
ctx:moveTo(x, y):lineTo(x + s, y):lineTo(x + s, y + s/2):lineCap(cap):stroke()
end
function stash(ctx)
ctx:lineWidth(0.05)
tri(ctx, 0.3, 0.1, 0.4, Canvas.lineCapButt)
tri(ctx, 0.2, 0.2, 0.4, Canvas.lineCapRound)
tri(ctx, 0.1, 0.3, 0.4, Canvas.lineCapSquare)
end
Forge an atom object of type Canvas.LineJoin. Set current line join style.
-- Forge LineJoin
local function tri(ctx, x, y, s, join)
ctx:moveTo(x, y):lineTo(x + s, y):lineTo(x + s, y + s/2):lineJoin(join):stroke()
end
function stash(ctx)
ctx:lineWidth(0.05)
tri(ctx, 0.3, 0.1, 0.4, Canvas.lineJoinMiter)
tri(ctx, 0.2, 0.2, 0.4, Canvas.lineJoinRound)
tri(ctx, 0.1, 0.3, 0.4, Canvas.lineJoinBevel)
end
Forge an atom object of type Canvas.MiterLimit. Set current line join miter limit.
-- Forge MiterLimit
local function tri(ctx, x, y, s, limit)
ctx:moveTo(x, y):lineTo(x + s, y):lineTo(x + s, y + s/2):lineJoin(Canvas.lineJoinMiter):miterLimit(limit):stroke()
end
function stash(ctx)
ctx:lineWidth(0.05)
tri(ctx, 0.3, 0.1, 0.4, 1.0)
tri(ctx, 0.2, 0.2, 0.4, 2.0)
end
Forge an atom object of type Canvas.Stroke. Stroke current path.
-- Forge Stroke
function stash(ctx)
ctx:rectangle(0.0, 0.0, 1.0, 1.0):stroke()
end
Forge an atom object of type Canvas.Fill. Fill current path.
-- Forge Fill
function stash(ctx)
ctx:rectangle(0.0, 0.0, 1.0, 1.0):fill()
end
Forge an atom object of type Canvas.Clip. Set current path as clip mask.
-- Forge Clip
function stash(ctx)
ctx:save()
ctx:arc(0.25, 0.25, 0.25):clip()
ctx:rectangle(0.25, 0.25, 0.5, 0.5):fill()
ctx:restore()
end
Forge an atom object of type Canvas.Save. Push/save current state.
-- Forge Save
function stash(ctx)
ctx:save():translate(0.5, 0.5):rotate(math.pi/4)
ctx:rectangle(-0.1, -0.1, 0.2, 0.2):stroke()
ctx:restore()
ctx:rectangle(0.4, 0.4, 0.2, 0.2):stroke()
end
Forge an atom object of type Canvas.Restore. Pop/restore previously saved state.
-- Forge Restore
function stash(ctx)
ctx:save():translate(0.5, 0.5):rotate(math.pi/4)
ctx:rectangle(-0.1, -0.1, 0.2, 0.2):stroke()
ctx:restore()
ctx:rectangle(0.4, 0.4, 0.2, 0.2):stroke()
end
Forge an atom object of type Canvas.Translate. Translate current render matrix.
-- Forge Translate
function stash(ctx)
ctx:save():translate(0.4, 0.4)
ctx:rectangle(0.0, 0.0, 0.2, 0.2):fill()
ctx:restore()
end
Forge an atom object of type Canvas.Scale. Scale current render matrix.
-- Forge Scale
function stash(ctx)
ctx:save():scale(2.0, 2.0)
ctx:rectangle(0.0, 0.0, 0.5, 0.5):fill()
ctx:restore()
end
Forge an atom object of type Canvas.Rotate. Rotate current render matrix.
-- Forge Rotate
function stash(ctx)
ctx:save():translate(0.4, 0.4):rotate(math.pi/4) -- rotate by 45 degrees
ctx:rectangle(-0.1, -0.1, 0.2, 0.2):fill()
ctx:restore()
end
Forge an atom object of type Canvas.Transform. Set current render matrix.
-- Forge Transform
function stash(ctx)
ctx:save():transform(-1.0, 0.0, 1.0, 1.0, 0.0, 0.0) -- mirror on x-asix
ctx:rectangle(-0.1, -0.1, 0.2, 0.2):fill()
ctx:restore()
end
Forge an atom object of type Canvas.Reset. Reset current render matrix to identity configuration.
-- Forge Reset
function stash(ctx)
ctx:translate(0.4, 0.4)
ctx:rectangle(0.0, 0.0, 0.2, 0.2):fill()
ctx:reset()
ctx:rectangle(0.0, 0.0, 0.2, 0.2):fill()
end
Forge an atom object of type Canvas.FontSize. Set font size.
-- Forge FontSize
function stash(ctx)
ctx:moveTo(0.5, 0.5):fontSize(0.1):fillText('Hello')
end
Forge an atom object of type Canvas.FillText. Fill/draw text at current position.
-- Forge FillText
function stash(ctx)
ctx:moveTo(0.5, 0.5):fontSize(0.1):style(0xff0000ff):fillText('Hello')
end
Instead of deserializing all LV2 event data to corresponding Lua types, Moony instead presents a proxy object to the user in form of a Lua userdata. This is needed because there are more LV2 atom types than native Lua types. It turns out to be more efficient, too, as the user usually wants to filter events before looking at their payload.
All atom types have some common attributes they can be queried for:
Atom types that contain single data as part of their body are referred to as primitives.
An empty atom, e.g. of type and size 0.
-- Atom Nil
-- serialize
function stash(forge)
end
function apply(atom)
-- attributes
assert(#atom == 0) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == 0) -- query type of atom
assert(atom.body == nil) -- get atom body
end
A boolean atom, e.g. of type Atom.Bool and size 4.
-- Atom Bool
-- serialize
function stash(forge)
forge:bool(true)
end
function apply(atom)
-- attributes
assert(#atom == 4) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Bool) -- query type of atom
assert(atom.body == true) -- get atom body
end
An integer atom, e.g. of type Atom.Int and size 4.
-- Atom Int
-- serialize
function stash(forge)
forge:int(12)
end
function apply(atom)
-- attributes
assert(#atom == 4) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Int) -- query type of atom
assert(atom.body == 12) -- get atom body
end
A long integer atom, e.g. of type Atom.Long and size 8.
-- Atom Long
-- serialize
function stash(forge)
forge:long(13)
end
function apply(atom)
-- attributes
assert(#atom == 8) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Long) -- query type of atom
assert(atom.body == 13) -- get atom body
end
A single-precision float atom, e.g. of type Atom.Float and size 4.
-- Atom Float
-- serialize
function stash(forge)
forge:float(1.5)
end
function apply(atom)
-- attributes
assert(#atom == 4) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Float) -- query type of atom
assert(atom.body == 1.5) -- get atom body
end
A double-precision float atom, e.g. of type Atom.Double and size 8.
-- Atom Double
-- serialize
function stash(forge)
forge:double(1.6)
end
function apply(atom)
-- attributes
assert(#atom == 8) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Double) -- query type of atom
assert(atom.body == 1.6) -- get atom body
end
An URID atom, e.g. of type Atom.URID and size 4.
-- Atom URID
local urid = Map['urn:uuid:6d82e244-ee66-403f-aea1-26b3d9823820#foo']
-- serialize
function stash(forge)
forge:urid(urid)
end
function apply(atom)
-- attributes
assert(#atom == 4) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.URID) -- query type of atom
assert(atom.body == urid) -- get atom body
end
An URI atom, e.g. of type Atom.URI and variable size. Atom size denotes to string size plus zero byte terminator.
-- Atom URI
local uri = 'urn:uuid:022ec18a-0a02-4a19-ad7a-a10403a0c4c3#foo'
-- serialize
function stash(forge)
forge:uri(uri)
end
function apply(atom)
-- attributes
assert(#atom == #uri + 1) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.URI) -- query type of atom
assert(atom.body == uri) -- get atom body
end
A string atom, e.g. of type Atom.String and variable size. Atom size denotes to string size plus zero byte terminator.
-- Atom String
local str = 'hello world'
-- serialize
function stash(forge)
forge:string(str)
end
function apply(atom)
-- attributes
assert(#atom == #str + 1) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.String) -- query type of atom
assert(atom.body == str) -- get atom body
end
A path atom, e.g. of type Atom.Path and variable size. Atom size denotes to string size plus zero byte terminator.
-- Atom Path
local tmp = '/tmp/xyz'
-- serialize
function stash(forge)
forge:path(tmp)
end
function apply(atom)
-- attributes
assert(#atom == #tmp + 1) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Path) -- query type of atom
assert(atom.body == tmp) -- get atom body
end
A literal atom, e.g. of type Atom.Literal and variable size. Atom size denotes to string size plus zero byte terminator. Atom literals may have either a data type or language attribute.
-- Atom Literal
local lit = 'Hallo Welt'
local urn = Mapper('urn:uuid:c2f619db-3eef-411c-82c9-ffe0df1c10fc3')
local datatype = urn.datatype
local lang = urn.lang
-- serialize
function stash(forge)
forge:literal(lit, datatype, lang)
end
function apply(atom)
-- attributes
assert(#atom == 4 + 4 + #lit + 1) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Literal) -- query type of atom
assert(atom.datatype == datatype) -- query datatype of literal
assert(atom.lang == lang) -- query datatype of literal
assert(atom.body == lit) -- get atom body
-- unpacking all attributes
local lit2, datatype2, lang2 = atom:unpack()
assert(lit2 == lit)
assert(datatype2 == datatype)
assert(lang2 == lang)
end
A chunk atom, e.g. of type Atom.Chunk and variable size. Atom size denotes to byte size of atom body.
-- Atom Chunk
local byt = string.char(0x1, 0x2, 0x3)
-- serialize
function stash(forge)
forge:chunk(byt)
end
function apply(atom)
-- attributes
assert(#atom == #byt) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == Atom.Chunk) -- query type of atom
assert(atom.body == byt) -- get atom body
-- unpacking all bytes
local byt1, byt2, byt3 = atom:unpack() -- equivalent to atom:unpack(1, #atom)
assert(byt1 == 0x1)
assert(byt2 == 0x2)
assert(byt3 == 0x3)
-- indexing individual bytes
assert(atom[1] == 0x1)
assert(atom[2] == 0x2)
assert(atom[3] == 0x3)
end
A MIDI atom, e.g. of type MIDI.MidiEvent and variable size. Atom size denotes to byte size of atom body. A MIDI atom is an equivalent to a Chunk atom and thus features the same class attributes and methods.
-- Atom MIDI
local byt = string.char(MIDI.NoteOn, 69, 64)
-- serialize
function stash(forge)
forge:midi(byt)
end
function apply(atom)
-- attributes
assert(#atom == #byt) -- query size of atom body
print(tostring(atom)) -- convert to string
assert(atom.type == MIDI.MidiEvent) -- query type of atom
assert(atom.body == byt) -- get atom body
-- unpack all bytes
local byt1, byt2, byt3 = atom:unpack() -- equivalent to atom:unpack(1, #atom)
assert(byt1 == MIDI.NoteOn)
assert(byt2 == 69)
assert(byt3 == 64)
-- indexing individual bytes
assert(atom[1] == MIDI.NoteOn)
assert(atom[2] == 69)
assert(atom[3] == 64)
end
Atom types that contain nested atoms as part of their body are referred to as containers.
A Sequence atom, e.g. of type Atom.Sequence and variable size. Atom size denotes to number of events contained in atom body. An atom sequence consists of individual atom events, each with a time stamp and event pyaload.
-- Atom Sequence
-- serialize
function stash(forge)
local seq = forge:sequence(Atom.frameTime)
seq:time(0):int(12)
seq:pop()
end
function apply(seq)
-- attributes
assert(#seq == 1) -- query number of events
print(tostring(seq)) -- convert to string
assert(seq.type == Atom.Sequence) -- query type of atom
assert(seq.unit == Atom.frameTime) -- query event time unit
-- iterate over
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.body == 12)
end
-- indexing
local atom = seq[1]
assert(atom.body == 12)
end
A Tuple atom, e.g. of type Atom.Tuple and variable size. Atom size denotes to number of items contained in atom body. An atom tuple consists of individual atom items.
-- Atom Tuple
-- serialize
function stash(forge)
local tup = forge:tuple()
tup:int(12)
tup:pop()
end
function apply(tup)
-- attributes
assert(#tup == 1) -- query number of items
print(tostring(tup)) -- convert to string
assert(tup.type == Atom.Tuple) -- query type of atom
-- iterate over
for i, atom in tup:foreach() do
assert(i == 1)
assert(atom.body == 12)
end
-- unpack
local a1 = tup:unpack()
assert(a1.body == 12)
-- indexing
a1 = tup[1]
assert(a1.body == 12)
end
An Object atom, e.g. of type Atom.Object and variable size. Atom size denotes to number of properties contained in atom body. An atom object consists of individual properties.
-- Atom Object
local urn = Mapper('urn:uuid:b110d31d-98c6-48bc-93d1-e326c9829be9#')
-- serialize
function stash(forge)
local obj = forge:object(urn.otype, urn.id)
obj:key(urn.key):int(12)
obj:pop()
end
function apply(obj)
-- attributes
assert(#obj == 1) -- query number of properties
print(tostring(obj)) -- convert to string
assert(obj.type == Atom.Object) -- query type of atom
assert(obj.id == urn.id) -- query id of object atom
assert(obj.otype == urn.otype) -- query type of object atom
-- iterate over
for k, atom, ctx in obj:foreach() do
assert(k == urn.key)
assert(atom.body == 12)
assert(ctx == 0)
end
-- indexing
local atom = obj[urn.key]
assert(atom.body == 12)
end
A Vector atom, e.g. of type Atom.Vector and variable size. Atom size denotes to number of items contained in atom body. An atom vector consists of equally typed atom items.
-- Atom Vector
-- serialize
function stash(forge)
forge:vector(Atom.Int):int(12):int(13):int(14):pop()
end
function apply(vec)
-- attributes
assert(#vec == 3) -- query number of items
print(tostring(vec)) -- convert to string
assert(vec.type == Atom.Vector) -- query type of atom
assert(vec.childSize == 4) -- query child size of atom
assert(vec.childType == Atom.Int) -- query child type of atom
-- iterate over
for i, atom in vec:foreach() do
if i == 1 then
assert(atom.body == 12)
elseif i == 2 then
assert(atom.body == 13)
elseif i == 3 then
assert(atom.body == 14)
end
end
-- unpack
local v1, v2, v3 = vec:unpack()
assert(v1.body == 12)
assert(v2.body == 13)
assert(v3.body == 14)
-- indexing
v1, v2, v3 = vec[1], vec[2], vec[3]
assert(v1.body == 12)
assert(v2.body == 13)
assert(v3.body == 14)
end
Sometimes it may be useful to not only be able to serialize atoms to forge objects provided to the user via one of the callback functions, but to be able to temporarily serialize some atoms to memory for later dispatch.
For these usage scenarios there is the stash object, which according to its name, functions as temporary stash. It is a special object in the way that it can either be an atom object (with all its attributes and methods) or a forge object (with all its methods), depending on whether it is in its reading or writing mode.
After creating a new stash, it is in its writing mode and thus can be used just like a forge object. After finishing the serialization procedure, the stash object may be switched into its reading mode and be used just like an atom object.
-- Stash
-- a new stash defaults to writing mode - equivalent to forge object
local io = Stash()
io:int(12)
-- switch to reading mode - equivalent to atom object
io:read()
assert(io.body == 12)
-- switch to writing mode - automatically clears previous content
io:write()
io:int(13)
-- switch to reading mode
io:read()
assert(io.body == 13)
Sometimes it is useful to know the sample rate (e.g. for timing) and size of sequence buffers and minimal and maximal to expect audio block frames. If the host exports those values, they may be queried via the options table.
-- Options
-- only available if exported by host
local sampleRate = Options[Param.sampleRate]
assert(sampleRate.body == 48000)
local sequenceSize = Options[Buf_Size.sequenceSize]
local minBlockLength = Options[Buf_Size.minBlockLength]
local maxBlockLength = Options[Buf_Size.maxBlockLength]
local updateRate = Options(Ui.updateRate)
for urid, atom in pairs(Options) do
print(Unmap[urid], atom)
end
Responders are convenience wrappers to ease development of frequently used logic event handling schemes via callbacks.
Moony offers simple responders for MIDI and OSC handling and more complex responders for time and state handling.
By using responder objects, common problems like event filtering and sequencing can be written with much less and more understandable code.
The StateResponder can be used to build simple user interfaces to make any moony script available to non-coders around the world.
Runs callbacks for received MIDI messages.
-- MIDIResponder
-- define MIDI responder object with callbacks
local midiR = MIDIResponder({
-- listen for NoteOn messages
[MIDI.NoteOn] = function(self, frames, forge, chan, note, vel)
assert(frames == 0)
assert(chan == 0x1)
assert(note == 69)
assert(vel == 0x7f)
-- send NoteOn message
forge:time(frames):midi(MIDI.NoteOn | chan, note + 1, vel) -- increase note by 1
end,
-- listen for NoteOff messages
[MIDI.NoteOff] = function(self, frames, forge, chan, note, vel)
assert(frames == 1)
assert(chan == 0x1)
assert(note == 69)
assert(vel == 0x0)
-- send NoteOff message
forge:time(frames):midi(MIDI.NoteOff | chan, note + 1, vel) -- increase note by 1
end
}, false) -- block all MIDI messages not explicitly handled by responder
-- check dynamically changing through flag
assert(midiR.through == false)
midiR.through = true -- route all MIDI messages not explicitely handled by responder
assert(midiR.through == true)
midiR.through = false -- block again
-- forge test messages
function stash_sequence(forge)
forge:time(0):midi(MIDI.NoteOn | 0x1, 69, 0x7f)
forge:time(1):midi(MIDI.NoteOff | 0x1, 69, 0x0)
forge:time(2):midi(MIDI.Bender | 0x1, 0x0, 0x7f)
end
-- process test messages with responder
function apply_sequence(n, seq, forge)
for frames, atom in seq:foreach() do
local handled = midiR(frames, forge, atom)
assert(handled == true)
end
end
-- check responder output
function check(seq)
assert(#seq == 2) -- only NoteOn and NoteOff have gone through
end
Runs callbacks for received OSC messages.
-- OSCResponder
-- define OSC responder object with callbacks
local oscR = OSCResponder({
-- listen for '/ping'
['/ping'] = function(self, frames, forge, fmt, var1, var2)
assert(frames == 0)
assert(fmt == 'if')
assert(var1 == 12)
assert(var2 == 12.5)
-- send a '/pong'
forge:time(frames):message('/pong', fmt, var1, var2)
end
}, true) -- route through not matched OSC messages
-- check dynamically changing through flag
assert(oscR.through == true)
oscR.through = false -- block all OSC messages not explicitely handled by responder
assert(oscR.through == false)
oscR.through = true -- route again
-- forge test messages
function stash_sequence(forge)
forge:time(0):message('/ping', 'if', 12, 12.5)
end
-- process test messages with responder
function apply_sequence(n, seq, forge)
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.otype == OSC.Message)
assert(atom[OSC.messagePath].body == '/ping')
local handled, matched = oscR(frames, forge, atom)
assert(handled == true)
assert(matched == true)
end
end
-- check responder output
function check(seq)
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.otype == OSC.Message)
assert(atom[OSC.messagePath].body == '/pong')
end
end
Runs callbacks for received Time messages.
-- TimeResponder
-- define time responder object with callbacks
local timeR = TimeResponder({
-- listen for speed change
[Time.speed] = function(self, frames, forge, speed)
assert( (speed == 0.0) or (speed == 1.0) )
self.rolling = speed ~= 0.0
end,
-- listen for bar change
[Time.bar] = function(self, frames, forge, bar)
assert(bar == 0)
end,
-- listen for barBeat change
[Time.barBeat] = function(self, frames, forge, barBeat)
if forge then -- forge==nil in timeR:apply()
assert(barBeat == 0.0)
-- send NoteOn message at each whole beat
if math.tointeger(barBeat) then
forge:time(frames):midi(MIDI.NoteOn, 69, 0x7f)
end
end
end,
-- listen for beatUnit change
[Time.beatUnit] = function(self, frames, forge, beatUnit)
assert(beatUnit == 4)
end,
-- listen for beatsPerBar change
[Time.beatsPerBar] = function(self, frames, forge, beatsPerBar)
assert(beatsPerBar == 4.0)
end,
-- listen for beatsPerMinute change
[Time.beatsPerMinute] = function(self, frames, forge, beatsPerMinute)
assert(beatsPerMinute == 120.0)
end,
-- listen for frame change
[Time.frame] = function(self, frames, forge, frame)
assert(frame == 0)
end,
-- listen for framesPerSecond change
[Time.framesPerSecond] = function(self, frames, forge, framesPerSecond)
assert(framesPerSecond == 48000.0)
end,
rolling = false
}, 1.0)
-- index current transport state, can be called at any time
assert(timeR[Time.speed] == 0)
assert(timeR[Time.bar] == 0)
assert(timeR[Time.barBeat] == 0.0)
assert(timeR[Time.beatUnit] == 4)
assert(timeR[Time.beatsPerBar] == 4.0)
assert(timeR[Time.beatsPerMinute] == 120.0)
assert(timeR[Time.frame] == 0)
assert(timeR[Time.framesPerSecond] == 48000.0)
assert(timeR.multiplier == 1.0)
-- push current responder state to temporary stash
function stash(forge)
assert(timeR:stash(forge) == forge)
end
-- pop and apply current responder state from temporary stash
function apply(atom)
local handled = timeR:apply(atom)
assert(handled == true)
end
-- forge test messages
function stash_sequence(forge)
local obj = forge:time(0):object(Time.Position)
obj:key(Time.speed):float(1.0) -- start transport
obj:pop()
end
-- process test messages with responder
function apply_sequence(n, seq, forge)
local from = 0 -- frame time of 'last event' aka 'period beginning'
for to, atom in seq:foreach() do
local handled = timeR(from, to, forge, atom)
assert(handled == true)
from = to -- update frame time of 'last event'
end
timeR(from, n, forge) -- we must call time responder until 'period ending'
end
-- check responder output
function check(seq)
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.type == MIDI.MidiEvent)
assert(atom.body == string.char(MIDI.NoteOn, 69, 0x7f))
end
end
Runs callbacks for state handling via patch messages.
-- StateResponder
local urn = Mapper('urn:uuid:e359d24c-e1fe-4b3b-bbda-b58f4b04234d#')
-- define read-only parameter
local period = Parameter{
[RDFS.label] = 'Period',
[RDFS.comment] = 'set period',
[RDFS.range] = Atom.Float,
[LV2.minimum] = 1.0,
[LV2.maximum] = 10.0,
[Units.unit] = Units.s,
[RDF.value] = 5.0
}
-- define read-write parameter
local frequency = Parameter{
[RDFS.label] = 'Frequency',
[RDFS.comment] = 'set frequency',
[RDFS.range] = Atom.Int,
[LV2.minimum] = 1,
[LV2.maximum] = 48000,
[Units.unit] = Units.hz,
[RDF.value] = 44100
}
-- define state responder object with callbacks
local stateR = StateResponder({
[Patch.readable] = {
[urn.period] = period
},
[Patch.writable] = {
[urn.frequency] = frequency
}
})
-- push current responder state to temporary stash
function stash(forge)
assert(stateR:stash(forge) == forge)
end
-- pop and apply current responder state from temporary stash
function apply(atom)
local handled = stateR:apply(atom)
assert(handled == true)
end
-- save current responder state to disk
function save(forge)
assert(stateR:stash(forge) == forge)
end
-- restore current responder state from disk
function restore(atom)
local handled = stateR:apply(atom)
assert(handled == true)
end
-- register state upon code reload
function once(n, control, notify, seq, forge)
assert(stateR:register(0, forge) == forge)
end
-- forge test messages
function stash_sequence(forge)
forge:time(0):get(urn.period)
end
-- process test messages with responder
function apply_sequence(n, seq, forge)
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.otype == Patch.Get)
local handled = stateR(frames, forge, atom)
assert(handled == true)
end
end
-- check responder output
function check(seq)
for frames, atom in seq:foreach() do
assert(frames == 0)
assert(atom.otype == Patch.Set)
assert(atom[Patch.property].body == urn.period)
assert(atom[Patch.value].body == 5.0)
end
end
Adds convenience accessor metamethods to any parameter property table.
-- Parameter
local foo = Parameter({
[RDFS.label] = 'Foo',
[RDFS.range] = Atom.Float,
[LV2.minimum] = 0.0,
[LV2.maximum] = 1.0,
[RDF.value] = 0.5
})
local bar = Parameter({
[RDFS.label] = 'Bar',
[RDFS.range] = Atom.Int,
[LV2.minimum] = 0,
[LV2.maximum] = 10,
[RDF.value] = 5
})
function stash(forge)
assert(foo() == 0.5)
assert(foo[RDF.value] == 0.5)
foo(0.8)
assert(foo() == 0.8)
foo[RDF.value] = 0.9
assert(foo[RDF.value] == 0.9)
--TODO
end
Various hopefully useful utility functions.
Conversion from MIDI note to Hertz.
-- midi2cps
assert(midi2cps(69.0) == 440.0) -- relative to 'A-5'==440 Hz
assert(midi2cps(60.0, 60, 12, 400) == 400.0) -- relative to 'C-5'==400 Hz
Conversion from Hertz to MIDI note.
-- cps2midi
assert(cps2midi(440.0) == 69.0) -- relative to 'A-5'==400 Hz
assert(cps2midi(400.0, 60, 12, 400) == 60.0) -- relative to 'C-5'==400 Hz
Conversion from MIDI note number to note name and vice-versa.
-- Note
assert(Note[60] == 'C+4')
assert(Note['A+4'] == 69)
AES-128 encryption of byte strings.
-- encrypt
local pass = '2b7e151628aed2a6abf7158809cf4f3c'
local value = 'hello world'
local secret, len = aes128.encode(value, pass)
assert(aes128.decode(secret, pass, len) == value)
AES-128 decryption of byte strings.
-- decrypt
local pass = string.char(
0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c)
local value = 'hello world'
local secret, len = aes128.encode(value, pass)
assert(aes128.decode(secret, pass, len) == value)
Base64 encoding of byte strings.
-- encode
local value = 'hello world'
local encoded = base64.encode(value)
Base64 decoding of byte strings.
-- decode
local value = 'hello world'
local encoded = base64.encode(value)
assert(base64.decode(encoded) == value)
Ascii85 encoding of byte strings.
-- encode
local value = 'hello world'
local encoded = ascii85.encode(value)
Ascii85 decoding of byte strings.
-- decode
local value = 'hello world'
local encoded = ascii85.encode(value)
assert(ascii85.decode(encoded) == value)
Frequently used URID values and all MIDI message and controller constants are premapped and conveniently accessible in their own tables.
-- Constants
local ctx = {
lv2 = Mapper('http://lv2plug.in/ns/lv2core#'),
param = Mapper('http://lv2plug.in/ns/ext/parameters#'),
atom = Mapper('http://lv2plug.in/ns/ext/atom#'),
time = Mapper('http://lv2plug.in/ns/ext/time#'),
midi = Mapper('http://lv2plug.in/ns/ext/midi#'),
bufsz = Mapper('http://lv2plug.in/ns/ext/buf-size#'),
patch = Mapper('http://lv2plug.in/ns/ext/patch#'),
units = Mapper('http://lv2plug.in/ns/extensions/units#'),
osc = Mapper('http://open-music-kontrollers.ch/lv2/osc#'),
rdf = Mapper('http://www.w3.org/1999/02/22-rdf-syntax-ns#'),
rdfs = Mapper('http://www.w3.org/2000/01/rdf-schema#'),
moony = Mapper('http://open-music-kontrollers.ch/lv2/moony#'),
lua = Mapper('http://lua.org#'),
}
-- Atom URID constants
assert(Atom.Bool == ctx.atom.Bool)
assert(Atom.Int == ctx.atom.Int)
assert(Atom.Long == ctx.atom.Long)
assert(Atom.Float == ctx.atom.Float)
assert(Atom.Double == ctx.atom.Double)
assert(Atom.URID == ctx.atom.URID)
assert(Atom.URI == ctx.atom.URI)
assert(Atom.String == ctx.atom.String)
assert(Atom.Literal == ctx.atom.Literal)
assert(Atom.Path == ctx.atom.Path)
assert(Atom.Chunk == ctx.atom.Chunk)
assert(Atom.Property == ctx.atom.Property)
assert(Atom.Object == ctx.atom.Object)
assert(Atom.Sequence == ctx.atom.Sequence)
assert(Atom.Tuple == ctx.atom.Tuple)
assert(Atom.Vector == ctx.atom.Vector)
assert(Atom.beatTime == ctx.atom.beatTime)
assert(Atom.frameTime == ctx.atom.frameTime)
assert(Atom.childType == ctx.atom.childType)
-- Time URID constants
assert(Time.Position == ctx.time.Position)
assert(Time.barBeat == ctx.time.barBeat)
assert(Time.bar == ctx.time.bar)
assert(Time.beat == ctx.time.beat)
assert(Time.beatUnit == ctx.time.beatUnit)
assert(Time.beatsPerBar == ctx.time.beatsPerBar)
assert(Time.beatsPerMinute == ctx.time.beatsPerMinute)
assert(Time.frame == ctx.time.frame)
assert(Time.framesPerSecond == ctx.time.framesPerSecond)
assert(Time.speed == ctx.time.speed)
-- MIDI URID constants
assert(MIDI.MidiEvent == ctx.midi.MidiEvent)
-- MIDI message and controller constants
for k, v in pairs(MIDI) do
if k ~= MIDI.MidiEvent then
print(k, v)
end
end
-- OSC URID constants
assert(OSC.Event == ctx.osc.Event)
assert(OSC.Packet == ctx.osc.Packet)
assert(OSC.Bundle == ctx.osc.Bundle)
assert(OSC.bundleTimetag == ctx.osc.bundleTimetag)
assert(OSC.bundleItems == ctx.osc.bundleItems)
assert(OSC.Message == ctx.osc.Message)
assert(OSC.messagePath == ctx.osc.messagePath)
assert(OSC.messageArguments == ctx.osc.messageArguments)
assert(OSC.Timetag == ctx.osc.Timetag)
assert(OSC.timetagIntegral == ctx.osc.timetagIntegral)
assert(OSC.timetagFraction == ctx.osc.timetagFraction)
assert(OSC.Nil == ctx.osc.Nil)
assert(OSC.Impulse == ctx.osc.Impulse)
assert(OSC.Char == ctx.osc.Char)
assert(OSC.RGBA == ctx.osc.RGBA)
-- LV2 Param URID constants
assert(Param.sampleRate == ctx.param.sampleRate)
-- LV2 LV2 URID constants
assert(LV2.minimum == ctx.lv2.minimum)
assert(LV2.maximum == ctx.lv2.maximum)
assert(LV2.scalePoint == ctx.lv2.scalePoint)
-- Buffer size URID constants
assert(Buf_Size.minBlockLength == ctx.bufsz.minBlockLength)
assert(Buf_Size.maxBlockLength == ctx.bufsz.maxBlockLength)
assert(Buf_Size.sequenceSize == ctx.bufsz.sequenceSize)
-- Patch URID constants
assert(Patch.Ack == ctx.patch.Ack)
assert(Patch.Delete == ctx.patch.Delete)
assert(Patch.Copy == ctx.patch.Copy)
assert(Patch.Error == ctx.patch.Error)
assert(Patch.Get == ctx.patch.Get)
assert(Patch.Message == ctx.patch.Message)
assert(Patch.Move == ctx.patch.Move)
assert(Patch.Insert == ctx.patch.Insert)
assert(Patch.Patch == ctx.patch.Patch)
assert(Patch.Post == ctx.patch.Post)
assert(Patch.Put == ctx.patch.Put)
assert(Patch.Request == ctx.patch.Request)
assert(Patch.Response == ctx.patch.Response)
assert(Patch.Set == ctx.patch.Set)
assert(Patch.add == ctx.patch.add)
assert(Patch.accept == ctx.patch.accept)
assert(Patch.body == ctx.patch.body)
assert(Patch.context == ctx.patch.context)
assert(Patch.destination == ctx.patch.destination)
assert(Patch.property == ctx.patch.property)
assert(Patch.readable == ctx.patch.readable)
assert(Patch.remove == ctx.patch.remove)
assert(Patch.request == ctx.patch.request)
assert(Patch.subject == ctx.patch.subject)
assert(Patch.sequenceNumber == ctx.patch.sequenceNumber)
assert(Patch.value == ctx.patch.value)
assert(Patch.wildcard == ctx.patch.wildcard)
assert(Patch.writable == ctx.patch.writable)
-- RDF URID constants
assert(RDF.value == ctx.rdf.value)
assert(RDF.type == ctx.rdf.type)
-- RDFS URID constants
assert(RDFS.label == ctx.rdfs.label)
assert(RDFS.range == ctx.rdfs.range)
assert(RDFS.comment == ctx.rdfs.comment)
-- Units URID constants
assert(Units.Conversion == ctx.units.Conversion)
assert(Units.Unit == ctx.units.Unit)
assert(Units.bar == ctx.units.bar)
assert(Units.beat == ctx.units.beat)
assert(Units.bpm == ctx.units.bpm)
assert(Units.cent == ctx.units.cent)
assert(Units.cm == ctx.units.cm)
assert(Units.coef == ctx.units.coef)
assert(Units.conversion == ctx.units.conversion)
assert(Units.db == ctx.units.db)
assert(Units.degree == ctx.units.degree)
assert(Units.frame == ctx.units.frame)
assert(Units.hz == ctx.units.hz)
assert(Units.inch == ctx.units.inch)
assert(Units.khz == ctx.units.khz)
assert(Units.km == ctx.units.km)
assert(Units.m == ctx.units.m)
assert(Units.mhz == ctx.units.mhz)
assert(Units.midiNote == ctx.units.midiNote)
assert(Units.midiController == ctx.units.midiController)
assert(Units.mile == ctx.units.mile)
assert(Units.min == ctx.units.min)
assert(Units.mm == ctx.units.mm)
assert(Units.ms == ctx.units.ms)
assert(Units.name == ctx.units.name)
assert(Units.oct == ctx.units.oct)
assert(Units.pc == ctx.units.pc)
assert(Units.prefixConversion == ctx.units.prefixConversion)
assert(Units.render == ctx.units.render)
assert(Units.s == ctx.units.s)
assert(Units.semitone12TET == ctx.units.semitone12TET)
assert(Units.symbol == ctx.units.symbol)
assert(Units.unit == ctx.units.unit)
-- Moony URID constants
assert(Moony.color == ctx.moony.color)
assert(Moony.syntax == ctx.moony.syntax)
-- Lua URID constants
assert(Lua.lang == ctx.lua.lang)
Copyright © 2015-2021 Hanspeter Portner (dev@open-music-kontrollers.ch)
This is free software: you can redistribute it and/or modify it under the terms of the Artistic License 2.0 as published by The Perl Foundation.
This source is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Artistic License 2.0 for more details.
You should have received a copy of the Artistic License 2.0 along the source as a COPYING file. If not, obtain it from http://www.perlfoundation.org/artistic_license_2_0.