%23%20gallery_category%3A%20Absorption%20%26%20Decay%0A%23%20gallery_description%3A%20Estimate%20two%20decay%20slopes%20per%20octave%20from%20a%20measured%20multi-room%20response%20and%20resynthesize%20them%20with%20parallel%20FDNs.%0A%23%20references%3A%20Neural_Network_For_Multi_Exponential_Sound_Energy_Decay_Analysis%2C%20Acoustic_Analysis_And_Dataset_Of_Transitions_Between_Coupled_Rooms%0A%23%20requires%3A%20multislope%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%20Multi-slope%20decay%3A%20from%20a%20measured%20response%20to%20an%20FDN%0A%0A%20%20%20%20A%20single%20room%20decays%20with%20one%20exponential%20slope%20per%20frequency%20band%2C%20and%20%60%60pyFDN.estimate_rt_bands%60%60%20is%20built%20for%20exactly%20that%20case.%20A%20room%20that%20opens%20onto%20another%20does%20not%3A%20energy%20leaks%20between%20the%20two%20spaces%2C%20so%20the%20energy%20decay%20curve%20(EDC)%20bends%20%E2%80%94%20a%20fast%20slope%20early%20on%2C%20a%20slow%20one%20later%2C%20and%20a%20single%20reverberation%20time%20fitted%20to%20it%20describes%20neither%20space.%0A%0A%20%20%20%20This%20example%20estimates%20a%20**multi-slope**%20decay%20from%20a%20measured%20response%20and%20turns%20it%20into%20an%20FDN%3A%0A%0A%20%20%20%201.%20Load%20a%20room%20impulse%20response%20measured%20across%20a%20room%20transition.%0A%20%20%20%202.%20Show%20that%20a%20single-slope%20RT%20lands%20between%20the%20two%20decay%20rates.%0A%20%20%20%203.%20Fit%20two%20slopes%20per%20octave%20band%20with%20**DecayFitNet**%2C%20from%20the%20%5B%60multislope%60%5D(https%3A%2F%2Fpypi.org%2Fproject%2Fmultislope%2F)%20package.%0A%20%20%20%204.%20Convert%20the%20fitted%20slope%20amplitudes%20into%20per-slope%20initial%20levels%20with%20%60pyFDN.slope_amplitude_to_level%60.%0A%20%20%20%205.%20Build%20**one%20FDN%20per%20slope**%2C%20sum%20them%2C%20and%20compare%20the%20octave-band%20EDCs%20with%20the%20measurement.%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%20from%20multislope%20import%20DecayFitNet%0A%20%20%20%20from%20plotly.subplots%20import%20make_subplots%0A%20%20%20%20from%20scipy.signal%20import%20sosfilt%2C%20stft%0A%0A%20%20%20%20import%20pyFDN%0A%0A%20%20%20%20return%20DecayFitNet%2C%20go%2C%20make_subplots%2C%20np%2C%20pyFDN%2C%20sosfilt%2C%20stft%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%23%20A%20measured%20multi-room%20response%0A%0A%20%20%20%20The%20response%20comes%20from%20the%20dataset%20accompanying%20%7BpyFDN.paper_link(%22Acoustic_Analysis_And_Dataset_Of_Transitions_Between_Coupled_Rooms%22)%7D%2C%20measured%20by%20walking%20an%20ambisonic%20microphone%20from%20a%20meeting%20room%20out%20into%20the%20hallway%20it%20opens%20onto%2C%20and%20published%20at%20%5Bdoi.org%2F10.5281%2Fzenodo.4636068%5D(https%3A%2F%2Fdoi.org%2F10.5281%2Fzenodo.4636068).%20The%20source%20stays%20inside%20the%20meeting%20room%20(the%20*No%20Continuous%20Line-of-Sight*%20configuration)%2C%20and%20this%20is%20the%20receiver%202.9%20m%20along%20that%20walk%2C%20right%20about%20where%20the%20direct%20path%20to%20the%20source%20is%20lost%20%E2%80%94%20so%20what%20reaches%20it%20is%20the%20hallway's%20own%20quick%20decay%20riding%20on%20the%20slower%20one%20leaking%20out%20of%20the%20meeting%20room.%20Only%20the%20omnidirectional%20component%20of%20the%20ambisonic%20response%20is%20used%20here.%0A%0A%20%20%20%20Measured%20responses%20set%20the%20terms%20of%20what%20can%20be%20recovered.%20This%20one%20carries%20roughly%2040%20dB%20of%20usable%20decay%20per%20octave%20band%20before%20it%20reaches%20the%20noise%20floor%20of%20the%20measurement%2C%20so%20the%20analysis%20below%20stays%20inside%20that%20range.%20The%20response%20already%20starts%20at%20its%20onset%2C%20so%20nothing%20is%20trimmed%20from%20the%20front%20%E2%80%94%20and%20with%20the%20receiver%20past%20the%20doorway%20the%20direct%20sound%20is%20weak%20enough%20that%20trimming%20to%20the%20peak%20would%20throw%20away%20genuine%20early%20energy%20rather%20than%20a%20dominant%20direct%20arrival.%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(%22meetingroom_to_hallway_290cm%22)%0A%20%20%20%20rir%20%3D%20rir%20%2F%20np.linalg.norm(rir)%0A%20%20%20%20nfft%20%3D%202**17%0A%0A%20%20%20%20print(f%22RIR%3A%20%7Blen(rir)%7D%20samples%20(%7Blen(rir)%20%2F%20fs%3A.2f%7D%20s)%20at%20%7Bfs%7D%20Hz%22)%0A%20%20%20%20return%20fs%2C%20nfft%2C%20rir%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%20One%20reverberation%20time%20per%20band%20is%20not%20enough%0A%0A%20%20%20%20%60pyFDN.estimate_rt_bands%60%20fits%20a%20single%20line%20to%20the%20Schroeder%20decay%20curve%20between%20-5%20dB%20and%20-35%20dB.%20On%20a%20double-slope%20decay%20that%20line%20is%20a%20compromise%3A%20the%20estimate%20sits%20between%20the%20two%20true%20decay%20times%20and%20follows%20neither.%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%20single_slope_rt%2C%20f_centre%20%3D%20pyFDN.estimate_rt_bands(rir%2C%20fs)%0A%20%20%20%20print(f%22Bands%20(Hz)%3A%20%20%20%20%20%20%20%20%7Bf_centre%7D%22)%0A%20%20%20%20print(f%22Single-slope%20RT%20(s)%3A%20%7Bsingle_slope_rt.round(2)%7D%22)%0A%20%20%20%20return%20f_centre%2C%20single_slope_rt%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%23%20Fitting%20two%20slopes%20with%20DecayFitNet%0A%0A%20%20%20%20%60multislope.DecayFitNet%60%20filters%20the%20RIR%20into%20octave%20bands%2C%20backward%20integrates%20each%20band%2C%20and%20predicts%20the%20decay%20times%20%60T%60%2C%20the%20slope%20amplitudes%20%60A%60%20and%20the%20noise%20floor%20%60N%60%20of%20a%20multi-exponential%20decay%20model.%0A%20%20%20%20The%20network%20is%20described%20in%20%7BpyFDN.paper_link(%22Neural_Network_For_Multi_Exponential_Sound_Energy_Decay_Analysis%22)%7D.%0A%0A%20%20%20%20Fitting%20a%20noise%20floor%20explicitly%20is%20what%20makes%20the%20network%20usable%20on%20a%20measurement%3A%20the%20flat%20tail%20a%20real%20RIR%20ends%20in%20is%20absorbed%20by%20%60N%60%20instead%20of%20being%20mistaken%20for%20a%20very%20slow%20third%20slope.%20The%20slopes%20come%20back%20sorted%20by%20ascending%20decay%20time%2C%20so%20column%200%20is%20the%20fast%20one%20throughout.%0A%0A%20%20%20%20%60N%60%20is%20kept%20here%20rather%20than%20discarded.%20It%20is%20the%20level%20the%20measurement%20stops%20decaying%20at%2C%20and%20the%20resynthesis%20needs%20it%20later%20to%20be%20compared%20with%20the%20measurement%20anywhere%20near%20that%20floor.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(DecayFitNet%2C%20f_centre%2C%20fs%2C%20pyFDN%2C%20rir)%3A%0A%20%20%20%20net%20%3D%20DecayFitNet(n_slopes%3D2%2C%20sample_rate%3Dfs%2C%20filter_frequencies%3Dlist(f_centre))%0A%20%20%20%20fit%20%3D%20net.estimate(rir)%0A%0A%20%20%20%20%23%20estimator-neutral%20arrays%3A%20decay%20times%20in%20seconds%20and%20EDC%20amplitudes%2C%0A%20%20%20%20%23%20de-normalised%20back%20to%20the%20physical%20energy%20scale%20of%20the%20RIR%0A%20%20%20%20decay_time%20%3D%20fit.t%20%20%23%20(n_bands%2C%20n_slopes)%0A%20%20%20%20amplitude%20%3D%20fit.a%20*%20fit.norm_vals%5B0%5D%5B%3A%2C%20None%5D%20%20%23%20(n_bands%2C%20n_slopes)%0A%20%20%20%20noise_power%20%3D%20fit.n%5B%3A%2C%200%5D%20*%20fit.norm_vals%5B0%5D%20%20%23%20per-sample%2C%20per%20band%0A%0A%20%20%20%20%23%20amplitude%20is%20the%20energy%20of%20a%20slope%3B%20convert%20it%20into%20the%20initial%0A%20%20%20%20%23%20amplitude%20of%20the%20corresponding%20exponential%20decay%0A%20%20%20%20slope_level%20%3D%20pyFDN.slope_amplitude_to_level(amplitude%2C%20decay_time%2C%20fs)%0A%0A%20%20%20%20print(f%22Decay%20time%2C%20fast%20slope%20(s)%3A%20%7Bdecay_time%5B%3A%2C%200%5D.round(2)%7D%22)%0A%20%20%20%20print(f%22Decay%20time%2C%20slow%20slope%20(s)%3A%20%7Bdecay_time%5B%3A%2C%201%5D.round(2)%7D%22)%0A%20%20%20%20print(f%22Level%2C%20fast%20slope%20(dB)%3A%20%20%20%20%20%7BpyFDN.lin_to_db(slope_level%5B%3A%2C%200%5D).round(1)%7D%22)%0A%20%20%20%20print(f%22Level%2C%20slow%20slope%20(dB)%3A%20%20%20%20%20%7BpyFDN.lin_to_db(slope_level%5B%3A%2C%201%5D).round(1)%7D%22)%0A%20%20%20%20print(f%22Noise%20floor%20(dB%2Fsample)%3A%20%20%20%20%7BpyFDN.sq_to_db(noise_power).round(1)%7D%22)%0A%20%20%20%20return%20decay_time%2C%20noise_power%2C%20slope_level%0A%0A%0A%40app.cell%0Adef%20_(decay_time%2C%20f_centre%2C%20go%2C%20single_slope_rt)%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(%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%3Ddecay_time%5B%3A%2C%200%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%20line%3D%7B%22color%22%3A%20%22%23636efa%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Fast%20slope%22%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%3Ddecay_time%5B%3A%2C%201%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%20line%3D%7B%22color%22%3A%20%22%23ef553b%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Slow%20slope%22%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%3Dsingle_slope_rt%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%2300cc96%22%2C%20%22dash%22%3A%20%22dash%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20name%3D%22Single-slope%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%22Decay%20times%3A%20two%20fitted%20slopes%20vs.%20one%20reverberation%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%2C%20%22range%22%3A%20%5B1.7%2C%204.0%5D%7D%2C%0A%20%20%20%20%20%20%20%20yaxis%3D%7B%22title%22%3A%20%22Decay%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%3D420%2C%0A%20%20%20%20)%0A%20%20%20%20fig_rt.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%20The%20single-slope%20estimate%20runs%20between%20the%20two%20fitted%20slopes%2C%20which%20is%20the%20point%3A%20no%20single%20reverberation%20time%20describes%20this%20decay.%20The%20contrast%20is%20largest%20from%20500%20Hz%20to%202%20kHz%2C%20where%20the%20slow%20slope%20runs%20two%20to%20three%20times%20as%20long%20as%20the%20fast%20one%20%E2%80%94%200.47%20s%20against%201.51%20s%20at%201%20kHz.%20It%20also%20starts%206%20to%2011%20dB%20below%20the%20fast%20slope%20there%2C%20which%20puts%20the%20knee%20in%20the%20EDC%20between%20-7%20and%20-10%20dB%3A%20early%20enough%20to%20be%20plainly%20visible%2C%20and%20far%20above%20the%20noise%20floor.%0A%0A%20%20%20%20Two%20bands%20are%20worth%20reading%20sceptically.%20At%20250%20Hz%20the%20two%20slopes%20come%20back%20nearly%20equal%20(1.33%20s%20and%201.47%20s)%2C%20so%20the%20split%20between%20them%20carries%20little%20meaning%3B%2062%20Hz%20is%20similar.%20A%20two-slope%20model%20is%20the%20right%20description%20of%20this%20response%20in%20the%20middle%20of%20the%20spectrum%2C%20not%20everywhere%20in%20it.%0A%0A%20%20%20%20%23%23%20One%20FDN%20per%20slope%0A%0A%20%20%20%20Each%20slope%20becomes%20its%20own%20FDN.%20A%20GEQ%20absorption%20filter%20per%20delay%20line%20gives%20the%20FDN%20the%20decay%20time%20of%20that%20slope%2C%20and%20an%20output%20GEQ%20sets%20its%20initial%20level.%20The%20level%20target%20is%20the%20difference%20between%20the%20level%20the%20slope%20should%20have%20and%20the%20level%20the%20unequalized%20FDN%20happens%20to%20produce%2C%20so%20the%20design%20corrects%20itself.%0A%0A%20%20%20%20The%20two%20GEQ%20designs%20work%20on%20a%2010-point%20grid%20(DC%2C%2063%20Hz%20%E2%80%A6%208%20kHz%2C%20Nyquist)%3B%0A%20%20%20%20the%20octave-band%20estimates%20are%20extended%20to%20it%20by%20repeating%20the%20edge%20bands.%0A%0A%20%20%20%20%60pyFDN.decay_to_geq%60%20and%20%60pyFDN.gain_to_bounded_geq%60%20both%20return%20normalized%20biquad%20sections%20in%20%60%5Bb0%2C%20b1%2C%20b2%2C%20a0%2C%20a1%2C%20a2%5D%60%20form%2C%20with%20%60a0%20%3D%201%60.%20The%20first%20maps%20reverberation-time%20targets%20onto%20in-loop%20attenuation%3B%20the%20second%20fits%20level%20corrections%20onto%20the%20output%20EQ%20while%20limiting%20every%20frequency-shaped%20section%20to%20%C2%B120%20dB%20of%20internal%20gain.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(decay_time%2C%20fs%2C%20nfft%2C%20np%2C%20pyFDN%2C%20rir%2C%20slope_level)%3A%0A%20%20%20%20def%20geq_grid(band_values)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Extend%208%20octave-band%20values%20onto%20the%2010-point%20GEQ%20design%20grid.%22%22%22%0A%20%20%20%20%20%20%20%20return%20np.concatenate((%5Bband_values%5B0%5D%5D%2C%20band_values%2C%20%5Bband_values%5B-1%5D%5D))%0A%0A%20%20%20%20resynthesis%20%3D%20np.zeros(len(rir))%0A%20%20%20%20slope_fdn_rt%20%3D%20%5B%5D%0A%0A%20%20%20%20for%20_slope%20in%20range(decay_time.shape%5B1%5D)%3A%0A%20%20%20%20%20%20%20%20_build%20%3D%20pyFDN.fdn_build_gallery(%0A%20%20%20%20%20%20%20%20%20%20%20%2016%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20fs%3Dfs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20delay_range%3D(500%2C%202500)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20io_type%3D%22ones%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20direct_gain%3D0.0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20rt%3DNone%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20rng%3D10%20%2B%20_slope%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_absorption%20%3D%20pyFDN.decay_to_geq(%0A%20%20%20%20%20%20%20%20%20%20%20%20geq_grid(decay_time%5B%3A%2C%20_slope%5D)%2C%20_build.delays%2C%20fs%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20%20%20%20%20%23%20unequalized%20FDN%3A%20reference%20level%20for%20the%20output%20GEQ%0A%20%20%20%20%20%20%20%20_ir_flat%20%3D%20pyFDN.flamo_time_response(%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.dss_to_flamo(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.A%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.B%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.C%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.delays%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Dnfft%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20post_delay%3D_absorption%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20shell%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20).squeeze()%5B%3A%20len(rir)%5D%0A%0A%20%20%20%20%20%20%20%20_rt_flat%2C%20_%20%3D%20pyFDN.estimate_rt_bands(_ir_flat%2C%20fs)%0A%20%20%20%20%20%20%20%20_level_flat%2C%20_%20%3D%20pyFDN.estimate_initial_level_bands(_ir_flat%2C%20_rt_flat%2C%20fs)%0A%20%20%20%20%20%20%20%20_gain_db%20%3D%20pyFDN.lin_to_db(slope_level%5B%3A%2C%20_slope%5D)%20-%20pyFDN.lin_to_db(%0A%20%20%20%20%20%20%20%20%20%20%20%20_level_flat%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20_eq%20%3D%20pyFDN.gain_to_bounded_geq(geq_grid(_gain_db)%2C%20fs%3Dfs)%0A%0A%20%20%20%20%20%20%20%20resynthesis%20%2B%3D%20pyFDN.flamo_time_response(%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.dss_to_flamo(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.A%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.B%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.C%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_build.delays%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20fs%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nfft%3Dnfft%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20post_delay%3D_absorption%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20post_output%3D_eq%5B%3A%2C%20%3A%2C%20np.newaxis%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20shell%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20).squeeze()%5B%3A%20len(rir)%5D%0A%0A%20%20%20%20%20%20%20%20slope_fdn_rt.append(_rt_flat)%0A%20%20%20%20%20%20%20%20print(f%22Slope%20%7B_slope%7D%3A%20FDN%20RT%20(s)%20%7B_rt_flat.round(2)%7D%22)%0A%20%20%20%20%20%20%20%20print(f%22%20%20%20%20%20%20%20%20%20%20output%20GEQ%20(dB)%20%7B_gain_db.round(1)%7D%22)%0A%0A%20%20%20%20slope_fdn_rt%20%3D%20np.stack(slope_fdn_rt%2C%20axis%3D1)%0A%20%20%20%20return%20resynthesis%2C%20slope_fdn_rt%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20mo%2C%20pyFDN%2C%20resynthesis)%3A%0A%20%20%20%20mo.audio(pyFDN.peak_normalize(resynthesis)%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%20Energy%20decay%20curves%0A%0A%20%20%20%20The%20comparison%20that%20matters%20is%20the%20octave-band%20EDC%3A%20the%20sum%20of%20the%20two%20FDNs%20should%20bend%20the%20same%20way%20as%20the%20measured%20response.%20Both%20curves%20are%20normalised%20to%200%20dB%20at%20the%20onset.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(f_centre%2C%20fs%2C%20noise_power%2C%20np%2C%20pyFDN%2C%20resynthesis%2C%20rir%2C%20sosfilt)%3A%0A%20%20%20%20_bands%2C%20_%20%3D%20pyFDN.octave_bands(fs%3Dfs)%0A%20%20%20%20band_sos%20%3D%20pyFDN.octave_band_filterbank(_bands%2C%20fs)%0A%0A%20%20%20%20%23%20The%20measurement%20stops%20decaying%20when%20it%20reaches%20its%20noise%20floor%3B%20the%20FDNs%2C%0A%20%20%20%20%23%20being%20noiseless%2C%20keep%20going.%20Giving%20the%20resynthesis%20the%20floor%20the%20fit%0A%20%20%20%20%23%20already%20estimated%20is%20what%20makes%20the%20two%20comparable%20that%20far%20down.%0A%20%20%20%20_rng%20%3D%20np.random.default_rng(0)%0A%20%20%20%20noise%20%3D%20np.zeros(len(rir))%0A%20%20%20%20for%20_k%20in%20range(len(f_centre))%3A%0A%20%20%20%20%20%20%20%20_band%20%3D%20sosfilt(band_sos%5B_k%5D%2C%20_rng.standard_normal(len(rir)))%0A%20%20%20%20%20%20%20%20noise%20%2B%3D%20_band%20*%20np.sqrt(noise_power%5B_k%5D%20%2F%20np.mean(_band**2))%0A%0A%20%20%20%20noisy_resynthesis%20%3D%20resynthesis%20%2B%20noise%0A%20%20%20%20return%20band_sos%2C%20noisy_resynthesis%0A%0A%0A%40app.cell%0Adef%20_(band_sos%2C%20f_centre%2C%20noisy_resynthesis%2C%20np%2C%20pyFDN%2C%20rir%2C%20sosfilt)%3A%0A%20%20%20%20def%20band_edc_db(signal%2C%20band_index)%3A%0A%20%20%20%20%20%20%20%20%22%22%22Octave-band%20EDC%20in%20dB%2C%20normalised%20to%200%20dB%20at%20the%20onset.%22%22%22%0A%20%20%20%20%20%20%20%20curve%20%3D%20pyFDN.sq_to_db(pyFDN.edc(sosfilt(band_sos%5Bband_index%5D%2C%20signal)))%0A%20%20%20%20%20%20%20%20return%20curve%20-%20curve%5B0%5D%0A%0A%20%20%20%20edc_target%20%3D%20np.stack(%5Bband_edc_db(rir%2C%20k)%20for%20k%20in%20range(len(f_centre))%5D)%0A%20%20%20%20edc_fdn%20%3D%20np.stack(%0A%20%20%20%20%20%20%20%20%5Bband_edc_db(noisy_resynthesis%2C%20k)%20for%20k%20in%20range(len(f_centre))%5D%0A%20%20%20%20)%0A%20%20%20%20return%20edc_fdn%2C%20edc_target%0A%0A%0A%40app.cell%0Adef%20_(edc_fdn%2C%20edc_target%2C%20f_centre%2C%20fs%2C%20go%2C%20np)%3A%0A%20%20%20%20fig_edc%20%3D%20go.Figure()%0A%20%20%20%20_time%20%3D%20np.arange(edc_target.shape%5B1%5D)%5B%3A%3A64%5D%20%2F%20fs%0A%20%20%20%20for%20_index%2C%20_colour%20in%20zip(%0A%20%20%20%20%20%20%20%20(2%2C%204%2C%206)%2C%20(%22%23636efa%22%2C%20%22%23ef553b%22%2C%20%22%2300cc96%22)%2C%20strict%3DTrue%0A%20%20%20%20)%3A%0A%20%20%20%20%20%20%20%20fig_edc.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3D_time%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3Dedc_target%5B_index%5D%5B%3A%3A64%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20_colour%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3Df%22%7Bf_centre%5B_index%5D%3A.0f%7D%20Hz%2C%20measured%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20fig_edc.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Scatter(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3D_time%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3Dedc_fdn%5B_index%5D%5B%3A%3A64%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20mode%3D%22lines%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20line%3D%7B%22color%22%3A%20_colour%2C%20%22dash%22%3A%20%22dash%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%3Df%22%7Bf_centre%5B_index%5D%3A.0f%7D%20Hz%2C%20two%20FDNs%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20)%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20fig_edc.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Octave-band%20energy%20decay%20curves%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Time%20(s)%22%2C%20%22range%22%3A%20%5B0%2C%201.5%5D%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-70%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%3D460%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%20Is%20the%20decay%20smooth%20across%20frequency%3F%0A%0A%20%20%20%20A%20GEQ%20fits%20a%20target%20on%20ten%20command%20points%2C%20and%20between%20them%20it%20is%20free%20to%20do%20what%20the%20biquads%20happen%20to%20do.%20If%20that%20freedom%20turned%20into%20resonances%2C%20the%20resynthesis%20would%20ring%20at%20isolated%20frequencies%20%E2%80%94%20visible%20as%20horizontal%20streaks%20persisting%20after%20everything%20around%20them%20has%20gone.%20Octave-band%20EDCs%20would%20not%20show%20it%3A%20they%20average%20exactly%20the%20detail%20in%20question%20away.%0A%0A%20%20%20%20The%20two%20spectrograms%20below%20are%20on%20a%20shared%20colour%20scale%2C%20and%20the%20resynthesis%20carries%20the%20same%20noise%20floor%20as%20before%2C%20so%20the%20panels%20differ%20only%20where%20the%20decay%20does.%20Streaks%20in%20the%20right%20panel%20that%20are%20absent%20on%20the%20left%20would%20be%20the%20FDN's%20own.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20make_subplots%2C%20noisy_resynthesis%2C%20np%2C%20rir%2C%20stft)%3A%0A%20%20%20%20def%20spectrogram_db(signal)%3A%0A%20%20%20%20%20%20%20%20%22%22%22STFT%20magnitude%20in%20dB%2C%20normalised%20to%200%20dB%20at%20its%20peak.%22%22%22%0A%20%20%20%20%20%20%20%20f%2C%20t%2C%20z%20%3D%20stft(signal%2C%20fs%3Dfs%2C%20nperseg%3D1024%2C%20noverlap%3D512)%0A%20%20%20%20%20%20%20%20keep%20%3D%20f%20%3C%3D%2016000%0A%20%20%20%20%20%20%20%20db%20%3D%2020%20*%20np.log10(np.abs(z%5Bkeep%5D)%20%2B%201e-30)%0A%20%20%20%20%20%20%20%20return%20f%5Bkeep%5D%2C%20t%2C%20db%20-%20db.max()%0A%0A%20%20%20%20_f%2C%20_t%2C%20_target_db%20%3D%20spectrogram_db(rir)%0A%20%20%20%20_%2C%20_%2C%20_fdn_db%20%3D%20spectrogram_db(noisy_resynthesis)%0A%0A%20%20%20%20fig_spec%20%3D%20make_subplots(%0A%20%20%20%20%20%20%20%20rows%3D1%2C%0A%20%20%20%20%20%20%20%20cols%3D2%2C%0A%20%20%20%20%20%20%20%20shared_yaxes%3DTrue%2C%0A%20%20%20%20%20%20%20%20subplot_titles%3D(%22Measured%22%2C%20%22Two%20FDNs%22)%2C%0A%20%20%20%20%20%20%20%20horizontal_spacing%3D0.06%2C%0A%20%20%20%20)%0A%20%20%20%20for%20_col%2C%20_data%20in%20((1%2C%20_target_db)%2C%20(2%2C%20_fdn_db))%3A%0A%20%20%20%20%20%20%20%20fig_spec.add_trace(%0A%20%20%20%20%20%20%20%20%20%20%20%20go.Heatmap(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20x%3D_t%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20y%3D_f%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20z%3D_data%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20zmin%3D-80%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20zmax%3D0%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20colorscale%3D%22Magma%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20showscale%3D_col%20%3D%3D%202%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20colorbar%3D%7B%22title%22%3A%20%22dB%22%7D%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%20row%3D1%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20col%3D_col%2C%0A%20%20%20%20%20%20%20%20)%0A%20%20%20%20_ticks%20%3D%20%5B63%2C%20125%2C%20250%2C%20500%2C%201000%2C%202000%2C%204000%2C%208000%2C%2016000%5D%0A%20%20%20%20fig_spec.update_yaxes(%0A%20%20%20%20%20%20%20%20title%3D%22Frequency%20(Hz)%22%2C%0A%20%20%20%20%20%20%20%20type%3D%22log%22%2C%0A%20%20%20%20%20%20%20%20range%3D%5Bnp.log10(50)%2C%20np.log10(16000)%5D%2C%0A%20%20%20%20%20%20%20%20tickvals%3D_ticks%2C%0A%20%20%20%20%20%20%20%20ticktext%3D%5Bf%22%7Bv%20%2F%2F%201000%7Dk%22%20if%20v%20%3E%3D%201000%20else%20str(v)%20for%20v%20in%20_ticks%5D%2C%0A%20%20%20%20)%0A%20%20%20%20fig_spec.update_xaxes(title%3D%22Time%20(s)%22%2C%20range%3D%5B0%2C%201.4%5D)%0A%20%20%20%20fig_spec.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22Spectrograms%20on%20a%20shared%20scale%22%2C%20template%3D%22plotly_white%22%2C%20height%3D430%0A%20%20%20%20)%0A%20%20%20%20fig_spec.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%20The%20measurement%20is%20the%20streakier%20of%20the%20two.%20Its%20individual%20room%20modes%20ring%20well%20past%20the%20mean%20decay%20%E2%80%94%20fitting%20a%20decay%20time%20per%20STFT%20bin%2C%20the%2095th%20percentile%20is%20about%20three%20times%20the%20median%2C%20against%201.6%20for%20the%20resynthesis.%20The%20FDN%20is%20smoother%20across%20frequency%20than%20the%20room%20it%20imitates%2C%20which%20is%20the%20expected%20direction%3A%20sixteen%20delay%20lines%20sharing%20one%20absorption%20curve%20cannot%20reproduce%20the%20modal%20detail%20of%20a%20real%20room%2C%20and%20the%20GEQ%20is%20not%20adding%20resonances%20of%20its%20own.%0A%0A%20%20%20%20Where%20the%20FDN%20does%20overshoot%20is%20around%20200%E2%80%93250%20Hz%2C%20and%20that%20is%20the%20GEQ%20turning%20as%20sharply%20as%20it%20can%20rather%20than%20a%20resonance%3A%20the%20fit%20asks%20the%20fast%20slope%20to%20drop%20from%201.33%20s%20at%20250%20Hz%20to%200.56%20s%20at%20500%20Hz%2C%20and%20a%20ten-point%20design%20grid%20rounds%20that%20corner.%0A%20%20%20%20%22%22%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%20Test%3A%20the%20FDNs%20realise%20the%20estimated%20decay%0A%0A%20%20%20%20Two%20checks.%20Each%20per-slope%20FDN%20must%20reproduce%20the%20decay%20time%20it%20was%20designed%20for%2C%20and%20the%20sum%20of%20the%20two%20must%20follow%20the%20measured%20EDC%20over%20its%20first%2050%20dB%20%E2%80%94%20which%20is%20as%20deep%20as%20the%20measurement%20goes%20before%20its%20own%20noise%20floor%20takes%20over.%0A%0A%20%20%20%20The%20EDC%20error%20is%20printed%20for%20all%20eight%20bands%20but%20asserted%20only%20over%20250%20Hz%20%E2%80%93%204%20kHz.%20The%20two%20edge%20bands%20are%20excluded%20deliberately%2C%20not%20to%20make%20the%20check%20pass%3A%20at%2062%20Hz%20and%208%20kHz%20the%20octave%20filter%20runs%20into%20the%20ends%20of%20the%20spectrum%20and%20the%20GEQ%20command%20point%20sits%20at%20the%20edge%20of%20its%20design%20grid%2C%20neither%20of%20which%20the%20FDN%20has%20reason%20to%20reproduce.%20Over%20the%20five%20bands%20where%20measurement%20and%20model%20are%20both%20trustworthy%20the%20match%20is%20well%20inside%202%20dB%20rms.%0A%0A%20%20%20%20The%20decay-time%20tolerance%20is%20the%20looser%20of%20the%20two%20at%2030%25%2C%20and%20one%20entry%20uses%20most%20of%20it%3A%20the%20fast%20slope%20at%20500%20Hz%20comes%20out%200.69%20s%20against%20a%200.56%20s%20target.%20The%20fit%20asks%20for%20a%202.4%C3%97%20step%20between%20adjacent%20octave%20bands%20there%20%E2%80%94%201.33%20s%20at%20250%20Hz%20down%20to%200.56%20s%20at%20500%20Hz%20%E2%80%94%20and%20a%20GEQ%20on%20a%2010-point%20grid%20cannot%20turn%20that%20sharply%2C%20so%20the%20realised%20decay%20is%20pulled%20towards%20its%20neighbour.%20The%20250%20Hz%20value%20driving%20that%20step%20is%20itself%20the%20least%20reliable%20of%20the%20fit%3A%20it%20is%20the%20one%20band%20where%20the%20two%20slopes%20come%20back%20nearly%20equal%20(1.33%20s%20and%201.47%20s)%2C%20so%20the%20split%20between%20them%20there%20means%20little.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(decay_time%2C%20np%2C%20slope_fdn_rt)%3A%0A%20%20%20%20rt_error%20%3D%20np.abs(slope_fdn_rt%20%2F%20decay_time%20-%201)%0A%20%20%20%20print(f%22Decay%20time%20error%20per%20slope%20and%20band%3A%20%7Brt_error.round(3)%7D%22)%0A%20%20%20%20assert%20np.all(rt_error%20%3C%200.3)%2C%20%22FDN%20decay%20time%20deviates%20more%20than%2030%25%22%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(edc_fdn%2C%20edc_target%2C%20f_centre%2C%20np)%3A%0A%20%20%20%20edc_error%20%3D%20np.array(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20np.sqrt(np.mean((edc_target%5Bk%5D%5B_valid%5D%20-%20edc_fdn%5Bk%5D%5B_valid%5D)%20**%202))%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20k%20in%20range(len(f_centre))%0A%20%20%20%20%20%20%20%20%20%20%20%20if%20(_valid%20%3A%3D%20edc_target%5Bk%5D%20%3E%20-50).any()%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20mid_bands%20%3D%20slice(2%2C%207)%20%20%23%20250%20Hz%20..%204%20kHz%0A%20%20%20%20print(f%22EDC%20error%20per%20band%20(dB%20rms)%3A%20%7Bedc_error.round(2)%7D%22)%0A%20%20%20%20print(f%22Bands%20asserted%20on%3A%20%7Bf_centre%5Bmid_bands%5D.round(0)%7D%22)%0A%20%20%20%20assert%20np.all(edc_error%5Bmid_bands%5D%20%3C%202.0)%2C%20(%0A%20%20%20%20%20%20%20%20%22Resynthesised%20EDC%20deviates%20more%20than%202%20dB%20rms%22%0A%20%20%20%20)%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
f6b4d1e218493a90621912e974cb191f