From f001184feab558847be6ff48c616b562d97c98b6 Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Fri, 27 Sep 2024 19:37:02 +0200 Subject: [PATCH] feat: add tiltfilter example --- examples/amp.lv2/amp.ttl | 2 +- examples/amp.lv2/manifest.ttl | 2 +- examples/miditranspose.lv2/manifest.ttl | 2 +- examples/multimodefilter.lv2/manifest.ttl | 4 +- .../multimodefilter.lv2/multimodefilter.ttl | 4 +- examples/tiltfilter.lv2/manifest.ttl | 8 + examples/tiltfilter.lv2/tiltfilter.ttl | 90 ++++++++++ examples/tiltfilter.nim | 165 ++++++++++++++++++ examples/tiltfilter_plugin.nim | 104 +++++++++++ nymph.nimble | 3 +- 10 files changed, 376 insertions(+), 8 deletions(-) create mode 100644 examples/tiltfilter.lv2/manifest.ttl create mode 100644 examples/tiltfilter.lv2/tiltfilter.ttl create mode 100644 examples/tiltfilter.nim create mode 100644 examples/tiltfilter_plugin.nim diff --git a/examples/amp.lv2/amp.ttl b/examples/amp.lv2/amp.ttl index 5399a0b..4b3fd3d 100644 --- a/examples/amp.lv2/amp.ttl +++ b/examples/amp.lv2/amp.ttl @@ -1,7 +1,7 @@ @prefix bufs: . @prefix doap: . @prefix foaf: . -@prefix lv2: . +@prefix lv2: . @prefix opts: . @prefix params: . @prefix rdfs: . diff --git a/examples/amp.lv2/manifest.ttl b/examples/amp.lv2/manifest.ttl index ecbc919..1f4fde9 100644 --- a/examples/amp.lv2/manifest.ttl +++ b/examples/amp.lv2/manifest.ttl @@ -1,4 +1,4 @@ -@prefix lv2: . +@prefix lv2: . @prefix rdfs: . diff --git a/examples/miditranspose.lv2/manifest.ttl b/examples/miditranspose.lv2/manifest.ttl index fc6e1e1..655b877 100644 --- a/examples/miditranspose.lv2/manifest.ttl +++ b/examples/miditranspose.lv2/manifest.ttl @@ -1,4 +1,4 @@ -@prefix lv2: . +@prefix lv2: . @prefix rdfs: . diff --git a/examples/multimodefilter.lv2/manifest.ttl b/examples/multimodefilter.lv2/manifest.ttl index 939e9ec..df7937a 100644 --- a/examples/multimodefilter.lv2/manifest.ttl +++ b/examples/multimodefilter.lv2/manifest.ttl @@ -1,7 +1,7 @@ -@prefix lv2: . +@prefix lv2: . @prefix rdfs: . - + a lv2:Plugin ; lv2:binary ; rdfs:seeAlso . diff --git a/examples/multimodefilter.lv2/multimodefilter.ttl b/examples/multimodefilter.lv2/multimodefilter.ttl index 01bc3e1..dcbae70 100644 --- a/examples/multimodefilter.lv2/multimodefilter.ttl +++ b/examples/multimodefilter.lv2/multimodefilter.ttl @@ -1,11 +1,11 @@ @prefix bufs: . @prefix doap: . @prefix foaf: . -@prefix lv2: . +@prefix lv2: . @prefix opts: . @prefix params: . @prefix props: . -@prefix rdf: . +@prefix rdf: . @prefix rdfs: . @prefix units: . diff --git a/examples/tiltfilter.lv2/manifest.ttl b/examples/tiltfilter.lv2/manifest.ttl new file mode 100644 index 0000000..9aae8fd --- /dev/null +++ b/examples/tiltfilter.lv2/manifest.ttl @@ -0,0 +1,8 @@ +@prefix lv2: . +@prefix rdfs: . + + + a lv2:Plugin ; + lv2:binary ; + rdfs:seeAlso . + diff --git a/examples/tiltfilter.lv2/tiltfilter.ttl b/examples/tiltfilter.lv2/tiltfilter.ttl new file mode 100644 index 0000000..2b1a531 --- /dev/null +++ b/examples/tiltfilter.lv2/tiltfilter.ttl @@ -0,0 +1,90 @@ +@prefix bufs: . +@prefix doap: . +@prefix foaf: . +@prefix lv2: . +@prefix opts: . +@prefix params: . +@prefix props: . +@prefix rdf: . +@prefix rdfs: . +@prefix units: . + + + a lv2:Plugin , lv2:FilterPlugin , 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 10000.0 ; + lv2:minimum 20.0 ; + lv2:maximum 20000.0 ; + units:unit units:hz ; + ], + [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 3 ; + lv2:name "Steepness" ; + lv2:symbol "steepness" ; + lv2:default 1.0 ; + lv2:minimum 0.0 ; + lv2:maximum 1.0 ; + ], + [ + a lv2:InputPort, lv2:ControlPort ; + lv2:index 4 ; + lv2:name "Filter mode" ; + lv2:symbol "mode" ; + lv2:default 0 ; + lv2:minimum 0 ; + lv2:maximum 2 ; + 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:comment """ +A tilt EQ audio filter. +""" ; + + doap:name "nymph tilt filter" ; + doap:license ; + + doap:maintainer [ + foaf:name "Christopher Arndt" ; + foaf:mbox ; + foaf:homepage ; + ] ; + + lv2:microVersion 0 ; + lv2:minorVersion 1 . + diff --git a/examples/tiltfilter.nim b/examples/tiltfilter.nim new file mode 100644 index 0000000..05bd5a8 --- /dev/null +++ b/examples/tiltfilter.nim @@ -0,0 +1,165 @@ +## +## Tilt Filter +## +## Nim translation of this Rust implementation: +## +## https://github.com/ardura/Actuate/blob/main/src/fx/ArduraFilter.rs +## +## Inspired by https://www.musicdsp.org/en/latest/Filters/267-simple-tilt-equalizer.html +## Lowpass, Bandpass, Highpass based off tilt filter code + +import math + +const + slopeNeg = -60.0 + amp = 6.0 / ln(2.0) + denorm = pow(10.0, -30.0) + minFreq = 20.0 + maxFreq = 20_000.0 + +type + FilterMode* = enum + fmLowPass, fmBandPass, fmHighPass + + TiltFilter* = object + # Filter parameters + sampleRate: float64 + centerFreq: float + steepness: float + mode: FilterMode + # Filter tracking / internal + needsUpdate: bool + sampleRateX3: float64 + lowGain: float + highGain: float + a: float + b: float + lpOut: float + # Band pass separate vars + bandALow: float + bandBLow: float + bandOutLow: float + bandAHigh: float + bandBHigh: float + bandOutHigh: float + + +# Super useful function to scale an sample 0-1 into other ranges +proc scaleRange(sample, minOutput, maxOutput: float): float = + result = clamp(sample * (maxOutput - minOutput) + minOutput, minOutput, maxOutput) + + +proc initTiltFilter*(centerFreq, steepness: float, mode: FilterMode = fmLowPass, sampleRate: float64 = 48_000.0): TiltFilter = + let sampleRateX3 = 3.0 * sampleRate + + case mode: + # These are the gains for the slopes when math happens later + of fmLowPass: + result.lowGain = exp(0.0 / amp) - 1.0 + result.highGain = exp(slopeNeg / amp) - 1.0 + of fmBandPass: + result.lowGain = exp(0.0 / amp) - 1.0 + result.highGain = exp(slopeNeg / amp) - 1.0 + of fmHighPass: + result.lowGain = exp(slopeNeg / amp) - 1.0 + result.highGain = exp(0.0 / amp) - 1.0 + + let omega = 2.0 * PI * centerFreq + let n = 1.0 / (scaleRange(steepness, 0.98, 1.2) * (sampleRateX3 + omega)) + result.a = 2.0 * omega * n + result.bandALow = result.a + result.bandAHigh = result.a + result.b = (sampleRateX3 - omega) * n + result.bandBLow = result.b + result.bandBHigh = result.b + result.lpOut = 0.0 + result.bandOutLow = 0.0 + result.bandOutHigh = 0.0 + result.centerFreq = centerFreq + result.sampleRateX3 = sampleRateX3 + result.steepness = steepness + result.sampleRate = sampleRate + result.mode = mode + result.needsUpdate = true + + +proc setMode*(self: var TiltFilter, mode: FilterMode) = + if mode != self.mode: + self.mode = mode + self.needsUpdate = true + + +proc setCenterFreq*(self: var TiltFilter, value: float) = + let freq = value.clamp(minFreq, maxFreq) + + if freq != self.centerFreq: + self.centerFreq = freq + self.needsUpdate = true + + +proc setSteepness*(self: var TiltFilter, value: float) = + let steepness = value.clamp(0.0, 1.0) + + if steepness != self.steepness: + self.steepness = steepness + self.needsUpdate = true + + +proc setSampleRate*(self: var TiltFilter, sampleRate: float) = + if sampleRate != self.sampleRate: + self.sampleRate = sampleRate + self.sampleRateX3 = self.sampleRate * 3.0 + self.needsUpdate = true + + +proc reset*(self: var TiltFilter) = + discard + + +proc update*(self: var TiltFilter) = + if self.needsUpdate: + case self.mode: + of fmLowPass: + let omega = 2.0 * PI * self.centerFreq + let n = 1.0 / (scaleRange(self.steepness, 0.98, 1.2) * (self.sample_rate_x3 + omega)) + self.b = (self.sampleRateX3 - omega) * n + self.lowGain = exp(0.0 / amp) - 1.0 + self.highGain = exp(slopeNeg / amp) - 1.0 + of fmBandPass: + let width = self.steepness * self.steepness * 500.0 + + let lowOmega = 2.0 * PI * (self.centerFreq - width).clamp(20.0, 16_000.0) + let lowN = 1.0 / (scaleRange(self.steepness, 0.98, 1.2) * (self.sampleRateX3 + lowOmega)) + self.bandALow = 2.0 * lowOmega * lowN + self.bandBLow = (self.sampleRateX3 - lowOmega) * lowN + + let highOmega = 2.0 * PI * (self.centerFreq + width).clamp(20.0, 16_000.0); + let highN = 1.0 / (scaleRange(self.steepness, 0.98, 1.2) * (self.sampleRateX3 + highOmega)) + self.bandAHigh = 2.0 * highOmega * highN + self.bandBHigh = (self.sampleRateX3 - highOmega) * highN + + self.lowGain = exp(0.0 / amp) - 1.0 + self.highGain = exp(slopeNeg / amp) - 1.0 + of fmHighPass: + let omega = 2.0 * PI * self.centerFreq + let n = 1.0 / (scaleRange(self.steepness, 0.98, 1.2) * (self.sampleRateX3 + omega)) + self.a = 2.0 * omega * n + self.b = (self.sampleRateX3 - omega) * n + self.lowGain = exp(slopeNeg / amp) - 1.0 + self.highGain = exp(0.0 / amp) - 1.0 + + self.needsUpdate = false + + +## +## Process the input sample using the tilt filter +## +proc process*(self: var TiltFilter, sample: float): float = + if self.mode == fmBandPass: + self.bandOutLow = self.bandALow * sample + self.bandBLow * self.bandOutLow + let temp = sample + self.highGain * self.bandOutLow + self.lowGain * (sample - self.bandOutLow) + self.bandOutHigh = self.bandAHigh * temp + self.bandBHigh * self.bandOutHigh + result = temp + self.lowGain * self.bandOutHigh + self.highGain * (temp - self.bandOutHigh) + denorm + else: + self.lpOut = self.a * sample + self.b * self.lpOut; + result = sample + self.lowGain * self.lpOut + self.highGain * (sample - self.lpOut) + denorm diff --git a/examples/tiltfilter_plugin.nim b/examples/tiltfilter_plugin.nim new file mode 100644 index 0000000..e1da952 --- /dev/null +++ b/examples/tiltfilter_plugin.nim @@ -0,0 +1,104 @@ +## A tilt EQ / filter LV2 plugin + +import nymph + +import paramsmooth +import tiltfilter + +const + PluginUri = "urn:nymph:examples:tiltfilter" + minFreq = 20.0 + maxFreq = 20_000.0 + +type + SampleBuffer = UncheckedArray[cfloat] + + PluginPort {.pure.} = enum + Input, Output, Frequency, Steepness, Mode + + TiltFilterPlugin = object + input: ptr SampleBuffer + output: ptr SampleBuffer + freq: ptr cfloat + steepness: ptr cfloat + mode: ptr cfloat + flt: TiltFilter + smoothFreq: ParamSmooth + + +proc instantiate(descriptor: ptr Lv2Descriptor; sampleRate: cdouble; + bundlePath: cstring; features: ptr UncheckedArray[ptr Lv2Feature]): + Lv2Handle {.cdecl.} = + try: + let plug = createShared(TiltFilterPlugin) + plug.flt = initTiltFilter(10_000.0, 1.0, fmLowPass, sampleRate) + plug.smoothFreq = initParamSmooth(20.0, sampleRate) + return cast[Lv2Handle](plug) + except OutOfMemDefect: + return nil + + +proc connectPort(instance: Lv2Handle; port: cuint; + dataLocation: pointer) {.cdecl.} = + let plug = cast[ptr TiltFilterPlugin](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.Frequency: + plug.freq = cast[ptr cfloat](dataLocation) + of PluginPort.Steepness: + plug.steepness = cast[ptr cfloat](dataLocation) + of PluginPort.Mode: + plug.mode = cast[ptr cfloat](dataLocation) + + +proc activate(instance: Lv2Handle) {.cdecl.} = + let plug = cast[ptr TiltFilterPlugin](instance) + plug.flt.reset() + + +proc run(instance: Lv2Handle; nSamples: cuint) {.cdecl.} = + let plug = cast[ptr TiltFilterPlugin](instance) + let freq = plug.freq[].clamp(minFreq, maxFreq) + + plug.flt.setMode(plug.mode[].clamp(0.0, 2.0).FilterMode) + plug.flt.setSteepness(plug.steepness[]) + + for pos in 0 ..< nSamples: + plug.flt.setCenterFreq(plug.smoothFreq.process(freq)) + plug.flt.update() + plug.output[pos] = plug.flt.process(plug.input[pos]) + + +proc deactivate(instance: Lv2Handle) {.cdecl.} = + discard + + +proc cleanup(instance: Lv2Handle) {.cdecl.} = + freeShared(cast[ptr TiltFilterPlugin](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 5e0a27c..41caee5 100644 --- a/nymph.nimble +++ b/nymph.nimble @@ -26,7 +26,8 @@ type Example = tuple const examples = to_table({ "amp": "urn:nymph:examples:amp", "miditranspose": "urn:nymph:examples:miditranspose", - "multimodefilter": "urn:nymph:examples:multimode-filter", + "multimodefilter": "urn:nymph:examples:multimodefilter", + "tiltfilter": "urn:nymph:examples:tiltfilter", })