Compare commits

...

2 Commits

Author SHA1 Message Date
Christopher Arndt ba6aaa22ab Implement MIDI procesing (WIP)
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2024-05-20 13:20:01 +02:00
Christopher Arndt ff6ddf77d2 Move wrapper code into sub-module; adapt/tweak examples
Signed-off-by: Christopher Arndt <chris@chrisarndt.de>
2024-05-18 15:11:02 +02:00
15 changed files with 1032 additions and 42 deletions

View File

@ -22,9 +22,12 @@ template db2coeff(db: cfloat): cfloat =
proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble; proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble;
bundlePath: cstring; features: ptr ptr Lv2Feature): bundlePath: cstring; features: ptr UncheckedArray[ptr Lv2Feature]):
Lv2Handle {.cdecl.} = Lv2Handle {.cdecl.} =
try:
return createShared(AmpPlugin) return createShared(AmpPlugin)
except OutOfMemDefect:
return nil
proc connectPort(instance: Lv2Handle; port: cuint; proc connectPort(instance: Lv2Handle; port: cuint;
@ -78,4 +81,3 @@ proc lv2Descriptor(index: cuint): ptr Lv2Descriptor {.
result.deactivate = deactivate result.deactivate = deactivate
result.cleanup = cleanup result.cleanup = cleanup
result.extensionData = extensionData result.extensionData = extensionData

View File

@ -0,0 +1,8 @@
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
<urn:nymph:examples:miditranspose>
a lv2:Plugin ;
lv2:binary <libmiditranspose.so> ;
rdfs:seeAlso <miditranspose.ttl> .

View File

@ -0,0 +1,66 @@
@prefix atom: <http://lv2plug.in/ns/ext/atom#> .
@prefix bufs: <http://lv2plug.in/ns/ext/buf-size#> .
@prefix doap: <http://usefulinc.com/ns/doap#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
@prefix midi: <http://lv2plug.in/ns/ext/midi#> .
@prefix opts: <http://lv2plug.in/ns/ext/options#> .
@prefix params: <http://lv2plug.in/ns/ext/parameters#> .
@prefix props: <http://lv2plug.in/ns/ext/port-props#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
@prefix urid: <http://lv2plug.in/ns/ext/urid#> .
<urn:nymph:examples:miditranspose>
a lv2:Plugin, lv2:MIDIPlugin , doap:Project ;
lv2:optionalFeature lv2:hardRTCapable , bufs:boundedBlockLength , opts:options ;
lv2:requiredFeature urid:map ;
opts:supportedOption bufs:nominalBlockLength ,
bufs:maxBlockLength ,
params:sampleRate ;
lv2:port [
a lv2:InputPort , atom:AtomPort ;
lv2:index 0 ;
atom:bufferType atom:Sequence ;
atom:supports midi:MidiEvent ;
lv2:designation lv2:control ;
lv2:symbol "midi_in" ;
lv2:name "MIDI In"
] ,
[
a lv2:OutputPort , atom:AtomPort ;
lv2:index 1 ;
atom:bufferType atom:Sequence ;
atom:supports midi:MidiEvent ;
lv2:symbol "midi_out" ;
lv2:name "MIDI Out"
] ,
[
a lv2:InputPort, lv2:ControlPort ;
lv2:index 2 ;
lv2:name "Transposition" ;
lv2:symbol "transposition" ;
lv2:portProperty lv2:integer ;
lv2:default 0 ;
lv2:minimum -12 ;
lv2:maximum 12 ;
units:unit units:semitone12TET
] ;
rdfs:comment "A simple MIDI transposition LV2 plugin." ;
doap:name "nymph miditranspose" ;
doap:license <https://spdx.org/licenses/MIT> ;
doap:maintainer [
foaf:name "Christopher Arndt" ;
foaf:mbox <mailto:info@chrisarndt.de> ;
foaf:homepage <https://gitlab.com/SpotlightKid/nymph> ;
] ;
lv2:microVersion 0 ;
lv2:minorVersion 1 .

View File

@ -0,0 +1,98 @@
## A simple MIDI event processor LV2 plugin
import std/[math, strformat, strutils]
import nymph/[atom, core, midi, util, urid]
import nymph/atom/util
const PluginUri = "urn:nymph:examples:miditranspose"
type
PluginPort {.pure.} = enum
Input, Output, Transposition
MidiTransposePlugin = object
input: ptr AtomSequence
output: ptr AtomSequence
transposition: ptr cfloat
map: ptr UridMap
midi_urid: Urid
proc printFeatures(features: ptr UncheckedArray[ptr Lv2Feature]) =
if features != nil:
var i = 0
while true:
let feature = features[i]
if feature == nil:
break
echo &"URI: {feature[].uri}"
echo &"Data: {cast[int](feature[].data)}"
inc i
proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble;
bundlePath: cstring; features: ptr UncheckedArray[ptr Lv2Feature]):
Lv2Handle {.cdecl.} =
printFeatures(features)
let amp: ptr MidiTransposePlugin = createShared(MidiTransposePlugin)
amp.map = cast[ptr UridMap](lv2FeaturesData(features, lv2UridMap))
assert amp.map != nil
amp.midi_urid = amp.map[].map(amp.map[].handle, lv2MidiMidiEvent)
echo &"{lv2MidiMidiEvent} = {amp.midi_urid.int}"
return cast[Lv2Handle](amp)
proc connectPort(instance: Lv2Handle; port: cuint;
dataLocation: pointer) {.cdecl.} =
let amp = cast[ptr MidiTransposePlugin](instance)
case cast[PluginPort](port)
of PluginPort.Input:
amp.input = cast[ptr AtomSequence](dataLocation)
of PluginPort.Output:
amp.output = cast[ptr AtomSequence](dataLocation)
of PluginPort.Transposition:
amp.transposition = cast[ptr cfloat](dataLocation)
proc activate(instance: Lv2Handle) {.cdecl.} =
discard
proc run(instance: Lv2Handle; nSamples: cuint) {.cdecl.} =
let amp = cast[ptr MidiTransposePlugin](instance)
if amp.input.atom.size > 8:
for ev in items(amp.input):
if Urid(ev.body.`type`) == amp.midi_urid:
# TODO
var msg = cast[ptr UncheckedArray[uint8]](ev.body.addr + 1)
echo &"0x{toHex(msg[0], 2)} 0x{toHex(msg[1], 2)} 0x{toHex(msg[2], 2)}"
proc deactivate(instance: Lv2Handle) {.cdecl.} =
discard
proc cleanup(instance: Lv2Handle) {.cdecl.} =
freeShared(cast[ptr MidiTransposePlugin](instance))
proc extensionData(uri: cstring): pointer {.cdecl.} =
return nil
proc NimMain() {.cdecl, importc.}
proc lv2Descriptor(index: cuint): ptr Lv2Descriptor {.
cdecl, exportc, dynlib, extern: "lv2_descriptor".} =
NimMain()
if index == 0:
result = createShared(Lv2Descriptor)
result.uri = cstring(PluginUri)
result.instantiate = instantiate
result.connectPort = connectPort
result.activate = activate
result.run = run
result.deactivate = deactivate
result.cleanup = cleanup
result.extensionData = extensionData

View File

@ -24,12 +24,15 @@ type
proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble; proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble;
bundlePath: cstring; features: ptr ptr Lv2Feature): bundlePath: cstring; features: ptr UncheckedArray[ptr Lv2Feature]):
Lv2Handle {.cdecl.} = Lv2Handle {.cdecl.} =
try:
let plug = createShared(SVFPlugin) let plug = createShared(SVFPlugin)
plug.svf = initFilterSV(fmLowPass, sampleRate) plug.svf = initFilterSV(fmLowPass, sampleRate)
plug.smoothCutoff = initParamSmooth(20.0, sampleRate) plug.smoothCutoff = initParamSmooth(20.0, sampleRate)
return plug return cast[Lv2Handle](plug)
except OutOfMemDefect:
return nil
proc connectPort(instance: Lv2Handle; port: cuint; proc connectPort(instance: Lv2Handle; port: cuint;

View File

@ -24,7 +24,7 @@ proc setSampleRate*(self: var ParamSmooth, sampleRate: float64) =
self.z = 0.0 self.z = 0.0
proc process*(self: var ParamSmooth, sample: float): float = proc process*(self: var ParamSmooth, sample: float): float {.inline.} =
self.z = (sample * self.b) + (self.z * self.a) self.z = (sample * self.b) + (self.z * self.a)
return self.z return self.z

View File

@ -26,6 +26,7 @@ type Example = tuple
const examples = to_table({ const examples = to_table({
"amp": "urn:nymph:examples:amp", "amp": "urn:nymph:examples:amp",
"multimode_filter": "urn:nymph:examples:multimode-filter", "multimode_filter": "urn:nymph:examples:multimode-filter",
"miditranspose": "urn:nymph:examples:miditranspose",
}) })
@ -83,7 +84,7 @@ task lv2lint, "Run lv2lint check on given example plugin":
let ex = getExample("lv2lint") let ex = getExample("lv2lint")
if fileExists(ex.dll): if fileExists(ex.dll):
exec(&"lv2lint -s NimMain -I \"{ex.bundle}\" \"{ex.uri}\"") exec(&"lv2lint -s NimMain -s NimDestroyGlobals -I \"{ex.bundle}\" \"{ex.uri}\"")
else: else:
echo &"Example '{ex.name}' shared library not found. Use task 'build_ex' to build it." echo &"Example '{ex.name}' shared library not found. Use task 'build_ex' to build it."

View File

@ -1,32 +1,2 @@
const LV2_CORE_URI* = "http://lv2plug.in/ns/lv2core" import nymph/core
export core
type Lv2Handle* = pointer
type Lv2Feature* = object
uri*: cstring
data*: pointer
type Lv2Descriptor* = object
uri*: cstring
instantiate*: proc(descriptor: ptr Lv2Descriptor, sampleRate: cdouble, bundlePath: cstring,
features: ptr ptr Lv2Feature): Lv2Handle {.cdecl.}
connectPort*: proc(instance: Lv2Handle, port: cuint, dataLocation: pointer) {.cdecl.}
activate*: proc(instance: Lv2Handle) {.cdecl.}
run*: proc(instance: Lv2Handle, sampleCount: cuint) {.cdecl.}
deactivate*: proc(instance: Lv2Handle) {.cdecl.}
cleanup*: proc(instance: Lv2Handle) {.cdecl.}
extensionData*: proc(uri: cstring): pointer {.cdecl.}
type lv2Descriptor* = proc(index: cuint): ptr Lv2Descriptor {.cdecl.}

201
src/nymph/atom.nim Normal file
View File

@ -0,0 +1,201 @@
## Copyright 2008-2016 David Robillard <d@drobilla.net>
## SPDX-License-Identifier: ISC
##
## A generic value container and several data types.
##
## See <http://lv2plug.in/ns/ext/atom> for details.
##
const
lv2AtomBaseUri ="http://lv2plug.in/ns/ext/atom"
lv2AtomPrefix = lv2AtomBaseUri & "#"
lv2AtomAtom = lv2AtomPrefix & "Atom"
lv2AtomAtomport = lv2AtomPrefix & "AtomPort"
lv2AtomBlank = lv2AtomPrefix & "Blank"
lv2AtomBool = lv2AtomPrefix & "Bool"
lv2AtomChunk = lv2AtomPrefix & "Chunk"
lv2AtomDouble = lv2AtomPrefix & "Double"
lv2AtomEvent = lv2AtomPrefix & "Event"
lv2AtomFloat = lv2AtomPrefix & "Float"
lv2AtomInt = lv2AtomPrefix & "Int"
lv2AtomLiteral = lv2AtomPrefix & "Literal"
lv2AtomLong = lv2AtomPrefix & "Long"
lv2AtomNumber = lv2AtomPrefix & "Number"
lv2AtomObject = lv2AtomPrefix & "Object"
lv2AtomPath = lv2AtomPrefix & "Path"
lv2AtomProperty = lv2AtomPrefix & "Property"
lv2AtomResource = lv2AtomPrefix & "Resource"
lv2AtomSequence = lv2AtomPrefix & "Sequence"
lv2AtomSound = lv2AtomPrefix & "Sound"
lv2AtomString = lv2AtomPrefix & "String"
lv2AtomTuple = lv2AtomPrefix & "Tuple"
lv2AtomUri = lv2AtomPrefix & "URI"
lv2AtomUrid = lv2AtomPrefix & "URID"
lv2AtomVector = lv2AtomPrefix & "Vector"
lv2AtomAtomtransfer = lv2AtomPrefix & "atomTransfer"
lv2AtomBeattime = lv2AtomPrefix & "beatTime"
lv2AtomBuffer= lv2AtomPrefix & "bufferType"
lv2AtomChild= lv2AtomPrefix & "childType"
lv2AtomEventtransfer = lv2AtomPrefix & "eventTransfer"
lv2AtomFrametime = lv2AtomPrefix & "frameTime"
lv2AtomSupports = lv2AtomPrefix & "supports"
lv2AtomTimeunit = lv2AtomPrefix & "timeUnit"
##
## Return a pointer to the contents of an Atom. The "contents" of an atom
## is the data past the complete type-specific header.
## @param type The type of the atom, for example AtomString.
## @param atom A variable-sized atom.
##
template atomContents*(`type`, atom: untyped): pointer =
cast[ptr uint8](atom) + sizeof(`type`)
#[
{.push header: "lv2/atom/atom.h".}
var atomReferenceType* {.importc: "LV2_ATOM_REFERENCE_TYPE".}: int
##
## Const version of LV2_ATOM_CONTENTS.
##
proc atomContentsConst*(`type`: untyped; atom: untyped) {.importc: "LV2_ATOM_CONTENTS_CONST".}
##
## Return a pointer to the body of an Atom. The "body" of an atom is the
## data just past the LV2_Atom head (i.e. the same offset for all types).
##
proc atomBody*(atom: untyped) {.importc: "LV2_ATOM_BODY".}
##
## Const version of LV2_ATOM_BODY.
##
proc atomBodyConst*(atom: untyped) {.importc: "LV2_ATOM_BODY_CONST".}
{.pop.}
]#
type
## The header of an atom:Atom.
Atom* {.bycopy.} = object
size*: uint32 ## Size in bytes, not including type and size.
`type`*: uint32 ## of this atom (mapped URI).
## An atom:Int or atom:Bool. May be cast to LV2_Atom.
AtomInt* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: int32 ## Integer value.
## An atom:Long. May be cast to LV2_Atom.
AtomLong* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: int64 ## Integer value.
## An atom:Float. May be cast to LV2_Atom.
AtomFloat* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: cfloat ## Floating point value.
## An atom:Double. May be cast to LV2_Atom.
AtomDouble* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: cdouble ## Floating point value.
## An atom:Bool. May be cast to LV2_Atom.
AtomBool* = distinct AtomInt
## An atom:URID. May be cast to LV2_Atom.
AtomUrid* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: uint32 ## URID.
## An atom:String. May be cast to LV2_Atom.
AtomString* {.bycopy.} = object
atom*: Atom ## Atom header.
## Contents (a null-terminated UTF-8 string) follow here.
## The body of an atom:Literal.
AtomLiteralBody* {.bycopy.} = object
datatype*: uint32 ## DataURID.
lang*: uint32 ## Language URID.
## Contents (a null-terminated UTF-8 string) follow here.
## An atom:Literal. May be cast to LV2_Atom.
AtomLiteral* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: AtomLiteralBody ## Body.
## An atom:Tuple. May be cast to LV2_Atom.
AtomTuple* {.bycopy.} = object
atom*: Atom ## Atom header.
## Contents (a series of complete atoms) follow here.
## The body of an atom:Vector.
AtomVectorBody* {.bycopy.} = object
childSize*: uint32 ## The size of each element in the vector.
childType*: uint32 ## The of each element in the vector.
## Contents (a series of packed atom bodies) follow here.
## An atom:Vector. May be cast to LV2_Atom.
AtomVector* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: AtomVectorBody ## Body.
## The body of an atom:Property (typically in an atom:Object).
AtomPropertyBody* {.bycopy.} = object
key*: uint32 ## Key (predicate) (mapped URI).
context*: uint32 ## Context URID (may be, and generally is, 0).
value*: Atom ## Value atom header.
## Value atom body follows here.
## An atom:Property. May be cast to LV2_Atom.
AtomProperty* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: AtomPropertyBody ## Body.
## The body of an atom:Object. May be cast to LV2_Atom.
AtomObjectBody* {.bycopy.} = object
id*: uint32 ## URID, or 0 for blank.
otype*: uint32 ## URID (same as rdf:type, for fast dispatch).
## Contents (a series of property bodies) follow here.
## An atom:Object. May be cast to LV2_Atom.
AtomObject* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: AtomObjectBody ## Body.
## The header of an atom:Event. Note this is NOT an LV2_Atom.
AtomEventTime* {.bycopy, union.} = object
frames*: int64 ## Time in audio frames.
beats*: cdouble ## Time in beats.
AtomEvent* {.bycopy.} = object
time*: AtomEventTime ## Time stamp. Which is valid is determined by context.
body*: Atom ## Event body atom header.
## Body atom contents follow here.
##
## The body of an atom:Sequence (a sequence of events).
##
## The unit field is either a URID that described an appropriate time stamp
## type, or may be 0 where a default stamp is known. For
## LV2_Descriptor::run(), the default stamp is audio frames.
##
## The contents of a sequence is a series of LV2_Atom_Event, each aligned
## to 64-bits, for example:
## <pre>
## | Event 1 (size 6) | Event 2
## | | | | | | | | |
## | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
## |FRAMES |SIZE |TYPE |DATADATADATAPAD|FRAMES |...
## </pre>
##
AtomSequenceBody* {.bycopy.} = object
unit*: uint32 ## URID of unit of event time stamps.
pad*: uint32 ## Currently unused.
## Contents (a series of events) follow here.
## An atom:Sequence.
AtomSequence* {.bycopy.} = object
atom*: Atom ## Atom header.
body*: AtomSequenceBody ## Body.

182
src/nymph/atom/util.nim Normal file
View File

@ -0,0 +1,182 @@
## Copyright 2008-2015 David Robillard <d@drobilla.net>
## SPDX-License-Identifier: ISC
##
## Helper functions for the LV2 Atom extension.
##
## Note these functions are all static inline, do not take their address.
##
## This header is non-normative, it is provided for convenience.
##
## Utilities for working with atoms.
##
from system/ansi_c import c_memcmp, c_memcpy
import ../atom
##
## Increment pointer `p` by `offset` that jumps memory in increments of
## the size of `T`.
##
proc `+`*[T](p: ptr T, offset: SomeInteger): ptr T =
return cast[ptr T](cast[int](p) +% (offset.int * sizeof(T)))
##
## Pad a size to 64 bits.
##
proc atomPadSize*(size: uint32): uint32 {.inline.} =
let mask: uint32 = 7
return (size + mask) and not mask
##
## Return the total size of `atom`, including the header.
##
proc atomTotalSize*(atom: ptr Atom): uint32 {.inline.} =
return cast[uint32](sizeof(atom)) + atom.size
##
## Return true iff `atom` is null.
##
proc atomIsNull*(atom: ptr Atom): bool {.inline.} =
return atom == nil or (atom.`type` == 0 and atom.size == 0)
##
## Return true iff `a` is equal to `b`.
##
proc atomEquals*(a: ptr Atom; b: ptr Atom): bool {.inline.} =
return (a == b) or
((a.`type` == b.`type`) and (a.size == b.size) and
c_memcmp(a + 1, b + 1, a.size) == 0)
##
## Sequence Iterator
##
## Get an iterator pointing to the first event in a Sequence body.
##
proc atomSequenceBegin*(body: ptr AtomSequenceBody): ptr AtomEvent {.inline.} =
return cast[ptr AtomEvent](body + 1)
##
## Get an iterator pointing to the end of a Sequence body.
##
proc atomSequenceEnd*(body: ptr AtomSequenceBody; size: uint32): ptr AtomEvent {.inline.} =
return cast[ptr AtomEvent](cast[ptr uint8](body) + atomPadSize(size))
##
## Return true iff `i` has reached the end of `body`.
##
proc atomSequenceIsEnd*(body: ptr AtomSequenceBody; size: Natural; i: ptr AtomEvent): bool {.inline.} =
return cast[ptr uint8](i) >= (cast[ptr uint8](body) + size.uint)
##
## Return an iterator to the element following `i`.
##
proc atomSequenceNext*(i: ptr AtomEvent): ptr AtomEvent {.inline.} =
return cast[ptr AtomEvent](i + sizeof(AtomEvent) + atomPadSize(i.body.size))
##
## An iterator for looping over all events in a Sequence.
## @param seq The sequence to iterate over
##
iterator items*(seq: ptr AtomSequence): ptr AtomEvent {.inline.} =
var event = atomSequenceBegin(seq.body.addr)
while not atomSequenceIsEnd(seq.body.addr, seq.atom.size, event):
yield event
event = atomSequenceNext(event)
## TODO: Like LV2_ATOM_SEQUENCE_FOREACH but for a headerless sequence body.
##
## Sequence Utilities
##
## Clear all events from `sequence`.
##
## This simply resets the size field, the other fields are left untouched.
##
proc atomSequenceClear*(seq: ptr AtomSequence) {.inline.} =
seq.atom.size = sizeof(AtomSequenceBody).uint32
##
## Append an event at the end of `sequence`.
##
## @param seq Sequence to append to.
## @param capacity Total capacity of the sequence atom
## (as set by the host for sequence output ports).
## @param event Event to write.
##
## @return A pointer to the newly written event in `seq`,
## or NULL on failure (insufficient space).
##
proc atomSequenceAppendEvent*(seq: ptr AtomSequence; capacity: uint32;
event: ptr AtomEvent): ptr AtomEvent {.inline.} =
let totalSize = sizeof(AtomEvent).uint32 + event.body.size
if capacity - seq.atom.size < totalSize:
return nil
var e = atomSequenceEnd(seq.body.addr, seq.atom.size)
c_memcpy(e, event, totalSize)
seq.atom.size += atomPadSize(totalSize)
return e
##
## Tuple Iterator
##
## Get an iterator pointing to the first element in `tup`.
##
proc atomTupleBegin*(tup: ptr AtomTuple): ptr Atom {.inline.} =
return cast[ptr Atom](atomContents(Atom, tup))
##
## Return true iff `i` has reached the end of `body`.
##
proc atomTupleIsEnd*(body: pointer; size: Natural; i: ptr Atom): bool {.inline.} =
return cast[ptr uint8](i) >= (cast[ptr uint8](body) + size.int)
##
## Return an iterator to the element following `i`.
##
proc atomTupleNext*(i: ptr Atom): ptr Atom {.inline.} =
return cast[ptr Atom](cast[ptr uint8](i) + sizeof(Atom) + atomPadSize(i.size))
## TODO:
##
## A iterator for looping over all properties of a Tuple.
## @param tuple The tuple to iterate over
## TODO:
## Like LV2_ATOM_TUPLE_FOREACH but for a headerless tuple body.
##
## Object Iterator
##
## Return a pointer to the first property in `body`.
##
proc atomObjectBegin*(body: ptr AtomObjectBody): ptr AtomPropertyBody {.inline.} =
return cast[ptr AtomPropertyBody](body + 1)
##
## Return true iff `i` has reached the end of `obj`.
##
proc atomObjectIsEnd*(body: ptr AtomObjectBody; size: Natural; i: ptr AtomPropertyBody): bool {.inline.} =
return cast[ptr uint8](i) >= (cast[ptr uint8](body) + size.int)
##
## Return an iterator to the property following `i`.
##
proc atomObjectNext*(i: ptr AtomPropertyBody): ptr AtomPropertyBody {.inline.} =
let value = cast[ptr Atom](cast[ptr uint8](i) + 2 * sizeof(uint32))
return cast[ptr AtomPropertyBody](cast[ptr uint8](i) + atomPadSize(sizeof(AtomPropertyBody).uint32 + value.size))
## TODO:
##
## A iterator for looping over all properties of an Object.
## @param obj The object to iterate over
##
## TODO:
##
## Like LV2_ATOM_OBJECT_FOREACH but for a headerless object body.
##

120
src/nymph/core.nim Normal file
View File

@ -0,0 +1,120 @@
const
lv2CoreBaseUri = "http://lv2plug.in/ns/lv2core"
lv2CorePrefix = lv2CoreBaseUri & "#"
# Classes (http://lv2plug.in/ns/lv2core#ref-classes):
lv2CoreClassAllpassPlugin = lv2CorePrefix & "AllpassPlugin"
lv2CoreClassAmplifierPlugin = lv2CorePrefix & "AmplifierPlugin"
lv2CoreClassAnalyserPlugin = lv2CorePrefix & "AnalyserPlugin"
lv2CoreClassAudioPort = lv2CorePrefix & "AudioPort"
lv2CoreClassBandpassPlugin = lv2CorePrefix & "BandpassPlugin"
lv2CoreClassCVPort = lv2CorePrefix & "CVPort"
lv2CoreClassChorusPlugin = lv2CorePrefix & "ChorusPlugin"
lv2CoreClassCombPlugin = lv2CorePrefix & "CombPlugin"
lv2CoreClassCompressorPlugin = lv2CorePrefix & "CompressorPlugin"
lv2CoreClassConstantPlugin = lv2CorePrefix & "ConstantPlugin"
lv2CoreClassControlPort = lv2CorePrefix & "ControlPort"
lv2CoreClassConverterPlugin = lv2CorePrefix & "ConverterPlugin"
lv2CoreClassDelayPlugin = lv2CorePrefix & "DelayPlugin"
lv2CoreClassDistortionPlugin = lv2CorePrefix & "DistortionPlugin"
lv2CoreClassDynamicsPlugin = lv2CorePrefix & "DynamicsPlugin"
lv2CoreClassEQPlugin = lv2CorePrefix & "EQPlugin"
lv2CoreClassEnvelopePlugin = lv2CorePrefix & "EnvelopePlugin"
lv2CoreClassExpanderPlugin = lv2CorePrefix & "ExpanderPlugin"
lv2CoreClassExtensionData = lv2CorePrefix & "ExtensionData"
lv2CoreClassFeature = lv2CorePrefix & "Feature"
lv2CoreClassFilterPlugin = lv2CorePrefix & "FilterPlugin"
lv2CoreClassFlangerPlugin = lv2CorePrefix & "FlangerPlugin"
lv2CoreClassFunctionPlugin = lv2CorePrefix & "FunctionPlugin"
lv2CoreClassGatePlugin = lv2CorePrefix & "GatePlugin"
lv2CoreClassGeneratorPlugin = lv2CorePrefix & "GeneratorPlugin"
lv2CoreClassHighpassPlugin = lv2CorePrefix & "HighpassPlugin"
lv2CoreClassInputPort = lv2CorePrefix & "InputPort"
lv2CoreClassInstrumentPlugin = lv2CorePrefix & "InstrumentPlugin"
lv2CoreClassLimiterPlugin = lv2CorePrefix & "LimiterPlugin"
lv2CoreClassLowpassPlugin = lv2CorePrefix & "LowpassPlugin"
lv2CoreClassMixerPlugin = lv2CorePrefix & "MixerPlugin"
lv2CoreClassModulatorPlugin = lv2CorePrefix & "ModulatorPlugin"
lv2CoreClassMultiEQPlugin = lv2CorePrefix & "MultiEQPlugin"
lv2CoreClassOscillatorPlugin = lv2CorePrefix & "OscillatorPlugin"
lv2CoreClassOutputPort = lv2CorePrefix & "OutputPort"
lv2CoreClassParaEQPlugin = lv2CorePrefix & "ParaEQPlugin"
lv2CoreClassPhaserPlugin = lv2CorePrefix & "PhaserPlugin"
lv2CoreClassPitchPlugin = lv2CorePrefix & "PitchPlugin"
lv2CoreClassPlugin = lv2CorePrefix & "Plugin"
lv2CoreClassPluginBase = lv2CorePrefix & "PluginBase"
lv2CoreClassPoint = lv2CorePrefix & "Point"
lv2CoreClassPort = lv2CorePrefix & "Port"
lv2CoreClassPortProperty = lv2CorePrefix & "Port"
lv2CoreClassResource = lv2CorePrefix & "Resource"
lv2CoreClassReverbPlugin = lv2CorePrefix & "ReverbPlugin"
lv2CoreClassScalePoint = lv2CorePrefix & "ScalePoint"
lv2CoreClassSimulatorPlugin = lv2CorePrefix & "SimulatorPlugin"
lv2CoreClassSpatialPlugin = lv2CorePrefix & "SpatialPlugin"
lv2CoreClassSpecification = lv2CorePrefix & "Specification"
lv2CoreClassSpectralPlugin = lv2CorePrefix & "SpectralPlugin"
lv2CoreClassUtilityPlugin = lv2CorePrefix & "UtilityPlugin"
lv2CoreClassWaveshaperPlugin = lv2CorePrefix & "WaveshaperPlugin"
# Properties (http://lv2plug.in/ns/lv2core#ref-properties):
lv2CorePropertyAppliesTo = lv2CorePrefix & "appliesTo"
lv2CorePropertyBinary = lv2CorePrefix & "binary"
lv2CorePropertyDefault = lv2CorePrefix & "default"
lv2CorePropertyDesignation = lv2CorePrefix & "designation"
lv2CorePropertyDocumentation = lv2CorePrefix & "documentation"
lv2CorePropertyExtensionData = lv2CorePrefix & "extensionData"
lv2CorePropertyIndex = lv2CorePrefix & "index"
lv2CorePropertyLatency = lv2CorePrefix & "latency"
lv2CorePropertyMaximum = lv2CorePrefix & "maximum"
lv2CorePropertyMicroVersion = lv2CorePrefix & "microVersion"
lv2CorePropertyMinimum = lv2CorePrefix & "minimum"
lv2CorePropertyMinorVersion = lv2CorePrefix & "minorVersion"
lv2CorePropertyName = lv2CorePrefix & "name"
lv2CorePropertyOptionalFeature = lv2CorePrefix & "optionalFeature"
lv2CorePropertyPort = lv2CorePrefix & "port"
lv2CorePropertyPortProperty = lv2CorePrefix & "portProperty"
lv2CorePropertyProject = lv2CorePrefix & "project"
lv2CorePropertyPrototype = lv2CorePrefix & "prototype"
lv2CorePropertyReportsLatency = lv2CorePrefix & "reportsLatency"
lv2CorePropertyRequiredFeature = lv2CorePrefix & "requiredFeature"
lv2CorePropertyScalePoint = lv2CorePrefix & "scalePoint"
lv2CorePropertySymbol = lv2CorePrefix & "symbol"
# Instances (http://lv2plug.in/ns/lv2core#ref-instances)
lv2CoreInstanceConnectionOptional = lv2CorePrefix & "connectionOptional"
lv2CoreInstanceControl = lv2CorePrefix & "control"
lv2CoreInstanceEnumeration = lv2CorePrefix & "enumeration"
lv2CoreInstanceFreeWheeling = lv2CorePrefix & "freeWheeling"
lv2CoreInstanceHardRTCapable = lv2CorePrefix & "hardRTCapable"
lv2CoreInstanceInPlaceBroken = lv2CorePrefix & "inPlaceBroken"
lv2CoreInstanceInteger = lv2CorePrefix & "integer"
lv2CoreInstanceIsLive = lv2CorePrefix & "isLive"
lv2CoreInstanceSampleRateInstance = lv2CorePrefix & "sampleRate"
lv2CoreInstanceToggledInstance = lv2CorePrefix & "toggled"
type Lv2Handle* = pointer
type Lv2Feature* = object
uri*: cstring
data*: pointer
type Lv2Descriptor* = object
uri*: cstring
instantiate*: proc(descriptor: ptr Lv2Descriptor, sampleRate: cdouble, bundlePath: cstring,
features: ptr UncheckedArray[ptr Lv2Feature]): Lv2Handle {.cdecl.}
connectPort*: proc(instance: Lv2Handle, port: cuint, dataLocation: pointer) {.cdecl.}
activate*: proc(instance: Lv2Handle) {.cdecl.}
run*: proc(instance: Lv2Handle, sampleCount: cuint) {.cdecl.}
deactivate*: proc(instance: Lv2Handle) {.cdecl.}
cleanup*: proc(instance: Lv2Handle) {.cdecl.}
extensionData*: proc(uri: cstring): pointer {.cdecl.}
type lv2Descriptor* = proc(index: cuint): ptr Lv2Descriptor {.cdecl.}

189
src/nymph/midi.nim Normal file
View File

@ -0,0 +1,189 @@
## Copyright 2012-2016 David Robillard <d@drobilla.net>
## SPDX-License-Identifier: ISC
##
## Definitions of standard MIDI messages.
##
## See <http://lv2plug.in/ns/ext/midi> for details.
##
const
lv2MidiBaseUri = "http://lv2plug.in/ns/ext/midi"
lv2MidiPrefix = lv2MidiBaseUri & "#"
lv2MidiActiveSense* = lv2MidiPrefix & "ActiveSense"
lv2MidiAftertouch* = lv2MidiPrefix & "Aftertouch"
lv2MidiBender* = lv2MidiPrefix & "Bender"
lv2MidiChannelPressure* = lv2MidiPrefix & "ChannelPressure"
lv2MidiChunk* = lv2MidiPrefix & "Chunk"
lv2MidiClock* = lv2MidiPrefix & "Clock"
lv2MidiContinue* = lv2MidiPrefix & "Continue"
lv2MidiController* = lv2MidiPrefix & "Controller"
lv2MidiMidiEvent* = lv2MidiPrefix & "MidiEvent"
lv2MidiNoteOff* = lv2MidiPrefix & "NoteOff"
lv2MidiNoteOn* = lv2MidiPrefix & "NoteOn"
lv2MidiProgramChange* = lv2MidiPrefix & "ProgramChange"
lv2MidiQuarterFrame* = lv2MidiPrefix & "QuarterFrame"
lv2MidiReset* = lv2MidiPrefix & "Reset"
lv2MidiSongPosition* = lv2MidiPrefix & "SongPosition"
lv2MidiSongSelect* = lv2MidiPrefix & "SongSelect"
lv2MidiStart* = lv2MidiPrefix & "Start"
lv2MidiStop* = lv2MidiPrefix & "Stop"
lv2MidiSystemCommon* = lv2MidiPrefix & "SystemCommon"
lv2MidiSystemExclusive* = lv2MidiPrefix & "SystemExclusive"
lv2MidiSystemMessage* = lv2MidiPrefix & "SystemMessage"
lv2MidiSystemRealtime* = lv2MidiPrefix & "SystemRealtime"
lv2MidiTick* = lv2MidiPrefix & "Tick"
lv2MidiTuneRequest* = lv2MidiPrefix & "TuneRequest"
lv2MidiVoiceMessage* = lv2MidiPrefix & "VoiceMessage"
lv2MidiPropBenderValue* = lv2MidiPrefix & "benderValue"
lv2MidiPropBinding* = lv2MidiPrefix & "binding"
lv2MidiPropByteNumber* = lv2MidiPrefix & "byteNumber"
lv2MidiPropChannel* = lv2MidiPrefix & "channel"
lv2MidiPropChunk* = lv2MidiPrefix & "chunk"
lv2MidiPropControllerNumber* = lv2MidiPrefix & "controllerNumber"
lv2MidiPropControllerValue* = lv2MidiPrefix & "controllerValue"
lv2MidiPropNoteNumber* = lv2MidiPrefix & "noteNumber"
lv2MidiPropPressure* = lv2MidiPrefix & "pressure"
lv2MidiPropProgramNumber* = lv2MidiPrefix & "programNumber"
lv2MidiPropProperty* = lv2MidiPrefix & "property"
lv2MidiPropSongNumber* = lv2MidiPrefix & "songNumber"
lv2MidiPropSongPosition* = lv2MidiPrefix & "songPosition"
lv2MidiPropStatus* = lv2MidiPrefix & "status"
lv2MidiPropStatusMask* = lv2MidiPrefix & "statusMask"
lv2MidiPropVelocity* = lv2MidiPrefix & "velocity"
##
## MIDI Message Type
##
## This includes both voice messages (which have a channel) and system messages
## (which do not), as well as a sentinel value for invalid messages. To get
## the type of a message suitable for use in a switch statement, use
## midiGetMessageType() on the status byte.
##
type
MidiMessageType* = enum
midiMsgInvalid = 0, # Invalid Message
midiMsgNoteOff = 0x80, # Note Off
midiMsgNoteOn = 0x90, # Note On
midiMsgNotePressure = 0xA0, # Note Pressure
midiMsgController = 0xB0, # Controller
midiMsgPgmChange = 0xC0, # Program Change
midiMsgChannelPressure = 0xD0, # Channel Pressure
midiMsgBender = 0xE0, # Pitch Bender
midiMsgSystemExclusive = 0xF0, # System Exclusive Begin
midiMsgMtcQuarter = 0xF1, # MTC Quarter Frame
midiMsgSongPos = 0xF2, # Song Position
midiMsgSongSelect = 0xF3, # Song Select
midiMsgTuneRequest = 0xF6, # Tune Request
midiMsgClock = 0xF8, # Clock
midiMsgStart = 0xFA, # Start
midiMsgContinue = 0xFB, # Continue
midiMsgStop = 0xFC, # Stop
midiMsgActiveSense = 0xFE, # Active Sensing
midiMsgReset = 0xFF # Reset
##
## Standard MIDI Controller Numbers
##
type
MidiController* = enum
midiCtlMsbBank = 0x00, ## Bank Selection
midiCtlMsbModwheel = 0x01, # Modulation
midiCtlMsbBreath = 0x02, # Breath
midiCtlMsbFoot = 0x04, # Foot
midiCtlMsbPortamentoTime = 0x05, # Portamento Time
midiCtlMsbDataEntry = 0x06, # Data Entry
midiCtlMsbMainVolume = 0x07, # Main Volume
midiCtlMsbBalance = 0x08, # Balance
midiCtlMsbPan = 0x0A, # Panpot
midiCtlMsbExpression = 0x0B, # Expression
midiCtlMsbEffect1 = 0x0C, # Effect1
midiCtlMsbEffect2 = 0x0D, # Effect2
midiCtlMsbGeneralPurpose1 = 0x10, # General Purpose 1
midiCtlMsbGeneralPurpose2 = 0x11, # General Purpose 2
midiCtlMsbGeneralPurpose3 = 0x12, # General Purpose 3
midiCtlMsbGeneralPurpose4 = 0x13, # General Purpose 4
midiCtlLsbBank = 0x20, # Bank Selection
midiCtlLsbModwheel = 0x21, # Modulation
midiCtlLsbBreath = 0x22, # Breath
midiCtlLsbFoot = 0x24, # Foot
midiCtlLsbPortamentoTime = 0x25, # Portamento Time
midiCtlLsbDataEntry = 0x26, # Data Entry
midiCtlLsbMainVolume = 0x27, # Main Volume
midiCtlLsbBalance = 0x28, # Balance
midiCtlLsbPan = 0x2A, # Panpot
midiCtlLsbExpression = 0x2B, # Expression
midiCtlLsbEffect1 = 0x2C, # Effect1
midiCtlLsbEffect2 = 0x2D, # Effect2
midiCtlLsbGeneralPurpose1 = 0x30, # General Purpose 1
midiCtlLsbGeneralPurpose2 = 0x31, # General Purpose 2
midiCtlLsbGeneralPurpose3 = 0x32, # General Purpose 3
midiCtlLsbGeneralPurpose4 = 0x33, # General Purpose 4
midiCtlSustain = 0x40, # Sustain Pedal
midiCtlPortamento = 0x41, # Portamento
midiCtlSostenuto = 0x42, # Sostenuto
midiCtlSoftPedal = 0x43, # Soft Pedal
midiCtlLegatoFootswitch = 0x44, # Legato Foot Switch
midiCtlHold2 = 0x45, # Hold2
midiCtlSc1SoundVariation = 0x46, # SC1 Sound Variation
midiCtlSc2Timbre = 0x47, # SC2 Timbre
midiCtlSc3ReleaseTime = 0x48, # SC3 Release Time
midiCtlSc4AttackTime = 0x49, # SC4 Attack Time
midiCtlSc5Brightness = 0x4A, # SC5 Brightness
midiCtlSc6 = 0x4B, # SC6
midiCtlSc7 = 0x4C, # SC7
midiCtlSc8 = 0x4D, # SC8
midiCtlSc9 = 0x4E, # SC9
midiCtlSc10 = 0x4F, # SC10
midiCtlGeneralPurpose5 = 0x50, # General Purpose 5
midiCtlGeneralPurpose6 = 0x51, # General Purpose 6
midiCtlGeneralPurpose7 = 0x52, # General Purpose 7
midiCtlGeneralPurpose8 = 0x53, # General Purpose 8
midiCtlPortamentoControl = 0x54, # Portamento Control
midiCtlE1ReverbDepth = 0x5B, # E1 Reverb Depth
midiCtlE2TremoloDepth = 0x5C, # E2 Tremolo Depth
midiCtlE3ChorusDepth = 0x5D, # E3 Chorus Depth
midiCtlE4DetuneDepth = 0x5E, # E4 Detune Depth
midiCtlE5PhaserDepth = 0x5F, # E5 Phaser Depth
midiCtlDataIncrement = 0x60, # Data Increment
midiCtlDataDecrement = 0x61, # Data Decrement
midiCtlNrpnLsb = 0x62, # Non-registered Parameter Number
midiCtlNrpnMsb = 0x63, # Non-registered Parameter Number
midiCtlRpnLsb = 0x64, # Registered Parameter Number
midiCtlRpnMsb = 0x65, # Registered Parameter Number
midiCtlAllSoundsOff = 0x78, # All Sounds Off
midiCtlResetControllers = 0x79, # Reset Controllers
midiCtlLocalControlSwitch = 0x7A, # Local Control Switch
midiCtlAllNotesOff = 0x7B, # All Notes Off
midiCtlOmniOff = 0x7C, # Omni Off
midiCtlOmniOn = 0x7D, # Omni On
midiCtlMono1 = 0x7E, # Mono1
midiCtlMono2 = 0x7F # Mono2
##
## Return true iff `msg` is a MIDI voice message (which has a channel).
##
proc midiIsVoiceMessage*(msg: UncheckedArray[uint8]): bool {.inline.} =
return msg[0] >= 0x80 and msg[0] < 0xF0
##
## Return true iff `msg` is a MIDI system message (which has no channel).
##
proc midiIsSystemMessage*(msg: UncheckedArray[uint8]): bool {.inline.} =
case msg[0]
of 0xF4, 0xF5, 0xF7, 0xF9, 0xFD:
return false
else:
return (msg[0] and 0xF0) == 0xF0
##
## Return the type of a MIDI message.
##
## @param msg Pointer to the start (status byte) of a MIDI message.
##
proc midiGetMessageType*(msg: UncheckedArray[uint8]): MidiMessageType {.inline.} =
if midiIsVoiceMessage(msg):
return cast[MidiMessageType](msg[0] and 0xF0)
if midiIsSystemMessage(msg):
return cast[MidiMessageType](msg[0])
return midiMsgInvalid

8
src/nymph/plugin.nim Normal file
View File

@ -0,0 +1,8 @@
plugin:
urid: urn:foobar#v1
port_group:
name: "stereo"
audio_input:
"in_l"
audio_input:
"in_r"

100
src/nymph/urid.nim Normal file
View File

@ -0,0 +1,100 @@
## Copyright 2008-2016 David Robillard <d@drobilla.net>
## Copyright 2011 Gabriel M. Beddingfield <gabrbedd@gmail.com>
## SPDX-License-Identifier: ISC
##
## Features for mapping URIs to and from integers.
##
## See <http://lv2plug.in/ns/ext/urid> for details.
##
const
lv2UridBaseUri* = "http://lv2plug.in/ns/ext/urid"
lv2UridPrefix* = lv2UridBaseUri & "#"
lv2UridMap* = lv2UridPrefix & "map"
lv2UridUnmap* = lv2UridPrefix & "unmap"
##
## Opaque pointer to host data for uridMap.
##
type UridMapHandle* = pointer
##
## Opaque pointer to host data for uridUnmap.
##
type UridUnmapHandle* = pointer
##
## URI mapped to an integer.
##
type Urid* = distinct uint32
##
## URID Map Feature (LV2_URID__map)
##
type UridMap* {.bycopy.} = object
##
## Opaque pointer to host data.
##
## This MUST be passed to map_uri() whenever it is called.
## Otherwise, it must not be interpreted in any way.
##
handle*: UridMapHandle
##
## Get the numeric ID of a URI.
##
## If the ID does not already exist, it will be created.
##
## This function is referentially transparent; any number of calls with the
## same arguments is guaranteed to return the same value over the life of a
## plugin instance. Note, however, that several URIs MAY resolve to the
## same ID if the host considers those URIs equivalent.
##
## This function is not necessarily very fast or RT-safe: plugins SHOULD
## cache any IDs they might need in performance critical situations.
##
## The return value 0 is reserved and indicates that an ID for that URI
## could not be created for whatever reason. However, hosts SHOULD NOT
## return 0 from this function in non-exceptional circumstances (i.e. the
## URI map SHOULD be dynamic).
##
## @param handle Must be the callback_data member of this struct.
## @param uri The URI to be mapped to an integer ID.
##
map*: proc (handle: UridMapHandle; uri: cstring): Urid
##
## URI Unmap Feature (LV2_URID__unmap)
##
type UridUnmap* {.bycopy.} = object
##
## Opaque pointer to host data.
##
## This MUST be passed to unmap() whenever it is called.
## Otherwise, it must not be interpreted in any way.
##
handle*: UridUnmapHandle
##
## Get the URI for a previously mapped numeric ID.
##
## Returns NULL if `urid` is not yet mapped. Otherwise, the corresponding
## URI is returned in a canonical form. This MAY not be the exact same
## string that was originally passed to LV2_URID_Map::map(), but it MUST be
## an identical URI according to the URI syntax specification (RFC3986). A
## non-NULL return for a given `urid` will always be the same for the life
## of the plugin. Plugins that intend to perform string comparison on
## unmapped URIs SHOULD first canonicalise URI strings with a call to
## map_uri() followed by a call to unmap_uri().
##
## @param handle Must be the callback_data member of this struct.
## @param urid The ID to be mapped back to the URI string.
##
unmap*: proc (handle: UridUnmapHandle; urid: Urid): cstring
proc `==`* (x: Urid, y: Urid): bool {.borrow.}

42
src/nymph/util.nim Normal file
View File

@ -0,0 +1,42 @@
import ../nymph
## Return the data for a feature in a features array.
## If the feature is not found, nil is returned. Note that this function is
## only useful for features with data, and can not detect features that are
## present but have nil data.
proc lv2FeaturesData*(features: ptr UncheckedArray[ptr Lv2Feature], uri: string): pointer =
if features != nil:
var i = 0
while true:
let feature = features[i]
if feature == nil:
break
if feature[].uri == uri.cstring:
return feature[].data
inc i
return nil
## Query a features array.
##
## This function allows getting several features in one call, and detect
## missing required features, with the same caveat of lv2_features_data().
##
## The arguments should be a series of const char* uri, void** data, bool
## required, terminated by a NULL URI. The data pointers MUST be initialized
## to NULL. For example:
##
## LV2_URID_Log* log = NULL;
## LV2_URID_Map* map = NULL;
## const char* missing = lv2_features_query(
## features,
## LV2_LOG__log, &log, false,
## LV2_URID__map, &map, true,
## NULL);
##
##
## @return NULL on success, otherwise the URI of this missing feature.