From 8fc3663bc34a036ec2b512b7291c3d267b4914f6 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sun, 28 Apr 2024 12:03:43 +0200 Subject: [PATCH] Add multi-mode state-variable filter example plugin Signed-off-by: Christopher Arndt --- examples/multimode_filter.lv2/manifest.ttl | 8 ++ .../multimode_filter.lv2/multimode_filter.ttl | 95 ++++++++++++++++++ examples/multimode_filter.nim | 97 +++++++++++++++++++ examples/svf.nim | 95 ++++++++++++++++++ nymph.nimble | 3 +- 5 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 examples/multimode_filter.lv2/manifest.ttl create mode 100644 examples/multimode_filter.lv2/multimode_filter.ttl create mode 100644 examples/multimode_filter.nim create mode 100644 examples/svf.nim diff --git a/examples/multimode_filter.lv2/manifest.ttl b/examples/multimode_filter.lv2/manifest.ttl new file mode 100644 index 0000000..9e2afd9 --- /dev/null +++ b/examples/multimode_filter.lv2/manifest.ttl @@ -0,0 +1,8 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + diff --git a/examples/multimode_filter.lv2/multimode_filter.ttl b/examples/multimode_filter.lv2/multimode_filter.ttl new file mode 100644 index 0000000..15e211d --- /dev/null +++ b/examples/multimode_filter.lv2/multimode_filter.ttl @@ -0,0 +1,95 @@ +@prefix bufs: . +@prefix doap: . +@prefix foaf: . +@prefix lv2: . +@prefix opts: . +@prefix params: . +@prefix props: . +@prefix rdf: . +@prefix rdfs: . +@prefix unit: . + + + a lv2:Plugin, lv2:AmplifierPlugin , doap:Project ; + + lv2:optionalFeature lv2:hardRTCapable , bufs:boundedBlockLength , opts:options ; + + opts:supportedOption bufs:nominalBlockLength , + bufs:maxBlockLength , + params:sampleRate ; + + lv2:port [ + a lv2:InputPort, lv2:AudioPort ; + lv2:index 0 ; + lv2:name "Audio In" ; + lv2:symbol "in" ; + ], + [ + a lv2:OutputPort, lv2:AudioPort ; + lv2:index 1 ; + lv2:name "Audio Out" ; + lv2:symbol "out" ; + ], + [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 2 ; + lv2:name "Cutoff" ; + lv2:symbol "cutoff" ; + lv2:default 7000.0 ; + lv2:minimum 16.0 ; + lv2:maximum 7000.0 ; + lv2:portProperty props:logarithmic; + unit:unit unit:hz ; + ], + [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 3 ; + lv2:name "Q" ; + lv2:symbol "q" ; + lv2:default 0.8 ; + lv2:minimum 0.8 ; + lv2:maximum 10.0 ; + ], + [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 4 ; + lv2:name "Filter mode" ; + lv2:symbol "mode" ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 3 ; + lv2:portProperty lv2:enumeration, lv2:integer ; + lv2:scalePoint [ + rdfs:label "Lowpass" ; + rdf:value 0 ; + ], + [ + rdfs:label "Highpass" ; + rdf:value 1 ; + ], + [ + rdfs:label "Bandpass" ; + rdf:value 2; + ], + [ + rdfs:label "Bandreject" ; + rdf:value 3 ; + ] ; + ]; + + rdfs:comment """ +A multi-mode (LPF/HPF/BPF/Notch) audio filter. +""" ; + + doap:name "nymph multi-mode filter" ; + doap:license ; + + doap:maintainer [ + foaf:name "Christopher Arndt" ; + foaf:mbox ; + foaf:homepage ; + ] ; + + lv2:microVersion 0 ; + lv2:minorVersion 1 . + diff --git a/examples/multimode_filter.nim b/examples/multimode_filter.nim new file mode 100644 index 0000000..6ad8096 --- /dev/null +++ b/examples/multimode_filter.nim @@ -0,0 +1,97 @@ +## A simple multi-mode audio filter LV2 plugin + +import nymph + +import svf + +const PluginUri = "urn:nymph:examples:multimode-filter" + +type + SampleBuffer = UncheckedArray[cfloat] + + PluginPort {.pure.} = enum + Input, Output, Cutoff, Q, Mode + + SVFPlugin = object + input: ptr SampleBuffer + output: ptr SampleBuffer + cutoff: ptr cfloat + q: ptr cfloat + mode: ptr cfloat + svf: FilterSV + + +proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble; + bundlePath: cstring; features: ptr ptr Lv2Feature): + Lv2Handle {.cdecl.} = + var plug = createShared(SVFPlugin) + plug.svf = initFilterSV(fmLowPass, sampleRate) + return plug + + +proc connectPort(instance: Lv2Handle; port: cuint; + dataLocation: pointer) {.cdecl.} = + let plug = cast[ptr SVFPlugin](instance) + case cast[PluginPort](port) + of PluginPort.Input: + plug.input = cast[ptr SampleBuffer](dataLocation) + of PluginPort.Output: + plug.output = cast[ptr SampleBuffer](dataLocation) + of PluginPort.Cutoff: + plug.cutoff = cast[ptr cfloat](dataLocation) + of PluginPort.Q: + plug.q = cast[ptr cfloat](dataLocation) + of PluginPort.Mode: + plug.mode = cast[ptr cfloat](dataLocation) + + +proc activate(instance: Lv2Handle) {.cdecl.} = + let plug = cast[ptr SVFPlugin](instance) + plug.svf.reset() + plug.svf.setMode(fmLowPass) + plug.svf.setCutoff(7_000.0) + plug.svf.setQ(0.8) + plug.svf.calcCoef() + + +proc run(instance: Lv2Handle; nSamples: cuint) {.cdecl.} = + let plug = cast[ptr SVFPlugin](instance) + plug.svf.setMode(plug.mode[].int.clamp(0, 4).FilterMode) + plug.svf.setCutoff(plug.cutoff[].clamp(16.0, 7_000.0)) + plug.svf.setQ(plug.q[].clamp(0.8, 10.0)) + plug.svf.calcCoef() + + for pos in 0 ..< nSamples: + plug.output[pos] = plug.svf.process(plug.input[pos]) + + +proc deactivate(instance: Lv2Handle) {.cdecl.} = + discard + + +proc cleanup(instance: Lv2Handle) {.cdecl.} = + freeShared(cast[ptr SVFPlugin](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/examples/svf.nim b/examples/svf.nim new file mode 100644 index 0000000..0d79d9e --- /dev/null +++ b/examples/svf.nim @@ -0,0 +1,95 @@ +## State Variable Filter after Hal Chamberlin, Musical Applications of Microprocessors +## Only stable up to fc ~= fs / 6.5 ! + +import std/math + +type + FilterMode* = enum + fmLowPass, fmHighPass, fmBandPass, fmBandReject + + FilterSV* = object + mode: FilterMode + cutoff: float + q: float + lowPass: float + hiPass: float + bandPass: float + bandReject: float + a: float + b: float + maxCutoff: float + sampleRate: float + needs_update: bool + + +proc reset*(self: var FilterSV) = + self.lowPass = 0 + self.hiPass = 0 + self.bandPass = 0 + self.bandReject = 0 + + +proc initFilterSV*(mode: FilterMode = fmLowPass, sampleRate: float = 48_000): FilterSV = + result.mode = mode + result.sampleRate = sampleRate + result.reset() + result.a = 0 + result.b = 0 + result.maxCutoff = sampleRate / 6.0 + result.needs_update = true + + +proc calcCoef*(self: var FilterSV) = + if self.needs_update: + self.a = 2.0 * sin(PI * self.cutoff / self.sampleRate) + + if self.q > 0.0: + self.b = 1.0 / self.q + else: + self.b = 0.0 + + self.needs_update = false + + +proc setCutoff*(self: var FilterSV, cutoff: float) = + let fc = if cutoff > self.maxCutoff: self.maxCutoff else: cutoff + + if fc != self.cutoff: + self.cutoff = fc + self.needs_update = true + + +proc setQ*(self: var FilterSV, q: float) = + if q != self.q: + self.q = q + self.needs_update = true + + +proc setMode*(self: var FilterSV, mode: FilterMode) = + self.mode = mode + + +proc setSampleRate*(self: var FilterSV, sampleRate: float) = + if sampleRate != self.sampleRate: + self.sampleRate = sampleRate + self.needs_update = true + self.reset() + self.calcCoef() + + +proc process*(self: var FilterSV, sample: float): float = + self.lowPass += self.a * self.bandPass + self.hiPass = sample - (self.lowPass + (self.b * self.bandPass)) + self.bandPass += self.a * self.hiPass + self.bandReject = self.hiPass + self.lowPass + + case self.mode: + of fmLowPass: + return self.lowPass + of fmHighPass: + return self.hiPass + of fmBandPass: + return self.bandPass + of fmBandReject: + return self.bandReject + diff --git a/nymph.nimble b/nymph.nimble index b9cc704..a0c214d 100644 --- a/nymph.nimble +++ b/nymph.nimble @@ -24,7 +24,8 @@ type Example = tuple const examples = to_table({ - "amp": "urn:nymph:examples:amp" + "amp": "urn:nymph:examples:amp", + "multimode_filter": "urn:nymph:examples:multimode-filter", })