aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-04 03:13:35 +0300
committerRobin Haberkorn <robin.haberkorn@googlemail.com>2023-09-05 23:39:23 +0300
commiteae78cb206f38b5f4334b621bce54c3ba9048abc (patch)
treea4f14722fefec86dc28084a60dfe7b4086882e6a
parent4fd919c5493d7e5100ed0ed944048c6ef8b59f50 (diff)
downloadapplause2-eae78cb206f38b5f4334b621bce54c3ba9048abc.tar.gz
factored out filters into filters.lua
-rw-r--r--applause.lua372
-rw-r--r--filters.lua361
2 files changed, 362 insertions, 371 deletions
diff --git a/applause.lua b/applause.lua
index 675748e..fa59168 100644
--- a/applause.lua
+++ b/applause.lua
@@ -410,27 +410,6 @@ function Stream.TriOsc(freq, phase)
end)
end
---
--- Filter shortcuts.
--- They have their own classes
---
-
-function Stream:LPF(freq)
- return LPFStream:new(self, freq)
-end
-
-function Stream:HPF(freq)
- return HPFStream:new(self, freq)
-end
-
-function Stream:BPF(freq, quality)
- return BPFStream:new(self, freq, quality)
-end
-
-function Stream:BRF(freq, quality)
- return BRFStream:new(self, freq, quality)
-end
-
-- Bit crusher effect
function Stream:crush(bits)
bits = bits or 8
@@ -1758,356 +1737,6 @@ function curves(...)
end
--
--- Filters
---
-
---[==[
---
--- Non-working FIR filters (FIXME)
---
-
--- Normalized Sinc function
-local function Sinc(x)
- return x == 0 and 1 or
- math.sin(2*math.pi*x)/(2*math.pi*x)
-end
-
-local function Hamming(n, window)
- local alpha = 0.54
- return alpha - (1-alpha)*math.cos((2*math.pi*n)/(window-1))
-end
-local function Blackman(n, window)
- local alpha = 0.16
- return (1-alpha)/2 -
- 0.5*math.cos((2*math.pi*n)/(window-1)) +
- alpha*0.5*math.cos((4*math.pi*n)/(window-1))
-end
-
-FIRStream = DeriveClass(Stream)
-
-function FIRStream:ctor(stream, freq_stream)
- self.stream = tostream(stream)
- self.freq_stream = tostream(freq_stream)
-end
-
-function FIRStream:gtick()
- local window = {}
-
- -- window size (max. 1024 samples)
- -- this is the max. latency introduced by the filter
- -- since the window must be filled before we can generate
- -- (filtered) samples
- local window_size = math.min(1024, self.stream:len())
- local window_p = window_size-1
- local accu = 0
-
- local blackman = {}
- for i = 1, window_size do blackman[i] = Blackman(i-1, window_size) end
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
-
- return function()
- -- fill buffer (initial)
- while #window < window_size-1 do
- table.insert(window, tick())
- end
-
- window[window_p+1] = tick()
- window_p = (window_p + 1) % window_size
-
- local period = freq_tick()/samplerate
-
- local sample = 0
- local i = window_p
- repeat
- -- FIXME
- sample = sample + window[(i % window_size)+1] *
- Sinc((i-window_p - window_size/2)/period) *
- blackman[i-window_p+1]
- i = i + 1
- until (i % window_size) == window_p
-
- return sample
- end
-end
-
-function FIRStream:len()
- return self.stream:len()
-end
-]==]
-
---
--- General-purpose IIR filters:
--- These are direct translations of ChucK's LPF, HPF, BPF and BRF
--- ugens which are in turn adapted from SuperCollider 3.
---
-
--- De-denormalize function adapted from ChucK.
--- Not quite sure why this is needed - properly to make the
--- IIR filters numerically more stable.
-local function ddn(f)
- return f >= 0 and (f > 1e-15 and f < 1e15 and f or 0) or
- (f < -1e-15 and f > -1e15 and f or 0)
-end
-
-LPFStream = DeriveClass(MuxableStream)
-
-function LPFStream:muxableCtor(stream, freq)
- self.stream = stream
- self.freq_stream = freq
-end
-
-function LPFStream:gtick()
- local a0, b1, b2
- local y1, y2 = 0, 0
-
- -- some cached constants
- local radians_per_sample = (2*math.pi)/samplerate
- local sqrt2 = math.sqrt(2)
-
- -- some cached math table lookups
- local tan = math.tan
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
- local cur_freq = nil
-
- return function()
- local sample = tick()
- local freq = freq_tick()
-
- if sample == nil or freq == nil then
- -- don't filter if we run out of frequency samples
- return sample
- elseif freq ~= cur_freq then
- -- calculate filter coefficients
- -- avoid recalculation for constant frequencies
- cur_freq = freq
-
- local pfreq = cur_freq * radians_per_sample * 0.5
-
- local C = 1/tan(pfreq)
- local C2 = C*C
- local sqrt2C = C * sqrt2
-
- a0 = 1/(1 + sqrt2C + C2)
- b1 = -2.0 * (1.0 - C2) * a0
- b2 = -(1.0 - sqrt2C + C2) * a0
- end
-
- local y0 = sample + b1*y1 + b2*y2
- local result = a0 * (y0 + 2*y1 + y2)
-
- y2 = ddn(y1)
- y1 = ddn(y0)
-
- return result
- end
-end
-
-function LPFStream:len()
- return self.stream:len()
-end
-
-HPFStream = DeriveClass(MuxableStream)
-
-function HPFStream:muxableCtor(stream, freq)
- self.stream = stream
- self.freq_stream = freq
-end
-
-function HPFStream:gtick()
- local a0, b1, b2
- local y1, y2 = 0, 0
-
- -- some cached constants
- local radians_per_sample = (2*math.pi)/samplerate
- local sqrt2 = math.sqrt(2)
-
- -- some cached math table lookups
- local tan = math.tan
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
- local cur_freq = nil
-
- -- NOTE: Very similar to LPFStream.gtick()
- -- Can we factor out the similarity without sacrificing
- -- too much performance?
- return function()
- local sample = tick()
- local freq = freq_tick()
-
- if sample == nil or freq == nil then
- -- don't filter if we run out of frequency samples
- return sample
- elseif freq ~= cur_freq then
- -- calculate filter coefficients
- -- avoid recalculation for constant frequencies
- cur_freq = freq
-
- local pfreq = cur_freq * radians_per_sample * 0.5
-
- local C = tan(pfreq)
- local C2 = C*C
- local sqrt2C = C * sqrt2
-
- a0 = 1/(1 + sqrt2C + C2)
- b1 = 2.0 * (1.0 - C2) * a0
- b2 = -(1.0 - sqrt2C + C2) * a0
- end
-
- local y0 = sample + b1*y1 + b2*y2
- local result = a0 * (y0 - 2*y1 + y2)
-
- y2 = ddn(y1)
- y1 = ddn(y0)
-
- return result
- end
-end
-
-function HPFStream:len()
- return self.stream:len()
-end
-
--- NOTE: The quality factor, indirectly proportional
--- to the passband width
-BPFStream = DeriveClass(MuxableStream)
-
-function BPFStream:muxableCtor(stream, freq, quality)
- self.stream = stream
- self.freq_stream = freq
- self.quality_stream = quality
-end
-
-function BPFStream:gtick()
- local a0, b1, b2
- local y1, y2 = 0, 0
-
- -- some cached constants
- local radians_per_sample = (2*math.pi)/samplerate
- local sqrt2 = math.sqrt(2)
-
- -- some cached math table lookups
- local tan = math.tan
- local cos = math.cos
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
- local quality_tick = self.quality_stream:gtick()
- local cur_freq, cur_quality
-
- return function()
- local sample = tick()
- local freq = freq_tick()
- local quality = quality_tick()
-
- if sample == nil or freq == nil or quality == nil then
- -- don't filter if we run out of frequency samples
- return sample
- elseif freq ~= cur_freq or quality ~= cur_quality then
- -- calculate filter coefficients
- -- avoid recalculation for constant frequencies
- -- and quality factors
- cur_freq = freq
- cur_quality = quality
-
- local pfreq = cur_freq * radians_per_sample
- local pbw = 1 / cur_quality*pfreq*0.5
-
- local C = 1/tan(pbw)
- local D = 2*cos(pfreq);
-
- a0 = 1/(1 + C)
- b1 = C*D*a0
- b2 = (1 - C)*a0
- end
-
- local y0 = sample + b1*y1 + b2*y2
- local result = a0 * (y0 - y2)
-
- y2 = ddn(y1)
- y1 = ddn(y0)
-
- return result
- end
-end
-
-function BPFStream:len()
- return self.stream:len()
-end
-
--- NOTE: The quality factor, indirectly proportional
--- to the passband width
-BRFStream = DeriveClass(MuxableStream)
-
-function BRFStream:muxableCtor(stream, freq, quality)
- self.stream = stream
- self.freq_stream = freq
- self.quality_stream = quality
-end
-
-function BRFStream:gtick()
- local a0, b1, b2
- local y1, y2 = 0, 0
-
- -- some cached constants
- local radians_per_sample = (2*math.pi)/samplerate
- local sqrt2 = math.sqrt(2)
-
- -- some cached math table lookups
- local tan = math.tan
- local cos = math.cos
-
- local tick = self.stream:gtick()
- local freq_tick = self.freq_stream:gtick()
- local quality_tick = self.quality_stream:gtick()
- local cur_freq, cur_quality
-
- -- NOTE: Very similar to BPFStream.gtick()
- return function()
- local sample = tick()
- local freq = freq_tick()
- local quality = quality_tick()
-
- if sample == nil or freq == nil or quality == nil then
- -- don't filter if we run out of frequency samples
- return sample
- elseif freq ~= cur_freq then
- -- calculate filter coefficients
- -- avoid recalculation for constant frequencies
- -- and quality factors
- cur_freq = freq
- cur_quality = quality
-
- local pfreq = cur_freq * radians_per_sample
- local pbw = 1 / cur_quality*pfreq*0.5
-
- local C = tan(pbw)
- local D = 2*cos(pfreq);
-
- a0 = 1/(1 + C)
- b1 = -D*a0
- b2 = (1 - C)*a0
- end
-
- local y0 = sample - b1*y1 - b2*y2
- local result = a0 * (y0 + y2) + b1*y1
-
- y2 = ddn(y1)
- y1 = ddn(y0)
-
- return result
- end
-end
-
-function BRFStream:len()
- return self.stream:len()
-end
-
---
-- Jack client abstractions. This passes low level signals
-- and works only with clients created via Stream.fork()
--
@@ -2140,6 +1769,7 @@ Client.__gc = Client.kill
-- Additional modules are loaded with dofile(),
-- so they react to reload()
--
+dofile "filters.lua"
dofile "dssi.lua"
dofile "midi.lua"
dofile "evdev.lua"
diff --git a/filters.lua b/filters.lua
new file mode 100644
index 0000000..e9b2900
--- /dev/null
+++ b/filters.lua
@@ -0,0 +1,361 @@
+--[==[
+--
+-- Non-working FIR filters (FIXME)
+--
+
+-- Normalized Sinc function
+local function Sinc(x)
+ return x == 0 and 1 or
+ math.sin(2*math.pi*x)/(2*math.pi*x)
+end
+
+local function Hamming(n, window)
+ local alpha = 0.54
+ return alpha - (1-alpha)*math.cos((2*math.pi*n)/(window-1))
+end
+local function Blackman(n, window)
+ local alpha = 0.16
+ return (1-alpha)/2 -
+ 0.5*math.cos((2*math.pi*n)/(window-1)) +
+ alpha*0.5*math.cos((4*math.pi*n)/(window-1))
+end
+
+FIRStream = DeriveClass(Stream)
+
+function FIRStream:ctor(stream, freq_stream)
+ self.stream = tostream(stream)
+ self.freq_stream = tostream(freq_stream)
+end
+
+function FIRStream:gtick()
+ local window = {}
+
+ -- window size (max. 1024 samples)
+ -- this is the max. latency introduced by the filter
+ -- since the window must be filled before we can generate
+ -- (filtered) samples
+ local window_size = math.min(1024, self.stream:len())
+ local window_p = window_size-1
+ local accu = 0
+
+ local blackman = {}
+ for i = 1, window_size do blackman[i] = Blackman(i-1, window_size) end
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+
+ return function()
+ -- fill buffer (initial)
+ while #window < window_size-1 do
+ table.insert(window, tick())
+ end
+
+ window[window_p+1] = tick()
+ window_p = (window_p + 1) % window_size
+
+ local period = freq_tick()/samplerate
+
+ local sample = 0
+ local i = window_p
+ repeat
+ -- FIXME
+ sample = sample + window[(i % window_size)+1] *
+ Sinc((i-window_p - window_size/2)/period) *
+ blackman[i-window_p+1]
+ i = i + 1
+ until (i % window_size) == window_p
+
+ return sample
+ end
+end
+
+function FIRStream:len()
+ return self.stream:len()
+end
+]==]
+
+--
+-- General-purpose IIR filters:
+-- These are direct translations of ChucK's LPF, HPF, BPF and BRF
+-- ugens which are in turn adapted from SuperCollider 3.
+--
+
+-- De-denormalize function adapted from ChucK.
+-- Not quite sure why this is needed - properly to make the
+-- IIR filters numerically more stable.
+local function ddn(f)
+ return f >= 0 and (f > 1e-15 and f < 1e15 and f or 0) or
+ (f < -1e-15 and f > -1e15 and f or 0)
+end
+
+LPFStream = DeriveClass(MuxableStream)
+
+function LPFStream:muxableCtor(stream, freq)
+ self.stream = stream
+ self.freq_stream = freq
+end
+
+function LPFStream:gtick()
+ local a0, b1, b2
+ local y1, y2 = 0, 0
+
+ -- some cached constants
+ local radians_per_sample = (2*math.pi)/samplerate
+ local sqrt2 = math.sqrt(2)
+
+ -- some cached math table lookups
+ local tan = math.tan
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+ local cur_freq = nil
+
+ return function()
+ local sample = tick()
+ local freq = freq_tick()
+
+ if sample == nil or freq == nil then
+ -- don't filter if we run out of frequency samples
+ return sample
+ elseif freq ~= cur_freq then
+ -- calculate filter coefficients
+ -- avoid recalculation for constant frequencies
+ cur_freq = freq
+
+ local pfreq = cur_freq * radians_per_sample * 0.5
+
+ local C = 1/tan(pfreq)
+ local C2 = C*C
+ local sqrt2C = C * sqrt2
+
+ a0 = 1/(1 + sqrt2C + C2)
+ b1 = -2.0 * (1.0 - C2) * a0
+ b2 = -(1.0 - sqrt2C + C2) * a0
+ end
+
+ local y0 = sample + b1*y1 + b2*y2
+ local result = a0 * (y0 + 2*y1 + y2)
+
+ y2 = ddn(y1)
+ y1 = ddn(y0)
+
+ return result
+ end
+end
+
+function LPFStream:len()
+ return self.stream:len()
+end
+
+function Stream:LPF(freq)
+ return LPFStream:new(self, freq)
+end
+
+HPFStream = DeriveClass(MuxableStream)
+
+function HPFStream:muxableCtor(stream, freq)
+ self.stream = stream
+ self.freq_stream = freq
+end
+
+function HPFStream:gtick()
+ local a0, b1, b2
+ local y1, y2 = 0, 0
+
+ -- some cached constants
+ local radians_per_sample = (2*math.pi)/samplerate
+ local sqrt2 = math.sqrt(2)
+
+ -- some cached math table lookups
+ local tan = math.tan
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+ local cur_freq = nil
+
+ -- NOTE: Very similar to LPFStream.gtick()
+ -- Can we factor out the similarity without sacrificing
+ -- too much performance?
+ return function()
+ local sample = tick()
+ local freq = freq_tick()
+
+ if sample == nil or freq == nil then
+ -- don't filter if we run out of frequency samples
+ return sample
+ elseif freq ~= cur_freq then
+ -- calculate filter coefficients
+ -- avoid recalculation for constant frequencies
+ cur_freq = freq
+
+ local pfreq = cur_freq * radians_per_sample * 0.5
+
+ local C = tan(pfreq)
+ local C2 = C*C
+ local sqrt2C = C * sqrt2
+
+ a0 = 1/(1 + sqrt2C + C2)
+ b1 = 2.0 * (1.0 - C2) * a0
+ b2 = -(1.0 - sqrt2C + C2) * a0
+ end
+
+ local y0 = sample + b1*y1 + b2*y2
+ local result = a0 * (y0 - 2*y1 + y2)
+
+ y2 = ddn(y1)
+ y1 = ddn(y0)
+
+ return result
+ end
+end
+
+function HPFStream:len()
+ return self.stream:len()
+end
+
+function Stream:HPF(freq)
+ return HPFStream:new(self, freq)
+end
+
+-- NOTE: The quality factor, indirectly proportional
+-- to the passband width
+BPFStream = DeriveClass(MuxableStream)
+
+function BPFStream:muxableCtor(stream, freq, quality)
+ self.stream = stream
+ self.freq_stream = freq
+ self.quality_stream = quality
+end
+
+function BPFStream:gtick()
+ local a0, b1, b2
+ local y1, y2 = 0, 0
+
+ -- some cached constants
+ local radians_per_sample = (2*math.pi)/samplerate
+ local sqrt2 = math.sqrt(2)
+
+ -- some cached math table lookups
+ local tan = math.tan
+ local cos = math.cos
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+ local quality_tick = self.quality_stream:gtick()
+ local cur_freq, cur_quality
+
+ return function()
+ local sample = tick()
+ local freq = freq_tick()
+ local quality = quality_tick()
+
+ if sample == nil or freq == nil or quality == nil then
+ -- don't filter if we run out of frequency samples
+ return sample
+ elseif freq ~= cur_freq or quality ~= cur_quality then
+ -- calculate filter coefficients
+ -- avoid recalculation for constant frequencies
+ -- and quality factors
+ cur_freq = freq
+ cur_quality = quality
+
+ local pfreq = cur_freq * radians_per_sample
+ local pbw = 1 / cur_quality*pfreq*0.5
+
+ local C = 1/tan(pbw)
+ local D = 2*cos(pfreq);
+
+ a0 = 1/(1 + C)
+ b1 = C*D*a0
+ b2 = (1 - C)*a0
+ end
+
+ local y0 = sample + b1*y1 + b2*y2
+ local result = a0 * (y0 - y2)
+
+ y2 = ddn(y1)
+ y1 = ddn(y0)
+
+ return result
+ end
+end
+
+function BPFStream:len()
+ return self.stream:len()
+end
+
+function Stream:BPF(freq, quality)
+ return BPFStream:new(self, freq, quality)
+end
+
+-- NOTE: The quality factor, indirectly proportional
+-- to the passband width
+BRFStream = DeriveClass(MuxableStream)
+
+function BRFStream:muxableCtor(stream, freq, quality)
+ self.stream = stream
+ self.freq_stream = freq
+ self.quality_stream = quality
+end
+
+function BRFStream:gtick()
+ local a0, b1, b2
+ local y1, y2 = 0, 0
+
+ -- some cached constants
+ local radians_per_sample = (2*math.pi)/samplerate
+ local sqrt2 = math.sqrt(2)
+
+ -- some cached math table lookups
+ local tan = math.tan
+ local cos = math.cos
+
+ local tick = self.stream:gtick()
+ local freq_tick = self.freq_stream:gtick()
+ local quality_tick = self.quality_stream:gtick()
+ local cur_freq, cur_quality
+
+ -- NOTE: Very similar to BPFStream.gtick()
+ return function()
+ local sample = tick()
+ local freq = freq_tick()
+ local quality = quality_tick()
+
+ if sample == nil or freq == nil or quality == nil then
+ -- don't filter if we run out of frequency samples
+ return sample
+ elseif freq ~= cur_freq then
+ -- calculate filter coefficients
+ -- avoid recalculation for constant frequencies
+ -- and quality factors
+ cur_freq = freq
+ cur_quality = quality
+
+ local pfreq = cur_freq * radians_per_sample
+ local pbw = 1 / cur_quality*pfreq*0.5
+
+ local C = tan(pbw)
+ local D = 2*cos(pfreq);
+
+ a0 = 1/(1 + C)
+ b1 = -D*a0
+ b2 = (1 - C)*a0
+ end
+
+ local y0 = sample - b1*y1 - b2*y2
+ local result = a0 * (y0 + y2) + b1*y1
+
+ y2 = ddn(y1)
+ y1 = ddn(y0)
+
+ return result
+ end
+end
+
+function BRFStream:len()
+ return self.stream:len()
+end
+
+function Stream:BRF(freq, quality)
+ return BRFStream:new(self, freq, quality)
+end