%23%20gallery_category%3A%20Getting%20Started%0A%23%20gallery_title%3A%20Build%20an%20FDN%20with%20process_fdn%0A%23%20gallery_description%3A%20Hands-on%20walk-through%20of%20the%20FDN%20knobs%20-%20delays%2C%20feedback%20matrix%2C%20in%2Fout%20gains%20and%20decay%20-%20assembled%20as%20a%20delay%20state%20space%20and%20simulated%20with%20pyFDN.process_fdn%2C%20the%20pure-NumPy%20time-domain%20path.%20Every%20step%20has%20experiments%20to%20try.%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%20Build%20an%20FDN%20with%20%60process_fdn%60%0A%0A%20%20%20%20A%20feedback%20delay%20network%20is%20four%20objects%20and%20a%20loop%3A%0A%0A%20%20%20%20%7C%20%7C%20%7C%20%7C%0A%20%20%20%20%7C%20---%20%7C%20---%20%7C%20---%20%7C%0A%20%20%20%20%7C%20%60delays%60%20%7C%20%24N%24%20delay%20lengths%20in%20samples%20%7C%20how%20*dense*%20it%20is%20%7C%0A%20%20%20%20%7C%20%60A%60%20%7C%20the%20%24N%20%5Ctimes%20N%24%20feedback%20matrix%20%7C%20how%20fast%20it%20*mixes*%20%7C%0A%20%20%20%20%7C%20%60B%60%2C%20%60C%60%2C%20%60D%60%20%7C%20input%2C%20output%20and%20direct%20gains%20%7C%20how%20it%20is%20*driven*%20and%20*tapped*%20%7C%0A%20%20%20%20%7C%20absorption%20%7C%20one%20filter%20per%20delay%20line%20%7C%20how%20long%20it%20*decays*%20%7C%0A%0A%20%20%20%20Together%20these%20are%20the%20**delay%20state%20space%20(DSS)**%20%E2%80%94%20the%20representation%20every%0A%20%20%20%20other%20view%20in%20pyFDN%20is%20translated%20from.%20This%20notebook%20turns%20each%20of%20them%20in%0A%20%20%20%20turn%2C%20renders%20the%20result%20with%20%60pyFDN.process_fdn%60%20(plain%20NumPy%2C%20no%20torch)%0A%20%20%20%20and%20listens%20to%20it.%0A%0A%20%20%20%20Each%20code%20cell%20ends%20with%20a%20**Try%20this**%20block%3A%20uncomment%20a%20line%2C%20or%20change%20a%0A%20%20%20%20number%2C%20and%20marimo%20re-runs%20everything%20downstream%20%E2%80%94%20plots%20and%20audio%20included.%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%20warnings%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%20%20%20%20from%20pyFDN%20import%20td%0A%0A%20%20%20%20return%20go%2C%20np%2C%20pyFDN%2C%20td%2C%20warnings%0A%0A%0A%40app.cell%0Adef%20_()%3A%0A%20%20%20%20fs%20%3D%2048_000%20%20%23%20sampling%20rate%20in%20Hz%0A%20%20%20%20N%20%3D%208%20%20%23%20number%20of%20delay%20lines%0A%20%20%20%20ir_len_seconds%20%3D%203.0%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20N%20%3D%204%20%20%20%20-%3E%20fewer%20lines%3A%20sparser%2C%20more%20obviously%20metallic%0A%20%20%20%20%23%20%20%20N%20%3D%2016%20%20%20-%3E%20denser%2C%20smoother%2C%20and%20four%20times%20the%20matrix%20multiply%0A%20%20%20%20return%20N%2C%20fs%2C%20ir_len_seconds%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%20Knob%201%20%E2%80%94%20the%20delays%20set%20the%20density%0A%0A%20%20%20%20Every%20delay%20line%20contributes%20its%20own%20set%20of%20resonances%2C%20and%20the%20sum%20of%20the%0A%20%20%20%20delay%20lengths%20fixes%20how%20many%20there%20are%3A%20long%20delays%20give%20a%20dense%2C%20low-pitched%0A%20%20%20%20ringing%2C%20short%20delays%20a%20sparse%20and%20audibly%20pitched%20one.%20Lengths%20that%20share%20a%0A%20%20%20%20common%20factor%20make%20their%20echoes%20coincide%2C%20which%20is%20heard%20as%20a%20metallic%20ring%2C%0A%20%20%20%20so%20the%20classic%20choice%20is%20mutually%20coprime%20lengths%20spread%20over%20a%20range.%0A%0A%20%20%20%20%60sample_delay_lengths%60%20draws%20them%20for%20you%3B%20you%20can%20also%20just%20type%20the%20numbers.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(N%2C%20fs%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20delays%20%3D%20pyFDN.sample_delay_lengths(%0A%20%20%20%20%20%20%20%20N%2C%0A%20%20%20%20%20%20%20%20delay_range%3D(1000%2C%203000)%2C%20%20%23%20samples%3A%20about%2021-62%20ms%20at%2048%20kHz%0A%20%20%20%20%20%20%20%20distribution%3D%22geometric%22%2C%20%20%23%20equal%20probability%20per%20octave%0A%20%20%20%20%20%20%20%20coprime%3DTrue%2C%20%20%23%20avoid%20coinciding%20echoes%0A%20%20%20%20%20%20%20%20rng%3D2%2C%0A%20%20%20%20)%0A%0A%20%20%20%20print(f%22delays%20%5Bsamples%5D%3A%20%7Bdelays%7D%22)%0A%20%20%20%20print(f%22delays%20%5Bms%5D%3A%20%20%20%20%20%20%7Bnp.round(1000%20*%20delays%20%2F%20fs%2C%201)%7D%22)%0A%20%20%20%20print(f%22mean%20delay%3A%20%20%20%20%20%20%20%7B1000%20*%20delays.mean()%20%2F%20fs%3A.1f%7D%20ms%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20delay_range%3D(300%2C%20600)%20%20%20%20%20-%3E%20flutter%2C%20an%20obvious%20pitch%2C%20a%20small%20box%0A%20%20%20%20%23%20%20%20delay_range%3D(2000%2C%209000)%20%20%20-%3E%20sparse%2C%20granular%20onset%2C%20a%20big%20hall%0A%20%20%20%20%23%20%20%20coprime%3DFalse%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20listen%20for%20the%20ring%20that%20comes%20back%0A%20%20%20%20%23%20%20%20distribution%3D%22uniform%22%20%20%20%20%20-%3E%20flat%20in%20samples%20instead%20of%20flat%20per%20octave%0A%20%20%20%20%23%0A%20%20%20%20%23%20Or%20set%20them%20by%20hand%2C%20in%20milliseconds%3A%0A%20%20%20%20%23%20%20%20delays%20%3D%20pyFDN.ms_to_smp(np.array(%5B20%2C%2027%2C%2031%2C%2037%2C%2043%2C%2053%2C%2061%2C%2071%5D)%2C%20fs)%0A%20%20%20%20return%20(delays%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%20Knob%202%20%E2%80%94%20the%20feedback%20matrix%20mixes%0A%0A%20%20%20%20%60A%60%20decides%20where%20each%20delay%20line%20sends%20its%20output.%20It%20has%20one%20job%20to%20do%0A%20%20%20%20correctly%20%E2%80%94%20be%20**lossless**%2C%20so%20the%20loop%20neither%20grows%20nor%20dies%20on%20its%20own%20%E2%80%94%0A%20%20%20%20and%20one%20job%20to%20do%20well%3A%20mix%20the%20lines%20quickly%2C%20so%20a%20single%20input%20echo%20has%0A%20%20%20%20spread%20across%20all%20%24N%24%20lines%20before%20the%20ear%20can%20count%20the%20individual%0A%20%20%20%20reflections.%20Losslessness%20is%20checkable%20(%60is_orthogonal%60)%3B%20mixing%20speed%20is%0A%20%20%20%20audible%2C%20and%20measurable%20as%20*echo%20density*.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(N%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20print(f%22%20%20%7B'matrix%20type'%3A32s%7D%7B'orthogonal'%3A12s%7Dlossless%22)%0A%20%20%20%20for%20_name%20in%20pyFDN.fdn_matrix_gallery()%3A%0A%20%20%20%20%20%20%20%20_matrix%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20_name)%0A%20%20%20%20%20%20%20%20%23%20Orthogonality%20is%20the%20usual%20sufficient%20condition%3B%20the%20weaker%20one%20is%0A%20%20%20%20%20%20%20%20%23%20%22diagonally%20similar%20to%20orthogonal%22%2C%20which%20is%20lossless%20just%20the%20same.%0A%20%20%20%20%20%20%20%20print(%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%20%20%7B_name%3A32s%7D%7Bstr(pyFDN.is_orthogonal(_matrix))%3A12s%7D%22%0A%20%20%20%20%20%20%20%20%20%20%20%20f%22%7BpyFDN.is_unilossless(_matrix)%7D%22%0A%20%20%20%20%20%20%20%20)%0A%0A%20%20%20%20np.random.seed(0)%20%20%23%20%22orthogonal%22%20is%20drawn%20at%20random%3A%20seed%20it%20to%20stay%20reproducible%0A%20%20%20%20A%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20%22orthogonal%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20A%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20%22hadamard%22)%20%20%20%20%20-%3E%20maximal%20mixing%2C%20%2B%2F-1%20only%0A%20%20%20%20%23%20%20%20A%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20%22householder%22)%20%20-%3E%20cheap%3A%20one%20inner%20product%0A%20%20%20%20%23%20%20%20A%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20%22permutation%22)%20%20-%3E%20lossless%20but%20never%20mixes%0A%20%20%20%20%23%20%20%20A%20%3D%20pyFDN.fdn_matrix_gallery(N%2C%20%22circulant%22)%0A%20%20%20%20return%20(A%2C)%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20pyFDN)%3A%0A%20%20%20%20pyFDN.plot_matrix(A%2C%20title%3D%22Feedback%20matrix%20A%22).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%23%20Mixing%20is%20audible%2C%20and%20measurable%0A%0A%20%20%20%20Normalised%20echo%20density%20(Abel%20%26%20Huang%202006)%20reaches%201%20when%20the%20response%20has%0A%20%20%20%20become%20statistically%20indistinguishable%20from%20Gaussian%20noise%20%E2%80%94%20that%20is%20the%20point%0A%20%20%20%20at%20which%20the%20ear%20stops%20hearing%20separate%20echoes.%20The%20*mixing%20time*%20is%20when%20that%0A%20%20%20%20happens.%0A%0A%20%20%20%20The%20comparison%20below%20changes%20**only**%20the%20matrix%3A%20same%20delays%2C%20same%0A%20%20%20%20losslessness.%20A%20permutation%20matrix%20routes%20each%20line%20to%20exactly%20one%20other%20line%2C%0A%20%20%20%20so%20the%20delays%20never%20combine%20%E2%80%94%20the%20result%20is%20a%20bank%20of%20comb%20filters%2C%20and%20it%20is%0A%20%20%20%20the%20sound%20of%20a%20bad%20reverb.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20N%2C%20delays%2C%20fs%2C%20np%2C%20pyFDN%2C%20warnings)%3A%0A%20%20%20%20_B%20%3D%20np.ones((N%2C%201))%20%2F%20np.sqrt(N)%0A%20%20%20%20_C%20%3D%20np.ones((1%2C%20N))%20%2F%20np.sqrt(N)%0A%20%20%20%20_D%20%3D%20np.zeros((1%2C%201))%0A%0A%20%20%20%20for%20_label%2C%20_matrix%20in%20%5B%0A%20%20%20%20%20%20%20%20(%22orthogonal%22%2C%20A)%2C%0A%20%20%20%20%20%20%20%20(%22permutation%22%2C%20pyFDN.fdn_matrix_gallery(N%2C%20%22permutation%22))%2C%0A%20%20%20%20%5D%3A%0A%20%20%20%20%20%20%20%20_ir%20%3D%20pyFDN.dss_to_impz(2%20*%20fs%2C%20delays%2C%20_matrix%2C%20_B%2C%20_C%2C%20_D).squeeze()%0A%20%20%20%20%20%20%20%20with%20warnings.catch_warnings()%3A%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20%22never%20mixes%22%20is%20a%20result%20here%2C%20not%20a%20problem%3A%20echo_density%20warns%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20when%20the%20density%20does%20not%20reach%20the%20threshold%2C%20which%20is%20exactly%0A%20%20%20%20%20%20%20%20%20%20%20%20%23%20what%20a%20permutation%20matrix%20does.%0A%20%20%20%20%20%20%20%20%20%20%20%20warnings.simplefilter(%22ignore%22%2C%20UserWarning)%0A%20%20%20%20%20%20%20%20%20%20%20%20_mixing_time%2C%20_%20%3D%20pyFDN.echo_density(_ir%2C%20n%3D1024%2C%20fs%3Dfs%2C%20hop%3D256)%0A%20%20%20%20%20%20%20%20_mixed%20%3D%20f%22%7B_mixing_time%3A.0f%7D%20ms%22%20if%20_mixing_time%20else%20%22never%22%0A%20%20%20%20%20%20%20%20print(f%22%7B_label%3A12s%7D%20mixes%20after%20%7B_mixed%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%20add%20%22hadamard%22%20or%20%22circulant%22%20to%20the%20list%20and%20rank%20them.%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%20Wiring%20%E2%80%94%20input%2C%20output%20and%20direct%20gains%0A%0A%20%20%20%20%60B%60%20distributes%20the%20input%20over%20the%20delay%20lines%2C%20%60C%60%20sums%20them%20back%20down%20to%20the%0A%20%20%20%20output%2C%20and%20%60D%60%20is%20the%20direct%20(dry)%20path%20around%20the%20loop.%20For%20a%20mono%20reverb%0A%20%20%20%20the%20flat%20choice%20below%20is%20hard%20to%20beat%3B%20the%20interesting%20variations%20are%0A%20%20%20%20*decorrelated*%20output%20gains%2C%20which%20is%20how%20one%20FDN%20feeds%20several%20loudspeakers%0A%20%20%20%20from%20the%20same%20tail.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(N%2C%20np)%3A%0A%20%20%20%20B%20%3D%20np.ones((N%2C%201))%20%2F%20np.sqrt(N)%20%20%23%20(N%2C%20num_inputs)%0A%20%20%20%20C%20%3D%20np.ones((1%2C%20N))%20%2F%20np.sqrt(N)%20%20%23%20(num_outputs%2C%20N)%0A%20%20%20%20D%20%3D%20np.zeros((1%2C%201))%20%20%23%20(num_outputs%2C%20num_inputs)%20%E2%80%94%20wet%20only%0A%0A%20%20%20%20print(f%22B%3A%20%7BB.shape%7D%2C%20C%3A%20%7BC.shape%7D%2C%20D%3A%20%7BD.shape%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20D%20%3D%20np.full((1%2C%201)%2C%200.5)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20mix%20the%20dry%20signal%20back%20in%0A%20%20%20%20%23%20%20%20C%2C%20D%20%3D%20pyFDN.random_orthogonal(N)%5B%3A2%5D%2C%20np.zeros((2%2C%201))%0A%20%20%20%20%23%20%20%20%20%20%20%20-%3E%20a%20stereo%20FDN%3A%20two%20orthonormal%20taps%20on%20the%20same%20tail.%20Orthogonal%2C%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20not%20merely%20different%20--%20Gaussian%20rows%20keep%20a%20leftover%20inner%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20product%2C%20and%20the%20channels%20stay%20audibly%20correlated.%0A%20%20%20%20%23%20%20%20B%20%3D%20np.eye(N)%5B%3A%2C%20%3A1%5D%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-%3E%20drive%20one%20delay%20line%20only%0A%20%20%20%20return%20B%2C%20C%2C%20D%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%20Render%20it%20%E2%80%94%20the%20lossless%20FDN%0A%0A%20%20%20%20That%20is%20a%20complete%20FDN%20already.%20%60dss_to_impz%60%20runs%20the%20delay%20state%20space%20in%0A%20%20%20%20the%20time%20domain%20and%20hands%20back%20an%20impulse%20response.%0A%0A%20%20%20%20With%20an%20orthogonal%20%60A%60%20and%20nothing%20in%20the%20loop%2C%20the%20FDN%20is%20lossless%3A%20energy%0A%20%20%20%20circulates%20forever.%20The%20tail%20below%20does%20not%20decay%20at%20all%2C%20which%20is%20exactly%0A%20%20%20%20what%20the%20plot%20and%20the%20audio%20player%20should%20show.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20B%2C%20C%2C%20D%2C%20delays%2C%20fs%2C%20ir_len_seconds%2C%20mo%2C%20pyFDN)%3A%0A%20%20%20%20ir_lossless%20%3D%20pyFDN.dss_to_impz(%0A%20%20%20%20%20%20%20%20int(ir_len_seconds%20*%20fs)%2C%20delays%2C%20A%2C%20B%2C%20C%2C%20D%0A%20%20%20%20).squeeze()%0A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_impulse_response(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ir_lossless%2C%20fs%3Dfs%2C%20title%3D%22Lossless%20FDN%20%E2%80%94%20the%20loop%20never%20lets%20go%22%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%20pyFDN.labeled_audio(%22lossless%22%2C%20pyFDN.peak_normalize(ir_lossless)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%20(ir_lossless%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%20Knob%203%20%E2%80%94%20absorption%20sets%20the%20decay%0A%0A%20%20%20%20To%20make%20it%20decay%2C%20attenuate%20the%20signal%20every%20time%20it%20goes%20round.%20The%0A%20%20%20%20attenuation%20has%20to%20be%20**proportional%20to%20the%20delay%20length**%2C%20otherwise%20short%0A%20%20%20%20lines%20die%20before%20long%20ones%20and%20the%20decay%20is%20no%20longer%20a%20single%20exponential.%0A%20%20%20%20One%20gain%20per%20sample%2C%20raised%20to%20the%20delay%20length%2C%20does%20exactly%20that%3A%0A%0A%20%20%20%20%60A_lossy%20%3D%20diag(g%20**%20delays)%20%40%20A%60%0A%0A%20%20%20%20This%20is%20*homogeneous*%20decay%3A%20every%20resonance%20dies%20at%20the%20same%20rate%2C%20so%20the%0A%20%20%20%20reverberation%20time%20is%20the%20one%20you%20asked%20for.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20B%2C%20C%2C%20D%2C%20delays%2C%20fs%2C%20ir_len_seconds%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20rt%20%3D%201.8%20%20%23%20target%20T60%20in%20seconds%0A%0A%20%20%20%20g%20%3D%20pyFDN.rt_to_gain_per_sample(rt%2C%20fs)%0A%20%20%20%20A_lossy%20%3D%20np.diag(g**delays)%20%40%20A%0A%20%20%20%20ir_broadband%20%3D%20pyFDN.dss_to_impz(%0A%20%20%20%20%20%20%20%20int(ir_len_seconds%20*%20fs)%2C%20delays%2C%20A_lossy%2C%20B%2C%20C%2C%20D%0A%20%20%20%20).squeeze()%0A%0A%20%20%20%20print(f%22gain%20per%20sample%3A%20%7Bg%3A.8f%7D%22)%0A%20%20%20%20print(f%22per-round-trip%20attenuation%20%5BdB%5D%3A%20%7Bnp.round(pyFDN.lin_to_db(g**delays)%2C%202)%7D%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20rt%20%3D%200.4%20%20%20-%3E%20a%20small%2C%20dry%20room%0A%20%20%20%20%23%20%20%20rt%20%3D%206.0%20%20%20-%3E%20a%20cathedral%3B%20raise%20ir_len_seconds%20to%20see%20the%20whole%20tail%0A%20%20%20%20%23%20%20%20A_lossy%20%3D%20np.diag(g%20**%20delays.mean())%20%40%20A%0A%20%20%20%20%23%20%20%20%20%20%20%20-%3E%20the%20same%20gain%20on%20every%20line%3A%20no%20longer%20homogeneous.%20Look%20at%20the%20EDC.%0A%20%20%20%20return%20(ir_broadband%2C)%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir_broadband%2C%20ir_lossless%2C%20mo%2C%20pyFDN)%3A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_edc(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ir_lossless%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ir_broadband%2C%0A%20%20%20%20%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%20%20%20%20%20labels%3D%5B%22lossless%22%2C%20%22broadband%20decay%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20normalize%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3D%22Energy%20decay%20curve%20%E2%80%94%20the%20-60%20dB%20crossing%20is%20T60%22%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%20pyFDN.labeled_audio(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22broadband%20decay%22%2C%20pyFDN.peak_normalize(ir_broadband)%2C%20fs%3Dfs%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%5D%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%20Knob%203%2C%20per%20band%20%E2%80%94%20frequency-dependent%20decay%0A%0A%20%20%20%20Real%20rooms%20absorb%20high%20frequencies%20faster%20than%20low%20ones%2C%20so%20a%20single%20number%0A%20%20%20%20is%20not%20enough.%20Replace%20the%20scalar%20gain%20with%20a%20**filter%20per%20delay%20line**%20whose%0A%20%20%20%20attenuation%20follows%20the%20target%20%24T_%7B60%7D%24%20across%20frequency.%0A%20%20%20%20%60decay_to_geq%60%20designs%20those%20filters%20from%20a%20target%20curve%20at%20ten%20bands%20(DC%2C%0A%20%20%20%20the%20eight%20octave%20bands%2063%20Hz%20%E2%80%93%208%20kHz%2C%20and%20Nyquist).%0A%0A%20%20%20%20The%20filters%20live%20*inside*%20the%20loop%2C%20so%20the%20feedback%20matrix%20goes%20back%20to%20being%0A%20%20%20%20the%20plain%20lossless%20%60A%60%20%E2%80%94%20all%20the%20decay%20is%20now%20in%20the%20filters.%20Bundling%20the%0A%20%20%20%20parts%20into%20an%20%60FDNBuild%60%20lets%20%60build_to_impz%60%20render%20the%20whole%20thing.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20B%2C%20C%2C%20D%2C%20delays%2C%20fs%2C%20ir_len_seconds%2C%20np%2C%20pyFDN)%3A%0A%20%20%20%20target_rt%20%3D%20np.array(%5B2.4%2C%202.4%2C%202.3%2C%202.1%2C%201.8%2C%201.4%2C%201.0%2C%200.7%2C%200.5%2C%200.5%5D)%0A%0A%20%20%20%20absorption%20%3D%20pyFDN.decay_to_geq(target_rt%2C%20delays%2C%20fs)%20%20%23%20(n_sections%2C%206%2C%20N)%0A%0A%20%20%20%20build%20%3D%20pyFDN.FDNBuild(%0A%20%20%20%20%20%20%20%20A%3DA%2C%20B%3DB%2C%20C%3DC%2C%20D%3DD%2C%20delays%3Ddelays%2C%20fs%3Dfs%2C%20post_delay%3Dabsorption%0A%20%20%20%20)%0A%20%20%20%20ir%20%3D%20pyFDN.build_to_impz(build%2C%20int(ir_len_seconds%20*%20fs)).squeeze()%0A%0A%20%20%20%20print(f%22absorption%20SOS%20bank%3A%20%7Babsorption.shape%7D%20%20(sections%2C%20coefficients%2C%20lines)%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20target_rt%20%3D%20np.full(10%2C%201.5)%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20flat%20decay%2C%20still%20filtered%0A%20%20%20%20%23%20%20%20target_rt%20%3D%20np.linspace(4.0%2C%200.2%2C%2010)%20%20%20%20%20%20%20%20%20%20%20%20%20%20-%3E%20a%20very%20dark%20room%0A%20%20%20%20%23%20%20%20target_rt%20%3D%20np.array(%5B4%2C%204%2C%203%2C%202%2C%201%2C%20.6%2C%20.4%2C%20.3%2C%20.2%2C%20.2%5D)%0A%20%20%20%20%23%20%20%20%20%20%20%20-%3E%20ask%20for%20something%20extreme%20and%20see%20where%20the%20design%20stops%20delivering%0A%20%20%20%20return%20absorption%2C%20build%2C%20ir%2C%20target_rt%0A%0A%0A%40app.cell%0Adef%20_(build%2C%20pyFDN)%3A%0A%20%20%20%20%23%20Every%20parameter%20of%20the%20finished%20FDN%20in%20one%20figure%3A%20delays%2C%20A%2C%20B%2C%20C%2C%20D%20and%0A%20%20%20%20%23%20the%20absorption%20response%20of%20each%20line.%0A%20%20%20%20pyFDN.plot_FDN_build(build%2C%20title%3D%22The%20complete%20FDN%22)%0A%0A%20%20%20%20%23%20Try%20this%3A%20pyFDN.plot_db_per_sample(absorption%2C%20delays%2C%20fs%3Dfs%2C%20nfft%3D2**14)%0A%20%20%20%20%23%20%20%20-%3E%20the%20attenuation%20each%20line%20applies%20per%20sample%2C%20which%20is%20the%20quantity%0A%20%20%20%20%23%20%20%20%20%20%20the%20absorption%20design%20actually%20solves%20for.%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%23%20Did%20you%20get%20what%20you%20asked%20for%3F%0A%0A%20%20%20%20%60estimate_rt_bands%60%20fits%20the%20Schroeder%20decay%20of%20the%20rendered%20impulse%20response%0A%20%20%20%20in%20octave%20bands.%20This%20is%20the%20loop%20that%20matters%3A%20*state%20a%20target%2C%20render%2C%0A%20%20%20%20measure%2C%20compare.*%20Everything%20analytic%20in%20pyFDN%20is%20meant%20to%20be%20checked%20this%0A%20%20%20%20way%20%E2%80%94%20nothing%20about%20the%20design%20has%20to%20be%20taken%20on%20faith.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20go%2C%20ir%2C%20np%2C%20pyFDN%2C%20target_rt)%3A%0A%20%20%20%20rt_measured%2C%20f_centre%20%3D%20pyFDN.estimate_rt_bands(ir%2C%20fs)%0A%0A%20%20%20%20_fig%20%3D%20go.Figure()%0A%20%20%20%20_fig.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%20%20%23%20the%20same%20eight%20octave%20bands%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%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%20_fig.add_trace(%0A%20%20%20%20%20%20%20%20go.Scatter(x%3Df_centre%2C%20y%3Drt_measured%2C%20mode%3D%22lines%2Bmarkers%22%2C%20name%3D%22measured%22)%0A%20%20%20%20)%0A%20%20%20%20_fig.update_layout(%0A%20%20%20%20%20%20%20%20title%3D%22T60%3A%20asked%20for%20vs.%20delivered%22%2C%0A%20%20%20%20%20%20%20%20xaxis%3D%7B%22title%22%3A%20%22Frequency%20%5BHz%5D%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%22T60%20%5Bs%5D%22%2C%20%22range%22%3A%20%5B0%2C%20None%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%20_fig.show()%0A%0A%20%20%20%20print(f%22target%20%20%20%5Bs%5D%3A%20%7Bnp.round(target_rt%5B1%3A9%5D%2C%202)%7D%22)%0A%20%20%20%20print(f%22measured%20%5Bs%5D%3A%20%7Bnp.round(rt_measured%2C%202)%7D%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%20Analyse%20%E2%80%94%20and%20listen%0A%0A%20%20%20%20Three%20views%20of%20the%20same%20impulse%20response%2C%20and%20then%20your%20ears.%20The%20spectrogram%0A%20%20%20%20shows%20the%20absorption%20as%20a%20tilt%3A%20the%20top%20of%20the%20picture%20empties%20out%20first%2C%0A%20%20%20%20because%20that%20is%20where%20the%20target%20%24T_%7B60%7D%24%20was%20shortest.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir%2C%20pyFDN)%3A%0A%20%20%20%20pyFDN.plot_spectrogram(ir%2C%20fs%2C%20title%3D%22Impulse%20response%20%E2%80%94%20time-frequency%20energy%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir%2C%20mo%2C%20pyFDN)%3A%0A%20%20%20%20_mixing_time%2C%20_%20%3D%20pyFDN.echo_density(ir%2C%20n%3D1024%2C%20fs%3Dfs%2C%20hop%3D256)%0A%20%20%20%20print(f%22mixing%20time%3A%20%7B_mixing_time%3A.0f%7D%20ms%22)%0A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_impulse_response(ir%2C%20fs%3Dfs%2C%20title%3D%22Impulse%20response%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22impulse%20response%22%2C%20pyFDN.peak_normalize(ir)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%5D%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%23%20Run%20audio%20through%20it%0A%0A%20%20%20%20%60process_fdn%60%20is%20the%20same%20recursion%2C%20driven%20by%20a%20signal%20instead%20of%20an%20impulse.%0A%20%20%20%20The%20absorption%20filters%20go%20into%20the%20%60post_delay%60%20hook%20%E2%80%94%20the%20point%20in%20the%20loop%0A%20%20%20%20just%20after%20the%20delay%20outputs%2C%20which%20is%20where%20%60build_to_impz%60%20put%20them%20too.%0A%0A%20%20%20%20Pad%20the%20input%20with%20silence%2C%20or%20the%20tail%20is%20cut%20off%20where%20the%20signal%20ends.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20B%2C%20C%2C%20D%2C%20absorption%2C%20delays%2C%20fs%2C%20mo%2C%20np%2C%20pyFDN%2C%20td)%3A%0A%20%20%20%20dry%2C%20_%20%3D%20pyFDN.load_audio(%22synth_dry%22%2C%20fs%3Dfs)%0A%20%20%20%20x%20%3D%20np.pad(dry%2C%20(0%2C%202%20*%20fs))%20%20%23%20room%20for%20the%20tail%0A%0A%20%20%20%20wet%20%3D%20pyFDN.process_fdn(%0A%20%20%20%20%20%20%20%20x%2C%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20A%2C%0A%20%20%20%20%20%20%20%20B%2C%0A%20%20%20%20%20%20%20%20C%2C%0A%20%20%20%20%20%20%20%20D%2C%0A%20%20%20%20%20%20%20%20post_delay%3Dtd.SOSBank(absorption)%2C%20%20%23%20absorption%20inside%20the%20loop%0A%20%20%20%20)%0A%0A%20%20%20%20mo.hstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22dry%22%2C%20dry%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22wet%22%2C%20pyFDN.peak_normalize(wet)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%5D%0A%20%20%20%20)%0A%20%20%20%20return%20wet%2C%20x%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%20Three%20hooks%2C%20one%20loop%0A%0A%20%20%20%20%60process_fdn%60%20takes%20a%20filter%20at%20three%20points%2C%20and%20each%20one%20is%20an%20entire%20family%0A%20%20%20%20of%20reverbs%3A%0A%0A%20%20%20%20%7C%20hook%20%7C%20where%20it%20sits%20%7C%20what%20it%20buys%20%7C%0A%20%20%20%20%7C%20---%20%7C%20---%20%7C%20---%20%7C%0A%20%20%20%20%7C%20%60post_delay%60%20%7C%20after%20the%20delay%20outputs%20%7C%20absorption%20%E2%80%94%20used%20above%20%7C%0A%20%20%20%20%7C%20%60post_matrix%60%20%7C%20after%20the%20feedback%20matrix%20%7C%20time%20variation%2C%20non-linearity%20%7C%0A%20%20%20%20%7C%20%60post_output%60%20%7C%20on%20the%20wet%20signal%20%7C%20output%20EQ%2C%20voicing%20%7C%0A%0A%20%20%20%20A%20hook%20is%20any%20object%20with%20a%20%60.filter(block)%60%20method%2C%20so%20your%20own%20DSP%20drops%0A%20%20%20%20straight%20in.%20The%20cell%20below%20is%20the%20same%20FDN%20with%20a%20moving%20matrix%20in%20the%20loop%3A%0A%20%20%20%20%60TimeVaryingMatrix%60%20stays%20orthogonal%20at%20every%20sample%2C%20so%20the%20decay%20is%0A%20%20%20%20unchanged%20%E2%80%94%20but%20the%20modes%20never%20sit%20still%2C%20and%20the%20metallic%20ringing%20of%20a%0A%20%20%20%20static%20FDN%20cannot%20build%20up.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(A%2C%20B%2C%20C%2C%20D%2C%20absorption%2C%20delays%2C%20fs%2C%20mo%2C%20np%2C%20pyFDN%2C%20td%2C%20wet%2C%20x)%3A%0A%20%20%20%20np.random.seed(11)%20%20%23%20TimeVaryingMatrix%20draws%20its%20phases%20from%20the%20global%20stream%0A%20%20%20%20wet_moving%20%3D%20pyFDN.process_fdn(%0A%20%20%20%20%20%20%20%20x%2C%0A%20%20%20%20%20%20%20%20delays%2C%0A%20%20%20%20%20%20%20%20A%2C%0A%20%20%20%20%20%20%20%20B%2C%0A%20%20%20%20%20%20%20%20C%2C%0A%20%20%20%20%20%20%20%20D%2C%0A%20%20%20%20%20%20%20%20post_delay%3Dtd.SOSBank(absorption)%2C%0A%20%20%20%20%20%20%20%20post_matrix%3Dtd.TimeVaryingMatrix(len(delays)%2C%2010.0%2C%201.1%2C%20fs%2C%200.7)%2C%0A%20%20%20%20)%0A%0A%20%20%20%20%23%20Try%20this%3A%0A%20%20%20%20%23%20%20%20post_matrix%3Dtd.AbsoluteValue(len(delays))%0A%20%20%20%20%23%20%20%20%20%20%20%20-%3E%20a%20rectifier%20in%20the%20loop.%20The%20harmonics%20were%20never%20in%20the%20input%3B%0A%20%20%20%20%23%20%20%20%20%20%20%20%20%20%20the%20FDN%20is%20generating%20them.%20No%20transfer%20function%20exists%20any%20more.%0A%20%20%20%20%23%20%20%20post_output%3Dtd.SOSBank(...)%20%20%20-%3E%20voice%20the%20wet%20signal%20with%20pyFDN.gain_to_geq%0A%20%20%20%20mo.hstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22static%20matrix%22%2C%20pyFDN.peak_normalize(wet)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22time-varying%20matrix%22%2C%20pyFDN.peak_normalize(wet_moving)%2C%20fs%3Dfs%0A%20%20%20%20%20%20%20%20%20%20%20%20)%2C%0A%20%20%20%20%20%20%20%20%5D%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%20Now%20match%20a%20real%20room%0A%0A%20%20%20%20%60s3_r4_o%60%20is%20a%20measured%20impulse%20response%20of%20the%20Promenadikeskus%20concert%20hall%0A%20%20%20%20in%20Pori%2C%20Finland.%20It%20is%20the%20target.%0A%0A%20%20%20%20Go%20back%20up%20and%20turn%20the%20knobs%20%E2%80%94%20%60N%60%2C%20the%20delay%20range%2C%20the%20matrix%2C%20%60target_rt%60%2C%0A%20%20%20%20the%20hooks%20%E2%80%94%20until%20your%20FDN%20gets%20as%20close%20to%20it%20as%20you%20can.%20The%20printout%20below%0A%20%20%20%20compares%20the%20two%20on%20decay%20and%20mixing%20time%3B%20the%20plot%20and%20the%20two%20players%0A%20%20%20%20compare%20them%20on%20everything%20else.%0A%0A%20%20%20%20Nobody%20gets%20all%20the%20way%20there.%20Where%20you%20stop%2C%20and%20what%20is%20still%20different%0A%20%20%20%20when%20you%20do%2C%20is%20the%20interesting%20part.%0A%20%20%20%20%22%22%22)%0A%20%20%20%20return%0A%0A%0A%40app.cell%0Adef%20_(fs%2C%20ir%2C%20mo%2C%20np%2C%20pyFDN%2C%20warnings)%3A%0A%20%20%20%20room%2C%20_%20%3D%20pyFDN.load_audio(%22s3_r4_o%22%2C%20fs%3Dfs)%0A%20%20%20%20room%20%3D%20room%5Bint(np.argmax(np.abs(room)))%20%3A%5D%20%20%23%20trim%20to%20the%20direct%20sound%0A%0A%20%20%20%20_room_rt%2C%20_bands%20%3D%20pyFDN.estimate_rt_bands(room%2C%20fs)%0A%20%20%20%20_your_rt%2C%20_%20%3D%20pyFDN.estimate_rt_bands(ir%2C%20fs)%0A%20%20%20%20with%20warnings.catch_warnings()%3A%0A%20%20%20%20%20%20%20%20warnings.simplefilter(%22ignore%22%2C%20UserWarning)%0A%20%20%20%20%20%20%20%20_room_mix%2C%20_%20%3D%20pyFDN.echo_density(room%2C%20n%3D1024%2C%20fs%3Dfs%2C%20hop%3D256)%0A%20%20%20%20%20%20%20%20_your_mix%2C%20_%20%3D%20pyFDN.echo_density(ir%2C%20n%3D1024%2C%20fs%3Dfs%2C%20hop%3D256)%0A%0A%20%20%20%20print(f%22band%20%5BHz%5D%20%20%7Bnp.array2string(_bands%2C%20precision%3D0)%7D%22)%0A%20%20%20%20print(f%22the%20room%20%20%20%7Bnp.round(_room_rt%2C%202)%7D%20%20mixes%20after%20%7B_room_mix%3A.0f%7D%20ms%22)%0A%20%20%20%20print(%0A%20%20%20%20%20%20%20%20f%22your%20FDN%20%20%20%7Bnp.round(_your_rt%2C%202)%7D%20%20mixes%20after%20%22%0A%20%20%20%20%20%20%20%20f%22%7Bf'%7B_your_mix%3A.0f%7D%20ms'%20if%20_your_mix%20else%20'never'%7D%22%0A%20%20%20%20)%0A%0A%20%20%20%20mo.vstack(%0A%20%20%20%20%20%20%20%20%5B%0A%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.plot_edc(%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20room%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20ir%2C%0A%20%20%20%20%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%20%20%20%20%20labels%3D%5B%22the%20room%22%2C%20%22your%20FDN%22%5D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20normalize%3DTrue%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20title%3D%22Energy%20decay%20curve%20%E2%80%94%20the%20target%20and%20your%20FDN%22%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%20mo.hstack(%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%20pyFDN.labeled_audio(%22the%20room%22%2C%20pyFDN.peak_normalize(room)%2C%20fs%3Dfs)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20pyFDN.labeled_audio(%22your%20FDN%22%2C%20pyFDN.peak_normalize(ir)%2C%20fs%3Dfs)%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%5D%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%20Checkpoint%0A%0A%20%20%20%20Get%20an%20FDN%20you%20like%20the%20sound%20of.%20Then%20break%20it%20on%20purpose%20%E2%80%94%20make%20it%20metallic%2C%0A%20%20%20%20make%20it%20flutter%2C%20make%20it%20dark%20%E2%80%94%20and%20say%20which%20knob%20did%20it.%0A%0A%20%20%20%20Where%20to%20go%20from%20here%3A%0A%0A%20%20%20%20-%20%60example_vanilla_FDN%60%20%E2%80%94%20the%20same%20FDN%20through%20FLAMO%2C%20the%20differentiable%20path%0A%20%20%20%20-%20%60example_absorption_geq%60%20%E2%80%94%20the%20absorption%20design%20on%20its%20own%2C%20in%20detail%0A%20%20%20%20-%20%60example_delay_matrix_density%60%20%E2%80%94%20buying%20echo%20density%20with%20delays%20in%20the%0A%20%20%20%20%20%20feedback%20path%0A%20%20%20%20-%20%60example_fdn_gallery%60%20%E2%80%94%20every%20feedback%20matrix%20in%20the%20gallery%2C%20side%20by%20side%0A%20%20%20%20-%20%60example_time_varying_fdn%60%2C%20%60example_scattering_fdn%60%20%E2%80%94%20beyond%20the%20vanilla%20FDN%0A%20%20%20%20-%20%60example_train_fdn_to_rir%60%20%E2%80%94%20the%20same%20room%2C%20this%20time%20matched%20by%20gradient%0A%20%20%20%20%20%20descent%20instead%20of%20by%20hand%0A%20%20%20%20%22%22%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
e7f3e2860cb0e8d3ad28ff9c46d1a485