Since b2.4 (April 2026), masking noise drops out for 16 ms about once every 10 s, in about one of every five or six trials of a blab masking phase
Affects blab's current experiments on b2.4 and later.
Relevant blab studies affected. Affected, on data collected with b2.4 or later (since April 2026) only: runners that load the noise once per session, so its position runs on across trials. Public: simonSingleWord v2, v1, coAdapt, simonMultisyllable v2, attentionComp, attentionAdapt, uhdapter, vsaCentralize (its committed experiment script calls a runner that is not in the repository), vsaGeneralize, vsaSentence, the free-speech template (each link is the line that loads the noise once). Unpublished: timeAdapt, timeWrap, varMod, sponAdapt, vsaRetention, diphthongAdapt, brut, vin, and the simon and cerebellar studies. Safe as run: runners that reload the noise before every trial, which rewinds it (the MRI and iEEG interleaved runners, noGoAdapt, varModAdapt, compRetest). How audible: in fb 3 (most runners; babble at gain 0.02 under the participant's own voice) a 16 ms dropout is subtle; fb 2 studies (brut, vin, and the masking phases of simonSingleWord v2, uhdapter and vsaSentence) and fb 4 studies (vsaGeneralize, biteblockAdapt, diphthongAdapt, sponAdapt, sponAdapt2, vsaRetention) are more exposed, because the noise is all or most of what is heard.
What happens
In feedback modes 2–5 Audapter plays the loaded noise (datapb) in a loop. It loops at the end of its 480,000-sample (10 s) buffer, not at the end of the noise that was loaded, and the unused end of the buffer is silence. The bundled babble is 479,230 samples, 770 short of the buffer, so once per 10 s of running time the participant hears 16 ms of silence instead of noise. The noise position carries on from trial to trial (reset does not rewind it), so the dropout lands at a different point in each affected trial. A noise file much shorter than 10 s gives a correspondingly longer silence.
When it matters
Matters whenever masking noise is meant to be continuous, most of all in fb 2 masking phases (transfer or noise-baseline phases), where the noise is all the participant hears and a dropout briefly unmasks their own voice. In fb 3 phases the babble is quiet (fb3Gain 0.02) and a 16 ms dropout is less noticeable. Safe if the loaded noise is exactly 480,000 samples, or on versions before b2.4.
Example
The masking phase of blab's simonSingleWord v2 experiment (fb 2, fb2Gain 0.16, 1.8 s trials, AudapterIO('init') every trial, the bundled babble loaded once with get_noiseSource). Over 24 trials the current build plays 4 silences of 16 ms, in trials 6, 12, 17, 23, at 0.98 s, 0.18 s, 1.18 s, 0.38 s into the trial. With the pre-b2.4 buffer the same session plays none.
Settings used
- Source
- blab's simonSingleWord v2 experiment, masking (transfer) phase: fb 2 at fb2Gain 0.16, 1.8 s trials, init every trial, the bundled babble via free-speech get_noiseSource.
- Build
- Observed: blab-lab audapter_mex as shipped (@169cadf, buffer 480,000). Expected: the same session with the pre-b2.4 buffer (230,400), which get_noiseSource then truncates the babble to.
All settings: parameters, OST and PCF, each line explained
Parameters passed to AudapterIO('init', p) that differ from getAudapterDefaultParams('female'):
| none |
Download settings (JSON) · Open this test case in the Playground (replays the exact trial sequence and compares with this card) · Open these settings in the Playground
Trials 5–7 of the session, end to end (the gap in trial 6 is widened in the drawing so it is visible). Silence is detected on the audio of each trial.
Expected blab before b2.4 (buffer 230,400 samples): the babble loops seamlessly
Observed blab since b2.4 (buffer 480,000 samples): what Audapter plays now
Expected blab before b2.4 (buffer 230,400 samples): the babble loops seamlessly
Observed blab since b2.4 (buffer 480,000 samples): what Audapter plays now
Listen
Expected, trial 6. 0.6–1.4 s, pre-b2.4 buffer: no silence 0.8 s
Observed, trial 6. 0.6–1.4 s, current build: 16 ms of silence at 0.98 s 0.8 s
A 16 ms dropout at 0.38 s into the clip; listen closely.
A/B, trial 6. expected, then observed 2.0 s
No playhead: two clips back to back.
These clips share one playback gain and are the masking noise alone, as heard in fb 2 (the participant also hears their own voice through bone conduction). The dropout is short: listen on headphones. There is no speech in these clips: in fb 2 the participant hears only the masking noise, so a real voice would not change what is heard.
Run it in your browser
The general case, beyond the bundled babble: a noise file shorter than 10 s. This runs a 12 s trial through the real Audapter C++ core compiled to WebAssembly from the pinned commit, once as released and once with the noise looping at the length that was loaded. Both builds keep the shipped 480000-sample playback buffer. Pick the length of the noise file, cut from the bundled babble and loaded as runExperiment.m does it (zero mean, unit RMS). The page ships only every third sample of the babble, the only samples the fb 2–5 loop reads, and rebuilds the 48 kHz buffer from them, so Audapter reads the same values it would read from the full-rate file. The bars come from a noise-only run (fb 2), and Play gives the voice + noise run (fb 3). Both use the 48 kHz output Audapter returns for every frame, which lags signalOut by a few milliseconds. The build used here records only 10 s, so its signalOut would not cover a 12 s trial.
The change in the “fix-i-01” build: report/patches/i-01-datapb-loop-length.patch
Audapter.cpppbLen = maxPBSize; /* audit fix I-01 */pbLen = (len > 0) ? len : maxPBSize; /* audit fix I-01: loop at the loaded length */if (pbCounter >= maxPBSize)pbCounter -= maxPBSize;if (pbCounter >= pbLen) /* audit fix I-01: was maxPBSize */pbCounter -= pbLen;Audapter.hint pbLen; /* audit fix I-01: length of the loaded datapb (loop point) */Each run loads the Audapter core in this page, about 150–300 MB of memory per build. On a phone, run one panel at a time. Try your own recordings and settings in the Playground.
How big, how often
| simonSingleWord v2 | Silent gaps in 24 masking trials (current build) | 4, each 16 ms, in trials 6, 12, 17, 23 |
|---|---|---|
| simonSingleWord v2 | Same session, pre-b2.4 buffer | 0 gaps |
| real sessions (labrun) | modelExpt and vsaGeneralize full sessions run with their own scripts | 15.4 ms holes every 10.0 s of noise running time (9–12 and 22–34 holes per session with eight AI-generated voices) |
| simonSingleWord v2 | How often | every 10.0 s of running time: about 1 in 5.6 trials of 1.8 s |
| general case | A 5 s noise file in a 12 s trial | no noise 5.00–10.00 s |
Data: data.json from the export script.
Cause
The fb 2–5 loop advances pbCounter and wraps it at the compile-time maxPBSize. Setting datapb zero-fills the rest of the buffer, and only setting datapb resets pbCounter.
pbCounter += p.downFact;if (pbCounter >= maxPBSize) pbCounter -= maxPBSize;The wrap uses the buffer capacity.
if (ns == string("datapb")) { /* Zero out the remaining part */ for (int i = len; i < maxPBSize; i++) *((dtype*)ptr + i) = 0.0;}Loading a shorter datapb fills the rest of the buffer with zeros.
static const int maxPBSize = 480000; /* Maximum length (samples) for waveform playback */The loop length, whatever length was loaded (it was 230400 before the blab-lab fork).
maxPBSize = Audapter('getMaxPBLen');if length(mbw) > maxPBSize mbw = mbw(1 : maxPBSize);end Audapter(3, 'datapb', mbw, 0);Callers truncate to the maximum but never pass the actual length to the playback loop.
Only since blab b2.4 (commit 3c90a3a, 6 April 2026), which enlarged the playback buffer from 230,400 to 480,000 samples. Data collected with earlier versions are not affected: the bundled babble was truncated to the old buffer and looped seamlessly.
Workaround and fix
Workarounds for current sessions (any one): (1) reload the noise with Audapter('setParam', 'datapb', w, 1) before each trial, which rewinds it, as blab's per-trial runners do (a trial then never reaches the 10 s loop point); (2) pad or trim the noise to exactly 480,000 samples before loading it, for example by appending the bundled babble's own first 770 samples; (3) apply the loop-at-length patch below. This applies only to data collected with b2.4 or later (since April 2026). Fix: store the loaded length when datapb is set and wrap there (report/patches/i-01-datapb-loop-length.patch), and rewind pbCounter in reset(). Check past data: if signalOut was saved for fb 2–5 trials, look for runs of exact zeros; otherwise the gap's position can be reconstructed from the trial durations, since the noise position runs on across trials.
How we know
- H interface check:
scratch/interface/t_iface.mT1fb 2: mean |out| 0.100 for 0–4.9 s, 0.000 for 5.1–9.9 s - H report export (simonSingleWord v2 masking phase, 24 trials, both buffer sizes):
report_i01_session.m - H public experiments run with their own scripts (labrun):
labrun/README.mdLABRUN-3, VOICES-2: 15.4 ms holes every 10.0 s of noise running time in the modelExpt (9–12) and vsaGeneralize (22–34) full sessions with eight AI-generated voices (holes under the voice are not counted) - H real session structure:
exp_noise_session.mEXP-3: 4 gaps of 16 ms in 24 trials on the current build, none with the pre-b2.4 buffer - H report export (generic case, a 5 s noise file):
report_i01.m
Reproduce cd audit/harness && ./run-oct.sh report_i01_session.m