%23%20gallery_category%3A%20Absorption%20%26%20Decay%0A%23%20gallery_title%3A%20Absorption%20GEQ%20in%20an%20FDN%0A%23%20gallery_description%3A%20Design%20per-delay%20graphic-EQ%20absorption%20filters%20and%20confirm%20that%20the%20rendered%20FDN%20follows%20a%20target%20frequency-dependent%20reverberation%20time.%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%2C%20pyFDN)%3A%0A%20%20%20%20mo.md(f%22%22%22%0A%20%20%20%20%23%20Absorption%20GEQ%20in%20an%20FDN%0A%0A%20%20%20%20Demonstrates%20%60pyFDN.decay_to_geq%60%3A%20frequency-dependent%20attenuation%20designed%20as%20a%2010-band%20graphic%20EQ%20(11%20biquad%20sections)%20targeting%20a%20given%20reverberation%20time%20curve.%0A%0A%20%20%20%20The%20absorption%20filters%20are%20applied%20per%20delay%20line.%20%20Here%20we%3A%0A%0A%20%20%20%201.%20Design%20the%20filters%20from%20a%20target%20T60%20curve.%0A%20%20%20%202.%20Render%20a%20one-channel%20FDN%20in%20the%20time%20domain.%0A%20%20%20%203.%20Estimate%20T60%20from%20the%20impulse%20response%20and%20compare%20with%20the%20target.%0A%0A%20%20%20%20Reference%3A%20*%7BpyFDN.paper_link(%22Scattering_in_Feedback_Delay_Networks%22)%7D*%0A%0A%20%20%20%20Reference%3A%20*%7BpyFDN.paper_link(%22Valimaki2016AllAudioEqualization%22)%7D*%0A%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%20dataclasses%0A%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%0A%20%20%20%20return%20dataclasses%2C%20go%2C%20np%2C%20pyFDN%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%20FDN%20to%20be%20absorbed%0A%0A%20%20%20%20Eight%20delay%20lines%20with%20an%20orthogonal%20feedback%20matrix%20%E2%80%94%20lossless%20on%20its%20own.%20All%20of%20the%20decay%20comes%20from%20the%20filters%20designed%20in%20the%20next%20cell.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(pyFDN)%3A%0A%20%20%20%20fs%20%3D%2048000%0A%20%20%20%20num_delays%20%3D%208%0A%20%20%20%20rir_len%20%3D%203%20*%20fs%20%20%23%203%20seconds%0A%0A%20%20%20%20%23%20rt%3DNone%20leaves%20the%20post_delay%20hook%20empty%3B%20the%20GEQ%20below%20fills%20it.%0A%20%20%20%20build%20%3D%20pyFDN.fdn_build_gallery(%0A%20%20%20%20%20%20%20%20num_delays%2C%0A%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20%20%20%20%20delay_range%3D(500%2C%202001)%2C%0A%20%20%20%20%20%20%20%20sort_delays%3DTrue%2C%0A%20%20%20%20%20%20%20%20io_type%3D%22ones%22%2C%0A%20%20%20%20%20%20%20%20input_scale%3D1%20%2F%20num_delays%2C%0A%20%20%20%20%20%20%20%20direct_gain%3D0.0%2C%0A%20%20%20%20%20%20%20%20rt%3DNone%2C%0A%20%20%20%20%20%20%20%20rng%3D5%2C%0A%20%20%20%20)%0A%20%20%20%20return%20build%2C%20fs%2C%20rir_len%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%20Design%20absorption%20filters%0A%0A%20%20%20%20%60decay_to_geq%60%20converts%20T60%20to%20a%20per-sample%20dB%20slope%2C%20fits%20a%20graphic%20EQ%2C%20and%20returns%20SOS%20coefficients%20for%20each%20delay%20line.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(build%2C%20dataclasses%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20%23%20Target%20RT%20at%20the%2010%20GEQ%20bands%20(seconds)%0A%20%20%20%20target_rt%20%3D%20np.array(%5B2.0%2C%202.0%2C%202.2%2C%202.3%2C%202.1%2C%201.5%2C%201.1%2C%200.8%2C%200.7%2C%200.7%5D)%0A%0A%20%20%20%20%23%20decay_to_geq%20uses%20the%208%20interior%20RT%20values%20(bands%201..8)%0A%20%20%20%20%23%20The%20outer%20two%20are%20the%20shelf%20bounds%3B%20strip%20them%20to%20match%20the%2010%20GEQ%20bands%0A%20%20%20%20sos_absorption%20%3D%20pyFDN.decay_to_geq(target_rt%2C%20build.delays%2C%20build.fs)%0A%20%20%20%20print(f%22Absorption%20SOS%20shape%3A%20%7Bsos_absorption.shape%7D%22)%0A%20%20%20%20%23%20shape%3A%20(11%2C%206%2C%20num_delays)%20%20-%3E%20(n_sections%2C%206%2C%20N)%0A%0A%20%20%20%20%23%20The%20bank%20goes%20straight%20into%20the%20build's%20post_delay%20hook%3A%20the%20filter%20on%20the%0A%20%20%20%20%23%20delay%20output%2C%20inside%20the%20loop%2C%20which%20is%20what%20sets%20the%20decay.%0A%20%20%20%20geq_build%20%3D%20dataclasses.replace(build%2C%20post_delay%3Dsos_absorption)%0A%20%20%20%20return%20geq_build%2C%20sos_absorption%2C%20target_rt%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%20Absorption%20filter%20magnitude%20responses%0A%0A%20%20%20%20Plot%20the%20cascaded%20per-delay%20absorption%20filter%20response%20for%20each%20of%20the%208%20delay%20lines.%20The%20curves%20should%20decay%20toward%20lower%20dB%20at%20higher%20frequencies%20(shorter%20T60%20%3D%20more%20attenuation%20per%20sample%20at%20HF).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(build%2C%20pyFDN%2C%20sos_absorption)%3A%0A%20%20%20%20pyFDN.plot_db_per_sample(%0A%20%20%20%20%20%20%20%20sos_absorption%2C%0A%20%20%20%20%20%20%20%20build.delays%2C%0A%20%20%20%20%20%20%20%20fs%3Dbuild.fs%2C%0A%20%20%20%20%20%20%20%20nfft%3D2**14%2C%0A%20%20%20%20%20%20%20%20title%3D%22Per-delay%20absorption%20filter%20magnitude%20(one%20application)%22%2C%0A%20%20%20%20)%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%20Compute%20impulse%20response%0A%0A%20%20%20%20%60build_to_impz%60%20renders%20the%20build%20straight%20to%20a%20time-domain%20impulse%20response%20%E2%80%94%20one%20%60process_fdn%60%20block%20simulation.%20Signal%20path%3A%20input%20%E2%86%92%20B%20%E2%86%92%20%5Bdelays%20%E2%86%92%20SOS%20%E2%86%92%20A%5D%20%E2%86%92%20C%20%E2%86%92%20output.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(geq_build%2C%20np%2C%20pyFDN%2C%20rir_len)%3A%0A%20%20%20%20rir%20%3D%20pyFDN.peak_normalize(pyFDN.build_to_impz(geq_build%2C%20rir_len).squeeze())%0A%20%20%20%20print(f%22RIR%20computed%3A%20%7Brir_len%7D%20samples%2C%20peak%20at%20sample%20%7Bnp.argmax(np.abs(rir))%7D%22)%0A%20%20%20%20return%20(rir%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%20Impulse%20response%0A%0A%20%20%20%20The%20high%20end%20dies%20first%20and%20the%20low%20end%20rings%20on%2C%20which%20is%20the%20target%20T60%20curve%20made%20audible.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20pyFDN.plot_spectrogram(%0A%20%20%20%20%20%20%20%20rir%2C%20fs%2C%20title%3D%22FDN%20impulse%20response%20%E2%80%94%20time-frequency%20energy%22%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20mo%2C%20np%2C%20rir)%3A%0A%20%20%20%20mo.audio(np.asarray(rir)%2C%20fs)%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%20RT%20estimate%20vs%20target%0A%0A%20%20%20%20Estimate%20RT%20in%20octave%20bands%20(63%E2%80%938000%20Hz)%20by%20Butterworth%20bandpass%20filtering%20and%20compare%20with%20the%20design%20target.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20pyFDN%2C%20rir%2C%20target_rt)%3A%0A%20%20%20%20rt_est%2C%20f_centre%20%3D%20pyFDN.estimate_rt_bands(rir%2C%20fs)%0A%0A%20%20%20%20%23%20target_rt%5B1%3A9%5D%20covers%20the%20same%208%20octave%20bands%20(63%E2%80%938k%20Hz)%0A%20%20%20%20fig_rt%20%3D%20go.Figure()%0A%20%20%20%20fig_rt.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%3Df_centre%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Dtarget_rt%5B1%3A9%5D%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%20name%3D%22Target%20RT%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22dash%22%3A%20%22dash%22%7D%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_rt.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%3Df_centre%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20y%3Drt_est%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%20name%3D%22Estimated%20RT%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_rt.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22RT%3A%20estimated%20vs%20target%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Frequency%20(Hz)%22%2C%20%22type%22%3A%20%22log%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22RT%20(s)%22%7D%2C%0A%20%20%20%20%20%20%20%20yaxis_range%3D%5B0%2C%20None%5D%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_rt.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
f05d20e4b2e4360f68c26a7b29b5cb63