%23%20gallery_category%3A%20Special%20FDNs%0A%23%20gallery_description%3A%20Reverberation%20enhancement%20system%20built%20with%20the%20pyFDN.td%20graph%3A%20an%20FDN%20inside%20an%20acoustic%20feedback%20loop%2C%20with%20a%20time-varying%20feedback%20matrix%20raising%20the%20stable%20gain%20before%20feedback.%0A%23%20requires%3A%20pyroomacoustics%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.24.0%22%0Aapp%20%3D%20marimo.App()%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20marimo%20as%20mo%0A%0A%20%20%20%20return%20(mo%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%20Reverberation%20enhancement%20with%20a%20time-varying%20FDN%0A%0A%20%20%20%20A%20**reverberation%20enhancement%20system**%20(RES)%20makes%20a%20room%20sound%20more%20reverberant%20electroacoustically%3A%20microphones%20pick%20up%20the%20room%2C%20a%20reverberator%20processes%20the%20signal%2C%20and%20loudspeakers%20play%20it%20back%20%E2%80%94%20adding%20energy%20to%20the%20reverberant%20field.%20The%20catch%20is%20that%20the%20loudspeakers%20leak%20back%20into%20the%20microphones%2C%20so%20the%20reverberator%20sits%20*inside*%20an%20acoustic%20feedback%20loop.%20Too%20much%20loop%20gain%20and%20the%20system%20colours%20(rings)%20or%20howls%3B%20the%20usable%20gain%20before%20that%20happens%20is%20the%20**maximum%20stable%20gain**%20(MSG).%0A%0A%20%20%20%20A%20**time-varying%20FDN**%20raises%20the%20MSG%3A%20by%20continuously%20modulating%20the%20feedback%20matrix%20it%20stops%20any%20single%20loop%20mode%20from%20building%20up%2C%20so%20the%20same%20enhancement%20can%20be%20driven%20harder%20before%20it%20rings.%0A%0A%20%20%20%20This%20example%20wires%20up%20a%20real%20RES%3A%0A%0A%20%20%20%20*%20%60pyroomacoustics%60%20places%20a%20performer%2C%20a%20listener%2C%20**6%20microphones**%20over%20the%20stage%20and%20**6%20loudspeakers**%20over%20the%20audience%2C%20and%20computes%20every%20room%20impulse%20response%20%E2%80%94%20including%20the%20loudspeaker%E2%86%92microphone%20coupling%20that%20closes%20the%20loop.%0A%20%20%20%20*%20the%20reverberator%20is%20a%206-in%2F6-out%20FDN%20built%20from%20%60pyFDN.td%60%20operators%2C%20with%20an%20optional%20%60td.TimeVaryingMatrix%60%20on%20its%20feedback%20path.%0A%20%20%20%20*%20the%20**entire**%20system%20%E2%80%94%20room%20paths%2C%20coupling%20and%20FDN%20%E2%80%94%20is%20assembled%20as%20a%20single%20%60td%60%20operator%20tree%20and%20run%20by%20one%20%60.process(source)%60%20call.%20The%20whole%20electroacoustic%20feedback%20loop%20is%20just%20a%20%60td.Recursion%60%20whose%20feedback%20path%20is%20the%20room%20coupling%20and%20whose%20forward%20path%20is%20the%20FDN.%0A%0A%20%20%20%20We%20then%20(1)%20confirm%20the%20RES%20enhances%20reverberation%20and%20(2)%20show%20the%20time-varying%20FDN%20stays%20stable%20at%20a%20loop%20gain%20where%20the%20static%20one%20already%20rings.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20import%20numpy%20as%20np%0A%20%20%20%20import%20plotly.graph_objects%20as%20go%0A%0A%20%20%20%20import%20pyFDN%0A%20%20%20%20from%20pyFDN%20import%20td%0A%0A%20%20%20%20return%20go%2C%20np%2C%20pyFDN%2C%20td%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Room%2C%20stage%20and%20audience%20layout%0A%0A%20%20%20%20A%2024%20%C3%97%2018%20%C3%97%209%20m%20hall%20(%E2%89%880.6%20s%20natural%20reverberation).%20The%20performer%20is%20at%20the%20front%20of%20the%20stage%20and%20the%20listener%20sits%20in%20the%20audience.%20The%206%20microphone%20hang%20low%20over%20the%20stage%20apron%3B%20the%206%20loudspeakers%20are%20high%20over%20the%20audience%20%E2%80%94%20a%20separation%20that%20keeps%20the%20loudspeaker%E2%86%92microphone%20coupling%20modest%2C%20as%20a%20real%20install%20would.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(go%2C%20np)%3A%0A%20%20%20%20import%20pyroomacoustics%20as%20pra%0A%0A%20%20%20%20fs%20%3D%2048_000%0A%20%20%20%20room_dim%20%3D%20%5B24.0%2C%2018.0%2C%209.0%5D%0A%0A%20%20%20%20performer%20%3D%20%5B12.0%2C%203.0%2C%201.7%5D%20%20%23%20front%20of%20stage%0A%20%20%20%20listener%20%3D%20%5B12.0%2C%2013.0%2C%201.2%5D%20%20%23%20in%20the%20audience%0A%20%20%20%20mics%20%3D%20np.array(%20%20%23%20over%20the%20stage%20apron%2C%20low%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B7%2C%204%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B12%2C%204%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B17%2C%204%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B9%2C%206%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B12%2C%206%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B15%2C%206%2C%202.2%5D%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20dtype%3Dfloat%2C%0A%20%20%20%20)%0A%20%20%20%20speakers%20%3D%20np.array(%20%20%23%20over%20the%20audience%2C%20high%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B4%2C%2010%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B12%2C%2010%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B20%2C%2010%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B4%2C%2016%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B12%2C%2016%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B20%2C%2016%2C%207.5%5D%2C%0A%20%20%20%20%20%20%20%20%5D%2C%0A%20%20%20%20%20%20%20%20dtype%3Dfloat%2C%0A%20%20%20%20)%0A%0A%20%20%20%20e_absorption%2C%20max_order%20%3D%20pra.inverse_sabine(0.6%2C%20room_dim)%0A%20%20%20%20room%20%3D%20pra.ShoeBox(%0A%20%20%20%20%20%20%20%20room_dim%2C%0A%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20%20%20%20%20materials%3Dpra.Material(e_absorption)%2C%0A%20%20%20%20%20%20%20%20max_order%3Dmin(max_order%2C%208)%2C%0A%20%20%20%20)%0A%20%20%20%20for%20position%20in%20%5Bperformer%2C%20*speakers%5D%3A%0A%20%20%20%20%20%20%20%20room.add_source(position)%0A%20%20%20%20room.add_microphone_array(pra.MicrophoneArray(np.vstack(%5Bmics%2C%20listener%5D).T%2C%20fs))%0A%20%20%20%20room.compute_rir()%0A%0A%20%20%20%20%23%20Top-view%20layout.%0A%20%20%20%20fig_room%20%3D%20go.Figure()%0A%20%20%20%20fig_room.add_shape(%0A%20%20%20%20%20%20%20%20type%3D%22rect%22%2C%0A%20%20%20%20%20%20%20%20x0%3D0%2C%0A%20%20%20%20%20%20%20%20y0%3D0%2C%0A%20%20%20%20%20%20%20%20x1%3Droom_dim%5B0%5D%2C%0A%20%20%20%20%20%20%20%20y1%3Droom_dim%5B1%5D%2C%0A%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20%22%23444%22%7D%2C%0A%20%20%20%20%20%20%20%20fillcolor%3D%22rgba(0%2C0%2C0%2C0)%22%2C%0A%20%20%20%20)%0A%20%20%20%20fig_room.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dmics%5B%3A%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dmics%5B%3A%2C%201%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22microphones%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2011%2C%20%22symbol%22%3A%20%22circle%22%2C%20%22color%22%3A%20%22%234f8a5e%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_room.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dspeakers%5B%3A%2C%200%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dspeakers%5B%3A%2C%201%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22loudspeakers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2013%2C%20%22symbol%22%3A%20%22square%22%2C%20%22color%22%3A%20%22%233d6d9e%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_room.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3D%5Bperformer%5B0%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3D%5Bperformer%5B1%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22performer%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2015%2C%20%22symbol%22%3A%20%22star%22%2C%20%22color%22%3A%20%22%23c0392b%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_room.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3D%5Blistener%5B0%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3D%5Blistener%5B1%5D%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22markers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22listener%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20marker%3D%7B%22size%22%3A%2015%2C%20%22symbol%22%3A%20%22diamond%22%2C%20%22color%22%3A%20%22%237b5ea7%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_room.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22RES%20layout%20(top%20view)%3A%20stage%20at%20front%2C%20audience%20behind%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22x%20(m)%22%2C%20%22range%22%3A%20%5B-1%2C%2025%5D%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22y%20(m)%22%2C%20%22range%22%3A%20%5B-1%2C%2019%5D%2C%20%22scaleanchor%22%3A%20%22x%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D460%2C%0A%20%20%20%20)%0A%20%20%20%20fig_room.show()%0A%20%20%20%20return%20fs%2C%20room%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20Extract%20the%20room%20transfer%20paths%0A%0A%20%20%20%20From%20the%20impulse%20responses%20we%20pull%20out%20four%20filter%20matrices%20the%20tree%20needs%3A%20performer%E2%86%92microphones%20(the%20excitation)%2C%20performer%E2%86%92listener%20(dry%20direct)%2C%20loudspeaker%E2%86%92listener%20(what%20the%20RES%20delivers)%2C%20and%20the%206%20%C3%97%206%20loudspeaker%E2%86%92microphone%20**coupling**%20that%20closes%20the%20loop.%20Each%20becomes%20a%20%60td.MatrixConvolver%60.%20The%20coupling%20is%20truncated%20to%20its%20first%20~43%20ms%20%E2%80%94%20the%20early%20part%20that%20dominates%20feedback%20colouration%20%E2%80%94%20because%20it%20runs%20inside%20the%20loop%2C%20once%20per%20block%3B%20the%20rest%20use%20the%20full%20room%20responses.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20np%2C%20room)%3A%0A%20%20%20%20rir%20%3D%20room.rir%0A%20%20%20%20coupling_taps%20%3D%202048%20%20%23%20~43%20ms%20of%20loudspeaker%20-%3E%20mic%20coupling%20(early%2C%20in-loop)%0A%20%20%20%20room_taps%20%3D%20max(len(rir%5Bo%5D%5Bs%5D)%20for%20o%20in%20range(len(rir))%20for%20s%20in%20range(len(rir%5B0%5D)))%0A%0A%20%20%20%20def%20_pad(h%2C%20n)%3A%0A%20%20%20%20%20%20%20%20h%20%3D%20np.asarray(h%2C%20dtype%3Dfloat)%0A%20%20%20%20%20%20%20%20return%20np.pad(h%2C%20(0%2C%20max(0%2C%20n%20-%20len(h))))%5B%3An%5D%0A%0A%20%20%20%20def%20_matrix(out_rows%2C%20in_cols%2C%20taps)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Stack%20chosen%20RIRs%20into%20a%20(n_out%2C%20n_in%2C%20taps)%20filter%20matrix.%22%22%22%0A%20%20%20%20%20%20%20%20return%20np.stack(%5B%5B_pad(rir%5Bo%5D%5Bi%5D%2C%20taps)%20for%20i%20in%20in_cols%5D%20for%20o%20in%20out_rows%5D)%0A%0A%20%20%20%20%23%20source%20index%200%20%3D%20performer%2C%201..6%20%3D%20loudspeakers%3B%20mic%20index%200..5%2C%206%20%3D%20listener.%0A%20%20%20%20_mic%2C%20_spk%2C%20_src%2C%20_lis%20%3D%20range(6)%2C%20range(1%2C%207)%2C%20%5B0%5D%2C%20%5B6%5D%0A%20%20%20%20source_to_mic%20%3D%20_matrix(_mic%2C%20_src%2C%20room_taps)%20%20%23%20(6%2C%201%2C%20L)%3A%20excitation%0A%20%20%20%20coupling%20%3D%20_matrix(_mic%2C%20_spk%2C%20coupling_taps)%20%20%23%20(6%2C%206%2C%20taps)%3A%20closes%20the%20loop%0A%20%20%20%20speaker_to_listener%20%3D%20_matrix(_lis%2C%20_spk%2C%20room_taps)%20%20%23%20(1%2C%206%2C%20L)%3A%20delivered%0A%20%20%20%20source_to_listener%20%3D%20_matrix(_lis%2C%20_src%2C%20room_taps)%20%20%23%20(1%2C%201%2C%20L)%3A%20dry%20direct%0A%0A%20%20%20%20sig_len%20%3D%20int(0.8%20*%20fs)%0A%20%20%20%20print(f%22Peak%20loudspeaker-%3Emic%20coupling%3A%20%7Bnp.abs(coupling).max()%3A.3f%7D%22)%0A%20%20%20%20print(f%22Room%20RIRs%3A%20%7Broom_taps%7D%20taps%3B%20coupling%20truncated%20to%20%7Bcoupling_taps%7D%20taps%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20coupling%2C%0A%20%20%20%20%20%20%20%20sig_len%2C%0A%20%20%20%20%20%20%20%20source_to_listener%2C%0A%20%20%20%20%20%20%20%20source_to_mic%2C%0A%20%20%20%20%20%20%20%20speaker_to_listener%2C%0A%20%20%20%20)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20The%20reverberator%3A%20a%206%C3%976%20FDN%0A%0A%20%20%20%20The%20reverberator%20maps%20the%206%20microphones%20to%20the%206%20loudspeakers%20through%20an%208-line%20FDN%20with%20frequency-dependent%20absorption%20(its%20own%20~1.2%20s%20decay).%20The%20feedback%20path%20is%20either%20the%20static%20mixing%20matrix%20%60A%60%2C%20or%20%60Series(%5BGain(A)%2C%20td.TimeVaryingMatrix(...)%5D)%60%20%E2%80%94%20the%20only%20change%20needed%20to%20make%20the%20loop%20time-varying.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20np%2C%20pyFDN%2C%20td)%3A%0A%20%20%20%20n_lines%20%3D%208%0A%20%20%20%20fdn_delays%20%3D%20np.array(%5B557%2C%20619%2C%20691%2C%20757%2C%20821%2C%20887%2C%20953%2C%201021%5D)%0A%20%20%20%20np.random.seed(1)%0A%20%20%20%20A%20%3D%20pyFDN.random_orthogonal(n_lines)%0A%20%20%20%20gen%20%3D%20np.random.default_rng(1)%0A%20%20%20%20B_fdn%20%3D%20gen.standard_normal((n_lines%2C%206))%20%2F%20np.sqrt(n_lines)%20%20%23%20mics%20-%3E%20lines%0A%20%20%20%20C_fdn%20%3D%20gen.standard_normal((6%2C%20n_lines))%20%2F%20np.sqrt(%0A%20%20%20%20%20%20%20%20n_lines%0A%20%20%20%20)%20%20%23%20lines%20-%3E%20loudspeakers%0A%20%20%20%20fdn_absorption%20%3D%20pyFDN.decay_to_first_order_shelf(1.2%2C%200.6%2C%20None%2C%20fdn_delays%2C%20fs)%0A%20%20%20%20fdn_block%20%3D%2064%20%20%23%20Recursion%20block%20size%3B%20must%20be%20at%20most%20half%20the%20shortest%20delay%0A%0A%20%20%20%20def%20make_reverberator(time_varying%2C%20g)%3A%0A%20%20%20%20%20%20%20%20%22%22%226-in%2F6-out%20FDN%20operator%20tree.%0A%0A%20%20%20%20%20%20%20%20The%20loop%20gain%20%60%60g%60%60%20is%20folded%20into%20the%20output%20gain%20%60%60C%60%60%2C%20and%20the%0A%20%20%20%20%20%20%20%20feedback%20path%20is%20optionally%20made%20time-varying.%0A%20%20%20%20%20%20%20%20%22%22%22%0A%20%20%20%20%20%20%20%20%23%20The%20Recursion%20inserts%20fdn_block%20samples%20of%20delay%20into%20the%20loop%2C%20so%20the%0A%20%20%20%20%20%20%20%20%23%20delay%20lines%20are%20shortened%20by%20that%20much%20to%20keep%20the%20loop%20delays%20exact.%0A%20%20%20%20%20%20%20%20forward%20%3D%20td.Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Btd.Delay(fdn_delays%20-%20fdn_block)%2C%20td.SOSBank(fdn_absorption)%5D%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20if%20time_varying%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20np.random.seed(3)%20%20%23%20deterministic%20modulation%20across%20rebuilds%0A%20%20%20%20%20%20%20%20%20%20%20%20tvm%20%3D%20td.TimeVaryingMatrix(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20N%3Dn_lines%2C%20cycles_per_second%3D1.2%2C%20amplitude%3D0.7%2C%20fs%3Dfs%2C%20spread%3D0.2%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20%20%20%20%20feedback%20%3D%20td.Series(%5Btd.Gain(A)%2C%20tvm%5D)%0A%20%20%20%20%20%20%20%20else%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20feedback%20%3D%20td.Gain(A)%0A%20%20%20%20%20%20%20%20return%20td.Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.Gain(B_fdn)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.Recursion(forward%2C%20feedback%2C%20block_size%3Dfdn_block)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.Gain(g%20*%20C_fdn)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20return%20(make_reverberator%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%20The%20entire%20RES%20as%20one%20%60td%60%20tree%0A%0A%20%20%20%20Everything%20now%20assembles%20into%20a%20single%20operator%20tree%3A%0A%0A%20%20%20%20%60%60%60%0A%20%20%20%20Parallel(%0A%20%20%20%20%20%20%20%20Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20MatrixConvolver(source%20%E2%86%92%20mics)%2C%20%20%20%20%20%20%20%20%23%20excitation%0A%20%20%20%20%20%20%20%20%20%20%20%20Recursion(%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20the%20electroacoustic%20loop%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fF%20%3D%20Series(Delay(latency)%2C%20FDN)%2C%20%20%23%20%20%20mics%20%E2%86%92%20loudspeakers%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fB%20%3D%20MatrixConvolver(coupling)%2C%20%20%20%20%23%20%20%20loudspeakers%20%E2%86%92%20mics%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20block_size%20%3D%20loop_block%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20MatrixConvolver(loudspeakers%20%E2%86%92%20listener)%2C%0A%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20MatrixConvolver(source%20%E2%86%92%20listener)%2C%20%20%20%20%20%20%20%20%23%20dry%20direct%20path%0A%20%20%20%20)%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20The%20feedback%20loop%20%60loudspeaker%20%E2%86%92%20room%20%E2%86%92%20mic%20%E2%86%92%20FDN%20%E2%86%92%20loudspeaker%60%20is%20literally%20a%20%60td.Recursion%60.%20A%20short%20%60Delay%60%20(the%20RES%20processing%20latency%2C%20~5%20ms)%20leads%20its%20forward%20path%2C%20which%20is%20what%20lets%20the%20block%20recursion%20break%20the%20loop%20%E2%80%94%20the%20same%20role%20the%20FDN's%20own%20delays%20play%20inside%20the%20FDN.%20A%20%60Recursion%60%20processes%20%60block_size%60%20samples%20at%20a%20time%20and%20so%20inserts%20that%20many%20samples%20of%20delay%20into%0A%20%20%20%20its%20loop%2C%20which%20is%20why%20the%20latency%20delay%20(and%2C%20inside%20the%20FDN%2C%20the%20delay%20lines)%20is%20shortened%20by%20exactly%20one%20block.%20One%20%60.process(source)%60%20runs%20the%20whole%20system.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20coupling%2C%0A%20%20%20%20make_reverberator%2C%0A%20%20%20%20np%2C%0A%20%20%20%20sig_len%2C%0A%20%20%20%20source_to_listener%2C%0A%20%20%20%20source_to_mic%2C%0A%20%20%20%20speaker_to_listener%2C%0A%20%20%20%20td%2C%0A)%3A%0A%20%20%20%20latency%20%3D%20256%20%20%23%20RES%20processing%20latency%20(~5.3%20ms)%3B%20breaks%20the%20electroacoustic%20loop%0A%20%20%20%20loop_block%20%3D%2064%20%20%23%20Recursion%20block%20size%20of%20the%20electroacoustic%20loop%0A%0A%20%20%20%20def%20build_res(time_varying%2C%20g)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Assemble%20the%20whole%20reverberation%20enhancement%20system%20as%20a%20td%20tree.%22%22%22%0A%20%20%20%20%20%20%20%20electroacoustic_loop%20%3D%20td.Recursion(%0A%20%20%20%20%20%20%20%20%20%20%20%20td.Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20shortened%20by%20loop_block%2C%20which%20the%20Recursion%20adds%20back%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.Delay(np.full(6%2C%20latency%20-%20loop_block))%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20make_reverberator(time_varying%2C%20g)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20td.MatrixConvolver(coupling)%2C%20%20%23%20loudspeakers%20-%3E%20mics%20(closes%20the%20loop)%0A%20%20%20%20%20%20%20%20%20%20%20%20block_size%3Dloop_block%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20res_path%20%3D%20td.Series(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.MatrixConvolver(source_to_mic)%2C%20%20%23%20source%20-%3E%206%20mics%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20electroacoustic_loop%2C%20%20%23%206%20mics%20-%3E%206%20loudspeakers%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20td.MatrixConvolver(speaker_to_listener)%2C%20%20%23%206%20loudspeakers%20-%3E%20listener%0A%20%20%20%20%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20return%20td.Parallel(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5Bres_path%2C%20td.MatrixConvolver(source_to_listener)%5D%2C%20sum_output%3DTrue%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20impulse%20%3D%20np.zeros(sig_len)%0A%20%20%20%20impulse%5B0%5D%20%3D%201.0%0A%0A%20%20%20%20def%20render(time_varying%2C%20g)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Listener%20impulse%20response%20of%20the%20RES%20at%20loop%20gain%20%60%60g%60%60.%22%22%22%0A%20%20%20%20%20%20%20%20return%20build_res(time_varying%2C%20g).process(impulse).squeeze()%0A%0A%20%20%20%20return%20(render%2C)%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%201.%20Reverberation%20enhancement%0A%0A%20%20%20%20At%20a%20comfortable%20loop%20gain%20the%20RES%20adds%20a%20long%20reverberant%20tail%20to%20the%20dry%20response%20%E2%80%94%20the%20energy%20decay%20curve%20at%20the%20listener%20decays%20far%20more%20slowly%20with%20the%20system%20on.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20np%2C%20render)%3A%0A%20%20%20%20def%20edc_db(x)%3A%0A%20%20%20%20%20%20%20%20energy%20%3D%20np.cumsum(x%5B%3A%3A-1%5D%20**%202)%5B%3A%3A-1%5D%0A%20%20%20%20%20%20%20%20return%2010%20*%20np.log10(energy%20%2F%20energy%5B0%5D%20%2B%201e-20)%0A%0A%20%20%20%20g_operating%20%3D%200.8%0A%20%20%20%20enhanced%20%3D%20render(time_varying%3DTrue%2C%20g%3Dg_operating)%0A%20%20%20%20dry%20%3D%20render(time_varying%3DFalse%2C%20g%3D0.0)%20%20%23%20g%3D0%20-%3E%20only%20the%20dry%20direct%20path%0A%0A%20%20%20%20late%20%3D%20slice(int(0.4%20*%20fs)%2C%20None)%0A%20%20%20%20enhancement_ratio%20%3D%20np.sqrt((enhanced%5Blate%5D%20**%202).mean())%20%2F%20np.sqrt(%0A%20%20%20%20%20%20%20%20(dry%5Blate%5D%20**%202).mean()%0A%20%20%20%20)%0A%20%20%20%20print(f%22Late-tail%20RMS%20gain%20(enhanced%20%2F%20dry)%3A%20%7Benhancement_ratio%3A.1f%7Dx%22)%0A%20%20%20%20assert%20np.isfinite(enhanced).all()%0A%20%20%20%20assert%20enhancement_ratio%20%3E%205.0%0A%0A%20%20%20%20t%20%3D%20np.arange(len(dry))%20%2F%20fs%0A%20%20%20%20fig_edc%20%3D%20go.Figure()%0A%20%20%20%20fig_edc.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(x%3Dt%2C%20y%3Dedc_db(dry)%2C%20name%3D%22dry%20room%22%2C%20line%3D%7B%22color%22%3A%20%22%23888%22%7D)%0A%20%20%20%20)%0A%20%20%20%20fig_edc.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dt%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dedc_db(enhanced)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3Df%22RES%20on%20(g%3D%7Bg_operating%7D)%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20%22%237b5ea7%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_edc.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Energy%20decay%20at%20the%20listener%3A%20RES%20extends%20the%20reverberation%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Time%20(s)%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22Energy%20decay%20(dB)%22%2C%20%22range%22%3A%20%5B-60%2C%202%5D%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20fig_edc.show()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%202.%20Maximum%20stable%20gain%3A%20static%20vs%20time-varying%0A%0A%20%20%20%20Now%20push%20the%20loop%20gain%20up.%20We%20track%20the%20short-time%20energy%20envelope%20of%20the%20listener%20response%3A%20a%20stable%20(well-enhanced)%20system%20**decays**%2C%20an%20over-driven%20one%20**grows**%20as%20a%20loop%20mode%20regenerates.%20At%20the%20same%20high%20gain%20the%20static%20FDN%20is%20already%20growing%20(ringing)%2C%20while%20the%20time-varying%20FDN%20still%20decays%20%E2%80%94%20its%20modulation%20breaks%20up%20the%20runaway%20mode.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20np%2C%20render)%3A%0A%20%20%20%20def%20envelope_db(x%2C%20win%3D2048%2C%20hop%3D512)%3A%0A%20%20%20%20%20%20%20%20starts%20%3D%20np.arange(0%2C%20len(x)%20-%20win%2C%20hop)%0A%20%20%20%20%20%20%20%20env%20%3D%20np.array(%0A%20%20%20%20%20%20%20%20%20%20%20%20%5B10%20*%20np.log10((x%5Bi%20%3A%20i%20%2B%20win%5D%20**%202).mean()%20%2B%201e-20)%20for%20i%20in%20starts%5D%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20return%20starts%20%2F%20fs%2C%20env%0A%0A%20%20%20%20g_challenge%20%3D%201.6%0A%20%20%20%20rec_static%20%3D%20render(time_varying%3DFalse%2C%20g%3Dg_challenge)%0A%20%20%20%20rec_varying%20%3D%20render(time_varying%3DTrue%2C%20g%3Dg_challenge)%0A%0A%20%20%20%20def%20growth(x)%3A%20%20%23%20last-quarter%20energy%20%2F%20first-quarter%20energy%3B%20%3E1%20means%20growing%0A%20%20%20%20%20%20%20%20q%20%3D%20len(x)%20%2F%2F%204%0A%20%20%20%20%20%20%20%20return%20(x%5B-q%3A%5D%20**%202).mean()%20%2F%20((x%5B%3Aq%5D%20**%202).mean()%20%2B%201e-30)%0A%0A%20%20%20%20growth_static%2C%20growth_varying%20%3D%20growth(rec_static)%2C%20growth(rec_varying)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22g%3D%7Bg_challenge%7D%3A%20growth%20static%3D%7Bgrowth_static%3A.2f%7D%20%20varying%3D%7Bgrowth_varying%3A.2f%7D%22%0A%20%20%20%20)%0A%20%20%20%20assert%20np.isfinite(rec_static).all()%20and%20np.isfinite(rec_varying).all()%0A%20%20%20%20assert%20growth_static%20%3E%201.0%20%20%23%20static%20loop%20is%20regenerating%20(unstable%2Fcolouring)%0A%20%20%20%20assert%20growth_varying%20%3C%201.0%20%20%23%20time-varying%20loop%20still%20decays%0A%20%20%20%20assert%20growth_varying%20%3C%20growth_static%0A%0A%20%20%20%20fig_msg%20%3D%20go.Figure()%0A%20%20%20%20ts%2C%20es%20%3D%20envelope_db(rec_static)%0A%20%20%20%20tv%2C%20ev%20%3D%20envelope_db(rec_varying)%0A%20%20%20%20fig_msg.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(x%3Dts%2C%20y%3Des%2C%20name%3D%22static%20FDN%20(rings)%22%2C%20line%3D%7B%22color%22%3A%20%22%23c0392b%22%7D)%0A%20%20%20%20)%0A%20%20%20%20fig_msg.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dtv%2C%20y%3Dev%2C%20name%3D%22time-varying%20FDN%20(stable)%22%2C%20line%3D%7B%22color%22%3A%20%22%237b5ea7%22%7D%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_msg.update_layout(%0A%20%20%20%20%20%20%20%20title%3Df%22Short-time%20energy%20at%20g%3D%7Bg_challenge%7D%3A%20static%20grows%2C%20time-varying%20decays%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Time%20(s)%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22Energy%20(dB)%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D380%2C%0A%20%20%20%20)%0A%20%20%20%20fig_msg.show()%0A%20%20%20%20return%0A%0A%0A%40app.cell(hide_code%3DTrue)%0Adef%20_(mo)%3A%0A%20%20%20%20mo.md(r%22%22%22%0A%20%20%20%20%23%23%203.%20The%20stability%20margin%0A%0A%20%20%20%20Sweeping%20the%20loop%20gain%20makes%20the%20gain%20in%20maximum%20stable%20gain%20explicit%3A%20the%20growth%20ratio%20crosses%201%20(the%20stability%20boundary)%20at%20a%20higher%20gain%20for%20the%20time-varying%20FDN.%20The%20horizontal%20distance%20between%20the%20two%20crossings%20is%20the%20extra%20gain%20%E2%80%94%20a%20few%20dB%20%E2%80%94%20that%20time%20variation%20buys.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(go%2C%20np%2C%20render)%3A%0A%20%20%20%20gains%20%3D%20np.array(%5B1.0%2C%201.4%2C%201.8%2C%202.2%2C%202.6%5D)%0A%0A%20%20%20%20def%20growth_ratio(x)%3A%0A%20%20%20%20%20%20%20%20q%20%3D%20len(x)%20%2F%2F%204%0A%20%20%20%20%20%20%20%20return%20(x%5B-q%3A%5D%20**%202).mean()%20%2F%20((x%5B%3Aq%5D%20**%202).mean()%20%2B%201e-30)%0A%0A%20%20%20%20ratios_static%2C%20ratios_varying%20%3D%20%5B%5D%2C%20%5B%5D%0A%20%20%20%20for%20g%20in%20gains%3A%0A%20%20%20%20%20%20%20%20ratios_static.append(growth_ratio(render(time_varying%3DFalse%2C%20g%3Dg)))%0A%20%20%20%20%20%20%20%20ratios_varying.append(growth_ratio(render(time_varying%3DTrue%2C%20g%3Dg)))%0A%0A%20%20%20%20fig_sweep%20%3D%20go.Figure()%0A%20%20%20%20fig_sweep.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dgains%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dratios_static%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22static%20FDN%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%2Bmarkers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20%22%23c0392b%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_sweep.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20x%3Dgains%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dratios_varying%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22time-varying%20FDN%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%2Bmarkers%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20%22%237b5ea7%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_sweep.add_hline(%0A%20%20%20%20%20%20%20%20y%3D1.0%2C%0A%20%20%20%20%20%20%20%20line%3D%7B%22dash%22%3A%20%22dash%22%2C%20%22color%22%3A%20%22%23444%22%7D%2C%0A%20%20%20%20%20%20%20%20annotation_text%3D%22stability%20boundary%22%2C%0A%20%20%20%20)%0A%20%20%20%20fig_sweep.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Tail%20growth%20vs%20loop%20gain%3A%20time%20variation%20raises%20the%20maximum%20stable%20gain%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Loop%20gain%20g%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22Tail%20growth%20ratio%22%2C%20%22type%22%3A%20%22log%22%7D%2C%0A%20%20%20%20%20%20%20%20template%3D%22plotly_white%22%2C%0A%20%20%20%20%20%20%20%20height%3D400%2C%0A%20%20%20%20)%0A%20%20%20%20fig_sweep.show()%0A%20%20%20%20return%0A%0A%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20app.run()%0A
028837435f6f2d04233a16a78d02e50e