import numpy as np
import scipy
from sklearn.linear_model import LinearRegression
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button
import copy
import re
import time
from common import framer
from irtools import IRv
from visuals import ToggleText, SliderGroup, slider_guard
class feedback_delay_network:
def __init__(self, fs, m, A_type, A_gain, b, c, d = 1, A = None):
self.fs = fs
self.m = np.array(m, dtype=np.int64)
self.A_type = A_type
self.A_gain = A_gain
self.b = np.array(b)
self.c = np.array(c)
self.d = d
self.A = self.factory_A(A_type, self.m.size) * self.A_gain
def get_config_dict(self):
return dict(fs=self.fs, m=self.m, A_type=self.A_type, A_gain=self.A_gain, b=self.b, c=self.c, d=self.d)
def _filt(self, x, zi):
if len(x) > min(self.m): raise ValueError('Block size of FDN shall not exceed any delay length.')
y = self.d * x + self.c.dot(zi[0][:, :len(x)])
ss = self.A.dot(zi[0][:, :len(x)]) + np.transpose([self.b]).dot([x])
zi[0] = np.hstack([zi[0][:, len(x):], np.zeros_like(zi[0][:, :len(x)])])
for i, m in enumerate(self.m): zi[0][i, m - len(x) : m] = ss[i]
return y, [zi[0], zi[1]]
def filt(self, x, zi):
if zi[0].shape != (len(self.m), max(self.m)): zi = self.make_z(zi)
y = np.zeros_like(x)
for block, pos in framer(x, min(self.m), min(self.m), ret_pos=True):
y1, zi = self._filt(block, zi)
y[pos : pos + len(block)] = y1
return y, zi
def filter_1(self, x):
zi = self.make_z()
return self.filt(x, zi)[0]
def filter(self, x):
s = np.zeros((len(self.m), len(x) + np.max(self.m)))
for n, xn in enumerate(x):
ss = self.A.dot(s[:, n]) + xn * self.b # feedback
s[range(len(self.m)), n + self.m] = ss # delay
y = self.d * x + self.c.dot(s[:, :len(x)])
return y
def make_z(self, zi = None):
# print('make_z', self.m)
new_z = np.zeros((len(self.m), max(self.m)))
if zi != None:
old_z, old_m = zi
for i in range(min(len(self.m), len(old_m))):
copy_z = min(self.m[i], old_m[i])
new_z[i][self.m[i] - copy_z : self.m[i]] = old_z[i][old_m[i] - copy_z : old_m[i]]
return [new_z, self.m]
def set_A(self):
self.A = self.factory_A(self.A_type, self.m.size) * self.A_gain
def set_A_gain(self, A_gain):
if self.A_gain != A_gain:
self.A_gain = A_gain
self.set_A()
def set_A_type(self, A_type):
if self.A_type != A_type:
self.A_type = A_type
self.set_A()
def set_order(self, M):
if M < self.m.size:
self.m = self.m[:M]
self.b = self.b[:M]
self.c = self.c[:M]
self.set_A()
elif M > self.m.size:
self.m = self.extend_m(self.m, M)
self.b = self.extend_repeat(self.b, M)
self.c = self.extend_repeat(self.c, M)
self.set_A()
def short_desc(self):
return [f'{self.fs}Hz', self.A_type + str(self.m.size) + f'x{self.A_gain:.3g}', f'm: {min(self.m)}~{max(self.m)}']
@staticmethod
def extend_repeat(m, M):
new_m = np.zeros(M)
new_m[:m.size] = m
new_m[m.size:] = m[-1]
return new_m
@staticmethod
def extend_m(m, M):
new_m = np.zeros(M, dtype=np.int64)
new_m[:m.size] = m
new_m[m.size:] = np.rint(m[-1] * 1.216589714650669 ** np.arange(1, M - m.size + 1)).astype(np.int64)
return new_m
@staticmethod
def factory_A(A_type, order = None):
if order is not None:
type_str = A_type
else:
r = re.search('([A-Za-z]+)([0-9]+)', A_type)
type_str = r.group(1)
order = int(r.group(2))
if type_str == 'Stautner': A = feedback_delay_network.Stautner_A(order)
elif type_str == 'Hadamard': A = feedback_delay_network.Hadamard_A(order)
else: raise ValueError('FDN: unrecognized feedback matrix type \'' + A_type + '\'')
return A
@staticmethod
def Stautner_A(order = 4):
A = np.array([[0., 1, 1, 0], [-1, 0, 0, -1], [1, 0, 0, -1], [0, 1, -1, 0]]) / np.sqrt(2)
if order == 4: return A
A = np.kron(A, rot_matrix(np.pi/4))
if order == 8: return A
A = np.kron(A, rot_matrix(np.pi/8))
if order == 16: return A
@staticmethod
def Hadamard_A(order = 4):
return scipy.linalg.hadamard(order) / np.sqrt(order)
def rot_matrix(theta):
return np.array([[np.cos(theta/2), -np.sin(theta/2)], [np.sin(theta/2), np.cos(theta/2)]])
# FDN form is used to specify an FDN. It provides several configuration inputs for FDN parameters,
# as well as visual and audio support for examining the impulse response.
# The expected use scenario is:
# . an external caller opens this form, supplying an external fdn instance
# . the form's contents (fdn settings) are populated by those of the external fdn
# . the user interacts with the form, changes the paramters and examines the resultant fdn
# . the user applys the changes with Apply or OK buttons, or discards them with Cancel button
# . the form is closed with OK or cancel buttons
class FDNForm: # parent class IRv provides visual / audio support for examining the impulse response
def __init__(self, fdn: feedback_delay_network, auto_apply = False, on_apply = None):
# set up form layout
fig, ax = plt.subplot_mosaic('AA;BC', height_ratios=[1, 4])
fig.subplots_adjust(left=0.05, right=0.95)
fig.subplots_adjust(top=0.95, bottom=0.5)
self.figure = fig
self.canvas = fig.canvas
# axes 'A' is used as a panel to host several FDN controls
ax['A'].set_axis_off()
# FDN control: feedback matrix type
self.Stautner_Toggle = ToggleText(-0.04, 0.2, 'S', ax['A'], on_action=self.on_type_toggle, tag='Stautner')
self.Hadamard_Toggle = ToggleText(-0.01, 0.2, 'H', ax['A'], on_action=self.on_type_toggle, tag='Hadamard')
# FDN control: number of delay lines
self.Order4_Toggle = ToggleText(0.09, 0.2, '4', ax['A'], on_action=self.on_type_toggle, tag=4)
self.Order8_Toggle = ToggleText(0.12, 0.2, '8', ax['A'], on_action=self.on_type_toggle, tag=8)
self.Order16_Toggle = ToggleText(0.15, 0.2, '16', ax['A'], on_action=self.on_type_toggle, tag=16)
# axes 'B' and 'C' are passed to IRv
# super().__init__(ax['B'], ax['C'])
self.irv = IRv(ax['B'], ax['C'])
# IRv does not provide a textual label, so we add it here. ir_ax is from parent class IRv
self.ir_label = self.irv.ir_ax.axes.text(0.01, 1.01, '', transform=self.irv.ir_ax.axes.transAxes, fontsize = 'x-small', picker=1, va='bottom', wrap=True)
# FDN controls: feedback gain
self.gain_slider = Slider(fig.add_axes([0.10, 0.92, 0.8, 0.04]), label="FB gain", valmin=0, valmax=1.0, valinit=0.95, valstep=0.005)
self.gain_slider.on_changed(self.on_gain_slider_change)
# FDN control: delays, forward gains and output gains
self.Mgroup = SliderGroup(self.figure, [0.05, 0.1, 0.3, 0.3], [], valmin=1, valmax=1000, valstep=1, label='Delays', on_change=self.on_M_change, sizeup=0.4, sizemm=0.15, allow_alt=True)
self.Bgroup = SliderGroup(self.figure, [0.4, 0.1, 0.25, 0.3], [], label='B', on_change=self.on_B_change, allow_alt=True)
self.Cgroup = SliderGroup(self.figure, [0.7, 0.1, 0.25, 0.3], [], label='C', on_change=self.on_C_change, allow_alt=True)
# apply/ok/cancel buttons
self.ApplyButton = Button(fig.add_axes([0.65, 0.03, 0.1, 0.05]), 'Apply')
self.ApplyButton.on_clicked(self.ApplyClick)
self.OkButton = Button(fig.add_axes([0.75, 0.03, 0.1, 0.05]), 'OK')
self.OkButton.on_clicked(self.OkClick)
self.CancelButton = Button(fig.add_axes([0.85, 0.03, 0.1, 0.05]), 'Cancel')
self.CancelButton.on_clicked(self.CancelClick)
self.auto_apply = auto_apply
self.on_apply = on_apply
# fig.canvas.mpl_connect('pick_event', self._pick)
self.set_fdn(fdn, init=True)
# set feedback matrix (by type or size), triggered by ToggleText buttons
def on_type_toggle(self, sender : ToggleText):
if sender in [self.Stautner_Toggle, self.Hadamard_Toggle]:
# # This shows how one creates a new FDN to replace the old
# self.set_fdn(feedback_delay_network(**{**self.fdn.get_config_dict(), 'A_type':sender.tag}))
# This shows how one updates the imcumbent FDN
self.fdn.set_A_type(sender.tag)
self.set_fdn()
elif sender in [self.Order4_Toggle, self.Order8_Toggle, self.Order16_Toggle]:
self.fdn.set_order(sender.tag)
self.set_fdn()
def on_gain_slider_change(self, val):
self.fdn.set_A_gain(val)
self.set_fdn()
def on_B_change(self, sg : SliderGroup, sender : Slider):
self.fdn.b[sender.index] = sender.val
self.set_fdn()
def on_C_change(self, sg : SliderGroup, sender : Slider):
self.fdn.c[sender.index] = sender.val
self.set_fdn()
def on_M_change(self, sg : SliderGroup, sender : Slider):
self.fdn.m[sender.index] = sender.val
self.set_fdn()
def set_fdn(self, fdn : feedback_delay_network = None, init = False):
# set_fdn(...) may be called with or without the fdn argument.
# .when called with an external fdn, it keeps a reference of the external fdn and makes a
# copy of it as the internal fdn. Form contents are repopulated to reflect the setup
# of the new internal fdn.
# .when called without fdn argument, it is assumed that the interal fdn is modified by user.
# Form contents are updated to synchronize with that change.
if fdn:
self.ref_fdn = fdn
self.fdn = copy.deepcopy(fdn)
if not hasattr(self, 'fs') or self.fs != fdn.fs:
self.irv.set_fs(fdn.fs) # IRv uses a filterbank that needs the sample rate
self.ir_label.set_text((['FDN '] + self.fdn.short_desc())) # textual description of current (internal) FDN
self.Stautner_Toggle.set_on(self.fdn.A_type == 'Stautner') # type of FDN feedback matrix
self.Hadamard_Toggle.set_on(self.fdn.A_type == 'Hadamard')
self.Order4_Toggle.set_on(self.fdn.m.size == 4) # size of FDN feedback matrix (number of delay lines)
self.Order8_Toggle.set_on(self.fdn.m.size == 8)
self.Order16_Toggle.set_on(self.fdn.m.size == 16)
with slider_guard(self.gain_slider) as gs: gs.set_val(self.fdn.A_gain) # FDN feedback gain
self.Mgroup.set_data(self.fdn.m, init=init) # FDN delays
self.Bgroup.set_data(self.fdn.b, init=init) # FDN forward gains
self.Cgroup.set_data(self.fdn.c, init=init) # FDN output gains
# compute and show IR, RT
M = max(self.fdn.m)
L = M * 2
zi = self.fdn.make_z()
self.ir, zi = self.fdn.filt(scipy.signal.unit_impulse(L, idx=1), zi)
e0 = np.sum(self.ir ** 2) / 2
while self.ir.size < self.fdn.fs * 10:
ir, zi = self.fdn.filt(np.zeros(M), zi)
self.ir = np.concatenate([self.ir, ir])
e = np.sum(ir ** 2)
e0 = max(e0, e)
if e < e0 * 1e-6: break
# reserve extra length for delays in analysis filters
L_ex = np.ceil(max(self.irv.octave_filter_bank.delays)).astype(np.int64)
ir_ex = self.fdn.filt(np.zeros(L_ex), zi)[0]
self.ir = np.c_[self.ir]
# present ir
self.irv.put_ir(self.ir[:, 0], ir_ex)
if self.auto_apply: self._apply()
def _apply(self):
_tmp_fdn = copy.deepcopy(self.fdn)
self.ref_fdn.__dict__.update(_tmp_fdn.__dict__)
if self.on_apply: self.on_apply(self)
def ApplyClick(self, event):
self._apply()
def CancelClick(self, event):
plt.close(self.figure)
def OkClick(self, event):
self.ApplyClick(event)
self.CancelClick(event)
def main():
fs = 48000
# n = feedback_delay_network(fs, [149, 211, 263, 293], 'Stautner4', 1 / sqrt(6.2), np.ones(4), np.ones(4) * .8, 1)
fdn_N = 4
fdn_m = [149, 211, 263, 293, 331, 347, 391, 433, 513, 696, 837, 1003, 1245, 1449, 1838, 2257]
n = feedback_delay_network(fs, fdn_m[:fdn_N], 'Stautner', 0.95, np.ones_like(fdn_m[:fdn_N]) * 0.5, np.ones_like(fdn_m[:fdn_N]) * .8, 1)
dsf = FDNForm(n)
plt.show()
dsf = FDNForm(n)
plt.show()
return
这个代码文件的功能是什么