From d4958f15690313e9aea941f921412b45c9411f2c Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Wed, 14 Oct 2026 06:04:42 -0500 Subject: [PATCH 1/2] Bump MuTap to 9a27f2e (main; DspTap stays 2137d86) The production-readiness M0 tree (tap/MuTap#91): the NaN watchdog in every hot path, the README namespace fix and snippet test, PROTOCOL.md, v0.1.0, the claims table and the IP notices, plus everything between 449cf6c and main (tap/MuTap#83-#90: the reverb stages, the spectral reverb, the guard's lost_in_probation_strikes default, the detector's __restrict bank, TapHouse v7). Moved with scripts/repin.sh. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A --- submodules/MuTap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/MuTap b/submodules/MuTap index 449cf6c..9a27f2e 160000 --- a/submodules/MuTap +++ b/submodules/MuTap @@ -1 +1 @@ -Subproject commit 449cf6c8b6bd29e7ea463c6a117d6be042dc7fbf +Subproject commit 9a27f2ef16e53849415fbc593afec9af9a50c268 From d518d354322c9f916d02c556f396270fcc438ee2 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Wed, 14 Oct 2026 06:04:42 -0500 Subject: [PATCH 2/2] mutap.afc~ / mutap.aec~: report the NaN watchdog as `watchdog n` on the right outlet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MuTap-Max half of production-readiness M0 (tap/MuTap#91, plan §3 rule 3). Each external keeps, per built engine, the last count it reported — mutap.afc~ the canceller's pem_afc::watchdog_trips() plus the guard's howl_guard::watchdog_trips(0), mutap.aec~ every engine's watchdog_trips() (a chain counts a stage's trip once) — and sends `watchdog n` on its rightmost outlet whenever the count moves, from a pre-allocated atoms member on the audio thread; a healthy patch never hears it, and a rebuild starts a fresh count. The affected block is one block of silence, as the library guarantees. Tests (Min-level, the mock kernel): a NaN in the microphone after 32 blocks yields exactly one `watchdog 1` and a cleaned output with no non-finite sample, for the default afc engine, for the guarded one (the guard goes back to ARMING; the trip counts once) and for the aec engine; a healthy run sends none. The afc test's room gained a `poison` flag; the aec test gained the room driver and the fifo-outlet wait the afc test already had. The @ceiling scenario's expectation is re-recorded under the new pin: at the default ceiling the guard still opens (report 47) and the detector never trips, but the lost-verdict duck at report 143 is now ONE strike, since MuTap's lost_in_probation_strikes defaulted on (tap/MuTap#88, after the previous pin), and the doubled re-arm timer keeps it ducked for the rest of the 12 s; the assertion is now "at most one strike, and the guard opened before it". README (both objects), both maxrefs and both help patchers (a comment beside the right outlet's existing notes) describe the message. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DDhgJqxUsdmWPKh6pKrV5A --- README.md | 12 +- docs/mutap.aec~.maxref.xml | 2 +- docs/mutap.afc~.maxref.xml | 2 +- help/mutap.aec~.maxhelp | 2271 +++---- help/mutap.afc~.maxhelp | 5694 +++++++++-------- .../mutap.aec_tilde/mutap.aec_tilde.cpp | 22 +- .../mutap.aec_tilde/mutap.aec_tilde_test.cpp | 111 + .../mutap.afc_tilde/mutap.afc_tilde.cpp | 71 +- .../mutap.afc_tilde/mutap.afc_tilde_test.cpp | 100 +- 9 files changed, 4289 insertions(+), 3996 deletions(-) diff --git a/README.md b/README.md index b5052fd..e61d1ea 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,13 @@ Early scaffold. Two objects so far: `shadow_ratio_db` is the main filter's residual power over the shadow comparator's (see `shadow`): below 0 dB while the long filter out-cancels the short fast one, rising toward and past 0 dB when the path moves; it - reads 0 dB with `@shadow 0`. Without the guard the external applies **no + reads 0 dB with `@shadow 0`. The same outlet sends **`watchdog n`** + whenever MuTap's NaN watchdog trips (`include/mutap/watchdog.h`): a NaN or + infinity that reached the canceller — a divide by zero in the patch, a + disconnected inlet — resets it as `reset` does, sends the guard back to + ARMING, comes out as one block of silence, and counts; `n` is the count + since the engine was built (the canceller's trips plus the guard's), sent + only when it moves, so a healthy patch never hears it. Without the guard the external applies **no thresholds** — that is the patch's policy; MuTap's `pem_afc.h` and `fd_kalman.h` record what each statistic was measured to catch and miss, and the help patcher carries an example policy with its calibration conditions. Creation arg: @@ -150,6 +156,10 @@ Early scaffold. Two objects so far: in the MuTap test suite (`tests/test_aec.cpp`: at 0 dB double-talk the naive update is kicked past useless while the Kalman core holds a deep estimate with ~13 dB echo suppression through the segment, config-free). + Its right outlet (the IPC, or the echo-explained fraction with a + post-filter) also sends **`watchdog n`** whenever MuTap's NaN watchdog + trips, as `mutap.afc~`'s does: the engine resets, outputs one block of + silence, and `n` counts since it was built. `@postfilter 1` engages the full measured **AEC chain** — the raw Kalman canceller plus MuTap's coherence-driven residual suppressor, comfort noise matched to the room's noise floor, and the initial receive guard: diff --git a/docs/mutap.aec~.maxref.xml b/docs/mutap.aec~.maxref.xml index 9ebf309..98dd64f 100644 --- a/docs/mutap.aec~.maxref.xml +++ b/docs/mutap.aec~.maxref.xml @@ -7,7 +7,7 @@ Acoustic echo canceller - Acoustic echo canceller. Subtracts an adaptive estimate of the loudspeaker-to-microphone echo path from the microphone signal — the open-loop cousin of mutap.afc~, for the case where a clean far-end reference exists. Adaptation runs on a PEM-prewhitened signal pair, so it survives double-talk without a double-talk detector (FDAF-PEM, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the far-end signal feeding the loudspeaker; the cleaned output is delayed by @block samples. The right outlet reports the IPC double-talk indicator (0..1). @warp selects the frequency-warped near-end model for music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2), the measured double-talk winner. @postfilter selects the residual-echo post-filter engine: 1 engages the full measured AEC chain — raw Kalman canceller, coherence-driven residual suppressor, comfort noise, initial receive guard — the configuration MuTap's ITU-T compliance battery certifies; 2 swaps the suppressor for a small trained network (see @model). + Acoustic echo canceller. Subtracts an adaptive estimate of the loudspeaker-to-microphone echo path from the microphone signal — the open-loop cousin of mutap.afc~, for the case where a clean far-end reference exists. Adaptation runs on a PEM-prewhitened signal pair, so it survives double-talk without a double-talk detector (FDAF-PEM, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the far-end signal feeding the loudspeaker; the cleaned output is delayed by @block samples. The right outlet reports the IPC double-talk indicator (0..1), and 'watchdog n' whenever a NaN or infinity reached the canceller (it resets and outputs one block of silence; n counts since the last rebuild). @warp selects the frequency-warped near-end model for music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2), the measured double-talk winner. @postfilter selects the residual-echo post-filter engine: 1 engages the full measured AEC chain — raw Kalman canceller, coherence-driven residual suppressor, comfort noise, initial receive guard — the configuration MuTap's ITU-T compliance battery certifies; 2 swaps the suppressor for a small trained network (see @model). diff --git a/docs/mutap.afc~.maxref.xml b/docs/mutap.afc~.maxref.xml index 14b4209..37f4b56 100644 --- a/docs/mutap.afc~.maxref.xml +++ b/docs/mutap.afc~.maxref.xml @@ -7,7 +7,7 @@ Acoustic feedback (howling) canceller - Acoustic feedback (howling) canceller. Subtracts an adaptive estimate of the loudspeaker-to-microphone feedback path from the microphone signal, adapted with PEM prewhitening so the closed loop does not bias the estimate (FDAF-PEM-AFROW, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the signal feeding the loudspeaker; the cleaned output is delayed by @block samples. The second outlet carries the guard's post-reverb gain (1 with the guard off); the third reports the IPC double-talk indicator (0..1); the right outlet reports two raw convergence statistics in dB (uncertainty_db shadow_ratio_db), plus the guard's state with @guard on. @warp selects the frequency-warped near-end model for music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2); @shadow sizes the shadow comparator; @guard adds the anti-howl safety layer (MuTap howl_guard: arming, duck, re-arm, back-off, soundcheck calibration, cap). + Acoustic feedback (howling) canceller. Subtracts an adaptive estimate of the loudspeaker-to-microphone feedback path from the microphone signal, adapted with PEM prewhitening so the closed loop does not bias the estimate (FDAF-PEM-AFROW, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the signal feeding the loudspeaker; the cleaned output is delayed by @block samples. The second outlet carries the guard's post-reverb gain (1 with the guard off); the third reports the IPC double-talk indicator (0..1); the right outlet reports two raw convergence statistics in dB (uncertainty_db shadow_ratio_db), plus the guard's state with @guard on, and 'watchdog n' whenever a NaN or infinity reached the canceller (it resets and outputs one block of silence; n counts since the last rebuild). @warp selects the frequency-warped near-end model for music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2); @shadow sizes the shadow comparator; @guard adds the anti-howl safety layer (MuTap howl_guard: arming, duck, re-arm, back-off, soundcheck calibration, cap). diff --git a/help/mutap.aec~.maxhelp b/help/mutap.aec~.maxhelp index 0fee4d2..5cb5ab2 100644 --- a/help/mutap.aec~.maxhelp +++ b/help/mutap.aec~.maxhelp @@ -1,1130 +1,1145 @@ { - "patcher": { - "fileversion": 1, - "appversion": { - "major": 8, - "minor": 0, - "revision": 0, - "architecture": "x64", - "modernui": 1 - }, - "classnamespace": "box", - "rect": [ - 60.0, - 80.0, - 1020.0, - 700.0 - ], - "bglocked": 0, - "openinpresentation": 0, - "default_fontsize": 12.0, - "default_fontface": 0, - "default_fontname": "Arial", - "gridonopen": 1, - "gridsize": [ - 15.0, - 15.0 - ], - "gridsnaponopen": 1, - "objectsnaponopen": 1, - "statusbarvisible": 2, - "toolbarvisible": 1, - "boxes": [ - { - "box": { - "id": "obj-1", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 14.0, - 400.0, - 22.0 - ], - "text": "mutap.aec~", - "fontsize": 16.0 - } - }, - { - "box": { - "id": "obj-2", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 38.0, - 720.0, - 40.0 - ], - "text": "Acoustic echo canceller. Left inlet: the microphone (echo + you). Right inlet: the far-end reference \u2014 the SAME signal the patch sends to the speaker. Left outlet: the mic with the echo removed, delayed by @block samples. Right outlet: the IPC double-talk indicator (0..1)." - } - }, - { - "box": { - "id": "obj-3", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 40.0, - 100.0, - 52.0, - 22.0 - ], - "text": "noise~", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-4", - "maxclass": "gain~", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 130.0, - 30.0, - 100.0 - ], - "outlettype": [ - "signal", - "int" - ] - } - }, - { - "box": { - "id": "obj-5", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 90.0, - 240.0, - 70.0, - 22.0 - ], - "text": "tapin~ 100", - "outlettype": [ - "tapconnect" - ] - } - }, - { - "box": { - "id": "obj-6", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 90.0, - 270.0, - 72.0, - 22.0 - ], - "text": "tapout~ 42", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-7", - "maxclass": "newobj", - "numinlets": 3, - "numoutlets": 1, - "patching_rect": [ - 90.0, - 300.0, - 100.0, - 22.0 - ], - "text": "lores~ 2500 0.3", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-8", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 90.0, - 330.0, - 52.0, - 22.0 - ], - "text": "*~ 0.4", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-9", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 200.0, - 270.0, - 520.0, - 54.0 - ], - "text": "The simulated room: 42 ms delay + lowpass + attenuation stands in for the speaker-to-mic echo path, so this patcher demonstrates without an acoustic loop. In a real rig, delete this chain \u2014 the echo arrives through the air, and the mic signal goes straight to the left inlet. The reference MUST be tapped where the signal actually reaches the speaker (post-fader)." - } - }, - { - "box": { - "id": "obj-10", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 240.0, - 100.0, - 50.0, - 22.0 - ], - "text": "adc~", - "outlettype": [ - "signal", - "signal" - ] - } - }, - { - "box": { - "id": "obj-11", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 90.0, - 370.0, - 40.0, - 22.0 - ], - "text": "+~", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-12", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 90.0, - 410.0, - 160.0, - 22.0 - ], - "text": "mutap.aec~ 2048", - "outlettype": [ - "signal", - "" - ] - } - }, - { - "box": { - "id": "obj-13", - "maxclass": "gain~", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 90.0, - 450.0, - 30.0, - 100.0 - ], - "outlettype": [ - "signal", - "int" - ] - } - }, - { - "box": { - "id": "obj-14", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 0, - "patching_rect": [ - 90.0, - 570.0, - 50.0, - 22.0 - ], - "text": "dac~", - "outlettype": [] - } - }, - { - "box": { - "id": "obj-15", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 270.0, - 450.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-16", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 334.0, - 450.0, - 420.0, - 40.0 - ], - "text": "IPC 0..1 (right outlet): high = unmodeled echo dominates (adapting hard), low = you are talking over it (double-talk). Reports 0 with @kalman (its gating is internal)." - } - }, - { - "box": { - "id": "obj-17", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 96.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-18", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 120.0, - 70.0, - 22.0 - ], - "text": "adapt $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-19", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 626.0, - 120.0, - 280.0, - 20.0 - ], - "text": "freeze / resume adaptation (default on)" - } - }, - { - "box": { - "id": "obj-20", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 150.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-21", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 174.0, - 64.0, - 22.0 - ], - "text": "gate $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-22", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 620.0, - 174.0, - 380.0, - 20.0 - ], - "text": "classic engine: IPC step scaling + transient freeze (default on; rebuilds)" - } - }, - { - "box": { - "id": "obj-23", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 520.0, - 204.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-24", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 586.0, - 204.0, - 60.0, - 22.0 - ], - "text": "mu $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-25", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 652.0, - 204.0, - 300.0, - 20.0 - ], - "text": "NLMS step size, (0, 2), default 0.5 (applied live)" - } - }, - { - "box": { - "id": "obj-26", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 234.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-27", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 258.0, - 64.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "warp $1" - } - }, - { - "box": { - "id": "obj-28", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 620.0, - 258.0, - 400.0, - 20.0 - ], - "outlettype": [], - "text": "frequency-warped near-end model for music/tonal material in the room (default off; rebuilds)" - } - }, - { - "box": { - "id": "obj-29", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 288.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-30", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 312.0, - 80.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "kalman $1" - } - }, - { - "box": { - "id": "obj-31", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 636.0, - 312.0, - 400.0, - 20.0 - ], - "outlettype": [], - "text": "v2 Kalman engine \u2014 the measured double-talk winner (default off; rebuilds)" - } - }, - { - "box": { - "id": "obj-32", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 342.0, - 80.0, - 22.0 - ], - "text": "block 256", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-33", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 606.0, - 342.0, - 380.0, - 20.0 - ], - "text": "block size = adaptation hop = added latency (power of 2; rebuilds)" - } - }, - { - "box": { - "id": "obj-34", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 372.0, - 44.0, - 22.0 - ], - "text": "reset", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-35", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 570.0, - 372.0, - 300.0, - 20.0 - ], - "text": "zero the learned echo-path estimate" - } - }, - { - "box": { - "id": "obj-36", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 600.0, - 760.0, - 95.0 - ], - "text": "Try it: start the audio, raise the far-end fader \u2014 the noise 'echo' appears in the mic and the canceller learns it away within a second or two. Now TALK: that is double-talk, the hard part of echo cancellation. A naive filter would chase your voice and wreck its echo estimate; this one keeps adapting through it \u2014 the PEM near-end model whitens your voice out of the update, and @kalman 1 tracks it per frequency bin (the measured best, no detector, no tuning; see MuTap tests/test_aec.cpp). For the last 20-30 dB, @postfilter 1 engages the full ITU-certified chain: the residual the linear filter cannot reach is suppressed by coherence and replaced with comfort noise matched to the room, so the far end hears neither echo nor a breathing noise floor. @postfilter 2 swaps that suppressor for a small trained network (stronger single-talk echo removal on speech at equal transparency; the classical engine stays the certified default) \u2014 @model loads your own trained weights, and @block follows the model's trained block size. Creation arg = echo-path length in samples (default 2048); partitions = filter length / block." - } - }, - { - "box": { - "id": "obj-37", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 402.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-38", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 426.0, - 96.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "postfilter $1" - } - }, - { - "box": { - "id": "obj-39", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 656.0, - 426.0, - 380.0, - 33.0 - ], - "text": "post-filter engine: 0 off, 1 the ITU-certified chain, 2 the learned (neural) chain (rebuilds; +1 block latency)" - } - }, - { - "box": { - "id": "obj-40", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 520.0, - 500.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-41", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 524.0, - 80.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "comfort $1" - } - }, - { - "box": { - "id": "obj-42", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 640.0, - 524.0, - 376.0, - 20.0 - ], - "text": "comfort-noise fill at the room's noise floor (postfilter only; default on)" - } - }, - { - "box": { - "id": "obj-90", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "outlettype": [ - "" - ], - "patching_rect": [ - 550.0, - 450.0, - 82.0, - 22.0 - ], - "text": "postfilter 2" - } - } - ], - "lines": [ - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-3", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-5", - 0 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 1 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-14", - 1 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-6", - 0 - ], - "source": [ - "obj-5", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-7", - 0 - ], - "source": [ - "obj-6", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-8", - 0 - ], - "source": [ - "obj-7", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-11", - 0 - ], - "source": [ - "obj-8", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-11", - 1 - ], - "source": [ - "obj-10", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-11", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-13", - 0 - ], - "source": [ - "obj-12", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-14", - 0 - ], - "source": [ - "obj-13", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-15", - 0 - ], - "source": [ - "obj-12", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-18", - 0 - ], - "source": [ - "obj-17", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-18", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-21", - 0 - ], - "source": [ - "obj-20", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-21", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-24", - 0 - ], - "source": [ - "obj-23", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-24", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-27", - 0 - ], - "source": [ - "obj-26", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-27", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-30", - 0 - ], - "source": [ - "obj-29", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-30", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-32", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-34", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-38", - 0 - ], - "source": [ - "obj-37", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-38", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-41", - 0 - ], - "source": [ - "obj-40", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-41", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-12", - 0 - ], - "source": [ - "obj-90", - 0 - ] - } - } - ] - } + "patcher": { + "fileversion": 1, + "appversion": { + "major": 8, + "minor": 0, + "revision": 0, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 60.0, + 80.0, + 1020.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 12.0, + "default_fontface": 0, + "default_fontname": "Arial", + "gridonopen": 1, + "gridsize": [ + 15.0, + 15.0 + ], + "gridsnaponopen": 1, + "objectsnaponopen": 1, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxes": [ + { + "box": { + "id": "obj-1", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 14.0, + 400.0, + 22.0 + ], + "text": "mutap.aec~", + "fontsize": 16.0 + } + }, + { + "box": { + "id": "obj-2", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 38.0, + 720.0, + 40.0 + ], + "text": "Acoustic echo canceller. Left inlet: the microphone (echo + you). Right inlet: the far-end reference — the SAME signal the patch sends to the speaker. Left outlet: the mic with the echo removed, delayed by @block samples. Right outlet: the IPC double-talk indicator (0..1)." + } + }, + { + "box": { + "id": "obj-3", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 40.0, + 100.0, + 52.0, + 22.0 + ], + "text": "noise~", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-4", + "maxclass": "gain~", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 130.0, + 30.0, + 100.0 + ], + "outlettype": [ + "signal", + "int" + ] + } + }, + { + "box": { + "id": "obj-5", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 90.0, + 240.0, + 70.0, + 22.0 + ], + "text": "tapin~ 100", + "outlettype": [ + "tapconnect" + ] + } + }, + { + "box": { + "id": "obj-6", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 90.0, + 270.0, + 72.0, + 22.0 + ], + "text": "tapout~ 42", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-7", + "maxclass": "newobj", + "numinlets": 3, + "numoutlets": 1, + "patching_rect": [ + 90.0, + 300.0, + 100.0, + 22.0 + ], + "text": "lores~ 2500 0.3", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-8", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 90.0, + 330.0, + 52.0, + 22.0 + ], + "text": "*~ 0.4", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-9", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 200.0, + 270.0, + 520.0, + 54.0 + ], + "text": "The simulated room: 42 ms delay + lowpass + attenuation stands in for the speaker-to-mic echo path, so this patcher demonstrates without an acoustic loop. In a real rig, delete this chain — the echo arrives through the air, and the mic signal goes straight to the left inlet. The reference MUST be tapped where the signal actually reaches the speaker (post-fader)." + } + }, + { + "box": { + "id": "obj-10", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 240.0, + 100.0, + 50.0, + 22.0 + ], + "text": "adc~", + "outlettype": [ + "signal", + "signal" + ] + } + }, + { + "box": { + "id": "obj-11", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 90.0, + 370.0, + 40.0, + 22.0 + ], + "text": "+~", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-12", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 90.0, + 410.0, + 160.0, + 22.0 + ], + "text": "mutap.aec~ 2048", + "outlettype": [ + "signal", + "" + ] + } + }, + { + "box": { + "id": "obj-13", + "maxclass": "gain~", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 90.0, + 450.0, + 30.0, + 100.0 + ], + "outlettype": [ + "signal", + "int" + ] + } + }, + { + "box": { + "id": "obj-14", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 0, + "patching_rect": [ + 90.0, + 570.0, + 50.0, + 22.0 + ], + "text": "dac~", + "outlettype": [] + } + }, + { + "box": { + "id": "obj-15", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 270.0, + 450.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-16", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 334.0, + 450.0, + 420.0, + 40.0 + ], + "text": "IPC 0..1 (right outlet): high = unmodeled echo dominates (adapting hard), low = you are talking over it (double-talk). Reports 0 with @kalman (its gating is internal)." + } + }, + { + "box": { + "id": "obj-17", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 96.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-18", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 120.0, + 70.0, + 22.0 + ], + "text": "adapt $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-19", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 626.0, + 120.0, + 280.0, + 20.0 + ], + "text": "freeze / resume adaptation (default on)" + } + }, + { + "box": { + "id": "obj-20", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 150.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-21", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 174.0, + 64.0, + 22.0 + ], + "text": "gate $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-22", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 620.0, + 174.0, + 380.0, + 20.0 + ], + "text": "classic engine: IPC step scaling + transient freeze (default on; rebuilds)" + } + }, + { + "box": { + "id": "obj-23", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 520.0, + 204.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-24", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 586.0, + 204.0, + 60.0, + 22.0 + ], + "text": "mu $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-25", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 652.0, + 204.0, + 300.0, + 20.0 + ], + "text": "NLMS step size, (0, 2), default 0.5 (applied live)" + } + }, + { + "box": { + "id": "obj-26", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 234.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-27", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 258.0, + 64.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "warp $1" + } + }, + { + "box": { + "id": "obj-28", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 620.0, + 258.0, + 400.0, + 20.0 + ], + "outlettype": [], + "text": "frequency-warped near-end model for music/tonal material in the room (default off; rebuilds)" + } + }, + { + "box": { + "id": "obj-29", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 288.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-30", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 312.0, + 80.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "kalman $1" + } + }, + { + "box": { + "id": "obj-31", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 636.0, + 312.0, + 400.0, + 20.0 + ], + "outlettype": [], + "text": "v2 Kalman engine — the measured double-talk winner (default off; rebuilds)" + } + }, + { + "box": { + "id": "obj-32", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 342.0, + 80.0, + 22.0 + ], + "text": "block 256", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-33", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 606.0, + 342.0, + 380.0, + 20.0 + ], + "text": "block size = adaptation hop = added latency (power of 2; rebuilds)" + } + }, + { + "box": { + "id": "obj-34", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 372.0, + 44.0, + 22.0 + ], + "text": "reset", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-35", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 570.0, + 372.0, + 300.0, + 20.0 + ], + "text": "zero the learned echo-path estimate" + } + }, + { + "box": { + "id": "obj-36", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 600.0, + 760.0, + 95.0 + ], + "text": "Try it: start the audio, raise the far-end fader — the noise 'echo' appears in the mic and the canceller learns it away within a second or two. Now TALK: that is double-talk, the hard part of echo cancellation. A naive filter would chase your voice and wreck its echo estimate; this one keeps adapting through it — the PEM near-end model whitens your voice out of the update, and @kalman 1 tracks it per frequency bin (the measured best, no detector, no tuning; see MuTap tests/test_aec.cpp). For the last 20-30 dB, @postfilter 1 engages the full ITU-certified chain: the residual the linear filter cannot reach is suppressed by coherence and replaced with comfort noise matched to the room, so the far end hears neither echo nor a breathing noise floor. @postfilter 2 swaps that suppressor for a small trained network (stronger single-talk echo removal on speech at equal transparency; the classical engine stays the certified default) — @model loads your own trained weights, and @block follows the model's trained block size. Creation arg = echo-path length in samples (default 2048); partitions = filter length / block." + } + }, + { + "box": { + "id": "obj-37", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 402.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-38", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 426.0, + 96.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "postfilter $1" + } + }, + { + "box": { + "id": "obj-39", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 656.0, + 426.0, + 380.0, + 33.0 + ], + "text": "post-filter engine: 0 off, 1 the ITU-certified chain, 2 the learned (neural) chain (rebuilds; +1 block latency)" + } + }, + { + "box": { + "id": "obj-40", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 520.0, + 500.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-41", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 524.0, + 80.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "comfort $1" + } + }, + { + "box": { + "id": "obj-42", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 640.0, + 524.0, + 376.0, + 20.0 + ], + "text": "comfort-noise fill at the room's noise floor (postfilter only; default on)" + } + }, + { + "box": { + "id": "obj-90", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 550.0, + 450.0, + 82.0, + 22.0 + ], + "text": "postfilter 2" + } + }, + { + "box": { + "id": "obj-91", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 334.0, + 698.0, + 300, + 60 + ], + "text": "Also on the right outlet: \"watchdog n\" whenever a NaN or infinity reached the canceller (it resets, outputs one block of silence, and counts since the last rebuild)." + } + } + ], + "lines": [ + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-3", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-5", + 0 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 1 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-14", + 1 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-6", + 0 + ], + "source": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-7", + 0 + ], + "source": [ + "obj-6", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-8", + 0 + ], + "source": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-11", + 0 + ], + "source": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-11", + 1 + ], + "source": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-13", + 0 + ], + "source": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-14", + 0 + ], + "source": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-15", + 0 + ], + "source": [ + "obj-12", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-18", + 0 + ], + "source": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-21", + 0 + ], + "source": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-21", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-24", + 0 + ], + "source": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-24", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-27", + 0 + ], + "source": [ + "obj-26", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-27", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-30", + 0 + ], + "source": [ + "obj-29", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-32", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-34", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-38", + 0 + ], + "source": [ + "obj-37", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-38", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-41", + 0 + ], + "source": [ + "obj-40", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-41", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-12", + 0 + ], + "source": [ + "obj-90", + 0 + ] + } + } + ] + } } \ No newline at end of file diff --git a/help/mutap.afc~.maxhelp b/help/mutap.afc~.maxhelp index fc1235f..7dd509f 100644 --- a/help/mutap.afc~.maxhelp +++ b/help/mutap.afc~.maxhelp @@ -1,2834 +1,2864 @@ { - "patcher": { - "fileversion": 1, - "appversion": { - "major": 8, - "minor": 0, - "revision": 0, - "architecture": "x64", - "modernui": 1 - }, - "classnamespace": "box", - "rect": [ - 60.0, - 80.0, - 980.0, - 820.0 - ], - "bglocked": 0, - "openinpresentation": 0, - "default_fontsize": 12.0, - "default_fontface": 0, - "default_fontname": "Arial", - "gridonopen": 1, - "gridsize": [ - 15.0, - 15.0 - ], - "gridsnaponopen": 1, - "objectsnaponopen": 1, - "statusbarvisible": 2, - "toolbarvisible": 1, - "boxes": [ - { - "box": { - "id": "obj-1", - "maxclass": "newobj", - "numinlets": 0, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 20.0, - 50.0, - 22.0 - ], - "text": "p basic", - "patcher": { - "fileversion": 1, - "appversion": { - "major": 8, - "minor": 0, - "revision": 0, - "architecture": "x64", - "modernui": 1 - }, - "classnamespace": "box", - "rect": [ - 0.0, - 26.0, - 980.0, - 820.0 - ], - "bglocked": 0, - "openinpresentation": 0, - "default_fontsize": 12.0, - "default_fontface": 0, - "default_fontname": "Arial", - "gridonopen": 1, - "gridsize": [ - 15.0, - 15.0 - ], - "gridsnaponopen": 1, - "objectsnaponopen": 1, - "statusbarvisible": 2, - "toolbarvisible": 1, - "boxes": [ - { - "box": { - "id": "obj-1", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 14.0, - 400.0, - 22.0 - ], - "text": "mutap.afc~", - "fontsize": 16.0 - } - }, - { - "box": { - "id": "obj-2", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 38.0, - 700.0, - 54.0 - ], - "text": "Acoustic feedback (howling) canceller. Left inlet: the microphone. Right inlet: the SAME signal the patch sends to the speaker (the reference). Outlets, left to right: the cleaned mic, delayed by @block samples; the guard's post-reverb gain (a signal, 1 while @guard is off; see the guard tab); the IPC double-talk indicator (0..1); the convergence statistics, uncertainty_db shadow_ratio_db." - } - }, - { - "box": { - "id": "obj-3", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 120.0, - 50.0, - 22.0 - ], - "text": "adc~", - "outlettype": [ - "signal", - "signal" - ] - } - }, - { - "box": { - "id": "obj-4", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 4, - "patching_rect": [ - 40.0, - 200.0, - 160.0, - 22.0 - ], - "text": "mutap.afc~ 2048", - "outlettype": [ - "signal", - "signal", - "", - "" - ] - } - }, - { - "box": { - "id": "obj-5", - "maxclass": "gain~", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 260.0, - 30.0, - 120.0 - ], - "outlettype": [ - "signal", - "int" - ] - } - }, - { - "box": { - "id": "obj-6", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 0, - "patching_rect": [ - 40.0, - 420.0, - 50.0, - 22.0 - ], - "text": "dac~", - "outlettype": [] - } - }, - { - "box": { - "id": "obj-7", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 90.0, - 262.0, - 560.0, - 40.0 - ], - "text": "The closed loop: mic -> afc~ -> gain -> speaker. Raise the gain slider toward howling onset; the canceller buys added stable gain. The gain~ output is tapped back into afc~'s right inlet \u2014 the reference MUST be the signal that actually reaches the speaker." - } - }, - { - "box": { - "id": "obj-8", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 180.0, - 240.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-9", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 244.0, - 240.0, - 420.0, - 22.0 - ], - "text": "IPC 0..1 (third outlet): high = feedback dominates (adapting hard), low = you are talking (double-talk, updates gated). Meter it to watch the M4 robustness layer work." - } - }, - { - "box": { - "id": "obj-10", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 96.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-11", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 510.0, - 120.0, - 70.0, - 22.0 - ], - "text": "adapt $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-12", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 586.0, - 120.0, - 260.0, - 20.0 - ], - "text": "freeze / resume adaptation (default on)" - } - }, - { - "box": { - "id": "obj-13", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 150.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-14", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 510.0, - 174.0, - 64.0, - 22.0 - ], - "text": "gate $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-15", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 580.0, - 174.0, - 340.0, - 20.0 - ], - "text": "IPC step scaling + transient freeze (default on; rebuilds)" - } - }, - { - "box": { - "id": "obj-16", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 480.0, - 204.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-17", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 546.0, - 204.0, - 60.0, - 22.0 - ], - "text": "mu $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-18", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 612.0, - 204.0, - 300.0, - 20.0 - ], - "text": "NLMS step size, (0, 2), default 0.5 (applied live)" - } - }, - { - "box": { - "id": "obj-19", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 360.0, - 44.0, - 22.0 - ], - "text": "reset", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-20", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 530.0, - 360.0, - 300.0, - 20.0 - ], - "text": "zero the learned feedback-path estimate" - } - }, - { - "box": { - "id": "obj-21", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 330.0, - 80.0, - 22.0 - ], - "text": "block 256", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-22", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 566.0, - 330.0, - 380.0, - 20.0 - ], - "text": "block size = adaptation hop = added latency (power of 2; rebuilds)" - } - }, - { - "box": { - "id": "obj-23", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 460.0, - 700.0, - 54.0 - ], - "text": "Why PEM: in a closed loop the speaker signal is correlated with your voice, so a naive adaptive filter cancels program material, not feedback. mutap.afc~ re-fits a near-end model (LPC + pitch) every block, prewhitens both signals with it, and adapts on the whitened pair while cancelling on the raw ones (FDAF-PEM-AFROW). Creation arg = filter length in samples (default 2048); partitions = filter length / block." - } - }, - { - "box": { - "id": "obj-24", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 234.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-25", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 510.0, - 258.0, - 64.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "warp $1" - } - }, - { - "box": { - "id": "obj-26", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 580.0, - 258.0, - 400.0, - 20.0 - ], - "outlettype": [], - "text": "frequency-warped near-end model for music/tonal sources (default off; rebuilds, keeps IPC scaling on)" - } - }, - { - "box": { - "id": "obj-27", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 264.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ], - "parameter_enable": 0 - } - }, - { - "box": { - "id": "obj-28", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 510.0, - 288.0, - 80.0, - 22.0 - ], - "outlettype": [ - "" - ], - "text": "kalman $1" - } - }, - { - "box": { - "id": "obj-29", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 596.0, - 288.0, - 400.0, - 20.0 - ], - "outlettype": [], - "text": "v2 Kalman engine: mu ignored, gate = burst floor (default off; rebuilds)" - } - }, - { - "box": { - "id": "obj-30", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 180.0, - 314.0, - 150.0, - 22.0 - ], - "text": "unpack 0. 0.", - "outlettype": [ - "float", - "float" - ] - } - }, - { - "box": { - "id": "obj-31", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 180.0, - 344.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-32", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 310.0, - 344.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-33", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 100.0, - 370.0, - 150.0, - 34.0 - ], - "text": "uncertainty dB (Kalman core; 0 dB with NLMS)" - } - }, - { - "box": { - "id": "obj-34", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 260.0, - 370.0, - 210.0, - 34.0 - ], - "text": "shadow ratio dB (0 dB with @shadow 0)" - } - }, - { - "box": { - "id": "obj-35", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 100.0, - 404.0, - 370.0, - 50.0 - ], - "text": "Right outlet, every 8 blocks: raw statistics, no thresholds. Uncertainty: sum P / sum P at reset (0 dB = nothing identified; falls as the path is learned). Shadow ratio: main / shadow residual power; rises toward 0 dB and past it when the path moves." - } - }, - { - "box": { - "id": "obj-36", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 480.0, - 392.0, - 64.0, - 22.0 - ], - "text": "shadow 2", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-37", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 550.0, - 392.0, - 64.0, - 22.0 - ], - "text": "shadow 0", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-38", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 620.0, - 392.0, - 350.0, - 48.0 - ], - "text": "shadow comparator partitions (default 2, 0 = off; clamped to the partition count at each build; rebuilds). Measured with the Kalman engine only." - } - }, - { - "box": { - "id": "obj-39", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 530.0, - 700.0, - 22.0 - ], - "text": "Example convergence policy (patch-side: without @guard, mutap.afc~ applies no thresholds; the guard tab shows the built-in safety layer)", - "fontface": 1 - } - }, - { - "box": { - "id": "obj-40", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 40.0, - 560.0, - 230.0, - 22.0 - ], - "text": "expr ($f1 < -23.8) && ($f2 < -1.24)", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-41", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 3, - "patching_rect": [ - 40.0, - 590.0, - 50.0, - 22.0 - ], - "text": "change", - "outlettype": [ - "", - "int", - "int" - ] - } - }, - { - "box": { - "id": "obj-42", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 620.0, - 50.0, - 22.0 - ], - "text": "t b i", - "outlettype": [ - "bang", - "int" - ] - } - }, - { - "box": { - "id": "obj-43", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 40.0, - 650.0, - 70.0, - 22.0 - ], - "text": "delay 300", - "outlettype": [ - "bang" - ] - } - }, - { - "box": { - "id": "obj-44", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 40.0, - 680.0, - 40.0, - 22.0 - ], - "text": "i", - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-45", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 3, - "patching_rect": [ - 40.0, - 710.0, - 50.0, - 22.0 - ], - "text": "change", - "outlettype": [ - "", - "int", - "int" - ] - } - }, - { - "box": { - "id": "obj-46", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 40.0, - 740.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-47", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 70.0, - 742.0, - 220.0, - 20.0 - ], - "text": "converged (each state held 0.3 s)" - } - }, - { - "box": { - "id": "obj-48", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 300.0, - 560.0, - 660.0, - 180.0 - ], - "text": "\"Converged\" when uncertainty < -23.8 dB AND shadow ratio < -1.24 dB; a state flips only after the condition has held 0.3 s on the other side (delay 300 restarts on every change). Needs @kalman 1: with the NLMS core the uncertainty reads 0 dB and this never says converged.\n\nThese thresholds were calibrated at @block 64 / 48 kHz on 1024- and 2048-tap filters with @kalman 1 and @shadow 2 (medians over 50 cold starts in MuTap's convergence-indicator experiment). They are a calibration for that loop, not constants: recalibrate for any other block size, rate, filter length or room.\n\nMeasured there: it detects a walk to a different room at the change, but reports reconvergence 0.13-0.26 s early (ahead of the filter's measured misalignment). The uncertainty statistic regrows in digital silence, so a silent input drifts back to not converged." - } - } - ], - "lines": [ - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-3", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-5", - 0 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-6", - 0 - ], - "source": [ - "obj-5", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-6", - 1 - ], - "source": [ - "obj-5", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 1 - ], - "source": [ - "obj-5", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-8", - 0 - ], - "source": [ - "obj-4", - 2 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-11", - 0 - ], - "source": [ - "obj-10", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-11", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-14", - 0 - ], - "source": [ - "obj-13", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-14", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-17", - 0 - ], - "source": [ - "obj-16", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-17", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-19", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-21", - 0 - ] - } - }, - { - "patchline": { - "source": [ - "obj-24", - 0 - ], - "destination": [ - "obj-25", - 0 - ] - } - }, - { - "patchline": { - "source": [ - "obj-25", - 0 - ], - "destination": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "source": [ - "obj-27", - 0 - ], - "destination": [ - "obj-28", - 0 - ] - } - }, - { - "patchline": { - "source": [ - "obj-28", - 0 - ], - "destination": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-30", - 0 - ], - "source": [ - "obj-4", - 3 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-31", - 0 - ], - "source": [ - "obj-30", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-32", - 0 - ], - "source": [ - "obj-30", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-36", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-37", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-40", - 0 - ], - "source": [ - "obj-30", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-40", - 1 - ], - "source": [ - "obj-30", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-41", - 0 - ], - "source": [ - "obj-40", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-42", - 0 - ], - "source": [ - "obj-41", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-44", - 1 - ], - "source": [ - "obj-42", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-43", - 0 - ], - "source": [ - "obj-42", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-44", - 0 - ], - "source": [ - "obj-43", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-45", - 0 - ], - "source": [ - "obj-44", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-46", - 0 - ], - "source": [ - "obj-45", - 0 - ] - } - } - ], - "showontab": 1 - } - } - }, - { - "box": { - "id": "obj-2", - "maxclass": "newobj", - "numinlets": 0, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 50.0, - 50.0, - 22.0 - ], - "text": "p guard", - "patcher": { - "fileversion": 1, - "appversion": { - "major": 8, - "minor": 0, - "revision": 0, - "architecture": "x64", - "modernui": 1 - }, - "classnamespace": "box", - "rect": [ - 0.0, - 26.0, - 980.0, - 820.0 - ], - "bglocked": 0, - "openinpresentation": 0, - "default_fontsize": 12.0, - "default_fontface": 0, - "default_fontname": "Arial", - "gridonopen": 1, - "gridsize": [ - 15.0, - 15.0 - ], - "gridsnaponopen": 1, - "objectsnaponopen": 1, - "statusbarvisible": 2, - "toolbarvisible": 1, - "showontab": 1, - "boxes": [ - { - "box": { - "id": "obj-1", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 14.0, - 600.0, - 22.0 - ], - "text": "mutap.afc~ - the safety layer (@guard)", - "fontsize": 16.0 - } - }, - { - "box": { - "id": "obj-2", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 38.0, - 930.0, - 54.0 - ], - "text": "@guard 1 runs MuTap's howl_guard on this mic: the cleaned output sits @arming dB down (ARMING) until the canceller's verdict (shadow_ratio_db < @d_db AND uncertainty_db < @a_db) has held @hold s with the howl detector quiet, then opens; a howl trip ducks it @duck dB. It needs @kalman 1 and @shadow > 0. One mutap.afc~ is one mic: two objects duck independently (MuTap's afc_chain attributes a shared howl to a mic; this object cannot)." - } - }, - { - "box": { - "id": "obj-3", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 110.0, - 50.0, - 22.0 - ], - "text": "adc~", - "outlettype": [ - "signal", - "signal" - ] - } - }, - { - "box": { - "id": "obj-4", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 4, - "patching_rect": [ - 40.0, - 200.0, - 280.0, - 22.0 - ], - "text": "mutap.afc~ 2048 @kalman 1 @loop_ms 35 @guard 1", - "outlettype": [ - "signal", - "signal", - "", - "" - ] - } - }, - { - "box": { - "id": "obj-5", - "maxclass": "newobj", - "numinlets": 5, - "numoutlets": 1, - "patching_rect": [ - 110.0, - 250.0, - 150.0, - 22.0 - ], - "text": "comb~ 100 37.1 0. 1. 0.75", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-6", - "maxclass": "newobj", - "numinlets": 5, - "numoutlets": 1, - "patching_rect": [ - 110.0, - 280.0, - 150.0, - 22.0 - ], - "text": "comb~ 100 41.3 0. 1. 0.72", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-7", - "maxclass": "newobj", - "numinlets": 3, - "numoutlets": 1, - "patching_rect": [ - 110.0, - 310.0, - 120.0, - 22.0 - ], - "text": "allpass~ 50 5. 0.7", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-8", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 110.0, - 340.0, - 40.0, - 22.0 - ], - "text": "*~", - "outlettype": [ - "signal" - ] - } - }, - { - "box": { - "id": "obj-9", - "maxclass": "gain~", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 40.0, - 380.0, - 30.0, - 120.0 - ], - "outlettype": [ - "signal", - "int" - ] - } - }, - { - "box": { - "id": "obj-10", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 0, - "patching_rect": [ - 40.0, - 520.0, - 50.0, - 22.0 - ], - "text": "dac~", - "outlettype": [] - } - }, - { - "box": { - "id": "obj-11", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 270.0, - 250.0, - 300.0, - 84.0 - ], - "text": "A stand-in reverb (two comb~ into allpass~): put yours here. The guard's gain is already on the cleaned output, so a duck cuts what enters the reverb; the second outlet repeats the duck after it (*~), so a charged tail is cut too - MuTap's bus stage." - } - }, - { - "box": { - "id": "obj-12", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 160.0, - 341.0, - 400.0, - 34.0 - ], - "text": "Second outlet: 1 except while ducked or releasing, where it is the duck relative to the restore level (ARMING and strike levels are not applied twice)." - } - }, - { - "box": { - "id": "obj-13", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 600.0, - 380.0, - 50.0, - 22.0 - ], - "text": "open", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-14", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 660.0, - 380.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-15", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 600.0, - 420.0, - 70.0, - 22.0 - ], - "text": "sfplay~", - "outlettype": [ - "signal", - "bang" - ] - } - }, - { - "box": { - "id": "obj-16", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 690.0, - 410.0, - 280.0, - 96.0 - ], - "text": "Backing track: summed into the speaker feed after the guard, and into the reference. While ARMING holds the voice down it is the canceller's excitation: in MuTap's simulated cold starts every run declared with it, none without it in 20 s (docs/howl-guard.md)." - } - }, - { - "box": { - "id": "obj-17", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 600.0, - 100.0, - 360.0, - 20.0 - ], - "text": "1. Soundcheck (needs the guard running)", - "fontface": 1 - } - }, - { - "box": { - "id": "obj-18", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 600.0, - 124.0, - 64.0, - 22.0 - ], - "text": "calibrate", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-19", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 670.0, - 124.0, - 80.0, - 22.0 - ], - "text": "calibrate 0", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-20", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 600.0, - 148.0, - 360.0, - 62.0 - ], - "text": "Play the song through the loop for @cal_s (30 s): the guard then runs on the medians of the two statistics plus @cal_margin_d / @cal_margin_a (4 / 3 dB) and sends calibrate_done d_db a_db. 'calibrate 0' ends it early." - } - }, - { - "box": { - "id": "obj-21", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 600.0, - 214.0, - 360.0, - 20.0 - ], - "text": "2. The deployment cap", - "fontface": 1 - } - }, - { - "box": { - "id": "obj-22", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 600.0, - 238.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ], - "maximum": 0.0 - } - }, - { - "box": { - "id": "obj-23", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 666.0, - 238.0, - 60.0, - 22.0 - ], - "text": "cap $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-24", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 732.0, - 238.0, - 64.0, - 22.0 - ], - "text": "cap none", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-25", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 600.0, - 262.0, - 360.0, - 76.0 - ], - "text": "dB re your operating gain, at most 0 (MuTap's protocol: the dry limit - 6 dB). After 10 s in ARMING without a verdict the guard says unprotected; with a cap it opens to it (open_capped), with none it stays in ARMING, @arming dB down." - } - }, - { - "box": { - "id": "obj-26", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 600.0, - 342.0, - 40.0, - 22.0 - ], - "text": "clear", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-27", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 644.0, - 342.0, - 330.0, - 20.0 - ], - "text": "strikes 0, restore level 0 dB, latch released" - } - }, - { - "box": { - "id": "obj-28", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 820.0, - 100.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-29", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 850.0, - 100.0, - 60.0, - 22.0 - ], - "text": "guard $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-30", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 850.0, - 124.0, - 44.0, - 22.0 - ], - "text": "reset", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-31", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 900.0, - 124.0, - 70.0, - 34.0 - ], - "text": "back to ARMING" - } - }, - { - "box": { - "id": "obj-32", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 300.0, - 560.0, - 400.0, - 20.0 - ], - "text": "3. The guard, every 8 blocks (right outlet)", - "fontface": 1 - } - }, - { - "box": { - "id": "obj-33", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 2, - "patching_rect": [ - 300.0, - 584.0, - 140.0, - 22.0 - ], - "text": "route calibrate_done", - "outlettype": [ - "", - "" - ] - } - }, - { - "box": { - "id": "obj-34", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 300.0, - 614.0, - 70.0, - 22.0 - ], - "text": "unpack 0. 0.", - "outlettype": [ - "float", - "float" - ] - } - }, - { - "box": { - "id": "obj-35", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 300.0, - 644.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-36", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 366.0, - 644.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-37", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 300.0, - 668.0, - 200.0, - 48.0 - ], - "text": "calibrate_done d_db a_db: store them; set @d_db / @a_db to restore the soundcheck after a rebuild." - } - }, - { - "box": { - "id": "obj-38", - "maxclass": "newobj", - "numinlets": 1, - "numoutlets": 6, - "patching_rect": [ - 520.0, - 614.0, - 200.0, - 22.0 - ], - "text": "unpack 0. 0. s 0. 0 0", - "outlettype": [ - "float", - "float", - "", - "float", - "int", - "int" - ] - } - }, - { - "box": { - "id": "obj-39", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 520.0, - 680.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-40", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 586.0, - 680.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-41", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 652.0, - 644.0, - 70.0, - 22.0 - ], - "text": "prepend set", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-42", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 652.0, - 680.0, - 90.0, - 22.0 - ], - "text": "", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-43", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 748.0, - 680.0, - 60.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-44", - "maxclass": "toggle", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 814.0, - 680.0, - 24.0, - 24.0 - ], - "outlettype": [ - "int" - ] - } - }, - { - "box": { - "id": "obj-45", - "maxclass": "number", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 844.0, - 680.0, - 40.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ] - } - }, - { - "box": { - "id": "obj-46", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 520.0, - 706.0, - 460.0, - 20.0 - ], - "text": "uncertainty_db shadow_ratio_db guard_state gain_db unprotected strikes" - } - }, - { - "box": { - "id": "obj-47", - "maxclass": "newobj", - "numinlets": 2, - "numoutlets": 3, - "patching_rect": [ - 652.0, - 740.0, - 150.0, - 22.0 - ], - "text": "sel ducked latched", - "outlettype": [ - "bang", - "bang", - "" - ] - } - }, - { - "box": { - "id": "obj-48", - "maxclass": "button", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 652.0, - 770.0, - 24.0, - 24.0 - ], - "outlettype": [ - "bang" - ] - } - }, - { - "box": { - "id": "obj-49", - "maxclass": "button", - "numinlets": 1, - "numoutlets": 1, - "patching_rect": [ - 700.0, - 770.0, - 24.0, - 24.0 - ], - "outlettype": [ - "bang" - ] - } - }, - { - "box": { - "id": "obj-50", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 730.0, - 770.0, - 250.0, - 34.0 - ], - "text": "route on the state symbol: ducked, latched" - } - }, - { - "box": { - "id": "obj-51", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 20.0, - 560.0, - 270.0, - 230.0 - ], - "text": "guard_state: arming (held @arming dB down until the verdict declares), open (0 dB, less 3 dB per strike), open_capped (no verdict yet: at the cap, unprotected), ducked (@duck dB under the restore level, on a howl trip or a lost verdict), releasing (ramping back up), latched (three strikes: at the floor, unprotected). gain_db is the gain at the end of the last block; a duck releases on the verdict or after @rearm s x 2^strikes. reset returns to arming; so does every rebuild." - } - }, - { - "box": { - "id": "obj-52", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 300.0, - 100.0, - 290.0, - 20.0 - ], - "text": "0. The detector's calibrations", - "fontface": 1 - } - }, - { - "box": { - "id": "obj-53", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 300.0, - 124.0, - 70.0, - 22.0 - ], - "text": "loop_ms 35", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-54", - "maxclass": "flonum", - "numinlets": 1, - "numoutlets": 2, - "patching_rect": [ - 380.0, - 124.0, - 50.0, - 22.0 - ], - "outlettype": [ - "", - "bang" - ], - "maximum": 0.0 - } - }, - { - "box": { - "id": "obj-55", - "maxclass": "message", - "numinlets": 2, - "numoutlets": 1, - "patching_rect": [ - 436.0, - 124.0, - 70.0, - 22.0 - ], - "text": "ceiling $1", - "outlettype": [ - "" - ] - } - }, - { - "box": { - "id": "obj-56", - "maxclass": "comment", - "numinlets": 1, - "numoutlets": 0, - "patching_rect": [ - 300.0, - 148.0, - 290.0, - 48.0 - ], - "text": "loop_ms = one trip round YOUR loop: @block + Max's I/O vector in and out + converters + flight (48 kHz, @block 256, I/O 512, 3 m: about 35 ms). ceiling: dB re 1.0 on the residual, set with the cap. Each rebuilds the canceller: set them before guard 1." - } - } - ], - "lines": [ - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-3", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-9", - 0 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-5", - 0 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-6", - 0 - ], - "source": [ - "obj-4", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-7", - 0 - ], - "source": [ - "obj-5", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-7", - 0 - ], - "source": [ - "obj-6", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-8", - 0 - ], - "source": [ - "obj-7", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-8", - 1 - ], - "source": [ - "obj-4", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-9", - 0 - ], - "source": [ - "obj-8", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-10", - 0 - ], - "source": [ - "obj-9", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-10", - 1 - ], - "source": [ - "obj-9", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 1 - ], - "source": [ - "obj-9", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-15", - 0 - ], - "source": [ - "obj-13", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-15", - 0 - ], - "source": [ - "obj-14", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-10", - 0 - ], - "source": [ - "obj-15", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-10", - 1 - ], - "source": [ - "obj-15", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 1 - ], - "source": [ - "obj-15", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-18", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-19", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-23", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-24", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-26", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-29", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-30", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-23", - 0 - ], - "source": [ - "obj-22", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-29", - 0 - ], - "source": [ - "obj-28", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-33", - 0 - ], - "source": [ - "obj-4", - 3 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-34", - 0 - ], - "source": [ - "obj-33", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-35", - 0 - ], - "source": [ - "obj-34", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-36", - 0 - ], - "source": [ - "obj-34", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-38", - 0 - ], - "source": [ - "obj-33", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-39", - 0 - ], - "source": [ - "obj-38", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-40", - 0 - ], - "source": [ - "obj-38", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-41", - 0 - ], - "source": [ - "obj-38", - 2 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-42", - 0 - ], - "source": [ - "obj-41", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-43", - 0 - ], - "source": [ - "obj-38", - 3 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-44", - 0 - ], - "source": [ - "obj-38", - 4 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-45", - 0 - ], - "source": [ - "obj-38", - 5 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-47", - 0 - ], - "source": [ - "obj-38", - 2 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-48", - 0 - ], - "source": [ - "obj-47", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-49", - 0 - ], - "source": [ - "obj-47", - 1 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-53", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-4", - 0 - ], - "source": [ - "obj-55", - 0 - ] - } - }, - { - "patchline": { - "destination": [ - "obj-55", - 0 - ], - "source": [ - "obj-54", - 0 - ] - } - } - ] - } - } - } - ], - "lines": [] - } + "patcher": { + "fileversion": 1, + "appversion": { + "major": 8, + "minor": 0, + "revision": 0, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 60.0, + 80.0, + 980.0, + 820.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 12.0, + "default_fontface": 0, + "default_fontname": "Arial", + "gridonopen": 1, + "gridsize": [ + 15.0, + 15.0 + ], + "gridsnaponopen": 1, + "objectsnaponopen": 1, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxes": [ + { + "box": { + "id": "obj-1", + "maxclass": "newobj", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 20.0, + 50.0, + 22.0 + ], + "text": "p basic", + "patcher": { + "fileversion": 1, + "appversion": { + "major": 8, + "minor": 0, + "revision": 0, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 0.0, + 26.0, + 980.0, + 820.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 12.0, + "default_fontface": 0, + "default_fontname": "Arial", + "gridonopen": 1, + "gridsize": [ + 15.0, + 15.0 + ], + "gridsnaponopen": 1, + "objectsnaponopen": 1, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxes": [ + { + "box": { + "id": "obj-1", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 14.0, + 400.0, + 22.0 + ], + "text": "mutap.afc~", + "fontsize": 16.0 + } + }, + { + "box": { + "id": "obj-2", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 38.0, + 700.0, + 54.0 + ], + "text": "Acoustic feedback (howling) canceller. Left inlet: the microphone. Right inlet: the SAME signal the patch sends to the speaker (the reference). Outlets, left to right: the cleaned mic, delayed by @block samples; the guard's post-reverb gain (a signal, 1 while @guard is off; see the guard tab); the IPC double-talk indicator (0..1); the convergence statistics, uncertainty_db shadow_ratio_db." + } + }, + { + "box": { + "id": "obj-3", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 120.0, + 50.0, + 22.0 + ], + "text": "adc~", + "outlettype": [ + "signal", + "signal" + ] + } + }, + { + "box": { + "id": "obj-4", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 4, + "patching_rect": [ + 40.0, + 200.0, + 160.0, + 22.0 + ], + "text": "mutap.afc~ 2048", + "outlettype": [ + "signal", + "signal", + "", + "" + ] + } + }, + { + "box": { + "id": "obj-5", + "maxclass": "gain~", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 260.0, + 30.0, + 120.0 + ], + "outlettype": [ + "signal", + "int" + ] + } + }, + { + "box": { + "id": "obj-6", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 0, + "patching_rect": [ + 40.0, + 420.0, + 50.0, + 22.0 + ], + "text": "dac~", + "outlettype": [] + } + }, + { + "box": { + "id": "obj-7", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 90.0, + 262.0, + 560.0, + 40.0 + ], + "text": "The closed loop: mic -> afc~ -> gain -> speaker. Raise the gain slider toward howling onset; the canceller buys added stable gain. The gain~ output is tapped back into afc~'s right inlet — the reference MUST be the signal that actually reaches the speaker." + } + }, + { + "box": { + "id": "obj-8", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 180.0, + 240.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-9", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 244.0, + 240.0, + 420.0, + 22.0 + ], + "text": "IPC 0..1 (third outlet): high = feedback dominates (adapting hard), low = you are talking (double-talk, updates gated). Meter it to watch the M4 robustness layer work." + } + }, + { + "box": { + "id": "obj-10", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 96.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-11", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 510.0, + 120.0, + 70.0, + 22.0 + ], + "text": "adapt $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-12", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 586.0, + 120.0, + 260.0, + 20.0 + ], + "text": "freeze / resume adaptation (default on)" + } + }, + { + "box": { + "id": "obj-13", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 150.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-14", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 510.0, + 174.0, + 64.0, + 22.0 + ], + "text": "gate $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-15", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 580.0, + 174.0, + 340.0, + 20.0 + ], + "text": "IPC step scaling + transient freeze (default on; rebuilds)" + } + }, + { + "box": { + "id": "obj-16", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 480.0, + 204.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-17", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 546.0, + 204.0, + 60.0, + 22.0 + ], + "text": "mu $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-18", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 612.0, + 204.0, + 300.0, + 20.0 + ], + "text": "NLMS step size, (0, 2), default 0.5 (applied live)" + } + }, + { + "box": { + "id": "obj-19", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 360.0, + 44.0, + 22.0 + ], + "text": "reset", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-20", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 530.0, + 360.0, + 300.0, + 20.0 + ], + "text": "zero the learned feedback-path estimate" + } + }, + { + "box": { + "id": "obj-21", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 330.0, + 80.0, + 22.0 + ], + "text": "block 256", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-22", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 566.0, + 330.0, + 380.0, + 20.0 + ], + "text": "block size = adaptation hop = added latency (power of 2; rebuilds)" + } + }, + { + "box": { + "id": "obj-23", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 460.0, + 700.0, + 54.0 + ], + "text": "Why PEM: in a closed loop the speaker signal is correlated with your voice, so a naive adaptive filter cancels program material, not feedback. mutap.afc~ re-fits a near-end model (LPC + pitch) every block, prewhitens both signals with it, and adapts on the whitened pair while cancelling on the raw ones (FDAF-PEM-AFROW). Creation arg = filter length in samples (default 2048); partitions = filter length / block." + } + }, + { + "box": { + "id": "obj-24", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 234.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-25", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 510.0, + 258.0, + 64.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "warp $1" + } + }, + { + "box": { + "id": "obj-26", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 580.0, + 258.0, + 400.0, + 20.0 + ], + "outlettype": [], + "text": "frequency-warped near-end model for music/tonal sources (default off; rebuilds, keeps IPC scaling on)" + } + }, + { + "box": { + "id": "obj-27", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 264.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ], + "parameter_enable": 0 + } + }, + { + "box": { + "id": "obj-28", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 510.0, + 288.0, + 80.0, + 22.0 + ], + "outlettype": [ + "" + ], + "text": "kalman $1" + } + }, + { + "box": { + "id": "obj-29", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 596.0, + 288.0, + 400.0, + 20.0 + ], + "outlettype": [], + "text": "v2 Kalman engine: mu ignored, gate = burst floor (default off; rebuilds)" + } + }, + { + "box": { + "id": "obj-30", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 180.0, + 314.0, + 150.0, + 22.0 + ], + "text": "unpack 0. 0.", + "outlettype": [ + "float", + "float" + ] + } + }, + { + "box": { + "id": "obj-31", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 180.0, + 344.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-32", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 310.0, + 344.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-33", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 100.0, + 370.0, + 150.0, + 34.0 + ], + "text": "uncertainty dB (Kalman core; 0 dB with NLMS)" + } + }, + { + "box": { + "id": "obj-34", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 260.0, + 370.0, + 210.0, + 34.0 + ], + "text": "shadow ratio dB (0 dB with @shadow 0)" + } + }, + { + "box": { + "id": "obj-35", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 100.0, + 404.0, + 370.0, + 50.0 + ], + "text": "Right outlet, every 8 blocks: raw statistics, no thresholds. Uncertainty: sum P / sum P at reset (0 dB = nothing identified; falls as the path is learned). Shadow ratio: main / shadow residual power; rises toward 0 dB and past it when the path moves." + } + }, + { + "box": { + "id": "obj-36", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 480.0, + 392.0, + 64.0, + 22.0 + ], + "text": "shadow 2", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-37", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 550.0, + 392.0, + 64.0, + 22.0 + ], + "text": "shadow 0", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-38", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 620.0, + 392.0, + 350.0, + 48.0 + ], + "text": "shadow comparator partitions (default 2, 0 = off; clamped to the partition count at each build; rebuilds). Measured with the Kalman engine only." + } + }, + { + "box": { + "id": "obj-39", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 530.0, + 700.0, + 22.0 + ], + "text": "Example convergence policy (patch-side: without @guard, mutap.afc~ applies no thresholds; the guard tab shows the built-in safety layer)", + "fontface": 1 + } + }, + { + "box": { + "id": "obj-40", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 40.0, + 560.0, + 230.0, + 22.0 + ], + "text": "expr ($f1 < -23.8) && ($f2 < -1.24)", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-41", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 3, + "patching_rect": [ + 40.0, + 590.0, + 50.0, + 22.0 + ], + "text": "change", + "outlettype": [ + "", + "int", + "int" + ] + } + }, + { + "box": { + "id": "obj-42", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 620.0, + 50.0, + 22.0 + ], + "text": "t b i", + "outlettype": [ + "bang", + "int" + ] + } + }, + { + "box": { + "id": "obj-43", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 40.0, + 650.0, + 70.0, + 22.0 + ], + "text": "delay 300", + "outlettype": [ + "bang" + ] + } + }, + { + "box": { + "id": "obj-44", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 40.0, + 680.0, + 40.0, + 22.0 + ], + "text": "i", + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-45", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 3, + "patching_rect": [ + 40.0, + 710.0, + 50.0, + 22.0 + ], + "text": "change", + "outlettype": [ + "", + "int", + "int" + ] + } + }, + { + "box": { + "id": "obj-46", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 40.0, + 740.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-47", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 70.0, + 742.0, + 220.0, + 20.0 + ], + "text": "converged (each state held 0.3 s)" + } + }, + { + "box": { + "id": "obj-48", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 300.0, + 560.0, + 660.0, + 180.0 + ], + "text": "\"Converged\" when uncertainty < -23.8 dB AND shadow ratio < -1.24 dB; a state flips only after the condition has held 0.3 s on the other side (delay 300 restarts on every change). Needs @kalman 1: with the NLMS core the uncertainty reads 0 dB and this never says converged.\n\nThese thresholds were calibrated at @block 64 / 48 kHz on 1024- and 2048-tap filters with @kalman 1 and @shadow 2 (medians over 50 cold starts in MuTap's convergence-indicator experiment). They are a calibration for that loop, not constants: recalibrate for any other block size, rate, filter length or room.\n\nMeasured there: it detects a walk to a different room at the change, but reports reconvergence 0.13-0.26 s early (ahead of the filter's measured misalignment). The uncertainty statistic regrows in digital silence, so a silent input drifts back to not converged." + } + }, + { + "box": { + "id": "obj-49", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 100.0, + 766.0, + 370, + 50 + ], + "text": "Also on the right outlet: \"watchdog n\" whenever a NaN or infinity reached the canceller (from a divide by zero in the patch, a disconnected inlet): it resets, as 'reset' does, outputs one block of silence, and counts since the last rebuild. A healthy patch never hears it." + } + } + ], + "lines": [ + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-3", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-5", + 0 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-6", + 0 + ], + "source": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-6", + 1 + ], + "source": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 1 + ], + "source": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-8", + 0 + ], + "source": [ + "obj-4", + 2 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-11", + 0 + ], + "source": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-14", + 0 + ], + "source": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-17", + 0 + ], + "source": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-21", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-24", + 0 + ], + "destination": [ + "obj-25", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-25", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-27", + 0 + ], + "destination": [ + "obj-28", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-28", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-30", + 0 + ], + "source": [ + "obj-4", + 3 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-31", + 0 + ], + "source": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-32", + 0 + ], + "source": [ + "obj-30", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-36", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-37", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-40", + 0 + ], + "source": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-40", + 1 + ], + "source": [ + "obj-30", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-41", + 0 + ], + "source": [ + "obj-40", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-42", + 0 + ], + "source": [ + "obj-41", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-44", + 1 + ], + "source": [ + "obj-42", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-43", + 0 + ], + "source": [ + "obj-42", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-44", + 0 + ], + "source": [ + "obj-43", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-45", + 0 + ], + "source": [ + "obj-44", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-46", + 0 + ], + "source": [ + "obj-45", + 0 + ] + } + } + ], + "showontab": 1 + } + } + }, + { + "box": { + "id": "obj-2", + "maxclass": "newobj", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 50.0, + 50.0, + 22.0 + ], + "text": "p guard", + "patcher": { + "fileversion": 1, + "appversion": { + "major": 8, + "minor": 0, + "revision": 0, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 0.0, + 26.0, + 980.0, + 820.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 12.0, + "default_fontface": 0, + "default_fontname": "Arial", + "gridonopen": 1, + "gridsize": [ + 15.0, + 15.0 + ], + "gridsnaponopen": 1, + "objectsnaponopen": 1, + "statusbarvisible": 2, + "toolbarvisible": 1, + "showontab": 1, + "boxes": [ + { + "box": { + "id": "obj-1", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 14.0, + 600.0, + 22.0 + ], + "text": "mutap.afc~ - the safety layer (@guard)", + "fontsize": 16.0 + } + }, + { + "box": { + "id": "obj-2", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 38.0, + 930.0, + 54.0 + ], + "text": "@guard 1 runs MuTap's howl_guard on this mic: the cleaned output sits @arming dB down (ARMING) until the canceller's verdict (shadow_ratio_db < @d_db AND uncertainty_db < @a_db) has held @hold s with the howl detector quiet, then opens; a howl trip ducks it @duck dB. It needs @kalman 1 and @shadow > 0. One mutap.afc~ is one mic: two objects duck independently (MuTap's afc_chain attributes a shared howl to a mic; this object cannot)." + } + }, + { + "box": { + "id": "obj-3", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 110.0, + 50.0, + 22.0 + ], + "text": "adc~", + "outlettype": [ + "signal", + "signal" + ] + } + }, + { + "box": { + "id": "obj-4", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 4, + "patching_rect": [ + 40.0, + 200.0, + 280.0, + 22.0 + ], + "text": "mutap.afc~ 2048 @kalman 1 @loop_ms 35 @guard 1", + "outlettype": [ + "signal", + "signal", + "", + "" + ] + } + }, + { + "box": { + "id": "obj-5", + "maxclass": "newobj", + "numinlets": 5, + "numoutlets": 1, + "patching_rect": [ + 110.0, + 250.0, + 150.0, + 22.0 + ], + "text": "comb~ 100 37.1 0. 1. 0.75", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-6", + "maxclass": "newobj", + "numinlets": 5, + "numoutlets": 1, + "patching_rect": [ + 110.0, + 280.0, + 150.0, + 22.0 + ], + "text": "comb~ 100 41.3 0. 1. 0.72", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-7", + "maxclass": "newobj", + "numinlets": 3, + "numoutlets": 1, + "patching_rect": [ + 110.0, + 310.0, + 120.0, + 22.0 + ], + "text": "allpass~ 50 5. 0.7", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-8", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 110.0, + 340.0, + 40.0, + 22.0 + ], + "text": "*~", + "outlettype": [ + "signal" + ] + } + }, + { + "box": { + "id": "obj-9", + "maxclass": "gain~", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 40.0, + 380.0, + 30.0, + 120.0 + ], + "outlettype": [ + "signal", + "int" + ] + } + }, + { + "box": { + "id": "obj-10", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 0, + "patching_rect": [ + 40.0, + 520.0, + 50.0, + 22.0 + ], + "text": "dac~", + "outlettype": [] + } + }, + { + "box": { + "id": "obj-11", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 270.0, + 250.0, + 300.0, + 84.0 + ], + "text": "A stand-in reverb (two comb~ into allpass~): put yours here. The guard's gain is already on the cleaned output, so a duck cuts what enters the reverb; the second outlet repeats the duck after it (*~), so a charged tail is cut too - MuTap's bus stage." + } + }, + { + "box": { + "id": "obj-12", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 160.0, + 341.0, + 400.0, + 34.0 + ], + "text": "Second outlet: 1 except while ducked or releasing, where it is the duck relative to the restore level (ARMING and strike levels are not applied twice)." + } + }, + { + "box": { + "id": "obj-13", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 600.0, + 380.0, + 50.0, + 22.0 + ], + "text": "open", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-14", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 660.0, + 380.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-15", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 600.0, + 420.0, + 70.0, + 22.0 + ], + "text": "sfplay~", + "outlettype": [ + "signal", + "bang" + ] + } + }, + { + "box": { + "id": "obj-16", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 690.0, + 410.0, + 280.0, + 96.0 + ], + "text": "Backing track: summed into the speaker feed after the guard, and into the reference. While ARMING holds the voice down it is the canceller's excitation: in MuTap's simulated cold starts every run declared with it, none without it in 20 s (docs/howl-guard.md)." + } + }, + { + "box": { + "id": "obj-17", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 600.0, + 100.0, + 360.0, + 20.0 + ], + "text": "1. Soundcheck (needs the guard running)", + "fontface": 1 + } + }, + { + "box": { + "id": "obj-18", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 600.0, + 124.0, + 64.0, + 22.0 + ], + "text": "calibrate", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-19", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 670.0, + 124.0, + 80.0, + 22.0 + ], + "text": "calibrate 0", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-20", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 600.0, + 148.0, + 360.0, + 62.0 + ], + "text": "Play the song through the loop for @cal_s (30 s): the guard then runs on the medians of the two statistics plus @cal_margin_d / @cal_margin_a (4 / 3 dB) and sends calibrate_done d_db a_db. 'calibrate 0' ends it early." + } + }, + { + "box": { + "id": "obj-21", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 600.0, + 214.0, + 360.0, + 20.0 + ], + "text": "2. The deployment cap", + "fontface": 1 + } + }, + { + "box": { + "id": "obj-22", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 600.0, + 238.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ], + "maximum": 0.0 + } + }, + { + "box": { + "id": "obj-23", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 666.0, + 238.0, + 60.0, + 22.0 + ], + "text": "cap $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-24", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 732.0, + 238.0, + 64.0, + 22.0 + ], + "text": "cap none", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-25", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 600.0, + 262.0, + 360.0, + 76.0 + ], + "text": "dB re your operating gain, at most 0 (MuTap's protocol: the dry limit - 6 dB). After 10 s in ARMING without a verdict the guard says unprotected; with a cap it opens to it (open_capped), with none it stays in ARMING, @arming dB down." + } + }, + { + "box": { + "id": "obj-26", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 600.0, + 342.0, + 40.0, + 22.0 + ], + "text": "clear", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-27", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 644.0, + 342.0, + 330.0, + 20.0 + ], + "text": "strikes 0, restore level 0 dB, latch released" + } + }, + { + "box": { + "id": "obj-28", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 820.0, + 100.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-29", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 850.0, + 100.0, + 60.0, + 22.0 + ], + "text": "guard $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-30", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 850.0, + 124.0, + 44.0, + 22.0 + ], + "text": "reset", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-31", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 900.0, + 124.0, + 70.0, + 34.0 + ], + "text": "back to ARMING" + } + }, + { + "box": { + "id": "obj-32", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 300.0, + 560.0, + 400.0, + 20.0 + ], + "text": "3. The guard, every 8 blocks (right outlet)", + "fontface": 1 + } + }, + { + "box": { + "id": "obj-33", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 2, + "patching_rect": [ + 300.0, + 584.0, + 140.0, + 22.0 + ], + "text": "route calibrate_done", + "outlettype": [ + "", + "" + ] + } + }, + { + "box": { + "id": "obj-34", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 300.0, + 614.0, + 70.0, + 22.0 + ], + "text": "unpack 0. 0.", + "outlettype": [ + "float", + "float" + ] + } + }, + { + "box": { + "id": "obj-35", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 300.0, + 644.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-36", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 366.0, + 644.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-37", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 300.0, + 668.0, + 200.0, + 48.0 + ], + "text": "calibrate_done d_db a_db: store them; set @d_db / @a_db to restore the soundcheck after a rebuild." + } + }, + { + "box": { + "id": "obj-38", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 6, + "patching_rect": [ + 520.0, + 614.0, + 200.0, + 22.0 + ], + "text": "unpack 0. 0. s 0. 0 0", + "outlettype": [ + "float", + "float", + "", + "float", + "int", + "int" + ] + } + }, + { + "box": { + "id": "obj-39", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 520.0, + 680.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-40", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 586.0, + 680.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-41", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 652.0, + 644.0, + 70.0, + 22.0 + ], + "text": "prepend set", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-42", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 652.0, + 680.0, + 90.0, + 22.0 + ], + "text": "", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-43", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 748.0, + 680.0, + 60.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-44", + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 814.0, + 680.0, + 24.0, + 24.0 + ], + "outlettype": [ + "int" + ] + } + }, + { + "box": { + "id": "obj-45", + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 844.0, + 680.0, + 40.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ] + } + }, + { + "box": { + "id": "obj-46", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 520.0, + 706.0, + 460.0, + 20.0 + ], + "text": "uncertainty_db shadow_ratio_db guard_state gain_db unprotected strikes" + } + }, + { + "box": { + "id": "obj-47", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 3, + "patching_rect": [ + 652.0, + 740.0, + 150.0, + 22.0 + ], + "text": "sel ducked latched", + "outlettype": [ + "bang", + "bang", + "" + ] + } + }, + { + "box": { + "id": "obj-48", + "maxclass": "button", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 652.0, + 770.0, + 24.0, + 24.0 + ], + "outlettype": [ + "bang" + ] + } + }, + { + "box": { + "id": "obj-49", + "maxclass": "button", + "numinlets": 1, + "numoutlets": 1, + "patching_rect": [ + 700.0, + 770.0, + 24.0, + 24.0 + ], + "outlettype": [ + "bang" + ] + } + }, + { + "box": { + "id": "obj-50", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 730.0, + 770.0, + 250.0, + 34.0 + ], + "text": "route on the state symbol: ducked, latched" + } + }, + { + "box": { + "id": "obj-51", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 560.0, + 270.0, + 230.0 + ], + "text": "guard_state: arming (held @arming dB down until the verdict declares), open (0 dB, less 3 dB per strike), open_capped (no verdict yet: at the cap, unprotected), ducked (@duck dB under the restore level, on a howl trip or a lost verdict), releasing (ramping back up), latched (three strikes: at the floor, unprotected). gain_db is the gain at the end of the last block; a duck releases on the verdict or after @rearm s x 2^strikes. reset returns to arming; so does every rebuild." + } + }, + { + "box": { + "id": "obj-52", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 300.0, + 100.0, + 290.0, + 20.0 + ], + "text": "0. The detector's calibrations", + "fontface": 1 + } + }, + { + "box": { + "id": "obj-53", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 300.0, + 124.0, + 70.0, + 22.0 + ], + "text": "loop_ms 35", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-54", + "maxclass": "flonum", + "numinlets": 1, + "numoutlets": 2, + "patching_rect": [ + 380.0, + 124.0, + 50.0, + 22.0 + ], + "outlettype": [ + "", + "bang" + ], + "maximum": 0.0 + } + }, + { + "box": { + "id": "obj-55", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "patching_rect": [ + 436.0, + 124.0, + 70.0, + 22.0 + ], + "text": "ceiling $1", + "outlettype": [ + "" + ] + } + }, + { + "box": { + "id": "obj-56", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 300.0, + 148.0, + 290.0, + 48.0 + ], + "text": "loop_ms = one trip round YOUR loop: @block + Max's I/O vector in and out + converters + flight (48 kHz, @block 256, I/O 512, 3 m: about 35 ms). ceiling: dB re 1.0 on the residual, set with the cap. Each rebuilds the canceller: set them before guard 1." + } + }, + { + "box": { + "id": "obj-57", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 520.0, + 806.0, + 460, + 34 + ], + "text": "\"watchdog n\" on the same outlet: the canceller's NaN-watchdog trips plus the guard's; a trip sends the guard back to ARMING (strikes kept)." + } + } + ], + "lines": [ + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-3", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-9", + 0 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-5", + 0 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-6", + 0 + ], + "source": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-7", + 0 + ], + "source": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-7", + 0 + ], + "source": [ + "obj-6", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-8", + 0 + ], + "source": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-8", + 1 + ], + "source": [ + "obj-4", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-9", + 0 + ], + "source": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-10", + 0 + ], + "source": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-10", + 1 + ], + "source": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 1 + ], + "source": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-15", + 0 + ], + "source": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-15", + 0 + ], + "source": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-10", + 0 + ], + "source": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-10", + 1 + ], + "source": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 1 + ], + "source": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-24", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-26", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-29", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-23", + 0 + ], + "source": [ + "obj-22", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-29", + 0 + ], + "source": [ + "obj-28", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-33", + 0 + ], + "source": [ + "obj-4", + 3 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-34", + 0 + ], + "source": [ + "obj-33", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-35", + 0 + ], + "source": [ + "obj-34", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-36", + 0 + ], + "source": [ + "obj-34", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-38", + 0 + ], + "source": [ + "obj-33", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-39", + 0 + ], + "source": [ + "obj-38", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-40", + 0 + ], + "source": [ + "obj-38", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-41", + 0 + ], + "source": [ + "obj-38", + 2 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-42", + 0 + ], + "source": [ + "obj-41", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-43", + 0 + ], + "source": [ + "obj-38", + 3 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-44", + 0 + ], + "source": [ + "obj-38", + 4 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-45", + 0 + ], + "source": [ + "obj-38", + 5 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-47", + 0 + ], + "source": [ + "obj-38", + 2 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-48", + 0 + ], + "source": [ + "obj-47", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-49", + 0 + ], + "source": [ + "obj-47", + 1 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-53", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-4", + 0 + ], + "source": [ + "obj-55", + 0 + ] + } + }, + { + "patchline": { + "destination": [ + "obj-55", + 0 + ], + "source": [ + "obj-54", + 0 + ] + } + } + ] + } + } + } + ], + "lines": [] + } } \ No newline at end of file diff --git a/source/projects/mutap.aec_tilde/mutap.aec_tilde.cpp b/source/projects/mutap.aec_tilde/mutap.aec_tilde.cpp index be3ad34..edd6545 100644 --- a/source/projects/mutap.aec_tilde/mutap.aec_tilde.cpp +++ b/source/projects/mutap.aec_tilde/mutap.aec_tilde.cpp @@ -94,10 +94,11 @@ class mutap_aec : public object, public vector_operator<> { /// which never allocates. struct engine { std::variant aec; - std::vector x_block; ///< gathering far-end (reference) samples - std::vector y_block; ///< gathering microphone samples - std::vector e_block; ///< last processed block, being played out - size_t fill{0}; ///< samples gathered so far, == play-out position + std::vector x_block; ///< gathering far-end (reference) samples + std::vector y_block; ///< gathering microphone samples + std::vector e_block; ///< last processed block, being played out + size_t fill{0}; ///< samples gathered so far, == play-out position + size_t watchdog_reported{0}; ///< the last `watchdog n` sent for this engine template explicit engine(std::in_place_type_t which, const typename Aec::config& cfg, size_t block) @@ -150,6 +151,7 @@ class mutap_aec : public object, public vector_operator<> { engine* m_active{nullptr}; long m_report_countdown{k_ipc_report_blocks}; atoms m_ipc_atoms{0.0}; + atoms m_watchdog_atoms{symbol{"watchdog"}, 0}; ///< watchdog n (the NaN watchdog's count) public: MIN_DESCRIPTION{"Acoustic echo canceller. Subtracts an adaptive estimate of the " @@ -172,7 +174,8 @@ class mutap_aec : public object, public vector_operator<> { inlet<> m_in_mic{this, "(signal) microphone signal y (echo + near end)"}; inlet<> m_in_ref{this, "(signal) far-end / reference signal x (the signal sent to the loudspeaker)"}; outlet<> m_out{this, "(signal) cleaned signal e = y - estimated echo", "signal"}; - outlet m_ipc_out{this, "(float) IPC double-talk indicator, 0..1"}; + outlet m_ipc_out{ + this, "(float) IPC double-talk indicator, 0..1 (echo-explained fraction with a post-filter); watchdog n"}; /// First creation argument is the echo-path filter length in samples /// (default 2048); partitions = ceil(filter_length / block size). @@ -438,6 +441,15 @@ class mutap_aec : public object, public vector_operator<> { if (++eng.fill == b) { aec.process_block(eng.x_block.data(), eng.y_block.data(), eng.e_block.data()); eng.fill = 0; + // The NaN watchdog (MuTap watchdog.h): every engine + // counts the blocks a NaN or infinity reached it (a + // chain counts a stage's trip once); report the count + // on the right outlet as `watchdog n` when it moves. + if (const size_t trips = aec.watchdog_trips(); trips != eng.watchdog_reported) { + eng.watchdog_reported = trips; + m_watchdog_atoms[1] = static_cast(trips); + m_ipc_out.send(m_watchdog_atoms); + } if (--m_report_countdown <= 0) { m_report_countdown = k_ipc_report_blocks; if constexpr (requires { aec.ipc(); }) { diff --git a/source/projects/mutap.aec_tilde/mutap.aec_tilde_test.cpp b/source/projects/mutap.aec_tilde/mutap.aec_tilde_test.cpp index 4ae329b..728e101 100644 --- a/source/projects/mutap.aec_tilde/mutap.aec_tilde_test.cpp +++ b/source/projects/mutap.aec_tilde/mutap.aec_tilde_test.cpp @@ -3,9 +3,78 @@ // SPDX-License-Identifier: MIT // Copyright 2026 MuTap contributors +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "c74_min_unittest.h" // required unit-test header (defines main via Catch) #include "mutap.aec_tilde.cpp" // include the object source so we can instantiate it +namespace { + + constexpr int k_report_outlet = 1; ///< the right outlet: IPC, and `watchdog n` + constexpr long k_vector_size = 64; + + /// Open-loop stand-in for the room (as mutap.afc_tilde_test.cpp's): the + /// far end x is deterministic white noise, the microphone hears it + /// through a 7-sample delay at half amplitude; `poison` puts a NaN in the + /// next vector's first mic sample, once. + struct room { + static constexpr size_t k_delay = 7; + std::uint32_t seed{0x2545F491U}; + std::array history{}; + size_t head{0}; + bool poison{false}; + std::array x{}; + std::array y{}; + std::array e{}; + std::vector out; + + void drive(mutap_aec& obj, long samples) { + for (long done = 0; done < samples; done += k_vector_size) { + for (size_t i = 0; i < x.size(); ++i) { + seed = seed * 1664525U + 1013904223U; + x[i] = static_cast(seed >> 8) / static_cast(1U << 24) - 0.5; + y[i] = 0.5 * history[head]; + history[head] = x[i]; + head = (head + 1) % k_delay; + } + if (poison) { + y[0] = std::numeric_limits::quiet_NaN(); + poison = false; + } + std::array in{y.data(), x.data()}; + std::array outs{e.data()}; + obj(audio_bundle{in.data(), 2, k_vector_size}, audio_bundle{outs.data(), 1, k_vector_size}); + out.insert(out.end(), e.begin(), e.end()); + } + } + }; + + /// Wait for the right outlet's fifo to have delivered `count` messages + /// (the mock kernel delivers on its own clock thread; see the afc test). + bool wait_for_reports(mutap_aec& obj, size_t count) { + const c74::max::t_sequence& log = *c74::max::object_getoutput(obj.maxobj(), k_report_outlet); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (log.size() < count && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return log.size() >= count; + } + + std::string name_of(const atom& a) { + return a.a_type == c74::max::A_SYM ? std::string(atom_getsym(&a)->s_name) : std::string{}; + } + +} // namespace + SCENARIO("mutap.aec~ instantiates with the documented defaults") { ext_main(nullptr); // configure the class (required once per test executable) @@ -128,3 +197,45 @@ SCENARIO("mutap.aec~ clamps the adaptation step size into the stable open interv } } } + +// MuTap's NaN watchdog through the glue (as mutap.afc_tilde_test.cpp): a +// NaN in the microphone resets the engine, comes out as one block of +// silence, and the right outlet says `watchdog n` once per count. + +SCENARIO("mutap.aec~ reports the NaN watchdog as 'watchdog n' on the right outlet") { + ext_main(nullptr); + + GIVEN("a default instance driven 32 blocks, one NaN in the microphone, then 32 blocks") { + // Up to three fresh instances: the mock clock thread's start-up race + // (see the afc test's run_logged). + for (int attempt = 0; attempt < 3; ++attempt) { + test_wrapper an_instance; + mutap_aec& my_object = an_instance; + room the_room; + the_room.drive(my_object, 32 * 256); + the_room.poison = true; + the_room.drive(my_object, 32 * 256); + // 64 blocks: 8 IPC reports + 1 watchdog message. + if (!wait_for_reports(my_object, 9)) { + continue; + } + const c74::max::t_sequence& log = *c74::max::object_getoutput(my_object.maxobj(), k_report_outlet); + size_t watchdogs = 0; + double count = -1.0; + for (const auto& r : log) { + if (!r.empty() && name_of(r[0]) == "watchdog") { + ++watchdogs; + count = static_cast(r[1]); + } + } + THEN("exactly one 'watchdog 1' arrives and the output stays finite") { + REQUIRE(watchdogs == 1); + REQUIRE(count == 1.0); + REQUIRE( + std::all_of(the_room.out.begin(), the_room.out.end(), [](double v) { return std::isfinite(v); })); + } + return; + } + FAIL("no delivery in three attempts"); + } +} diff --git a/source/projects/mutap.afc_tilde/mutap.afc_tilde.cpp b/source/projects/mutap.afc_tilde/mutap.afc_tilde.cpp index 59c7730..7a23701 100644 --- a/source/projects/mutap.afc_tilde/mutap.afc_tilde.cpp +++ b/source/projects/mutap.afc_tilde/mutap.afc_tilde.cpp @@ -44,6 +44,14 @@ /// attributes are copied into a pending slot and applied by the audio thread /// at the top of the next vector; the main thread never touches a live guard. /// +/// THE NaN WATCHDOG (MuTap's include/mutap/watchdog.h): a NaN or infinity +/// reaching the canceller — from a patch's divide by zero, a disconnected +/// inlet — resets it as `reset` does (and sends the guard to ARMING), comes +/// out as one block of silence, and counts. The rightmost outlet reports the +/// count as `watchdog n` whenever it moves (the canceller's trips plus the +/// guard's, since the engine was built; never otherwise, so a healthy patch +/// hears nothing). A rebuild starts a fresh count. +/// /// The canceller works on fixed blocks of @block samples, independent of the /// host signal vector size: the perform routine gathers samples into /// constructor-allocated block buffers, calls process_block() every time a @@ -106,13 +114,14 @@ class mutap_afc : public object, public vector_operator<> { /// emplaced in place and the engine lives behind a pointer. struct engine { std::variant afc; - std::vector u_block; ///< gathering reference (loudspeaker) samples - std::vector y_block; ///< gathering microphone samples - std::vector e_block; ///< last processed block (guarded), being played out - std::vector g_block; ///< its bus-stage gain, being played out (1 without a guard) - std::optional guard; ///< the safety layer, when @guard is on and supported - size_t fill{0}; ///< samples gathered so far, == play-out position - bool calibrating{false}; ///< a soundcheck is running (audio thread) + std::vector u_block; ///< gathering reference (loudspeaker) samples + std::vector y_block; ///< gathering microphone samples + std::vector e_block; ///< last processed block (guarded), being played out + std::vector g_block; ///< its bus-stage gain, being played out (1 without a guard) + std::optional guard; ///< the safety layer, when @guard is on and supported + size_t fill{0}; ///< samples gathered so far, == play-out position + bool calibrating{false}; ///< a soundcheck is running (audio thread) + size_t watchdog_reported{0}; ///< the last `watchdog n` sent for this engine template explicit engine(std::in_place_type_t which, const typename Afc::config& cfg) @@ -214,20 +223,23 @@ class mutap_afc : public object, public vector_operator<> { /// uncertainty_db shadow_ratio_db guard_state gain_db unprotected strikes atoms m_guard_atoms{0.0, 0.0, symbol{"arming"}, 0.0, 0, 0}; atoms m_calibrated_atoms{symbol{"calibrate_done"}, 0.0, 0.0}; ///< calibrate_done d_db a_db + atoms m_watchdog_atoms{symbol{"watchdog"}, 0}; ///< watchdog n (the NaN watchdog's count) public: - MIN_DESCRIPTION{"Acoustic feedback (howling) canceller. Subtracts an adaptive estimate of the " - "loudspeaker-to-microphone feedback path from the microphone signal, adapted " - "with PEM prewhitening so the closed loop does not bias the estimate " - "(FDAF-PEM-AFROW, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the " - "signal feeding the loudspeaker; the cleaned output is delayed by @block samples. " - "The second outlet carries the guard's post-reverb gain (1 with the guard off); the " - "third reports the IPC double-talk indicator (0..1); the right outlet reports two raw " - "convergence statistics in dB (uncertainty_db shadow_ratio_db), plus the guard's " - "state with @guard on. @warp selects the frequency-warped near-end model for " - "music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2); " - "@shadow sizes the shadow comparator; @guard adds the anti-howl safety layer " - "(MuTap howl_guard: arming, duck, re-arm, back-off, soundcheck calibration, cap)."}; + MIN_DESCRIPTION{ + "Acoustic feedback (howling) canceller. Subtracts an adaptive estimate of the " + "loudspeaker-to-microphone feedback path from the microphone signal, adapted " + "with PEM prewhitening so the closed loop does not bias the estimate " + "(FDAF-PEM-AFROW, MuTap pem_afc). Inlet 1 takes the microphone, inlet 2 the " + "signal feeding the loudspeaker; the cleaned output is delayed by @block samples. " + "The second outlet carries the guard's post-reverb gain (1 with the guard off); the " + "third reports the IPC double-talk indicator (0..1); the right outlet reports two raw " + "convergence statistics in dB (uncertainty_db shadow_ratio_db), plus the guard's " + "state with @guard on, and 'watchdog n' whenever a NaN or infinity reached the " + "canceller (it resets and outputs one block of silence). @warp selects the frequency-warped near-end model for " + "music/tonal sources; @kalman selects the frequency-domain Kalman engine (v2); " + "@shadow sizes the shadow comparator; @guard adds the anti-howl safety layer " + "(MuTap howl_guard: arming, duck, re-arm, back-off, soundcheck calibration, cap)."}; MIN_TAGS{"audio, adaptive, feedback, howling, cleaning"}; MIN_AUTHOR{"MuTap contributors"}; MIN_RELATED{"adc~, dac~, adoutput~"}; @@ -239,7 +251,8 @@ class mutap_afc : public object, public vector_operator<> { "signal"}; outlet m_ipc_out{this, "(float) IPC double-talk indicator, 0..1"}; outlet m_conv_out{ - this, "(list) uncertainty_db shadow_ratio_db [guard_state gain_db unprotected strikes]; calibrate_done"}; + this, + "(list) uncertainty_db shadow_ratio_db [guard_state gain_db unprotected strikes]; calibrate_done; watchdog n"}; /// First creation argument is the feedback-path filter length in samples /// (default 2048); partitions = ceil(filter_length / block size). @@ -703,6 +716,7 @@ class mutap_afc : public object, public vector_operator<> { guard_block(eng, afc.uncertainty_ratio(), afc.shadow_residual_ratio()); } } + report_watchdog(eng, afc.watchdog_trips()); if (--m_report_countdown <= 0) { m_report_countdown = k_ipc_report_blocks; // Right to left, Max's outlet order. The NLMS core @@ -816,6 +830,23 @@ class mutap_afc : public object, public vector_operator<> { m_conv_out.send(m_guard_atoms); } + /// The NaN watchdog's count for this engine — the canceller's trips + /// (pem_afc::watchdog_trips(): one per block that reached it non-finite, + /// a core trip on the prewhitened pair counted once) plus the guard's + /// (howl_guard::watchdog_trips(0)) — sent as `watchdog n` on the right + /// outlet when it moves. Audio thread; allocation-free. + void report_watchdog(engine& eng, size_t canceller_trips) noexcept { + size_t trips = canceller_trips; + if (eng.guard) { + trips += eng.guard->watchdog_trips(0); + } + if (trips != eng.watchdog_reported) { + eng.watchdog_reported = trips; + m_watchdog_atoms[1] = static_cast(trips); + m_conv_out.send(m_watchdog_atoms); + } + } + // ----------------------------------------------- the guard, control side /// Whether the engine the current control state builds can host the diff --git a/source/projects/mutap.afc_tilde/mutap.afc_tilde_test.cpp b/source/projects/mutap.afc_tilde/mutap.afc_tilde_test.cpp index 8017ab7..c481d39 100644 --- a/source/projects/mutap.afc_tilde/mutap.afc_tilde_test.cpp +++ b/source/projects/mutap.afc_tilde/mutap.afc_tilde_test.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ namespace { size_t head{0}; double tone{0.0}; double phase{0.0}; + bool poison{false}; ///< put a NaN in the next vector's first mic sample, once std::array u{}; std::array y{}; std::array e{}; @@ -59,6 +61,10 @@ namespace { history[head] = u[i]; head = (head + 1) % k_delay; } + if (poison) { + y[0] = std::numeric_limits::quiet_NaN(); + poison = false; + } std::array in{y.data(), u.data()}; std::array outs{e.data(), g.data()}; obj(audio_bundle{in.data(), 2, k_vector_size}, audio_bundle{outs.data(), 2, k_vector_size}); @@ -157,6 +163,17 @@ namespace { return reports; } + /// The `watchdog n` messages, in order. + std::vector watchdogs(const c74::max::t_sequence& log) { + std::vector w; + for (const auto& r : log) { + if (!r.empty() && name_of(r[0]) == "watchdog") { + w.push_back(r); + } + } + return w; + } + /// The calibrate_done messages, in order. std::vector calibrations(const c74::max::t_sequence& log) { std::vector done; @@ -1032,17 +1049,84 @@ SCENARIO("mutap.afc~'s @ceiling decides whether a steady near-end tone trips the THEN("at the default ceiling the tone never trips; at -20 dB it trips from the first report on") { REQUIRE(at_default.size() == count); REQUIRE(at_low.size() == count); - // Measured: at -6 dB, 0 strikes in all 258 reports; the guard - // leaves ARMING (open at report 50 - the tone moves the statistics - // under the verdict's thresholds - then a lost-verdict duck and a - // release, no strike). At -20 dB, ARMING in every report with 8 - // strikes at report 0 and 30 (the strike counter's cap) from 25 on. + // Measured (MuTap 9a27f2e; mock rate 44.1 kHz, @block 256): at + // -6 dB the detector never trips. The guard leaves ARMING (open + // at report 47 - the tone moves the statistics under the + // verdict's thresholds), then a lost-verdict duck at report 143, + // inside the 10 s probation, which is ONE strike since MuTap's + // lost_in_probation_strikes defaulted on (tap/MuTap#88; at the + // previous pin, 449cf6c, that duck released with no strike), and + // the doubled re-arm timer keeps it ducked for the rest of the + // 12 s. So: at most one strike, and the guard opened before it. + // At -20 dB, ARMING in every report with 8 strikes at report 0 + // and 30 (the strike counter's cap) from 25 on. REQUIRE( - std::all_of(at_default.begin(), at_default.end(), [](const auto& r) { return value(r[5]) == 0.0; })); - REQUIRE(std::any_of(at_default.begin(), at_default.end(), - [](const auto& r) { return name_of(r[2]) != "arming"; })); + std::all_of(at_default.begin(), at_default.end(), [](const auto& r) { return value(r[5]) <= 1.0; })); + const auto first_open = std::find_if(at_default.begin(), at_default.end(), + [](const auto& r) { return name_of(r[2]) == "open"; }); + REQUIRE(first_open != at_default.end()); + REQUIRE(std::all_of(at_default.begin(), first_open, [](const auto& r) { return value(r[5]) == 0.0; })); REQUIRE(std::all_of(at_low.begin(), at_low.end(), [](const auto& r) { return name_of(r[2]) == "arming" && value(r[5]) >= 1.0; })); } } } + +// MuTap's NaN watchdog (include/mutap/watchdog.h) through the glue: a NaN in +// the microphone resets the canceller, comes out as one block of silence, +// and the right outlet says `watchdog n` once per count, never otherwise. + +SCENARIO("mutap.afc~ reports the NaN watchdog as 'watchdog n' on the right outlet") { + ext_main(nullptr); + + GIVEN("a default instance driven 32 blocks, one NaN in the microphone, then 32 blocks") { + // 64 blocks: 8 convergence reports + 1 watchdog message on the right + // outlet, 8 IPC reports. + const auto log = run_logged(9, 8, [](mutap_afc& my_object, room& the_room) { + the_room.drive(my_object, 32 * 256); + the_room.poison = true; + the_room.drive(my_object, 32 * 256); + }); + const auto w = watchdogs(log.reports); + + THEN("exactly one 'watchdog 1' arrives") { + REQUIRE(w.size() == 1); + REQUIRE(w[0].size() == 2); + REQUIRE(value(w[0][1]) == 1.0); + } + THEN("the cleaned output never carries a non-finite sample") { + REQUIRE(!log.out.empty()); + REQUIRE(std::all_of(log.out.begin(), log.out.end(), [](double v) { return std::isfinite(v); })); + } + } + + GIVEN("a healthy instance driven 64 blocks") { + const auto log = + run_logged(8, 8, [](mutap_afc& my_object, room& the_room) { the_room.drive(my_object, 64 * 256); }); + + THEN("no watchdog message is sent") { + REQUIRE(watchdogs(log.reports).empty()); + } + } + + GIVEN("the guard running (@kalman 1 @shadow 2 @guard 1), the same NaN") { + const auto log = run_logged(9, 8, [](mutap_afc& my_object, room& the_room) { + my_object.kalman = true; + my_object.guard = true; + the_room.drive(my_object, 32 * 256); + the_room.poison = true; + the_room.drive(my_object, 32 * 256); + }); + const auto w = watchdogs(log.reports); + + THEN("one 'watchdog 1': the canceller's trip, counted once; the guard saw the zeros and is back in ARMING") { + REQUIRE(w.size() == 1); + REQUIRE(value(w[0][1]) == 1.0); + const auto reports = guard_reports(log.reports); + REQUIRE(reports.size() == 8); + // The 4th report is the last before the NaN (block 32); the 5th + // the first after it: ARMING, whatever the state was before. + REQUIRE(name_of(reports[4][2]) == "arming"); + } + } +}