API reference
Sessions and I/O
Session discovery and BWF metadata for AmbiX recordings.
A session is a folder of WAV files from one recording occasion. Files whose BWF timestamps chain end-to-start (recorder 2 GB splits) are treated as one continuous take; otherwise they are separate takes on a common absolute timeline (seconds since the session's first midnight).
Take
dataclass
Source code in src/ambiscape/io.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
wyzx
property
Column indices of (W, Y, Z, X) for this take's convention.
Session
dataclass
Source code in src/ambiscape/io.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
clock(t)
Absolute seconds -> 'DD Mon HH:MM:SS' string.
Source code in src/ambiscape/io.py
95 96 97 98 | |
read_bext(path)
Parse the BWF 'bext' chunk (pure python RIFF walk).
Source code in src/ambiscape/io.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
channel_order(bext_description)
Detect B-format convention from the H3-VR's zTRK tags in the bext description: 'ambix' (W,Y,Z,X) or 'fuma' (W,X,Y,Z). Defaults to 'ambix' when no tags are present.
Source code in src/ambiscape/io.py
44 45 46 47 48 49 50 51 52 53 54 55 56 | |
open_session(folder)
Scan a session folder.
If calibration.json contains clock_offset_s, that many seconds are
added to every take's start time — the fix for a recorder whose clock was
found to be off (positive offset = clock was slow). All clock-labeled
outputs (figures, annotations, reports) then agree on corrected time.
Source code in src/ambiscape/io.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
open_recording(path)
Open a single WAV file as a one-take session ("scene").
The folder-as-session model of :func:open_session assumes every WAV in
a folder belongs to one recording occasion on a shared clock. A
contributed corpus is often the opposite: one folder per recordist, each
holding many independent one-off scenes from different places and dates.
This opens exactly one file as its own session (day0 = that file's BWF
date), so each scene can go through the full pipeline on its own. The
session name is the file stem.
Source code in src/ambiscape/io.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | |
read_span(sess, t0, dur, dtype='float32')
Read [t0, t0+dur) seconds (session time) from whichever take covers it.
Source code in src/ambiscape/io.py
170 171 172 173 174 175 176 177 178 179 180 | |
export_segment(sess, t0, dur, out_path)
Bit-exact 4-channel excerpt [t0, t0+dur) to a WAV.
Samples are copied in the source's own PCM subtype (no float round
trip), so the excerpt is archival: the representative segments of a
report stay citable against the raw takes. The span must lie within one
take (recorder 2 GB splits chain seamlessly only in read_span's
float path). Returns the output path.
Source code in src/ambiscape/io.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | |
stereo_preview(x, wyzx=(0, 1, 2, 3), az_deg=90.0)
Side-facing cardioid stereo decode of an AmbiX block, for previews.
Left/right cardioids at ±az_deg in the horizontal plane:
0.5 * (W ± sin(az) * Y) (SN3D). Returns an (n, 2) float array —
write it with soundfile for a listenable preview of an exported
segment.
Source code in src/ambiscape/io.py
209 210 211 212 213 214 215 216 217 218 219 220 | |
Feature extraction
Streaming per-second feature extraction from AmbiX B-format WAV files.
Designed for arbitrarily long recordings: files are read in 60-s blocks and never held in memory. Per second: broadband and A-weighted fast levels (125 ms), octave-band powers, spectral centroid/flatness, a 96-band log-frequency spectrogram row, per-octave pseudo-intensity vectors, broadband DOA (azimuth, elevation) and diffuseness. Per minute: full-resolution mean PSD (for narrowband hum tracking and fingerprinting).
Channel order is AmbiX ACN (W, Y, Z, X) as written by the Zoom H3-VR.
a_weighting_sos(fs)
IEC 61672 A-weighting as SOS (bilinear transform of the analog filter).
Source code in src/ambiscape/features.py
32 33 34 35 36 37 38 39 40 41 42 43 | |
extract_take(take, verbose=False)
Run the streaming extractor over one file; returns feature arrays.
Source code in src/ambiscape/features.py
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | |
extract_session(sess, out_dir, verbose=True)
Extract features for every take; save one npz per take. Returns paths.
Source code in src/ambiscape/features.py
182 183 184 185 186 187 188 189 190 191 192 193 194 195 | |
load_features(npz_paths)
Concatenate per-take feature files onto one absolute time axis.
Source code in src/ambiscape/features.py
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
Descriptors, events, reverberation
Session-level descriptors, event detection, and reverberation estimation.
Descriptor conventions follow the Intercontinental-database report (2026-07-10): fast level = 125 ms RMS on W; events = fast level exceeding a running background (10th percentile in a sliding 60 s window) by >= 8 dB for
= 0.25 s; diffuseness/DOA from per-second pseudo-intensity vectors.
detect_events(fast_db, fast_dt, thresh_db=8.0, min_dur=0.25)
Return list of dicts (onset index, length, peak index, exceedance).
Source code in src/ambiscape/analysis.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
intermittency_ratio(level_db, dt, k_db=3.0)
Intermittency ratio IR (Wunderli et al. 2016), in percent.
The share of total sound energy carried by "events": frames whose
level exceeds the whole-period Leq by k_db (3 dB per the original
definition, there on 1 s LAeq frames — here on the fast frames, which
is equivalent for events longer than the frame). IR ≈ 0 for steady
scenes (drones, dense traffic), high for scenes whose energy arrives
in distinct events (rail, church bells, sparse traffic).
Source code in src/ambiscape/analysis.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
decay_metrics(x, fs, bands=((250, 500), (500, 1000), (1000, 2000), (2000, 4000), (4000, 8000)))
T60, EDT, C50, C80 (dB) and D50 per octave band from an impulse.
Same truncated-Schroeder machinery as :func:decay_time (which is
kept unchanged — its output feeds frozen corpus reports), plus the
standard companions: EDT from the 0…−10 dB fit (perceived
reverberance), clarity C50/C80 = 10·log10 of the early/late energy
ratio at 50/80 ms, and definition D50 = early fraction at 50 ms.
Returns {band: {"T60", "EDT", "C50", "C80", "D50", "dr_db"}}.
Source code in src/ambiscape/analysis.py
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | |
circular_stats(az_deg, weights=None)
Energy-weighted circular mean (deg) and resultant length R.
Source code in src/ambiscape/analysis.py
122 123 124 125 126 | |
summarize(F)
Session descriptor dict from concatenated features (see features.load_features).
Source code in src/ambiscape/analysis.py
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | |
decay_time(x, fs, bands=((250, 500), (500, 1000), (1000, 2000), (2000, 4000), (4000, 8000)))
T60 estimates from an impulse via truncated Schroeder integration.
The decay is truncated at the first re-attack (envelope rising >= 8 dB above its running minimum) and at the noise floor; a linear fit of -5 dB .. max(-35 dB, floor + 8 dB) is extrapolated to 60 dB. Returns {band: (T60, dynamic_range_db)}.
Source code in src/ambiscape/analysis.py
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 | |
pick_segments(F, n=4, seg_s=600.0)
Suggest representative windows: quietest, most active, median-typical, and (if present) the strongest state transition.
Source code in src/ambiscape/analysis.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
Figures
Session figures.
Names follow ambiviz conventions where the plots correspond (https://github.com/fisheggg/ambiviz): the azimuth-vs-time panel is an anglegram and the polar energy histogram a directogram, computed here from streaming per-second pseudo-intensity features rather than a full AEM, so they scale to many-hour recordings. For rich spherical maps (AEM) of short excerpts, export a segment and use ambiviz directly.
overview(F, out_path, title='', clock=None)
4-row overview: fast level + background; log spectrogram; anglegram (energy-weighted azimuth x time); diffuseness. Takes separated by more than 10 minutes get their own column (width ~ duration).
Source code in src/ambiscape/figures.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
ltas_percentiles(F, out_path, title='')
10/50/90th percentile long-term spectra (background vs foreground).
Source code in src/ambiscape/figures.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
directogram(F, out_path, title='')
Polar azimuth histograms: foreground (loudest 25 %) vs background (quietest 25 %) energy-weighted. ambiviz-style directogram.
Source code in src/ambiscape/figures.py
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 | |
Spectral foreground
Per-band running background and spectral foreground decomposition.
The broadband event detector in :mod:analysis misses band-limited events
riding on a loud bed in other bands (distant bells over traffic move their
octave a few dB while the broadband level barely changes). This module works
on the cached 1 Hz log-band spectrogram (F["logspec"], 96 bands):
band_background— running low-percentile background per band;foreground— dB exceedance and the per-second foreground fraction (share of total power sitting above the spectral background);spectral_events— connected spectro-temporal regions of exceedance (time x band blobs), each with onset, duration, band span, and peak rise;summarize_foreground— session descriptors appended to the analyze summary and README.
All functions are pure array transforms on cached features — no audio pass.
band_background(logspec, win_s=300.0, pct=10.0)
Running pct-percentile background per log band.
logspec is the (nsec, nband) power array from the cached features;
the window is in seconds (= rows). Returns the same shape.
Source code in src/ambiscape/background.py
26 27 28 29 30 31 32 33 34 35 | |
foreground(logspec, bg)
dB rise above the spectral background and per-second foreground fraction (share of total power more than 3 dB above background).
Source code in src/ambiscape/background.py
38 39 40 41 42 43 44 | |
spectral_events(rise_db, logf, thresh_db=6.0, min_dur_s=2.0, min_bands=2)
Connected regions of band-wise exceedance as event dicts.
A spectral event is a blob in the (time x band) plane where the rise
exceeds thresh_db, lasting >= min_dur_s and spanning >=
min_bands bands. Returns onset/duration (s), band span (Hz), and
peak rise (dB), sorted by onset.
Source code in src/ambiscape/background.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | |
masking_index(F, active, quiet)
How much a dominant source hides the rest of the field — the "lo-fi" claim as a number.
active/quiet are boolean second-masks (source on / off). Per log
band, the floor elevation is the rise of the active-state median level
above the quiet-state median: ambient sounds in that band must now
exceed the elevated typical floor to be audible. Returns the median and
maximum elevation over 250 Hz–8 kHz, the fraction of bands elevated by
more than 6 dB, and the per-band curve.
Source code in src/ambiscape/background.py
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | |
source_fingerprint(F, active, quiet, fmin=25.0, fmax=16000.0, min_prom_db=6.0, max_peaks=20)
Spectral fingerprint of a source: active-minus-quiet mean PSDs.
active/quiet are boolean masks over the minutes of
F["minspec"] (source clearly on / clearly off, e.g. from
:func:ambiscape.states.state_segments). The rise curve is the dB
difference of the two mean spectra — the source's own spectrum with the
room ambience subtracted. Narrowband peaks of the rise are extracted and
passed through the harmonic sieve, so a blade-pass or compressor comb
reports its base frequency.
Returns dict: freqs/rise_db (the full curve), rise_max_db/
rise_max_hz (the turbulence hump), peaks (list of
{f_hz, rise_db}), and comb ({f0_hz, harmonicity} of the peak
set, f0_hz None when there are no peaks).
Source code in src/ambiscape/background.py
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | |
summarize_foreground(F, win_s=300.0)
Foreground descriptors for the analyze summary.
Source code in src/ambiscape/background.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
Machine states
Machine states: on/off segmentation, switch points, and duty cycles.
Domestic and mechanical sources (ventilation, fridges, pumps, HVAC) show up in a soundscape as a state — a band-limited floor that is either present or absent — rather than as events. This module segments a band-level timeline into those states from the cached features, no audio pass:
band_level— per-second dB level in a frequency band from the cached log-band spectrogram (the "machine band" of a source, e.g. 250–1000 Hz for a ventilation unit);state_segments— two-state (on/off) segmentation of that level with an automatic bimodal threshold, hysteresis, and a minimum duration, each segment carrying its median level and within-state stability (SD);switch_points— the transitions between segments (the 07:53:55 switch-off moments);duty_cycle— cycle statistics of a cycling machine (a fridge's ~24 min period at ~50 % duty): period, duty fraction, cycle count.
Typical use: segs = state_segments(band_level(F, (250, 1000))) and mask
other analyses (fingerprints, masking, taxonomy states) by segment.
band_level(F, band=(250.0, 1000.0))
Per-second dB level in band (Hz) from the cached logspec.
Source code in src/ambiscape/states.py
30 31 32 33 34 35 | |
bimodal_threshold(level_db)
Otsu's threshold on the level histogram: the split that best separates the two modes of an on/off timeline.
Source code in src/ambiscape/states.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | |
state_segments(level_db, thresh_db=None, smooth_s=11.0, hysteresis_db=1.0, min_dur_s=30.0)
Two-state segmentation of a 1 Hz band-level timeline.
The level is median-smoothed over smooth_s; the threshold defaults
to the bimodal (Otsu) split of the histogram — pass thresh_db when
the timeline is not clearly bimodal. Hysteresis of hysteresis_db
around the threshold suppresses chatter, and segments shorter than
min_dur_s are merged into their neighbors. Returns segments in time
order as dicts: state ('on'/'off'), t0_s/dur_s (seconds into the
timeline), median_db, and sd_db (within-state stability of the raw
level — a running machine is steady, ambience is not).
Source code in src/ambiscape/states.py
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | |
switch_points(segments)
Transitions between consecutive segments: time and direction ('on' = machine starts, 'off' = machine stops).
Source code in src/ambiscape/states.py
106 107 108 109 110 111 112 113 114 | |
duty_cycle(segments)
Cycle statistics of a cycling machine from its state segments: median period (consecutive on-starts), duty fraction (median on-time over period), and the number of complete cycles observed.
Source code in src/ambiscape/states.py
117 118 119 120 121 122 123 124 125 126 127 128 129 | |
Electric network frequency (ENF)
Electric network frequency (ENF) traces from mains hum.
Buildings hum at the mains frequency and its harmonics (50 Hz nominal in Europe; magnetostriction is strongest at 100 Hz), and the grid's actual frequency wanders by tens of millihertz as load and generation balance. A long indoor recording therefore carries a continuous, involuntary log of the grid — usable as a session descriptor (how electrified is this room?), as a source separator (a "50 Hz" line that does not follow the grid is a rotor, not electricity), and forensically: matched against published grid-frequency archives, an ENF trace timestamps a recording independently of the recorder clock.
hum_peak— sub-millihertz line frequency in one mono window (zero-padded FFT + parabolic interpolation) with its rise over the local spectral floor;enf_track— the trace: windows everystep_sacross a whole session, one or more harmonics, all scaled to the fundamental;enf_summary— mean/SD/max deviation, coverage, and cross-harmonic agreement — the latter is the authenticity check (independent acoustic lines reporting the same electrical frequency).
Needs raw audio (one streaming pass over the W channel); the cached per-minute spectra are far too coarse (5.9 Hz bins) for millihertz work.
hum_peak(w, fs, nominal=50.0, search_hz=0.2, nfft_mult=4)
Frequency and floor-rise of the strongest line near nominal.
Zero-padded FFT of the Hann-windowed mono signal, parabolic
interpolation of the log-power peak within nominal ± search_hz.
Returns (freq_hz, rise_db); rise is measured against the median
power in a ±1.5 Hz-widened neighborhood, so a genuine line scores
high even on a rumble shoulder.
Source code in src/ambiscape/enf.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | |
enf_track(sess, step_s=300.0, win_s=60.0, nominal=50.0, search_hz=0.2, harmonics=(1, 2), channel=0)
Track the mains hum across a whole session.
One window of win_s every step_s, per take (windows start 1 s
into each take and reads shorter than 90 % of the window are skipped —
recorder 2 GB splits overlap by a fraction of a second, so a read at an
exact take start can return a sliver of the previous file). Each
harmonic k is searched at k*nominal ± k*search_hz and reported
scaled to the fundamental.
Returns {"t": absolute seconds, "f": {k: freq_hz/k}, "rise":
{k: rise_db}}.
Source code in src/ambiscape/enf.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
enf_summary(track, nominal=50.0, min_rise_db=6.0)
Descriptors of an ENF trace.
Statistics use only windows where the first harmonic rises
min_rise_db above the floor; coverage is the fraction of
windows that qualify. harmonic_agreement_mhz is the median absolute
difference between the first two tracked harmonics (fundamental-scaled)
where both are detected — millihertz-level agreement authenticates the
line as electrical.
Source code in src/ambiscape/enf.py
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | |
Ecoacoustic indices
Ecoacoustic indices from the cached log-band spectrogram.
The standard soundscape-ecology battery, so sessions are reportable in the idiom global acoustic-monitoring corpora expect. All computed from the cached 1 Hz features — no audio pass:
- ACI (acoustic complexity, Pieretti et al. 2011): per-band temporal variation |ΔP|/ΣP summed over bands, averaged over 5-min chunks — sensitive to biophonic modulation, blind to steady drones;
- ADI / AEI (diversity / evenness, Villanueva-Rivera et al. 2011): Shannon entropy / Gini coefficient of the occupancy of 1 kHz bins (fraction of cells above a threshold re the band maximum);
- NDSI (Kasten et al. 2012): (biophony − anthropophony) / (biophony + anthropophony) with the conventional bands 2–8 kHz vs 1–2 kHz, in [−1, 1];
- BI (bioacoustic index, Boelman et al. 2007): area of the mean 2–8 kHz dB spectrum above its minimum;
- acoustic entropy H (Sueur et al. 2008): spectral entropy × temporal (envelope) entropy, in [0, 1].
Caveats for an ambisonic indoor corpus: these indices were designed for outdoor terrestrial monitoring; report them for comparability, but read NDSI/BI as "energy in the bird band", not as proof of birds — a 4 kHz ventilation hiss scores as "biophony". Combine with the taxonomy layer before interpreting.
aci(F, chunk_s=300.0)
Acoustic complexity index, mean over chunk_s chunks.
Source code in src/ambiscape/ecology.py
39 40 41 42 43 44 45 46 47 48 | |
adi_aei(F, **kw)
Acoustic diversity (Shannon, normalized) and evenness (Gini).
Source code in src/ambiscape/ecology.py
68 69 70 71 72 73 74 75 76 77 | |
ndsi(F, anthro=(1000.0, 2000.0), bio=(2000.0, 8000.0))
Normalized difference soundscape index in [−1, 1].
Source code in src/ambiscape/ecology.py
80 81 82 83 84 85 86 | |
bioacoustic_index(F, band=(2000.0, 8000.0))
Boelman BI: area of the mean band dB spectrum above its minimum.
Source code in src/ambiscape/ecology.py
89 90 91 92 93 94 | |
acoustic_entropy(F)
Sueur H = spectral entropy × temporal entropy, in [0, 1].
Source code in src/ambiscape/ecology.py
97 98 99 100 101 102 103 104 105 106 | |
indices(F)
The full battery as one dict.
Source code in src/ambiscape/ecology.py
109 110 111 112 113 114 115 116 117 118 119 | |
summarize_ecology(F)
Alias of :func:indices for the analyze-summary pipeline.
Source code in src/ambiscape/ecology.py
122 123 124 | |
Biophony
Biophony measures: capturing nature and animal sounds by structure.
The ecoacoustic battery in :mod:ambiscape.ecology (NDSI, BI, ADI) reads
energy in a band; it cannot tell a dawn chorus from a ventilation hiss.
Biophony is distinguished by how it is structured — narrowband, tonal,
bursty in time, and (in an ambisonic recording) arriving from many elevated
bearings at once. This module measures that structure from the cached
features, no audio pass:
narrowband_activity— persistent narrow spectral peaks in the bird band per minute (from the per-minute high-resolution PSD): birdsong is narrowband and tonal, wind and machines are broadband;band_temporal_entropy— Sueur temporal entropy of the bird-band envelope: structured vocalization concentrates energy in time (low Ht), a steady noise floor spreads it evenly (Ht → 1);band_activity— fraction of seconds and event rate where the bird band rises above its own running background (Towsey-style acoustic activity), restricted to the biophony band;spatial_dispersion— the ambisonic layer no other corpus tool has: the directional entropy and above-horizon energy fraction of the bird-band foreground — a chorus of many birds from many elevated directions is unmistakable, and it cross-checks a suspicious NDSI.
summarize_biophony returns the descriptor set for the analyze summary.
Caveats: these are acoustic-structure proxies, not detections. A tonal
alarm, a whistling kettle, or a squealing fan belt can mimic biophonic
structure; confirm species with the BirdNET hook
(:func:ambiscape.ml.birdnet_session, [ml] extra) on the hi-fi
windows. The default band (2–11 kHz) targets temperate birdsong; widen it
(insects reach 8–16 kHz, many mammals sit below 2 kHz) per habitat.
narrowband_activity(F, band=BIRD_BAND, min_prom_db=6.0, min_peaks=2)
Per-minute count of narrowband tonal peaks in band.
Uses :func:ambiscape.tonality.tonal_peaks on each row of the cached
per-minute PSD. Returns median peaks/min, the per-minute counts, and
the fraction of minutes with at least min_peaks (an "active"
biophonic minute).
On its own this is a weak biophony discriminator and must not be read
as a bird count: (a) minute-averaging smears frequency-swept birdsong,
so a busy dawn chorus shows only a modest per-minute peak count while
its max and active_minute_fraction spike; (b) steady machine
harmonics are narrowband too and score just as high. What separates
birds from machines is the combination with the temporal
(:func:band_temporal_entropy) and spatial
(:func:spatial_dispersion) measures — biophony is narrowband and
bursty and spread across elevated bearings; a machine tone is
narrowband but steady, low, and directional.
Source code in src/ambiscape/biophony.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
band_temporal_entropy(F, band=BIRD_BAND)
Sueur temporal entropy Ht of the bird-band envelope, in [0, 1].
Low = energy concentrated in time (structured vocalization); near 1 = even over time (steady band, no biophonic events).
Source code in src/ambiscape/biophony.py
91 92 93 94 95 96 97 98 99 100 | |
band_activity(F, band=BIRD_BAND, k_db=3.0, bg_win_s=300.0, min_dur_s=1)
Acoustic activity of the bird band above its running background.
The band envelope (dB) is compared to a running 10th-percentile
background over bg_win_s; seconds exceeding it by k_db are
active. Returns the active-second fraction, event rate per minute
(runs of >= min_dur_s active seconds), and median event duration.
Source code in src/ambiscape/biophony.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
spatial_dispersion(F, band=BIRD_BAND, nbins=36, limit_deg=10.0)
Directional spread and elevation of the bird-band foreground.
The azimuth histogram is weighted by the per-second bird-band
foreground energy (band level above its running background), so only
seconds carrying biophonic energy contribute. Returns the normalized
directional entropy (0 = one bearing, 1 = all around) and the fraction
of that foreground energy arriving from above limit_deg elevation
(birds aloft) versus at/below the horizon.
Source code in src/ambiscape/biophony.py
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | |
summarize_biophony(F, band=BIRD_BAND, min_active_fraction=0.02)
Biophony descriptors for the analyze summary.
The spatial biophony descriptors are only meaningful when the bird
band actually carries foreground energy: in a quiet, birdless room a
trickle of high-frequency energy that happens to arrive from above would
otherwise read as above_horizon_fraction = 1.0, a false positive.
When the band-active fraction is below min_active_fraction the
directional and horizon descriptors are therefore reported as None
rather than as spurious numbers.
Source code in src/ambiscape/biophony.py
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |
State-resolved descriptors
State-resolved descriptors: summarize each state of a session separately.
A single descriptor row for a multi-state session is a duration-weighted average of things that never coexisted — the Haarlem loft's row is dominated by the 9-hour air-pump night and barely reflects the hi-fi afternoon. This module slices the cached features by time and runs the full summary pipeline on each state, so "vent on" and "vent off" (or day / night, or any supplied intervals) get their own complete descriptor set.
slice_features— a sub-Frestricted to a set of time intervals, valid across every feature axis (1 s, 125 ms fast, 20 ms envelope, per-minute PSD) for all thesummarize_*functions;full_summary— the merged descriptor dict (level/event, spectral foreground, ecoacoustic, spatial, biophony) — the same setanalyzewrites, computed on anyF;resolve—{state: full_summary}for a dict of named intervals;machine_states/diel_states— auto-discover the states from a machine band (via :func:ambiscape.states.state_segments) or the wall clock (day / night).
Event detection and percentiles run per state, so an interval need not be contiguous; a state shorter than a few frames is skipped.
intervals_from_mask(t, mask)
Contiguous [start, stop) intervals (in t units) of a boolean
mask over the 1 s frames.
Source code in src/ambiscape/resolve.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | |
slice_features(F, intervals)
Restrict cached features to intervals (absolute seconds).
Returns a sub-F with every time-indexed array masked to the
intervals and every scalar/axis array copied through — accepted by all
the summarize_* functions.
Source code in src/ambiscape/resolve.py
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | |
full_summary(F)
The complete analyze descriptor set for any F (no calibration).
Source code in src/ambiscape/resolve.py
92 93 94 95 96 97 98 99 100 | |
resolve(F, states, min_frames=30)
{state: full_summary} for a dict of {label: intervals}.
States whose sliced 1 s length is below min_frames are skipped
(too short for stable percentiles/events).
Source code in src/ambiscape/resolve.py
103 104 105 106 107 108 109 110 111 112 113 114 | |
machine_states(F, band=(250.0, 1000.0), min_dur_s=120.0, labels=('machine_on', 'machine_off'))
Auto-discover on/off states from a machine band.
Segments the band level with :func:ambiscape.states.state_segments
and returns {labels[0]: on-intervals, labels[1]: off-intervals}
in absolute seconds (empty sides dropped).
Source code in src/ambiscape/resolve.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | |
auto_states(F, band=(250.0, 1000.0), min_dur_s=180.0, min_step_db=4.0, min_dur_frac=0.05)
Machine on/off states only if the session is genuinely two-state.
The gate for automatic use in analyze: returns the
:func:machine_states split when both states last at least
min_dur_s (and min_dur_frac of the session) and the band-level
step between them is at least min_step_db; otherwise {} (a
single-state session gets no state rows). Prevents spurious splitting of
a flat, steady soundscape.
Source code in src/ambiscape/resolve.py
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | |
diel_states(F, sess, night=(22, 6), labels=('night', 'day'))
Split a session into night / day by the wall clock.
night is (start_hour, end_hour) wrapping midnight; uses the
session's day0 to turn absolute seconds into hour-of-day.
Source code in src/ambiscape/resolve.py
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | |
Corpus catalog
Corpus aggregation: one cross-session table from cached summaries.
Every ambiscape analyze writes <session>/analysis/summary.json.
This module collects them across a corpus folder into one table — CSV for
analysis, a transposed Markdown table (descriptor rows, session columns) for
a consolidated report — plus simple ranking and outlier helpers. No audio,
no features: it reads only the cached summaries, so a whole corpus
aggregates in milliseconds.
The Markdown layout follows the Intercontinental-database
CONSOLIDATED.md convention. Sessions with differing descriptor sets
(older caches, optional modules) are handled by taking the union of keys
and leaving blanks where a session lacks one.
collect(corpus_dir, pattern='*/analysis/summary.json', include_states=False)
Map session name → summary dict for every summary under corpus_dir.
The session name is the top-level folder (the parent of analysis/).
Unreadable or malformed files are skipped. With include_states, each
session's analysis/states.json (if present) contributes extra
"<session>::<state>" rows right after the pooled session row — the
state-resolved corpus view.
Source code in src/ambiscape/catalog.py
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
to_csv(collected, path, keys=None)
Write a session-per-row CSV (union of keys, blanks for missing).
Uses the standard :mod:csv writer, so any field containing a comma,
quote, or newline is quoted correctly (a value like "interior,
morning" round-trips) — consistent with every other CSV the toolkit
emits.
Source code in src/ambiscape/catalog.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | |
to_markdown(collected, keys=None, labels=None)
Transposed Markdown table: one row per descriptor, one column per
session (the consolidated-report layout). labels optionally maps
descriptor keys to human labels.
Source code in src/ambiscape/catalog.py
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
rank(collected, key, descending=True)
(session, value) pairs sorted by key (numeric sessions only).
Source code in src/ambiscape/catalog.py
114 115 116 117 | |
outliers(collected, key, z=1.5)
(session, z-score) for sessions more than z SDs from the mean
on key, most extreme first — the cheap "what stands out" query.
Source code in src/ambiscape/catalog.py
120 121 122 123 124 125 126 127 128 129 130 131 132 | |
Longitudinal analysis
Longitudinal analysis: how a place sounds across weeks, months, a year.
The unit here is the dated session summary, not the audio. A year-long
study is best run as many short sessions---one a day, say, as in the
StillStanding archive---each analyzed to a small summary.json; a year is
then 365 tiny rows, so the longitudinal analysis is inherently out-of-core
however large the underlying audio was. (A single continuous multi-month
recording exceeds what the in-memory feature pipeline can hold; the
supported path for year-scale work is to segment it into per-day sessions
first.)
collect_series--- read every session's summary under a corpus, ordered by date (from adatefield, else parsed from the folder name), into per-descriptor time series;decompose--- additive split of one descriptor into a slow trend (day-windowed rolling median), a repeating seasonal component (monthly climatology of the detrended series), and the residual;seasonal_climatology/trend_slope--- the two components on their own (per-month means; long-term change per year);summarize_longitudinal--- trend per year, seasonal amplitude, peak and trough months, span;render--- a figure: the descriptor over time with its trend, plus the monthly climatology.
Everything is numpy-only. The motivating example is already in the StillStanding data: bird mentions peak in July and fall to zero in winter--- not because the birds leave, but because the windows close.
parse_date(name)
Parse a leading YYYY-MM-DD or YYYYMMDD date from a string.
Source code in src/ambiscape/longitudinal.py
41 42 43 44 45 46 47 48 49 50 | |
collect_series(corpus_dir, keys=None, pattern='*/analysis/summary.json')
Dated, date-ordered descriptor time series from a corpus of sessions.
Each session's date comes from the summary's date field if present,
otherwise from a date parsed out of the session folder name; sessions
with no resolvable date are skipped. Returns {"dates": [date, ...],
"sessions": [name, ...], "series": {key: np.array}} with all arrays in
date order. keys limits the descriptors (default: the union across
sessions).
Source code in src/ambiscape/longitudinal.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |
rolling_trend(dates, values, window_days=365.0)
Day-windowed rolling median: a slow trend robust to spikes and to
irregular sampling (each point is the median of all points within
window_days of it).
Source code in src/ambiscape/longitudinal.py
106 107 108 109 110 111 112 113 114 115 116 | |
seasonal_climatology(dates, values)
Per-calendar-month mean and count of a series (month 1..12).
Source code in src/ambiscape/longitudinal.py
119 120 121 122 123 124 125 126 127 128 | |
decompose(dates, values, window_days=365.0)
Additive decomposition: trend + seasonal (monthly) + residual.
Returns date-ordered arrays plus the monthly climatology (mean of the
detrended series per calendar month, mean-centered so the seasonal
component sums to ~0).
Source code in src/ambiscape/longitudinal.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
trend_slope(dates, values)
Long-term linear change per year (least-squares slope × 365.25).
Source code in src/ambiscape/longitudinal.py
155 156 157 158 159 160 161 162 163 | |
summarize_longitudinal(dates, values, window_days=365.0)
Trend/seasonal descriptors of one dated series.
Source code in src/ambiscape/longitudinal.py
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 | |
render(dates, values, out_path, key='', window_days=365.0)
Two-panel figure: series + rolling trend over time, and the monthly climatology.
Source code in src/ambiscape/longitudinal.py
191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 | |
run_corpus(corpus_dir, out_dir, keys=None, window_days=365.0)
CLI driver: per-descriptor longitudinal summaries + a figure for each,
writing longitudinal.json.
Source code in src/ambiscape/longitudinal.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | |
Visual features
Per-frame visual features: the light/vision analogue of the audio deposit.
The AMBIENT project treats a room as an audio-visual subject, and a room's
look has a diurnal rhythm just as its sound does. This module extracts a
compact descriptor from a single video frame so a camera can log visual
behaviour rather than store imagery --- the same "features, not recordings"
privacy stance as :mod:ambiscape.capture. It is numpy-only (no camera or
OpenCV dependency); frame grabbing lives in the capture rig, the feature
definitions live here so they are versioned and tested with ambiscape.
Per frame (:func:frame_features):
- brightness / brightness_sd --- mean and spread of Rec.709 luma (0 = dark, 1 = bright): the room's overall light level and its unevenness;
- r_frac / g_frac / b_frac, warm_cool_ratio --- colour balance and a warm/cool proxy (daylight vs incandescent vs the blue of a screen);
- saturation, colourfulness --- how colourful the scene is (Hasler--Susstrunk colourfulness), near zero for a grey room;
- spatial_entropy --- entropy of a coarse brightness grid: 1 when the room is evenly lit, low when light is concentrated (a lamp, a window) --- the visual analogue of acoustic diffuseness;
- bright_centroid_x / _y --- the luma-weighted centre of light in the frame (0..1): where the light comes from, a visual direction-of-arrival.
:func:frame_delta is a motion proxy (mean absolute luma change between two
frames); :func:summarize_vision rolls a day of per-frame features into a
vis_-prefixed summary that joins the audio summary.json.
luma(rgb)
Rec.709 luma of an RGB frame, in [0, 1].
Source code in src/ambiscape/vision.py
47 48 49 50 | |
frame_features(rgb, grid=3)
Visual descriptor of one RGB frame (uint8 0..255 or float 0..1).
Source code in src/ambiscape/vision.py
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | |
frame_delta(prev_rgb, cur_rgb)
Mean absolute luma change between two frames (motion proxy, 0..1).
Source code in src/ambiscape/vision.py
101 102 103 | |
summarize_vision(features, motion=None)
Roll a day of per-frame feature dicts into a vis_ day summary.
Source code in src/ambiscape/vision.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | |
Circular statistics
Circular statistics, shared by spatial (azimuth) and rhythm (phase) code.
Angles are radians throughout; the degree-facing wrapper lives in
:func:ambiscape.analysis.circular_stats, and period-facing helpers here
convert times to phases. The resultant length R in [0, 1] measures
concentration; circular SD = sqrt(-2 ln R); the Rayleigh test gives the
probability of R under uniformity (p ~ exp(-n R^2), adequate for n >= 10).
mean_resultant(angles, weights=None)
Weighted circular mean (rad) and resultant length R.
Source code in src/ambiscape/circstats.py
16 17 18 19 20 21 | |
circular_sd(R)
Circular standard deviation (rad) from a resultant length.
Source code in src/ambiscape/circstats.py
24 25 26 | |
rayleigh_p(R, n)
Rayleigh-test p-value (uniformity null), small-p approximation.
Source code in src/ambiscape/circstats.py
29 30 31 32 33 | |
phase_stats(times, period)
Circular statistics of event times folded at period.
Returns mean phase (cycles), R, circular SD in seconds, and the Rayleigh p-value — the standard summary for one strike stream.
Source code in src/ambiscape/circstats.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | |
relative_phase(times, ref_times, period)
Phase of each event relative to the preceding reference event.
The per-event lock between two streams sharing one period: mean offset (cycles and seconds), R, and circular SD in seconds. R near 1 means the two streams are phase-locked at strike level.
Source code in src/ambiscape/circstats.py
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
Modulation profile
Environmental rhythm: multi-scale envelope modulation profile.
Soundscapes are rhythmic on very different time scales at once — strike patterns (micro), traffic waves and surf (meso), duty cycles of machines and human activity (macro). This module measures all three from cached envelopes, no audio pass:
- micro (0.5–20 Hz) from the 20 ms broadband envelope (
env_hi, extractor ≥ 0.2 caches; older caches fall back to the 8 Hz fast level, which limits micro to < 4 Hz); - meso (0.01–0.5 Hz) from the 125 ms fast level;
- macro (below 0.01 Hz, floor set by session length) from the 1 s RMS.
profile returns, per scale, a log-frequency modulation spectrum with the
dominant modulation frequency, its prominence, and the band modulation depth.
modulation_spectrogram computes the windowed version — the "rhythm
spectrogram of the day" — and render writes the combined figure.
modulation_spectrum(env, dt, fmin, fmax, n_bins=48)
Welch modulation spectrum of a linear-power envelope, log-resampled.
The envelope is normalized to zero-mean unit-mean (x = env/mean − 1) so spectra are comparable across levels; returns (freqs, power density).
Source code in src/ambiscape/modulation.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
profile(F)
Three-scale modulation profile from cached features.
Source code in src/ambiscape/modulation.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
modulation_spectrogram(env, dt, win_s=600.0, step_s=120.0, fmin=0.02, fmax=20.0, n_bins=64)
Windowed modulation spectra: the rhythm spectrogram of the session.
Returns (t_centers, mod_freqs, S) with S in dB relative to each window's median (so rhythmic structure reads as ridges regardless of level).
Source code in src/ambiscape/modulation.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | |
render(F, prof, out_path, title='', clock=None)
Combined figure: per-scale spectra + rhythm spectrogram.
Source code in src/ambiscape/modulation.py
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | |
run_session(sess, out_dir)
CLI driver: profile + figure + modulation.json.
Source code in src/ambiscape/modulation.py
155 156 157 158 159 160 161 162 163 164 165 | |
Tonality and harmonicity
Tonalness timeline, harmonic sieve, and inharmonicity.
Works entirely from the cached per-minute mean PSD (minspec):
tonal_peaks— prominent narrowband components per minute (dB above a running spectral floor), the raw material for everything below;tonal_tracks— peaks linked across minutes into tracks (a hum, a bell partial, a beep), each with duration, median frequency, and cents drift: the tonalness timeline;harmonic_sieve— best f0 explaining a minute's peak set as a harmonic series; the unexplained remainder is the inharmonic tonal content. Voices, engines, and music score high harmonicity; bells score low (their partial series 1 : 2 : 2.4 : 3 : 4 is not harmonic);pitch_class_profile— tonal peak energy folded onto the 12 pitch classes: what "key" the soundscape hums in.
run_session writes tonality.json + tonality.png.
tonal_peaks(spec_row, freqs, fmin=40.0, fmax=8000.0, min_prom_db=8.0, max_n=40)
Prominent narrowband peaks in one mean spectrum.
The floor is a wide median filter on the log spectrum; peaks must rise
min_prom_db above it. Returns (freq, prominence_db, power) arrays.
Source code in src/ambiscape/tonality.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
tonal_tracks(minspec, freqs, tol_cents=40.0, min_minutes=2, **peak_kw)
Link per-minute peaks into tracks. Returns a list of dicts sorted by duration (longest first): median freq, span of minutes, mean prominence, and total drift in cents.
Source code in src/ambiscape/tonality.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 | |
harmonic_sieve(fq, power, f0_min=60.0, f0_max=1200.0, tol_cents=35.0, max_harm=12)
Best f0 explaining the peak set as harmonics k*f0.
Candidate f0s are every peak divided by k = 1..6; the score is the
power-weighted fraction of peaks within tol_cents of a harmonic.
Returns (f0, harmonicity in [0,1]) — harmonicity is the explained power
fraction; 1 − harmonicity is the inharmonicity index.
Source code in src/ambiscape/tonality.py
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | |
pitch_class_profile(minspec, freqs, minutes=None, **peak_kw)
Tonal peak power folded onto 12 pitch classes (A4 = 440 Hz).
Source code in src/ambiscape/tonality.py
113 114 115 116 117 118 119 120 121 122 123 | |
run_session(sess, out_dir)
Tonality timeline + per-minute tonalness/harmonicity + PCP + figure.
Source code in src/ambiscape/tonality.py
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | |
Strike-level rhythm
Strike-level rhythm analysis of quasi-periodic pitched sources (bells, machines, signals) in long ambisonic recordings.
The 1 Hz features in :mod:features are too coarse for strike rhythm, so this
module makes one extra streaming pass at ~20 ms resolution, restricted to the
narrowband partials of the sources of interest:
detect_partials— narrowband peaks from the cached per-minute mean PSD, contrasting source-active against quiet minutes;partial_pass— streaming STFT pass storing, per frame, the power envelope and pseudo-intensity (for DOA) at each partial, plus a broadband spectral-flux onset function;cluster_partials— group partials into sources by correlating their half-wave-rectified log-envelope derivatives (strike-synchronous);pick_strikes— adaptive, strongest-first onset picking per source;rayleigh_period/period_track— point-process periodicity (resultant length over a period grid, with harmonics for multi-strike cycles);cycle_grid— repetition-with-variation statistics: per-cycle timing residuals and amplitudes for each position of the repeating pattern;rise_spectrum— strike-triggered post/pre spectral rise, the tonal content of one rhythmic position (also exposes cross-talk: a stream whose rise spectrum shows another source's partials is leakage, not a strike).
All statistics are on the W channel except pseudo-intensity, which follows the AmbiX ACN (W, Y, Z, X) convention used throughout ambiscape.
detect_partials(F, active, quiet, band=(350.0, 4500.0), min_rise_db=6.0, max_n=30)
Narrowband partials of the active-state source(s).
active/quiet are boolean masks over the minutes of F["minspec"]
(source clearly present / clearly absent). Returns (freqs, rise_db)
sorted by frequency.
Source code in src/ambiscape/rhythm.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | |
partial_pass(take, pfreq, nfft=4096, hop=960)
Streaming STFT pass; per ~20 ms frame: 3-bin power envelope and pseudo-intensity at each partial, plus a 400-4500 Hz log-flux onset function. Returns dict of arrays keyed t/env/ix/iy/iz/odf/pfreq.
Source code in src/ambiscape/rhythm.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | |
cluster_partials(env, mask=None, th=0.75, min_size=3)
Group partial columns into sources: correlate rectified log-envelope
derivatives (with ±1-frame jitter tolerance), average-linkage cluster.
Returns a list of column-index lists, largest first; singletons and
groups below min_size are dropped (assign them afterwards with
strike-triggered statistics if needed).
Source code in src/ambiscape/rhythm.py
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
source_odf(env, cols)
Mean rectified log-envelope derivative over one source's partials.
Source code in src/ambiscape/rhythm.py
139 140 141 142 | |
pick_strikes(odf, t, min_sep=0.5, k=1.5, t_max=None)
Adaptive strongest-first onset picking.
Candidates are local maxima exceeding a running median by k MADs;
they are accepted strongest-first subject to a min_sep guard (set it
just below the shortest true inter-onset interval). Returns strike times.
Source code in src/ambiscape/rhythm.py
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | |
acf_structure(odf, dt, t_mask=None, max_lag_s=8.0, rel=0.25)
Cycle period and shortest intra-cycle gap from the ODF autocorrelation.
Returns (cycle_period, min_gap): the lag of the strongest ACF peak, and
the shortest peak lag whose value exceeds rel x the strongest —
use 0.8 * min_gap as the pick_strikes separation guard. Estimating
the cycle from the ACF first keeps the later Rayleigh refinement off
subharmonics.
Source code in src/ambiscape/rhythm.py
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | |
rayleigh_period(times, grid, harm=2)
Resultant length of the strike point process folded at each candidate
period, summed over harm harmonics (multi-strike cycles).
Source code in src/ambiscape/rhythm.py
196 197 198 199 200 201 202 203 204 | |
best_period(times, lo=0.5, hi=8.0, step=0.0005, harm=2)
Grid-search rayleigh_period with parabolic refinement.
Source code in src/ambiscape/rhythm.py
207 208 209 210 211 212 213 214 215 | |
period_track(times, P0, win=150.0, step=30.0, half_range=0.05, harm=2)
Sliding-window period estimates around P0; returns (t, P).
Source code in src/ambiscape/rhythm.py
218 219 220 221 222 223 224 225 226 227 228 | |
phase_clusters(times, P, width=0.12)
Split one stream into phase clusters at period P (histogram modes).
Returns (phase0, list of (center, mask)): phases are relative to the
dominant cluster.
Source code in src/ambiscape/rhythm.py
231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 | |
cycle_grid(streams, P, t_max)
Per-cycle timing residuals and hit rates for named event streams sharing one cycle period.
streams maps name -> strike times. The cycle phase reference is the
first stream. Returns per-stream position (s into cycle), residual array
(NaN = missed cycle), and summary stats: timing sd, lag-1 autocorrelation,
slow wander vs cycle-to-cycle sd, hit rate.
Source code in src/ambiscape/rhythm.py
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | |
rise_spectrum(take, times, nfft=8192, n_max=150, seed=1)
Strike-triggered mean post/pre log-spectral rise (dB) on W.
The tonal fingerprint of one rhythmic position. If a stream's rise spectrum reproduces another source's partials, that stream is cross-talk rather than a distinct strike. Returns (freqs, rise_db).
Source code in src/ambiscape/rhythm.py
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | |
run_session(sess, out_dir, n_sources=2, t_stop=None, verbose=True)
Full rhythm pipeline for a single-take session; writes
rhythm_overview.png and rhythm.json, returns the summary dict.
Source code in src/ambiscape/rhythm.py
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 | |
partial_fm(take, freq, period, t_max, nfft=8192, hop=960)
Frequency modulation of one partial at the cycle rate.
A genuinely swinging bell Doppler-shifts its partials by a few cents at the swing period; a chimed (hammer-struck) bell does not. Tracks the instantaneous frequency of the partial (5-bin spectral centroid around its bin, energy-gated), then complex-demodulates at 1/period and at an off-rate control (1/(1.37*period)). Returns FM depth in cents at both; a swing verdict needs depth well above the control.
Source code in src/ambiscape/rhythm.py
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 | |
strike_doa(P, times, cols, dur=0.25)
Median per-strike azimuth/elevation (deg) from the pass arrays,
energy-integrated over dur seconds after each strike.
Source code in src/ambiscape/rhythm.py
542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 | |
Spatial dynamics
Spatial dynamics at three time scales.
From the cached per-second spatial features (pseudo-intensity per octave, DOA, diffuseness) — no audio pass:
direct_diffuse_split— per-octave directness (1 − diffuseness proxy) per second: the spatial analogue of foreground/background;passby_events— level events whose azimuth sweeps monotonically through the event: moving sources, with sweep rate and direction;azimuth_organization— windowed, energy-weighted circular concentration R(t): how directionally organized the scene is over time.
run_session writes spatial.json + spatial.png.
direct_diffuse_split(F)
Per-octave directness in [0, 1]: |pseudo-intensity| / band power.
Uses the cached I_band (re W*X etc. per octave) and oct_pow.
A plane wave scores near 1, a diffuse field near 0. Returns
(directness[nsec, nband], per-band medians).
Source code in src/ambiscape/spatial.py
26 27 28 29 30 31 32 33 34 35 | |
passby_events(F, min_dur_s=4, min_sweep_deg=25.0, min_r2=0.7)
Level events whose azimuth sweeps steadily: moving sources.
Detects events with :func:ambiscape.analysis.detect_events, then fits
a line to the unwrapped per-second azimuth across each event lasting
=
min_dur_s. A sweep of >=min_sweep_degwith fit R^2 >=min_r2is a pass-by; the sweep sign gives the direction of travel (mic frame). Returns a list of dicts.
Source code in src/ambiscape/spatial.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
azimuth_organization(F, win_s=60.0, step_s=20.0)
Windowed energy-weighted circular concentration of the azimuth.
Returns (t_centers, R): R near 1 = one dominant direction, near 0 = directionally disorganized. Window in seconds (per-second features).
Source code in src/ambiscape/spatial.py
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
directional_entropy(F, nbins=36)
Normalized Shannon entropy of the energy-weighted azimuth histogram.
"How many directions does this place sound from": 0 = all energy from one bearing, 1 = energy spread evenly around the horizon — the spatial analogue of an acoustic diversity index, and something only an ambisonic corpus can report.
Source code in src/ambiscape/spatial.py
93 94 95 96 97 98 99 100 101 102 103 104 | |
horizon_fractions(F, limit_deg=10.0)
Energy fractions arriving from above / around / below the horizon.
Uses the per-second broadband DOA elevation, energy-weighted. A room heard from a couch splits mechanics on walls (above) from footsteps and structure-borne paths (level/below); outdoors it separates birds and building services from ground traffic.
Source code in src/ambiscape/spatial.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
fg_bg_az_overlap(F, nbins=36)
Bhattacharyya overlap of foreground vs background azimuth energy.
Foreground = loudest 25 % of seconds, background = quietest 25 % (the corpus convention). 1 = the foreground comes from where the background hums (one-source rooms), 0 = figure and ground occupy different directions (a street heard past a courtyard fountain).
Source code in src/ambiscape/spatial.py
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
summarize_spatial(F)
Spatial descriptors for the analyze summary.
Source code in src/ambiscape/spatial.py
142 143 144 145 146 147 148 149 150 | |
run_session(sess, out_dir)
CLI driver: split + pass-bys + R(t), figure + spatial.json.
Source code in src/ambiscape/spatial.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | |
Schedule matching
Schedule matching: test event/strike streams against civic time grids.
Bells, chimes, and sirens follow wall-clock schedules — hourly strikes,
quarter-hour chimes, fixed evening ringing. Given event times on the
session's absolute clock, match_periods folds them at candidate civic
periods and scores each with circular statistics; clock_offset turns a
known schedule into a recorder-clock correction (the workflow behind
clock_offset_s in calibration.json).
Times must be absolute seconds (session clock, i.e. take.start +
offset into the take), otherwise grid phases are meaningless.
match_periods(times_abs, periods=CIVIC_PERIODS)
Fold events at each candidate period; score alignment.
Returns one dict per period — phase of the grid the events cluster on
(seconds past the grid tick), R, circular SD, Rayleigh p — sorted by R.
A meaningful match needs both high R and enough events spread over
several grid cycles (n_cycles); R is trivially 1 when all events
fall inside one cycle.
Source code in src/ambiscape/schedule.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | |
grid_scan(F, period_s, phase_s=0.0, band=(300.0, 1500.0), win_s=120.0, min_rise_db=6.0, bg_win_s=300.0)
Targeted scan of every tick of a civic grid for band-limited strikes.
The complement of :func:match_periods: instead of asking which grid an
event stream fits, look at each tick of a known grid (every quarter
hour, every hour + phase_s) for energy in a band — a church clock in
the bell band, whether or not the broadband detector heard it. Uses the
cached features (band level above a running bg_win_s low-percentile
background), so the scan is instant.
Returns one dict per tick inside the feature timeline: t_tick
(absolute seconds), detected, rise_db (peak exceedance within
win_s centered on the tick), and offset_s of that peak from the
tick — a consistent nonzero offset across ticks is recorder-clock error
(see :func:clock_offset).
Source code in src/ambiscape/schedule.py
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | |
clock_offset(observed_abs, true_clock_s)
Recorder-clock correction from one event of known wall-clock time.
observed_abs is the event's time on the recorder clock (absolute
session seconds), true_clock_s the known true time (seconds since
midnight). Returns the clock_offset_s value for
calibration.json (positive = recorder clock was slow).
Source code in src/ambiscape/schedule.py
90 91 92 93 94 95 96 97 98 | |
run_session(sess, out_dir)
CLI driver: match cached event streams against civic periods.
Uses broadband events (always) and rhythm strikes when a prior
ambiscape rhythm run left rhythm.json phase data; writes
schedule.json.
Source code in src/ambiscape/schedule.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | |
Event timbre templates
Event timbre templates: recurring event classes without machine learning.
Every transient event gets a spectral fingerprint — the strike-triggered
post/pre rise spectrum (what appeared) plus a per-band decay slope
(how it faded). Fingerprints are clustered by correlation distance into
template classes: "the same sound again" across a whole session, fully
transparent and corpus-comparable. Complements PANNs tagging ([ml]).
run_session fingerprints the session's spectral events (see
:mod:background), clusters them, and writes timbre.json +
timbre.png (class templates + counts + exemplar times).
event_fingerprint(take, t_onset, nfft=8192, decay_s=1.0)
Rise spectrum (dB, mel-ish bands) + per-band decay slope (dB/s).
Source code in src/ambiscape/timbre.py
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | |
cluster_events(fps, th=0.35, min_size=2)
Average-linkage clustering of fingerprints by correlation distance. Returns labels (−1 = unclustered singleton).
Source code in src/ambiscape/timbre.py
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | |
run_session(sess, out_dir, max_events=150)
Fingerprint + cluster the session's spectral events.
Source code in src/ambiscape/timbre.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | |
MIR views (librosa)
Librosa-based tempogram and chromagram (optional [music] extra).
Complements the built-in analyses with the MIR-standard views: the
tempogram (onset autocorrelation over time, in BPM — against which the
windowed-ACF tempogram in :mod:rhythm can be cross-checked) and the
chromagram (12-bin pitch-class energy over time, the time-resolved
counterpart of :func:ambiscape.tonality.pitch_class_profile).
Audio is read from the W channel and resampled to 22.05 kHz; long sessions
are fine (a 25 min file takes on the order of a minute). Requires
pip install "ambiscape[music]".
load_w(take, t0=0.0, dur=None, sr=22050)
W channel of a take, resampled to sr.
Source code in src/ambiscape/music.py
30 31 32 33 34 35 36 37 38 39 | |
tempogram(y, sr, hop=512, win_s=8.0)
Autocorrelation tempogram of the onset-strength envelope.
Returns (times, bpm_axis, T, tempo_bpm): the tempogram plus librosa's global tempo estimate (which resolves the octave ambiguity a raw tempogram argmax suffers from).
Source code in src/ambiscape/music.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
chromagram(y, sr, hop=512)
STFT chromagram (12 pitch classes over time).
Source code in src/ambiscape/music.py
65 66 67 68 69 70 71 | |
run_session(sess, out_dir, t0=0.0, dur=None)
Tempogram + chromagram figure and summary for the first take.
Source code in src/ambiscape/music.py
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | |
Taxonomy
Schaeffer / Schafer taxonomy figures from per-session annotations.
The annotation file (annotations.json or .yml in the session folder)
is hand-authored: instruments detect when things sound, but assigning a
sound to Schaeffer's typo-morphology or Schafer's functional categories is an
interpretive act. This module turns that interpretation into two figures:
schaeffer_map— objects on the facture x mass plane, colored by Schafer function (keynote / signal / soundmark / figure);schafer_timeline— one lane per object on the session clock, keynote spans as bars, events as markers, lo-fi states shaded.
Annotation schema (JSON; YAML accepted if PyYAML is installed)::
{
"objects": [
{"name": "air-pump drone",
"label": "air-pump drone (130 Hz comb, 9 h)", # optional
"kind": "keynote", # keynote|signal|soundmark|figure
"soundmark": "dwelling", # optional: community|dwelling
"source": "anthropophony", # optional: ...|biophony|geophony
"mass": "noise", # tonic|tonic-complex|complex|noise
"facture": "unlimited", # impulse|iteration|sustained|unlimited
"spans": [["23:01:36", "1 07:53:55"]], # and/or
"events": ["1 04:42:51"]},
...
],
"states": [
{"label": "LO-FI (drone masks the field)",
"span": ["23:01:36", "1 07:53:55"]}
]
}
Times are "[D ]HH:MM:SS" where the optional leading integer D is days
after the session's first day (or plain seconds as a number).
schaeffer_map(ann, out_path, title='')
Objects on the facture x mass grid, colored by Schafer function.
Source code in src/ambiscape/taxonomy.py
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | |
schafer_timeline(ann, out_path, title='', session=None)
Lane timeline of annotated objects; lo-fi states shaded.
Source code in src/ambiscape/taxonomy.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 | |
render(folder, out_dir=None, session=None)
Load annotations from a session folder and write both figures.
Source code in src/ambiscape/taxonomy.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | |
Draft annotations
Draft-annotation generator.
Pre-fills annotations.draft.json for a session from its cached features:
steady level regimes become keynote candidates (with spans), detected
transient events become one unclassified "figure" object whose entries carry
listening hints (clock time, exceedance, azimuth/elevation, diffuseness).
The taxonomy fields that require a human ear (mass, facture, kind,
soundmark status) are left as "TODO" — edit, rename, delete, then save the
result as annotations.json and run ambiscape taxonomy.
Calibration and ISO indicators
ISO 12913-3-style psychoacoustic indicators + level calibration.
Calibration
A session is calibrated by <folder>/calibration.json::
{"dbfs_to_dbspl": 94.0,
"method": "SPL app next to mic, air pump running, LAeq 42 dB",
"date": "2026-07-16"}
dbfs_to_dbspl is the offset O such that a signal at −X dBFS corresponds
to (O − X) dB SPL. With it, dBFS descriptors become dB SPL and waveforms
convert to pascals for psychoacoustic metrics.
The same file may carry clock_offset_s — seconds added to every take's
start time when the recorder clock was found to be off (positive = clock was
slow; e.g. calibrated against a known external time reference). Applied in
:func:ambiscape.io.open_session, so all clock-labeled outputs agree. Both
keys are optional.
Indicators (via MoSQITo, optional dependency)
ISO 532-1 time-varying loudness (N5, N50), DIN 45692 sharpness, and
Daniel & Weber roughness, computed per ear on a binaural render of the
B-format signal. If ambiviz (with its HRIR-based binauralizer) is
installed it is used; otherwise a documented fallback renders a
back-to-back cardioid pair at ±90° — a pseudo-binaural approximation
without pinna/ILD spectral cues. Uncalibrated sessions are computed with an
assumed offset and flagged: absolute sone/acum values are then indicative
only (their ratios between segments remain meaningful).
apply_calibration(summary, cal)
Add dB SPL versions of the level descriptors to a summary dict.
Source code in src/ambiscape/iso.py
54 55 56 57 58 59 60 61 62 63 | |
binaural(x4, fs)
FOA (W,Y,Z,X columns) -> stereo ear signals.
Tries ambiviz's HRIR binauralizer; falls back to a ±90° cardioid pair (no pinna cues). Returns (n, 2) array and the method name.
Source code in src/ambiscape/iso.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | |
indicators(x_pa, fs, rough_dur=10.0)
ISO 532-1 loudness (N5/N50), DIN 45692 sharpness, D&W roughness for one calibrated (pascal) channel.
MoSQITo runs ~5x slower than realtime, and roughness is the costliest
metric; it is therefore computed on a central rough_dur-second slice
(roughness is a texture measure and stabilizes within seconds).
Source code in src/ambiscape/iso.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | |
segment_indicators(sess, F, folder, dur=30.0, offset=None)
Compute per-ear indicators on representative segments.
Segments come from analysis.pick_segments (typical / quietest /
most_active / transition); dur seconds from the start of each.
Source code in src/ambiscape/iso.py
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
room_criteria(oct_spl_db)
NR, NC, and RC ratings of an octave-band SPL spectrum.
oct_spl_db maps octave center frequency (Hz) to band SPL (dB).
Ratings are only physically meaningful for calibrated levels
(dbfs_to_dbspl in calibration.json); on uncalibrated dBFS they
are relative numbers, comparable within one recorder+gain setup only.
- NR (ISO/R 1996 Noise Rating): analytic curves
L = a + b*NR; the rating is the highest per-band NR value andNR_governing_hznames the band that sets it. - NC (ANSI S12.2 Noise Criterion): tangency against the tabulated curves, linearly interpolated per band (63 Hz–8 kHz).
- RC (Blazier Room Criterion, simplified): arithmetic mean of the
500/1000/2000 Hz levels; the reference line has a −5 dB/octave slope
through (1 kHz, RC).
RC_classis "R" (rumble) when any 31.5–250 Hz band exceeds the line by > 5 dB, "H" (hiss) when any 2–4 kHz band exceeds it by > 3 dB, "RH" for both, "N" (neutral) otherwise.
Source code in src/ambiscape/iso.py
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
background_octaves_db(F, pct=50.0, offset_db=0.0)
Per-octave percentile level (dB) from cached features, for
:func:room_criteria. offset_db is the dBFS→dB SPL calibration
offset (0 keeps uncalibrated dBFS).
Source code in src/ambiscape/iso.py
222 223 224 225 226 227 228 229 230 | |
Machine listening
Machine-listening helpers (optional [ml] extra).
- PANNs (CNN14, AudioSet, 527 classes) tags 10-s windows around detected
events and steady states; used by
ambiscape draftto suggest object names inannotations.draft.json. - silero-vad estimates the fraction of speech in a file or span — the privacy gate to run on every excerpt before publishing (Freesound etc.).
- BirdNET (
birdnetlib) identifies bird species in 3-s windows — the species layer for biophony, best run on the hi-fi windows the drone-free soundscape exposes (see :mod:ambiscape.biophonyfor the no-ML structural measures it confirms).
All models are trained on 16/32 kHz mono internet audio: the W (omni) channel is downmixed and resampled, spatial information is not used, and low-SNR domestic material is out of distribution — treat tags as suggestions to confirm by ear, not ground truth.
tag_window(x, fs, top_k=3, min_prob=0.1)
AudioSet tags for one mono window via PANNs CNN14 (32 kHz input).
Source code in src/ambiscape/ml.py
44 45 46 47 48 49 50 51 52 53 54 55 56 57 | |
birdnet_window(x, fs, lat=None, lon=None, week=-1, min_conf=0.25)
BirdNET species detections for one mono window (48 kHz input).
Analyzes the W channel resampled to 48 kHz. lat/lon and
week (1–48, ISO-ish) enable BirdNET's location/season species
filter — pass the session's coordinates to cut false positives.
Returns [{"species", "common_name", "confidence"}] above
min_conf. Requires the [ml] extra plus birdnetlib.
Source code in src/ambiscape/ml.py
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
birdnet_session(sess, F=None, windows=None, win_s=9.0, hifi_max_diffuse=None, lat=None, lon=None, min_conf=0.25)
Run BirdNET across a session, optionally only on hi-fi windows.
windows is an explicit list of absolute start seconds; if omitted,
the session is tiled in win_s steps. When F (cached features)
and hifi_max_diffuse are given, windows whose median diffuseness
exceeds the threshold are skipped — a cheap "is the room masked?" gate
so BirdNET runs where birds are actually legible, not under a drone.
Returns per-window detections and an aggregated species tally.
Source code in src/ambiscape/ml.py
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | |
speech_fraction(x, fs)
silero-vad speech statistics for one mono signal.
Source code in src/ambiscape/ml.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | |
speech_gate(path, threshold=0.01)
Privacy gate for a WAV file (any channel count; W/ch0 is analyzed).
Returns the speech statistics plus a pass/fail verdict against
threshold (default: fail if more than 1 % of the file is speech).
Source code in src/ambiscape/ml.py
168 169 170 171 172 173 174 175 176 177 178 179 | |
Deposit export
Non-identifying feature export in the StillStanding365 deposit schema.
Writes one TSV per take with the columns used by the StillStanding365 Zenodo
deposit (audio/{day}.tsv): per-second Time, level_dbfs,
centroid_hz, low_frac (< 250 Hz), high_frac (> 2 kHz). A 1 Hz
loudness/spectral envelope is far below speech timescales and carries no
intelligible content, so these files are safe to publish where raw audio is
not.
Method notes vs. the original extract_audio.py: levels here come from the
W (omni) channel at native rate (the original used an ffmpeg 4-channel
downmix at 8 kHz — offsets of a few tenths of a dB are expected), and band
fractions are power fractions from the cached log-spectrogram (the original
used magnitude fractions of an 8 kHz FFT). Trends and dynamics are directly
comparable; absolute fraction values differ slightly by construction.