Skip to content

Machine states and source fingerprints

Domestic and mechanical sources — ventilation units, fridges, pumps, HVAC — structure a soundscape as states rather than events: a band-limited floor that is either present or absent, sometimes for hours. Version 0.3 adds a notebook-oriented toolbox for working with them, developed on the 2026 Haarlem-loft case study (an air pump droning for nine hours, a fridge cycling every ~24 minutes, a church clock at the noise floor).

All of it runs from the cached features of a prior ambiscape analyze — no audio pass except segment export.

On/off segmentation (ambiscape.states)

from ambiscape import states

lvl  = states.band_level(F, (250, 1000))       # the source's "machine band"
segs = states.state_segments(lvl, min_dur_s=120)

state_segments median-smooths the band level, splits it at an automatic bimodal (Otsu) threshold with hysteresis, and merges segments shorter than min_dur_s. Each segment reports its median level and within-state SD — a running machine is steady (the Haarlem pump held ±0.2 dB for 9 h), ambience is not. switch_points lists the transitions (the moment someone presses the off button); duty_cycle summarizes a cycling machine as period, duty fraction, and cycle count (a fridge: ~24 min at ~50 %).

Pass an explicit thresh_db when the timeline is not clearly bimodal. Segment times (t0_s) index the 1 Hz feature rows — map them through F["t"] for absolute clock time in multi-take sessions.

Source fingerprints (background.source_fingerprint)

With minute masks for "source clearly on" and "clearly off" (e.g. derived from the state segments), the fingerprint is the dB difference of the two mean PSDs — the source's own spectrum with the room ambience subtracted:

fp = background.source_fingerprint(F, active_minutes, quiet_minutes)
fp["rise_max_db"], fp["rise_max_hz"]   # the broadband turbulence hump
fp["peaks"]                            # narrowband lines riding on it
fp["comb"]                             # {f0_hz, harmonicity} of the lines

A blade-pass or compressor comb reports its base frequency via the harmonic sieve — 130 Hz for the Haarlem pump (~1950 rpm × 4 blades). Combine with background.masking_index to quantify how much the source hides the rest of the field.

Civic grid scans (schedule.grid_scan)

The complement of schedule.match_periods: instead of asking which grid an event stream fits, look at every tick of a known grid for band-limited energy — a church clock in the bell band, whether or not the broadband detector heard it:

scans = schedule.grid_scan(F, 900.0, band=(350, 800), win_s=120)

Each quarter-hour tick reports detected, the peak rise_db above the running band background, and the offset_s of that peak from the tick. A consistent nonzero offset across ticks is recorder-clock error — feed it to schedule.clock_offset and store the result as clock_offset_s in calibration.json.

Segment export (io.export_segment, io.stereo_preview)

from ambiscape.io import export_segment, stereo_preview

export_segment(sess, t0, 600.0, "seg6_vent_switchoff.wav")   # bit-exact AmbiX
st = stereo_preview(x)                                       # ±90° cardioids

export_segment copies samples in the source's own PCM subtype (no float round trip), so a report's representative segments stay citable against the raw takes. stereo_preview decodes an AmbiX block to side-facing cardioids for listenable previews; write the result with soundfile.