feat: add tiltfilter example
This commit is contained in:
parent
75f3ce03b3
commit
f001184fea
|
@ -1,7 +1,7 @@
|
||||||
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||||
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
|
||||||
<urn:nymph:examples:multimode-filter>
|
<urn:nymph:examples:multimodefilter>
|
||||||
a lv2:Plugin ;
|
a lv2:Plugin ;
|
||||||
lv2:binary <libmultimodefilter.so> ;
|
lv2:binary <libmultimodefilter.so> ;
|
||||||
rdfs:seeAlso <multimodefilter.ttl> .
|
rdfs:seeAlso <multimodefilter.ttl> .
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
@prefix lv2: <http://lv2plug.in/ns/lv2core#> .
|
||||||
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
|
||||||
|
<urn:nymph:examples:tiltfilter>
|
||||||
|
a lv2:Plugin ;
|
||||||
|
lv2:binary <libtiltfilter.so> ;
|
||||||
|
rdfs:seeAlso <tiltfilter.ttl> .
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
@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 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 rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
||||||
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
||||||
|
@prefix units: <http://lv2plug.in/ns/extensions/units#> .
|
||||||
|
|
||||||
|
<urn:nymph:examples:tiltfilter>
|
||||||
|
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 <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 .
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -26,7 +26,8 @@ type Example = tuple
|
||||||
const examples = to_table({
|
const examples = to_table({
|
||||||
"amp": "urn:nymph:examples:amp",
|
"amp": "urn:nymph:examples:amp",
|
||||||
"miditranspose": "urn:nymph:examples:miditranspose",
|
"miditranspose": "urn:nymph:examples:miditranspose",
|
||||||
"multimodefilter": "urn:nymph:examples:multimode-filter",
|
"multimodefilter": "urn:nymph:examples:multimodefilter",
|
||||||
|
"tiltfilter": "urn:nymph:examples:tiltfilter",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue