Core concepts

Three dataclasses carry the audio through the pipeline: AudioSource, Unit, and SymbolStream. They are designed so that every downstream statistic remains tied to the audio it came from.

AudioSource

The result of load_audio(path). Holds:

  • samples — float32 mono array.
  • sr — current sample rate (Hz).
  • original_sr, original_channels — what was on disk before any downmix or resample.
  • path, source_url, label — provenance.
  • duration_s — convenience.

The contract is: write_wav(p, src.samples, src.sr) always produces a playable file.

An AudioSource in the wild
An AudioSource in the wild
Humpback song excerpt fetched via decipher.fetch — the samples, spectrogram, and source URL all travel together. 44100 Hz 30 s

Unit

A single acoustic unit returned by detect_units. Holds:

  • index — position in the detection sequence.
  • start_s, end_s — offsets into the parent waveform.
  • source_samples — the audio for this unit at sr Hz.
  • peak_db — peak amplitude at segmentation time.

A Unit is listenable on its own: write_wav(p, u.source_samples, u.sr).

A Unit — one listenable slice
A Unit — one listenable slice
First unit from detect_units on the humpback excerpt above; 375 ms. 44100 Hz 0.375 s

SymbolStream

A categorical sequence aligned 1:1 with its audio units. Holds:

  • symbols — int array of cluster ids in [0, alphabet_size). No -1 sentinels.
  • alphabet_size — number of distinct symbols.
  • units — parallel list of Unit objects.
  • source_sr — sample rate of the parent recording.
  • session_boundaries — list of half-open [start, end) intervals partitioning the sequence.
  • name — short label used in logs and figure filenames.

The constructor validates: symbols are 1-D integer, in range, and session boundaries (when present) cover [0, N) exactly once.

The round-trip invariant

This is the load-bearing rule of the toolkit. At every stage of processing — raw, bandpassed, segmented, clustered, embedded — there must be a way to get back to playable audio. In code:

  • Segmentation returns timestamped offsets and the audio slice (the Unit).
  • Clustering attaches labels to feature rows; the caller keeps the units alongside, so units[i] is the audio for cluster labels[i].
  • Embeddings are computed over Units; the audio lookup is the same parallel list.

The reason: any statistic computed downstream is interpretable only if you can re-listen. A novel "type" that turns out to be a single coughing engine is fixable in two minutes if you can play it.

Pipeline shape

load_audio  →  bandpass  →  detect_units  →  featurise  →  cluster_features  →  SymbolStream
   |              |               |                              |                    |
AudioSource    np.ndarray    list[Unit]                   ClusterResult         SymbolStream
                                                          (labels_assigned)

From a SymbolStream, downstream primitives compute entropy, mutual information, n-gram perplexity, transition graphs, Hill numbers, and the linguistic-laws tests (Zipf, Menzerath).