From ba6aaa22ab65787560d0a7da4f417e02c5f04c0c Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Mon, 20 May 2024 13:20:01 +0200 Subject: [PATCH] Implement MIDI procesing (WIP) Signed-off-by: Christopher Arndt --- examples/miditranspose.lv2/manifest.ttl | 8 + examples/miditranspose.lv2/miditranspose.ttl | 66 ++++++ examples/miditranspose.nim | 98 +++++++++ nymph.nimble | 3 +- src/nymph/atom.nim | 201 +++++++++++++++++++ src/nymph/atom/util.nim | 182 +++++++++++++++++ src/nymph/midi.nim | 189 +++++++++++++++++ src/nymph/plugin.nim | 8 + src/nymph/urid.nim | 100 +++++++++ src/nymph/util.nim | 42 ++++ 10 files changed, 896 insertions(+), 1 deletion(-) create mode 100644 examples/miditranspose.lv2/manifest.ttl create mode 100644 examples/miditranspose.lv2/miditranspose.ttl create mode 100644 examples/miditranspose.nim create mode 100644 src/nymph/atom.nim create mode 100644 src/nymph/atom/util.nim create mode 100644 src/nymph/midi.nim create mode 100644 src/nymph/plugin.nim create mode 100644 src/nymph/urid.nim create mode 100644 src/nymph/util.nim diff --git a/examples/miditranspose.lv2/manifest.ttl b/examples/miditranspose.lv2/manifest.ttl new file mode 100644 index 0000000..fc6e1e1 --- /dev/null +++ b/examples/miditranspose.lv2/manifest.ttl @@ -0,0 +1,8 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + diff --git a/examples/miditranspose.lv2/miditranspose.ttl b/examples/miditranspose.lv2/miditranspose.ttl new file mode 100644 index 0000000..24017ab --- /dev/null +++ b/examples/miditranspose.lv2/miditranspose.ttl @@ -0,0 +1,66 @@ +@prefix atom: . +@prefix bufs: . +@prefix doap: . +@prefix foaf: . +@prefix lv2: . +@prefix midi: . +@prefix opts: . +@prefix params: . +@prefix props: . +@prefix rdfs: . +@prefix units: . +@prefix urid: . + + + 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 ; + + doap:maintainer [ + foaf:name "Christopher Arndt" ; + foaf:mbox ; + foaf:homepage ; + ] ; + + lv2:microVersion 0 ; + lv2:minorVersion 1 . diff --git a/examples/miditranspose.nim b/examples/miditranspose.nim new file mode 100644 index 0000000..eec3ba5 --- /dev/null +++ b/examples/miditranspose.nim @@ -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 diff --git a/nymph.nimble b/nymph.nimble index a0c214d..df46a29 100644 --- a/nymph.nimble +++ b/nymph.nimble @@ -26,6 +26,7 @@ type Example = tuple const examples = to_table({ "amp": "urn:nymph:examples:amp", "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") 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: echo &"Example '{ex.name}' shared library not found. Use task 'build_ex' to build it." diff --git a/src/nymph/atom.nim b/src/nymph/atom.nim new file mode 100644 index 0000000..4a19e19 --- /dev/null +++ b/src/nymph/atom.nim @@ -0,0 +1,201 @@ +## Copyright 2008-2016 David Robillard +## SPDX-License-Identifier: ISC +## +## A generic value container and several data types. +## +## See 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: + ##
+    ## | Event 1 (size 6)                              | Event 2
+    ## |       |       |       |       |       |       |       |       |
+    ## | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
+    ## |FRAMES         |SIZE   |TYPE   |DATADATADATAPAD|FRAMES         |...
+    ## 
+ ## + 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. diff --git a/src/nymph/atom/util.nim b/src/nymph/atom/util.nim new file mode 100644 index 0000000..7193666 --- /dev/null +++ b/src/nymph/atom/util.nim @@ -0,0 +1,182 @@ +## Copyright 2008-2015 David Robillard +## 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. +## diff --git a/src/nymph/midi.nim b/src/nymph/midi.nim new file mode 100644 index 0000000..a67e456 --- /dev/null +++ b/src/nymph/midi.nim @@ -0,0 +1,189 @@ +## Copyright 2012-2016 David Robillard +## SPDX-License-Identifier: ISC +## +## Definitions of standard MIDI messages. +## +## See 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 diff --git a/src/nymph/plugin.nim b/src/nymph/plugin.nim new file mode 100644 index 0000000..34ef67f --- /dev/null +++ b/src/nymph/plugin.nim @@ -0,0 +1,8 @@ +plugin: + urid: urn:foobar#v1 + port_group: + name: "stereo" + audio_input: + "in_l" + audio_input: + "in_r" diff --git a/src/nymph/urid.nim b/src/nymph/urid.nim new file mode 100644 index 0000000..06c0d8c --- /dev/null +++ b/src/nymph/urid.nim @@ -0,0 +1,100 @@ +## Copyright 2008-2016 David Robillard +## Copyright 2011 Gabriel M. Beddingfield +## SPDX-License-Identifier: ISC +## +## Features for mapping URIs to and from integers. +## +## See 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.} \ No newline at end of file diff --git a/src/nymph/util.nim b/src/nymph/util.nim new file mode 100644 index 0000000..38833ef --- /dev/null +++ b/src/nymph/util.nim @@ -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.