From 35f1acef821de8efa2a502e4bc4cace16f0c61e5 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Sun, 18 Oct 2020 23:00:07 +0200 Subject: [PATCH 1/4] Bugfixing --- camilladsp_plot/filter_eval.py | 95 +++++++++++++++++----------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/camilladsp_plot/filter_eval.py b/camilladsp_plot/filter_eval.py index 37a0fc6..2991bda 100644 --- a/camilladsp_plot/filter_eval.py +++ b/camilladsp_plot/filter_eval.py @@ -1,5 +1,7 @@ import numpy as np import numpy.fft as fft +import cmath +import math import csv import yaml import sys @@ -260,7 +262,7 @@ def __init__(self, conf, fs): if ftype == "Highpass": freq = conf["freq"] q = conf["q"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs sn = np.sin(omega) cs = np.cos(omega) alpha = sn / (2.0 * q) @@ -273,7 +275,7 @@ def __init__(self, conf, fs): elif ftype == "Lowpass": freq = conf["freq"] q = conf["q"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs sn = np.sin(omega) cs = np.cos(omega) alpha = sn / (2.0 * q) @@ -287,9 +289,9 @@ def __init__(self, conf, fs): freq = conf["freq"] q = conf["q"] gain = conf["gain"] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + omega = 2.0 * math.pi * freq / fs + sn = math.sin(omega) + cs = math.cos(omega) ampl = 10.0 ** (gain / 40.0) alpha = sn / (2.0 * q) b0 = 1.0 + (alpha * ampl) @@ -301,9 +303,9 @@ def __init__(self, conf, fs): elif ftype == "HighshelfFO": freq = conf["freq"] gain = conf["gain"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs ampl = 10.0 ** (gain / 40.0) - tn = np.tan(omega / 2) + tn = math.tan(omega / 2) b0 = ampl * tn + ampl ** 2 b1 = ampl * tn - ampl ** 2 b2 = 0.0 @@ -314,16 +316,16 @@ def __init__(self, conf, fs): freq = conf["freq"] slope = conf["slope"] gain = conf["gain"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs ampl = 10.0 ** (gain / 40.0) - sn = np.sin(omega) - cs = np.cos(omega) + sn = math.sin(omega) + cs = math.cos(omega) alpha = ( sn / 2.0 - * np.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope / 12.0) - 1.0) + 2.0) + * math.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope / 12.0) - 1.0) + 2.0) ) - beta = 2.0 * np.sqrt(ampl) * alpha + beta = 2.0 * math.sqrt(ampl) * alpha b0 = ampl * ((ampl + 1.0) + (ampl - 1.0) * cs + beta) b1 = -2.0 * ampl * ((ampl - 1.0) + (ampl + 1.0) * cs) b2 = ampl * ((ampl + 1.0) + (ampl - 1.0) * cs - beta) @@ -333,9 +335,9 @@ def __init__(self, conf, fs): elif ftype == "LowshelfFO": freq = conf["freq"] gain = conf["gain"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs ampl = 10.0 ** (gain / 40.0) - tn = np.tan(omega / 2) + tn = math.tan(omega / 2) b0 = ampl ** 2 * tn + ampl b1 = ampl ** 2 * tn - ampl b2 = 0.0 @@ -346,16 +348,16 @@ def __init__(self, conf, fs): freq = conf["freq"] slope = conf["slope"] gain = conf["gain"] - omega = 2.0 * np.pi * freq / fs + omega = 2.0 * math.pi * freq / fs ampl = 10.0 ** (gain / 40.0) - sn = np.sin(omega) - cs = np.cos(omega) + sn = math.sin(omega) + cs = math.cos(omega) alpha = ( sn / 2.0 - * np.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope / 12.0) - 1.0) + 2.0) + * math.sqrt((ampl + 1.0 / ampl) * (1.0 / (slope / 12.0) - 1.0) + 2.0) ) - beta = 2.0 * np.sqrt(ampl) * alpha + beta = 2.0 * math.sqrt(ampl) * alpha b0 = ampl * ((ampl + 1.0) - (ampl - 1.0) * cs + beta) b1 = 2.0 * ampl * ((ampl - 1.0) - (ampl + 1.0) * cs) b2 = ampl * ((ampl + 1.0) - (ampl - 1.0) * cs - beta) @@ -365,7 +367,7 @@ def __init__(self, conf, fs): elif ftype == "LowpassFO": freq = conf["freq"] omega = 2.0 * np.pi * freq / fs - k = np.tan(omega / 2.0) + k = math.tan(omega / 2.0) alpha = 1 + k a0 = 1.0 a1 = -((1 - k) / alpha) @@ -375,8 +377,8 @@ def __init__(self, conf, fs): b2 = 0 elif ftype == "HighpassFO": freq = conf["freq"] - omega = 2.0 * np.pi * freq / fs - k = np.tan(omega / 2.0) + omega = 2.0 * math.pi * freq / fs + k = math.tan(omega / 2.0) alpha = 1 + k a0 = 1.0 a1 = -((1 - k) / alpha) @@ -387,9 +389,9 @@ def __init__(self, conf, fs): elif ftype == "Notch": freq = conf["freq"] q = conf["q"] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + omega = 2.0 * math.pi * freq / fs + sn = math.sin(omega) + cs = math.cos(omega) alpha = sn / (2.0 * q) b0 = 1.0 b1 = -2.0 * cs @@ -400,9 +402,9 @@ def __init__(self, conf, fs): elif ftype == "Bandpass": freq = conf["freq"] q = conf["q"] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + omega = 2.0 * math.pi * freq / fs + sn = math.sin(omega) + cs = math.cos(omega) alpha = sn / (2.0 * q) b0 = alpha b1 = 0.0 @@ -413,9 +415,9 @@ def __init__(self, conf, fs): elif ftype == "Allpass": freq = conf["freq"] q = conf["q"] - omega = 2.0 * np.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + omega = 2.0 * math.pi * freq / fs + sn = math.sin(omega) + cs = math.cos(omega) alpha = sn / (2.0 * q) b0 = 1.0 - alpha b1 = -2.0 * cs @@ -425,8 +427,8 @@ def __init__(self, conf, fs): a2 = 1.0 - alpha elif ftype == "AllpassFO": freq = conf["freq"] - omega = 2.0 * np.pi * freq / fs - tn = np.tan(omega / 2.0) + omega = 2.0 * math.pi * freq / fs + tn = math.tan(omega / 2.0) alpha = (tn + 1.0) / (tn - 1.0) b0 = 1.0 b1 = alpha @@ -440,13 +442,13 @@ def __init__(self, conf, fs): qt = conf["q_target"] ft = conf["freq_target"] - d0i = (2.0 * np.pi * f0) ** 2 - d1i = (2.0 * np.pi * f0) / q0 - c0i = (2.0 * np.pi * ft) ** 2 - c1i = (2.0 * np.pi * ft) / qt + d0i = (2.0 * math.pi * f0) ** 2 + d1i = (2.0 * math.pi * f0) / q0 + c0i = (2.0 * math.pi * ft) ** 2 + c1i = (2.0 * math.pi * ft) / qt fc = (ft + f0) / 2.0 - gn = 2 * np.pi * fc / math.tan(np.pi * fc / fs) + gn = 2 * math.pi * fc / math.tan(math.pi * fc / fs) cci = c0i + gn * c1i + gn ** 2 b0 = (d0i + gn * d1i + gn ** 2) / cci @@ -463,17 +465,16 @@ def __init__(self, conf, fs): self.b1 = b1 / a0 self.b2 = b2 / a0 - def complex_gain(self, f): - z = np.exp(1j * 2 * np.pi * f / self.fs) - A = (self.b0 + self.b1 * z ** (-1) + self.b2 * z ** (-2)) / ( - 1.0 + self.a1 * z ** (-1) + self.a2 * z ** (-2) - ) - return f, A + def complex_gain(self, freq): + zvec = [cmath.exp(1j * 2 * math.pi * f / self.fs) for f in freq] + A = [((self.b0 + self.b1 * z ** (-1) + self.b2 * z ** (-2)) / ( + 1.0 + self.a1 * z ** (-1) + self.a2 * z ** (-2))) for z in zvec] + return freq, A def gain_and_phase(self, f): - _f, A = self.complex_gain(f) - gain = 20 * np.log10(np.abs(A)) - phase = 180 / np.pi * np.angle(A) + _f, Avec = self.complex_gain(f) + gain = [20 * math.log10(abs(A)) for A in Avec] + phase = [180 / math.pi * cmath.phase(A) for A in Avec] return f, gain, phase def is_stable(self): From e4867a14e7e1fedf299a9ebfd0dd20913e8924d2 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Tue, 20 Oct 2020 23:22:42 +0200 Subject: [PATCH 2/4] Filter eval without numpy --- camilladsp_plot/cooley_tukey.py | 29 ++++++ camilladsp_plot/filter_eval.py | 146 +++++++++++++++++------------ camilladsp_plot/plotcamillaconf.py | 2 +- 3 files changed, 117 insertions(+), 60 deletions(-) create mode 100644 camilladsp_plot/cooley_tukey.py diff --git a/camilladsp_plot/cooley_tukey.py b/camilladsp_plot/cooley_tukey.py new file mode 100644 index 0000000..7652037 --- /dev/null +++ b/camilladsp_plot/cooley_tukey.py @@ -0,0 +1,29 @@ +# Adapted from https://jeremykun.com/2012/07/18/the-fast-fourier-transform/ +import cmath +import math + +def omega(p, q): + return cmath.exp((2.0 * cmath.pi * 1j * q) / p) + +def _fft(signal): + n = len(signal) + if n == 1: + return signal + else: + Feven = _fft([signal[i] for i in range(0, n, 2)]) + Fodd = _fft([signal[i] for i in range(1, n, 2)]) + + combined = [0] * n + for m in range(int(n/2)): + combined[m] = Feven[m] + omega(n, -m) * Fodd[m] + combined[m + int(n/2)] = Feven[m] - omega(n, -m) * Fodd[m] + + return combined + +def fft(signal): + orig_len = len(signal) + fft_len = 2**(math.ceil(math.log2(orig_len))) + for _n in range(fft_len-orig_len): + signal.append(0.0) + fftsig = _fft(signal) + return fftsig \ No newline at end of file diff --git a/camilladsp_plot/filter_eval.py b/camilladsp_plot/filter_eval.py index 2991bda..b5d6efa 100644 --- a/camilladsp_plot/filter_eval.py +++ b/camilladsp_plot/filter_eval.py @@ -1,5 +1,3 @@ -import numpy as np -import numpy.fft as fft import cmath import math import csv @@ -8,17 +6,19 @@ from matplotlib import pyplot as plt import math import itertools +import struct +from .cooley_tukey import fft class Conv(object): DATATYPE = { - "FLOAT64LE": float, - "FLOAT32LE": np.float32, - "S16LE": np.int16, - "S24LE": np.int32, - "S24LE3": np.uint8, - "S32LE": np.int32, + "FLOAT64LE": "=(len(y)): + i1 = len(y)-1 + if i2>=(len(y)): + i2 = i1 + fract = idx - i1 + + newval = (1-fract)*y[i1] + fract*y[i2] + #newval = y[i1] + ynew.append(newval) + #print(i1, i2, fract, newval) + #print(y[0:20], ynew[0:20]) + return ynew + + + def gain_and_phase(self, f): - _f, A = self.complex_gain(f) - gain = 20 * np.log10(np.abs(A) + 1e-15) - phase = 180 / np.pi * np.angle(A) + f_fft, Avec = self.complex_gain(f) + gain = [20 * math.log10(abs(A)) for A in Avec] + gain = self.interpolate(gain, f_fft, f) + phase = [180 / math.pi * cmath.phase(A) for A in Avec] + phase = self.interpolate(phase, f_fft, f) return f, gain, phase - - def get_impulse(self): - t = np.linspace( - 0, len(self.impulse) / self.fs, len(self.impulse), endpoint=False - ) + t = [n/self.fs for n in range(len(self.impulse))] return t, self.impulse @@ -132,22 +155,27 @@ def __init__(self, conf, fs): if len(self.b) == 0: self.b = [1.0] - def complex_gain(self, f): - z = np.exp(1j * 2 * np.pi * f / self.fs) - print(self.a, self.b) - A1 = np.zeros(z.shape) + def complex_gain(self, freq): + + A = [((self.b0 + self.b1 * z ** (-1) + self.b2 * z ** (-2)) / ( + 1.0 + self.a1 * z ** (-1) + self.a2 * z ** (-2))) for z in zvec] + return freq, A + + def complex_gain(self, freq): + zvec = [cmath.exp(1j * 2 * math.pi * f / self.fs) for f in freq] + A1 = [0.0 for n in range(len(freq))] for n, bn in enumerate(self.b): - A1 = A1 + bn * z ** (-n) - A2 = np.zeros(z.shape) + A1 = [a1 + bn * z ** (-n) for a1 in A1] + A2 = [0.0 for n in range(len(freq))] for n, an in enumerate(self.a): - A2 = A2 + an * z ** (-n) - A = A1 / A2 + A2 = [a2 + an * z ** (-n) for a2 in A2] + A = [a1 / a2 for (a1, a2) in zip(A1, A2)] return f, A def gain_and_phase(self, f): - _f, A = self.complex_gain(f) - gain = 20 * np.log10(np.abs(A)) - phase = 180 / np.pi * np.angle(A) + _f, Avec = self.complex_gain(f) + gain = [20 * math.log10(abs(A)) for A in Avec] + phase = [180 / math.pi * cmath.phase(A) for A in Avec] return f, gain, phase def is_stable(self): @@ -161,16 +189,16 @@ def __init__(self, conf): self.inverted = conf["inverted"] def complex_gain(self, f): - ones = np.ones(f.shape) sign = -1.0 if self.inverted else 1.0 - gain = ones * 10.0**(self.gain/20.0) * sign - return f, gain + gain = 10.0**(self.gain/20.0) * sign + A = [gain for n in range(len(freq))] + return f, A def gain_and_phase(self, f): - ones = np.ones(f.shape) - phaseval = 180 / np.pi if self.inverted else 0 - gain = ones * self.gain - phase = ones * phaseval + Aval = 10.0**(self.gain/20.0) + gain = [Aval for n in range(len(freq))] + phaseval = 180 / math.pi if self.inverted else 0 + phase = [phaseval for n in range(len(freq))] return f, gain, phase def is_stable(self): @@ -235,17 +263,17 @@ def is_stable(self): # TODO return None - def complex_gain(self, f): - A = np.ones(f.shape) + def complex_gain(self, freq): + A = [1.0 for n in range(len(freq))] for bq in self.biquads: - _f, Atemp = bq.complex_gain(f) - A = A * Atemp - return f, A + _f, Atemp = bq.complex_gain(freq) + A = [a*atemp for (a, atemp) in zip(A, Atemp)] + return freq, A def gain_and_phase(self, f): - _f, A = self.complex_gain(f) - gain = 20 * np.log10(np.abs(A)) - phase = 180 / np.pi * np.angle(A) + _f, Avec = self.complex_gain(f) + gain = [20 * math.log10(abs(A)) for A in Avec] + phase = [180 / math.pi * cmath.phase(A) for A in Avec] return f, gain, phase @@ -263,8 +291,8 @@ def __init__(self, conf, fs): freq = conf["freq"] q = conf["q"] omega = 2.0 * math.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + sn = math.sin(omega) + cs = math.cos(omega) alpha = sn / (2.0 * q) b0 = (1.0 + cs) / 2.0 b1 = -(1.0 + cs) @@ -276,8 +304,8 @@ def __init__(self, conf, fs): freq = conf["freq"] q = conf["q"] omega = 2.0 * math.pi * freq / fs - sn = np.sin(omega) - cs = np.cos(omega) + sn = math.sin(omega) + cs = math.cos(omega) alpha = sn / (2.0 * q) b0 = (1.0 - cs) / 2.0 b1 = 1.0 - cs diff --git a/camilladsp_plot/plotcamillaconf.py b/camilladsp_plot/plotcamillaconf.py index 6e920cc..b4d7d1a 100644 --- a/camilladsp_plot/plotcamillaconf.py +++ b/camilladsp_plot/plotcamillaconf.py @@ -13,7 +13,7 @@ def main(): plot_pipeline(conf) plot_filters(conf) - plot_all_filtersteps(conf) + #plot_all_filtersteps(conf) plt.show() From 646a710718d973ff55bc82264d075e5c669895d0 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Wed, 21 Oct 2020 21:27:04 +0200 Subject: [PATCH 3/4] Remove numpy, separate eval and plotting --- camilladsp_plot/__init__.py | 1 + camilladsp_plot/eval_filterconfig.py | 72 +++++++++++++++++++ .../{filter_eval.py => filters.py} | 62 ++++++++++------ camilladsp_plot/plot_filters.py | 68 ++++++------------ camilladsp_plot/plot_pipeline.py | 4 -- camilladsp_plot/plotcamillaconf.py | 10 ++- setup.py | 2 +- 7 files changed, 143 insertions(+), 76 deletions(-) create mode 100644 camilladsp_plot/eval_filterconfig.py rename camilladsp_plot/{filter_eval.py => filters.py} (90%) diff --git a/camilladsp_plot/__init__.py b/camilladsp_plot/__init__.py index 2821ef5..3db35c0 100644 --- a/camilladsp_plot/__init__.py +++ b/camilladsp_plot/__init__.py @@ -1,2 +1,3 @@ from camilladsp_plot.plot_filters import plot_filters, plot_filter, plot_filterstep, plot_all_filtersteps from camilladsp_plot.plot_pipeline import plot_pipeline +from camilladsp_plot.eval_filterconfig import eval_filter, eval_filterstep diff --git a/camilladsp_plot/eval_filterconfig.py b/camilladsp_plot/eval_filterconfig.py new file mode 100644 index 0000000..e58e1d2 --- /dev/null +++ b/camilladsp_plot/eval_filterconfig.py @@ -0,0 +1,72 @@ +import math +import cmath +from camilladsp_plot.filters import Biquad, BiquadCombo, Conv, DiffEq, Gain + + +def logspace(minval, maxval, npoints): + logmin = math.log10(minval) + logmax = math.log10(maxval) + perstep = (logmax-logmin)/npoints + values = [10.0**(logmin+n*perstep) for n in range(npoints)] + return values + +def eval_filter(filterconf, name=None, samplerate=44100, npoints=1000): + fvect = logspace(1.0, samplerate*0.95/2.0, npoints) + result = {"name": name, "samplerate": samplerate, "f": fvect } + if name is None: + name = "unnamed {}".format(filterconf['type']) + if filterconf['type'] in ('Biquad', 'DiffEq', 'BiquadCombo'): + if filterconf['type'] == 'DiffEq': + currfilt = DiffEq(filterconf['parameters'], samplerate) + elif filterconf['type'] == 'BiquadCombo': + currfilt = BiquadCombo(filterconf['parameters'], samplerate) + else: + currfilt = Biquad(filterconf['parameters'], samplerate) + + _fplot, magn, phase = currfilt.gain_and_phase(fvect) + result["magnitude"] = magn + result["phase"] = phase + elif filterconf['type'] == 'Conv': + if 'parameters' in filterconf: + currfilt = Conv(filterconf['parameters'], samplerate) + else: + currfilt = Conv(None, samplerate) + _ftemp, magn, phase = currfilt.gain_and_phase(fvect) + t, impulse = currfilt.get_impulse() + result["magnitude"] = magn + result["phase"] = phase + result["time"] = t + result["impulse"] = impulse + return result + +def eval_filterstep(conf, pipelineindex, name="filterstep", npoints=1000, toimage=False): + + samplerate = conf['devices']['samplerate'] + fvect = logspace(1.0, samplerate*0.95/2.0, npoints) + pipelinestep = conf['pipeline'][pipelineindex] + totcgain=[1.0 for n in range(npoints)] + for filt in pipelinestep['names']: + filterconf = conf['filters'][filt] + if filterconf['type'] == 'DiffEq': + currfilt = DiffEq(filterconf['parameters'], samplerate) + elif filterconf['type'] == 'BiquadCombo': + currfilt = BiquadCombo(filterconf['parameters'], samplerate) + elif filterconf['type'] == "Biquad": + currfilt = Biquad(filterconf['parameters'], samplerate) + elif filterconf['type'] == "Conv": + currfilt = Conv(filterconf['parameters'], samplerate) + elif filterconf['type'] == "Gain": + currfilt = Gain(filterconf['parameters']) + else: + continue + _, cgainstep = currfilt.complex_gain(fvect) + totcgain = [cg * cgstep for (cg, cgstep) in zip(totcgain, cgainstep)] + gain = [20.0 * math.log10(abs(cg) + 1e-15) for cg in totcgain] + phase = [180 / math.pi * cmath.phase(cg) for cg in totcgain] + result = {"name": name, "samplerate": samplerate, "f": fvect, "magnitude": gain, "phase": phase} + return result + + + + + diff --git a/camilladsp_plot/filter_eval.py b/camilladsp_plot/filters.py similarity index 90% rename from camilladsp_plot/filter_eval.py rename to camilladsp_plot/filters.py index b5d6efa..17cfdff 100644 --- a/camilladsp_plot/filter_eval.py +++ b/camilladsp_plot/filters.py @@ -3,7 +3,6 @@ import csv import yaml import sys -from matplotlib import pyplot as plt import math import itertools import struct @@ -12,15 +11,24 @@ class Conv(object): - DATATYPE = { + TYPES_DIRECT = { "FLOAT64LE": "=(len(y)): i2 = i1 fract = idx - i1 - newval = (1-fract)*y[i1] + fract*y[i2] - #newval = y[i1] ynew.append(newval) - #print(i1, i2, fract, newval) - #print(y[0:20], ynew[0:20]) return ynew - - - def gain_and_phase(self, f): - f_fft, Avec = self.complex_gain(f) + f_fft, Avec = self.complex_gain(None) gain = [20 * math.log10(abs(A)) for A in Avec] gain = self.interpolate(gain, f_fft, f) phase = [180 / math.pi * cmath.phase(A) for A in Avec] diff --git a/camilladsp_plot/plot_filters.py b/camilladsp_plot/plot_filters.py index bc86851..164a8b1 100644 --- a/camilladsp_plot/plot_filters.py +++ b/camilladsp_plot/plot_filters.py @@ -1,6 +1,6 @@ -from camilladsp_plot.filter_eval import Biquad, BiquadCombo, Conv, DiffEq, Gain -import numpy as np +from camilladsp_plot.filters import Biquad, BiquadCombo, Conv, DiffEq, Gain +from camilladsp_plot.eval_filterconfig import eval_filter, eval_filterstep import matplotlib from matplotlib import pyplot as plt import io @@ -8,41 +8,33 @@ def plot_filter(filterconf, name=None, samplerate=44100, npoints=1000, toimage=False): if toimage: matplotlib.use('Agg') - fvect = np.logspace(np.log10(1.0), np.log10((samplerate*0.95)/2.0), num=npoints, base=10.0) - if name is None: - name = "unnamed {}".format(filterconf['type']) + filterdata = eval_filter(filterconf, name, samplerate, npoints) if filterconf['type'] in ('Biquad', 'DiffEq', 'BiquadCombo'): - if filterconf['type'] == 'DiffEq': - currfilt = DiffEq(filterconf['parameters'], samplerate) - elif filterconf['type'] == 'BiquadCombo': - currfilt = BiquadCombo(filterconf['parameters'], samplerate) - else: - currfilt = Biquad(filterconf['parameters'], samplerate) plt.figure(num=name) - fplot, magn, phase = currfilt.gain_and_phase(fvect) - stable = currfilt.is_stable() + fplot = filterdata["f"] + magn = filterdata["magnitude"] + phase = filterdata["phase"] plt.subplot(2,1,1) plt.semilogx(fplot, magn) - plt.title("{}, stable: {}".format(name, stable)) + plt.title(f"{name}") plt.ylabel("Magnitude") plt.subplot(2,1,2) - plt.semilogx(fvect, phase) + plt.semilogx(fplot, phase) plt.ylabel("Phase") elif filterconf['type'] == 'Conv': - if 'parameters' in filterconf: - currfilt = Conv(filterconf['parameters'], samplerate) - else: - currfilt = Conv(None, samplerate) plt.figure(num=name) - ftemp, magn, phase = currfilt.gain_and_phase(fvect) + fplot = filterdata["f"] + magn = filterdata["magnitude"] + phase = filterdata["phase"] + t = filterdata["time"] + impulse = filterdata["impulse"] plt.subplot(2,1,1) - plt.semilogx(ftemp, magn) + plt.semilogx(fplot, magn) plt.title("{}".format(name)) plt.ylabel("Magnitude") plt.gca().set(xlim=(10, samplerate/2.0)) - t, imp = currfilt.get_impulse() plt.subplot(2,1,2) - plt.plot(t, imp) + plt.plot(t, impulse) plt.ylabel("Impulse response") if toimage: buf = io.BytesIO() @@ -60,35 +52,17 @@ def plot_filters(conf): def plot_filterstep(conf, pipelineindex, name="filterstep", npoints=1000, toimage=False): if toimage: matplotlib.use('Agg') - samplerate = conf['devices']['samplerate'] - fvect = np.logspace(np.log10(1.0), np.log10((samplerate*0.95)/2.0), num=npoints, base=10.0) - pipelinestep = conf['pipeline'][pipelineindex] - cgain=np.ones(fvect.shape, dtype=complex) - for filt in pipelinestep['names']: - filterconf = conf['filters'][filt] - if filterconf['type'] == 'DiffEq': - currfilt = DiffEq(filterconf['parameters'], samplerate) - elif filterconf['type'] == 'BiquadCombo': - currfilt = BiquadCombo(filterconf['parameters'], samplerate) - elif filterconf['type'] == "Biquad": - currfilt = Biquad(filterconf['parameters'], samplerate) - elif filterconf['type'] == "Conv": - currfilt = Conv(filterconf['parameters'], samplerate) - elif filterconf['type'] == "Gain": - currfilt = Gain(filterconf['parameters']) - else: - continue - _, cgainstep = currfilt.complex_gain(fvect) - cgain = cgain * cgainstep - gain = 20 * np.log10(np.abs(cgain) + 1e-15) - phase = 180 / np.pi * np.angle(cgain) + filterdata = eval_filterstep(conf, pipelineindex, name, npoints) + fplot = filterdata["f"] + magn = filterdata["magnitude"] + phase = filterdata["phase"] plt.figure(num=name) plt.subplot(2,1,1) - plt.semilogx(fvect, gain) + plt.semilogx(fplot, magn) plt.title(name) plt.ylabel("Magnitude") plt.subplot(2,1,2) - plt.semilogx(fvect, phase) + plt.semilogx(fplot, phase) plt.ylabel("Phase") if toimage: buf = io.BytesIO() diff --git a/camilladsp_plot/plot_pipeline.py b/camilladsp_plot/plot_pipeline.py index 1731d8c..3764fe1 100644 --- a/camilladsp_plot/plot_pipeline.py +++ b/camilladsp_plot/plot_pipeline.py @@ -1,7 +1,3 @@ -# show_config.py - -import numpy as np -import numpy.fft as fft import csv import yaml import sys diff --git a/camilladsp_plot/plotcamillaconf.py b/camilladsp_plot/plotcamillaconf.py index b4d7d1a..20be93c 100644 --- a/camilladsp_plot/plotcamillaconf.py +++ b/camilladsp_plot/plotcamillaconf.py @@ -2,9 +2,15 @@ import sys from camilladsp_plot.plot_pipeline import plot_pipeline from camilladsp_plot.plot_filters import plot_filters, plot_all_filtersteps -from matplotlib import pyplot as plt +try: + from matplotlib import pyplot as plt +except ImportError: + plt = None def main(): + if plt is None: + print("Matplotlib is not available! Can't display plots.") + return fname = sys.argv[1] conffile = open(fname) @@ -13,7 +19,7 @@ def main(): plot_pipeline(conf) plot_filters(conf) - #plot_all_filtersteps(conf) + plot_all_filtersteps(conf) plt.show() diff --git a/setup.py b/setup.py index beca5a5..d1355d9 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ url="https://github.com/HEnquist/pycamilladsp-plot", packages=setuptools.find_packages(), python_requires=">=3", - install_requires=["PyYAML", "numpy", "matplotlib"], + install_requires=["PyYAML"], entry_points = { 'console_scripts': ['plotcamillaconf=camilladsp_plot.plotcamillaconf:main'], } From 0d79bbd2af12aafe7493211e3b784c5d2a918f21 Mon Sep 17 00:00:00 2001 From: HEnquist Date: Thu, 29 Oct 2020 22:49:35 +0100 Subject: [PATCH 4/4] Update readme, bump version 0.4.0 --- README.md | 28 ++++++++++++++++++++++------ camilladsp_plot/eval_filterconfig.py | 2 +- setup.py | 2 +- tests/test_filtereval.py | 11 ++++------- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 17ef4da..41d68fe 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ pip install . Note that on some systems the command is `pip3` instead of `pip`. ## Requirements -This library requires Python 3.6 or newer, and both `numpy` and `matplotlib`. +This library requires Python 3.6 or newer. For plotting configurations with the commandline tool `plotcamillaconf`, it also requires `numpy` and `matplotlib`. These are not required for using only with the web interface. These are the names of the packages needed: -| Distribution | python | numpy | matplotlib | +| Distribution | python | numpy (optional) | matplotlib (optional) | |--------------|--------|-------|------------| | Fedora | python3 | python3-numpy | python3-matplotlib | | Debian/Raspbian | python3 | python3-numpy | python3-matplotlib | @@ -30,8 +30,11 @@ On macOS use either Anaconda or Homebrew. The Anaconda procedure is the same as For Homebrew, install Python with `brew install python`, after which you can install the needed packages with pip, `pip3 install numpy` etc. -## Plotting a configuration -This library provides the console command `plotcamillaconf`. Once the library is installed, the command should be available in your terminal. + + + +## Plotting a configuration from the command line +This library provides the console command `plotcamillaconf`. Once the library is installed, togehter with the optional dependencies numpy and matplotlib, the command should be available in your terminal. To use it type: ```sh plotcamillaconf /path/to/some/config.yml @@ -40,7 +43,7 @@ plotcamillaconf /path/to/some/config.yml This will plot the frequency response of all the defined filters, and show a block diagram of the pipeline. -## Evaluating filters +## Plotting filters To plot the frequency response of a filter, use the function `plot_filter`. This is mostly meant for internal use by the `plotcamillaconf` command. ```python plot_filter(filterconf, name=None, samplerate=44100, npoints=1000, toimage=False) @@ -51,7 +54,7 @@ It's also possible to plot the combined frequency response of a Filter step in t ```python plot_filterstep(conf, pipelineindex, name="filterstep", npoints=1000, toimage=False) ``` -This command takes the full configuration as `conf` must be provided. This will plot the step with index `pipelineindex` in the pipeline where 0 is the first step. The `name` is used for the plot title. The number of points in the plot is set with `npoints`. If `toimage` is set to True, then it will instead return the plot as an svg image. +This command takes a full configuration as `conf`. It will then plot the step with index `pipelineindex` in the pipeline where 0 is the first step. The `name` is used for the plot title. The number of points in the plot is set with `npoints`. If `toimage` is set to True, then it will instead return the plot as an svg image. ## Plotting the pipeline To plot a block diagram of the pipeline, use the function `plot_pipeline`. This is mostly meant for internal use by the `plotcamillaconf` command. @@ -60,3 +63,16 @@ plot_pipeline(conf, toimage=False) ``` This takes a full CamillaDSP configuration, `conf`. It will then plot the pipeline using PyPlot. If `toimage` is set to True, then it will instead return the plot as an svg image. +## Evaluating filters +To evaluate the frequency response of a filter, use the function `eval_filter`. This is mostly meant for internal use by the `plotcamillaconf` command as well as the web gui. +```python +eval_filter(filterconf, name=None, samplerate=44100, npoints=1000) +``` +This will evaluate the filter and return the result as a dictionary. The filter configuration `filterconf` must be provided. The `samplerate` defaults to 44100 if not given. The filter `name` is used for labels. The number of points in the plot is set with `npoints`. The contents of the returned dictionary depends on the filter type. A Biquad returns `name`, `samplerate`, `f`, `magnitude` and `phase`. A Conv filter additionally return the impulse response in `time` and `impulse`. + +It's also possible to evaluate the combined frequency response of a Filter step in the pipeline. +```python +eval_filterstep(conf, pipelineindex, name="filterstep", npoints=1000) +``` +This command takes a full configuration as `conf`. It will evaluate the step with index `pipelineindex` in the pipeline where 0 is the first step. As for eval_filter, the result is returned as a dictionary with the same fields as for a Biquad. + diff --git a/camilladsp_plot/eval_filterconfig.py b/camilladsp_plot/eval_filterconfig.py index e58e1d2..f86b141 100644 --- a/camilladsp_plot/eval_filterconfig.py +++ b/camilladsp_plot/eval_filterconfig.py @@ -12,9 +12,9 @@ def logspace(minval, maxval, npoints): def eval_filter(filterconf, name=None, samplerate=44100, npoints=1000): fvect = logspace(1.0, samplerate*0.95/2.0, npoints) - result = {"name": name, "samplerate": samplerate, "f": fvect } if name is None: name = "unnamed {}".format(filterconf['type']) + result = {"name": name, "samplerate": samplerate, "f": fvect } if filterconf['type'] in ('Biquad', 'DiffEq', 'BiquadCombo'): if filterconf['type'] == 'DiffEq': currfilt = DiffEq(filterconf['parameters'], samplerate) diff --git a/setup.py b/setup.py index d1355d9..f269cb6 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setuptools.setup( name="camilladsp_plot", - version="0.3.0", + version="0.4.0", author="Henrik Enquist", author_email="henrik.enquist@gmail.com", description="A library for plotting configs and filters for CamillaDSP", diff --git a/tests/test_filtereval.py b/tests/test_filtereval.py index 59eaf4a..51feb94 100644 --- a/tests/test_filtereval.py +++ b/tests/test_filtereval.py @@ -1,12 +1,9 @@ import pytest -from camilladsp_plot import filter_eval +from camilladsp_plot import filters import numpy as np -def test_24bit(): - filt = filter_eval.Conv(None, 44100) - vect = np.asarray([1, 0, 0, 255, 0, 0, 0, 0, 1], dtype=np.uint8) - expected = np.array([2**16, -2**16, 1]) - new_vect = filt._repack_24bit(vect) - assert abs(np.sum(new_vect-expected))<1e-15 +def test_conv(): + filt = filters.Conv(None, 44100) + # TODO make proper tests