%23%20gallery_category%3A%20Absorption%20%26%20Filters%0A%0Aimport%20marimo%0A%0A__generated_with%20%3D%20%220.23.13%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%20Converting%20a%20room%20impulse%20response%20into%20an%20FDN%0A%0A%20%20%20%20Estimates%20the%20frequency-dependent%20decay%20of%20a%20measured%20room%20impulse%0A%20%20%20%20response%20and%20designs%20an%20FDN%20to%20match%20it%3A%0A%0A%20%20%20%201.%20Estimate%20RT%20and%20initial%20level%20in%20octave%20bands%20from%20the%20RIR.%0A%20%20%20%202.%20Design%20per-delay-line%20GEQ%20absorption%20filters%20matching%20the%20decay.%0A%20%20%20%203.%20Design%20an%20output%20GEQ%20matching%20the%20initial%20spectral%20level.%0A%20%20%20%204.%20Compare%20the%20FDN%20impulse%20response%20with%20the%20target%20RIR.%0A%0A%20%20%20%20The%20impulse%20response%20is%20from%20the%20Promenadikeskus%20concert%20hall%20in%20Pori%2C%0A%20%20%20%20Finland%2C%20published%20at%0A%20%20%20%20%5Blegacy.spa.aalto.fi%2Fprojects%2Fporirirs%5D(http%3A%2F%2Flegacy.spa.aalto.fi%2Fprojects%2Fporirirs%2F).%0A%0A%20%20%20%20Instead%20of%20DecayFitNet%20(used%20in%20the%20MATLAB%20original)%2C%20decay%20parameters%20are%0A%20%20%20%20estimated%20with%20%60estimate_rt_bands%60%20(Schroeder%20backward%20integration%20per%0A%20%20%20%20octave%20band)%20and%20%60estimate_initial_level_bands%60%20(band%20energy%20matched%20to%20an%0A%20%20%20%20exponential%20decay%20model).%20%20The%20output%20EQ%20is%20designed%20from%20the%20*difference*%0A%20%20%20%20between%20the%20target%20and%20the%20unequalized%20FDN%20band%20levels%2C%20which%20makes%20the%0A%20%20%20%20level%20match%20self-correcting.%0A%0A%20%20%20%20Original%20MATLAB%3A%20%60example_RIR2FDN.m%60%2C%20Sebastian%20J.%20Schlecht%2C%2028%20January%202023.%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%20%20%20%20import%20plotly.io%20as%20pio%0A%0A%20%20%20%20import%20pyFDN%0A%0A%20%20%20%20pio.renderers.default%20%3D%20%22sphinx_gallery%22%0A%20%20%20%20return%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%20Load%20room%20impulse%20response%0A%0A%20%20%20%20Trim%20to%20the%20onset%20(direct%20sound)%20and%20normalize%20the%20energy.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(np%2C%20pyFDN)%3A%0A%20%20%20%20rir%2C%20fs%20%3D%20pyFDN.load_audio(%22s3_r4_o.wav%22)%0A%20%20%20%20_onset%20%3D%20int(np.argmax(np.abs(rir)))%0A%20%20%20%20rir%20%3D%20rir%5B_onset%3A%5D%0A%20%20%20%20rir%20%3D%20rir%20%2F%20np.linalg.norm(rir)%0A%20%20%20%20rir_len%20%3D%20len(rir)%0A%0A%20%20%20%20print(f%22RIR%3A%20%7Brir_len%7D%20samples%20(%7Brir_len%20%2F%20fs%3A.2f%7D%20s)%20at%20%7Bfs%7D%20Hz%22)%0A%20%20%20%20return%20fs%2C%20rir%2C%20rir_len%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20mo%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20mo.audio(pyFDN.peak_normalize(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%20Estimate%20decay%20parameters%0A%0A%20%20%20%20RT%20and%20initial%20level%20per%20octave%20band%20(63%20Hz%20%E2%80%93%208%20kHz).%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%20est_rt%2C%20f_centre%20%3D%20pyFDN.estimate_rt_bands(rir%2C%20fs)%0A%20%20%20%20est_level%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(rir%2C%20est_rt%2C%20fs)%0A%0A%20%20%20%20print(f%22Bands%20(Hz)%3A%20%20%7Bf_centre%7D%22)%0A%20%20%20%20print(f%22RT%20(s)%3A%20%20%20%20%7Best_rt.round(2)%7D%22)%0A%20%20%20%20print(f%22Level%20(dB)%3A%20%20%7BpyFDN.lin_to_db(est_level).round(1)%7D%22)%0A%20%20%20%20return%20est_level%2C%20est_rt%2C%20f_centre%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%20Define%20FDN%20and%20absorption%20filters%0A%0A%20%20%20%20A%2016-delay%20FDN%20with%20a%20random%20orthogonal%20feedback%20matrix.%20%20The%20target%20RT%0A%20%20%20%20at%20the%2010%20GEQ%20design%20bands%20(DC%2C%2063%20Hz%20%E2%80%A6%208%20kHz%2C%20Nyquist)%20extends%20the%20octave%0A%20%20%20%20band%20estimates%2C%20shortening%20the%20lowest%20and%20the%20two%20highest%20bands%20(air%20and%0A%20%20%20%20boundary%20absorption%20shortens%20the%20decay%20at%20the%20spectral%20edges).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_rt%2C%20fs%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20np.random.seed(5)%0A%20%20%20%20num_delays%20%3D%2016%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%203500)%2C%0A%20%20%20%20%20%20%20%20io_type%3D%22ones%22%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%20delays%20%3D%20build.delays%0A%20%20%20%20feedback_matrix%20%3D%20build.A%0A%20%20%20%20input_gain%2C%20output_gain%2C%20direct_gain%20%3D%20build.B%2C%20build.C%2C%20build.D%0A%0A%20%20%20%20target_rt%20%3D%20np.concatenate((%5Best_rt%5B0%5D%5D%2C%20est_rt%2C%20%5Best_rt%5B-1%5D%5D))%0A%20%20%20%20target_rt%20%3D%20target_rt%20*%20np.array(%5B0.9%2C%201%2C%201%2C%201%2C%201%2C%201%2C%201%2C%201%2C%200.9%2C%200.5%5D)%0A%20%20%20%20sos_absorption%20%3D%20pyFDN.absorption_geq(target_rt%2C%20delays%2C%20fs)%0A%0A%20%20%20%20print(f%22Delays%3A%20%7Bdelays%7D%22)%0A%20%20%20%20print(f%22Target%20RT%20at%20GEQ%20bands%20(s)%3A%20%7Btarget_rt.round(2)%7D%22)%0A%20%20%20%20return%20(%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20direct_gain%2C%0A%20%20%20%20%20%20%20%20feedback_matrix%2C%0A%20%20%20%20%20%20%20%20input_gain%2C%0A%20%20%20%20%20%20%20%20output_gain%2C%0A%20%20%20%20%20%20%20%20sos_absorption%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%20Compute%20the%20unequalized%20FDN%20impulse%20response%0A%0A%20%20%20%20The%20absorption%20filters%20sit%20in%20the%20recursion%20loop%20of%20a%20FLAMO%20model%0A%20%20%20%20(input%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).%20%20This%20first%20model%20has%20no%0A%20%20%20%20output%20equalizer%20yet%3B%20its%20impulse%20response%20provides%20the%20reference%20level%0A%20%20%20%20for%20the%20EQ%20design.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20delays%2C%0A%20%20%20%20direct_gain%2C%0A%20%20%20%20feedback_matrix%2C%0A%20%20%20%20fs%2C%0A%20%20%20%20input_gain%2C%0A%20%20%20%20np%2C%0A%20%20%20%20output_gain%2C%0A%20%20%20%20pyFDN%2C%0A%20%20%20%20rir_len%2C%0A%20%20%20%20sos_absorption%2C%0A)%3A%0A%20%20%20%20nfft%20%3D%20int(2%20**%20np.ceil(np.log2(rir_len)))%0A%20%20%20%20_model%20%3D%20pyFDN.dss_to_flamo(%0A%20%20%20%20%20%20%20%20feedback_matrix%2C%0A%20%20%20%20%20%20%20%20input_gain%2C%0A%20%20%20%20%20%20%20%20output_gain%2C%0A%20%20%20%20%20%20%20%20direct_gain%2C%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20nfft%3Dnfft%2C%0A%20%20%20%20%20%20%20%20sos_filter%3Dsos_absorption%2C%0A%20%20%20%20%20%20%20%20shell%3DTrue%2C%0A%20%20%20%20)%0A%20%20%20%20ir_unequalized%20%3D%20pyFDN.flamo_time_response(_model).squeeze()%5B%3Arir_len%5D%0A%20%20%20%20print(f%22Unequalized%20FDN%20IR%20computed%3A%20%7Brir_len%7D%20samples%22)%0A%20%20%20%20return%20ir_unequalized%2C%20nfft%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%20Output%20equalization%0A%0A%20%20%20%20The%20initial%20level%20of%20the%20unequalized%20FDN%20is%20roughly%20flat%3B%20an%20output%20GEQ%0A%20%20%20%20shapes%20it%20to%20the%20spectral%20envelope%20of%20the%20target%20RIR.%20%20The%20GEQ%20target%20is%0A%20%20%20%20the%20band-wise%20dB%20difference%20between%20target%20and%20FDN%20initial%20levels%2C%20with%0A%20%20%20%20extra%20attenuation%20at%20the%20extrapolated%20DC%20and%20Nyquist%20bands.%0A%0A%20%20%20%20The%20equalizer%20is%20placed%20at%20the%20end%20of%20the%20FLAMO%20graph%20(%60output_filter%60)%2C%0A%20%20%20%20so%20the%20final%20model%20renders%20the%20complete%20RIR%20in%20one%20pass%3A%0A%20%20%20%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%20GEQ%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_(%0A%20%20%20%20delays%2C%0A%20%20%20%20direct_gain%2C%0A%20%20%20%20est_level%2C%0A%20%20%20%20feedback_matrix%2C%0A%20%20%20%20fs%2C%0A%20%20%20%20input_gain%2C%0A%20%20%20%20ir_unequalized%2C%0A%20%20%20%20nfft%2C%0A%20%20%20%20np%2C%0A%20%20%20%20output_gain%2C%0A%20%20%20%20pyFDN%2C%0A%20%20%20%20rir_len%2C%0A%20%20%20%20sos_absorption%2C%0A)%3A%0A%20%20%20%20fdn0_rt%2C%20_%20%3D%20pyFDN.estimate_rt_bands(ir_unequalized%2C%20fs)%0A%20%20%20%20fdn0_level%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(ir_unequalized%2C%20fdn0_rt%2C%20fs)%0A%0A%20%20%20%20_diff_db%20%3D%20pyFDN.lin_to_db(est_level)%20-%20pyFDN.lin_to_db(fdn0_level)%0A%20%20%20%20target_level_db%20%3D%20np.concatenate((%5B_diff_db%5B0%5D%5D%2C%20_diff_db%2C%20%5B_diff_db%5B-1%5D%5D))%0A%20%20%20%20target_level_db%20%3D%20target_level_db%20-%20np.array(%5B5%2C%200%2C%200%2C%200%2C%200%2C%200%2C%200%2C%200%2C%200%2C%2030%5D)%0A%0A%20%20%20%20equalization_sos%2C%20_%20%3D%20pyFDN.design_geq(target_level_db%2C%20fs%3Dfs)%0A%20%20%20%20equalization_sos%20%3D%20equalization_sos%20%2F%20equalization_sos%5B%3A%2C%203%3A4%5D%20%20%23%20a0%20%3D%201%0A%0A%20%20%20%20model_eq%20%3D%20pyFDN.dss_to_flamo(%0A%20%20%20%20%20%20%20%20feedback_matrix%2C%0A%20%20%20%20%20%20%20%20input_gain%2C%0A%20%20%20%20%20%20%20%20output_gain%2C%0A%20%20%20%20%20%20%20%20direct_gain%2C%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20nfft%3Dnfft%2C%0A%20%20%20%20%20%20%20%20sos_filter%3Dsos_absorption%2C%0A%20%20%20%20%20%20%20%20output_filter%3Dequalization_sos%5B%3A%2C%20%3A%2C%20np.newaxis%5D%2C%0A%20%20%20%20%20%20%20%20shell%3DTrue%2C%0A%20%20%20%20)%0A%20%20%20%20ir_fdn%20%3D%20pyFDN.flamo_time_response(model_eq).squeeze()%5B%3Arir_len%5D%0A%0A%20%20%20%20print(f%22GEQ%20target%20(dB)%3A%20%7Btarget_level_db.round(1)%7D%22)%0A%20%20%20%20return%20equalization_sos%2C%20ir_fdn%2C%20model_eq%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%20Visualize%20DSP%20graph%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(%0A%20%20%20%20delays%2C%0A%20%20%20%20direct_gain%2C%0A%20%20%20%20equalization_sos%2C%0A%20%20%20%20feedback_matrix%2C%0A%20%20%20%20fs%2C%0A%20%20%20%20input_gain%2C%0A%20%20%20%20model_eq%2C%0A%20%20%20%20output_gain%2C%0A%20%20%20%20pyFDN%2C%0A%20%20%20%20sos_absorption%2C%0A)%3A%0A%20%20%20%20pyFDN.plot_flamo_graph(model_eq)%0A%20%20%20%20pyFDN.plot_fdn_parameter(%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20feedback_matrix%2C%0A%20%20%20%20%20%20%20%20input_gain%2C%0A%20%20%20%20%20%20%20%20output_gain%2C%0A%20%20%20%20%20%20%20%20direct_gain%2C%0A%20%20%20%20%20%20%20%20attenuation_sos%3Dsos_absorption%2C%0A%20%20%20%20%20%20%20%20post_eq_sos%3Dequalization_sos%2C%0A%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_fdn%2C%20mo%2C%20pyFDN)%3A%0A%20%20%20%20mo.audio(pyFDN.peak_normalize(ir_fdn)%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%20Spectrograms%0A%0A%20%20%20%20Target%20RIR%20(top)%20and%20FDN%20impulse%20response%20(bottom).%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(rir%2C%20fs%2C%20title%3D%22Target%20RIR%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_fdn%2C%20pyFDN)%3A%0A%20%20%20%20pyFDN.plot_spectrogram(ir_fdn%2C%20fs%2C%20title%3D%22FDN%20impulse%20response%22)%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%20Reverberation%20time%20and%20initial%20level%20match%0A%0A%20%20%20%20Estimate%20the%20decay%20parameters%20of%20the%20FDN%20impulse%20response%20with%20the%20same%0A%20%20%20%20estimator%20and%20compare%20with%20the%20target%20RIR.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_fdn%2C%20pyFDN)%3A%0A%20%20%20%20fdn_rt%2C%20_%20%3D%20pyFDN.estimate_rt_bands(ir_fdn%2C%20fs)%0A%20%20%20%20fdn_level%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(ir_fdn%2C%20fdn_rt%2C%20fs)%0A%20%20%20%20return%20fdn_level%2C%20fdn_rt%0A%0A%0A%40app.cell%0Adef%20_(est_rt%2C%20f_centre%2C%20fdn_rt%2C%20go)%3A%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(x%3Df_centre%2C%20y%3Dest_rt%2C%20mode%3D%22lines%2Bmarkers%22%2C%20name%3D%22Target%20RIR%22)%0A%20%20%20%20)%0A%20%20%20%20fig_rt.add_trace(go.Scatter(x%3Df_centre%2C%20y%3Dfdn_rt%2C%20mode%3D%22lines%2Bmarkers%22%2C%20name%3D%22FDN%22))%0A%20%20%20%20fig_rt.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Reverberation%20time%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%22Reverberation%20time%20(s)%22%2C%20%22rangemode%22%3A%20%22tozero%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_rt.show()%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_level%2C%20f_centre%2C%20fdn_level%2C%20go%2C%20pyFDN)%3A%0A%20%20%20%20fig_level%20%3D%20go.Figure()%0A%20%20%20%20fig_level.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%3DpyFDN.lin_to_db(est_level)%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%20RIR%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_level.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%3DpyFDN.lin_to_db(fdn_level)%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%22FDN%22%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20)%0A%20%20%20%20fig_level.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Initial%20level%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%22Initial%20level%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_level.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%20Test%3A%20reverberation%20time%20accuracy%0A%0A%20%20%20%20The%20FDN%20reverberation%20time%20should%20be%20within%2020%25%20of%20the%20target%20in%20every%0A%20%20%20%20octave%20band%20(the%20two%20highest%20bands%20were%20deliberately%20shortened%20by%20design).%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(est_rt%2C%20fdn_rt%2C%20np)%3A%0A%20%20%20%20rt_error%20%3D%20np.abs(est_rt%20%2F%20fdn_rt%20-%201)%0A%20%20%20%20print(f%22RT%20error%20per%20band%3A%20%7Brt_error.round(3)%7D%22)%0A%20%20%20%20assert%20np.all(rt_error%20%3C%200.2)%2C%20%22FDN%20reverberation%20time%20deviates%20more%20than%2020%25%22%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
f7b16db9357104e1d498058eaeda42a0