decipher.audio

Provenance-tracked audio loading and signal-processing primitives. All functions preserve the round-trip invariant.

dataclass decipher.audio.AudioSource
@dataclass
class AudioSource:
    path: Path
    samples: np.ndarray         # float32 mono
    sr: int                     # current sample rate (Hz)
    original_sr: int            # what was on disk
    original_channels: int
    duration_s: float
    source_url: str = ""
    label: str = ""             # optional dataset/class label

The result of load_audio. samples is always float32 mono at sr. Writing it back via write_wav yields a playable WAV — that is the round-trip invariant.

dataclass decipher.audio.Unit
@dataclass
class Unit:
    index: int
    start_s: float              # offset in parent waveform
    end_s: float
    source_samples: np.ndarray  # the unit's audio, at sr
    sr: int
    peak_db: float = 0.0

    @property
    def duration_s(self) -> float

A single acoustic unit — a listenable slice of a parent waveform. Returned by detect_units.

function decipher.audio.load_audio
def load_audio(
    path: Path | str,
    source_url: str = "",
    label: str = "",
) -> AudioSource

Read a WAV/FLAC into an AudioSource. Stereo input is downmixed to mono by mean. The original sample rate and channel count are preserved on the dataclass.

function decipher.audio.write_wav
def write_wav(
    path: Path | str,
    audio: np.ndarray,
    sr: int,
) -> None

Write float audio to 16-bit PCM WAV with clipping protection (samples are scaled down if the peak exceeds 1.0).

function decipher.audio.render_spectrogram
def render_spectrogram(
    audio: np.ndarray,
    sr: int,
    *,
    out_path: Path | str,
    lo_hz: float | None = None,    # default: 20
    hi_hz: float | None = None,    # default: min(sr/2, 24000)
    n_fft: int = 1024,
    hop: int = 256,
    title: str | None = None,
    width_px: int = 800,
    height_px: int = 300,
    dpi: int = 120,
) -> Path

Render a log-magnitude STFT spectrogram of audio to a PNG. The lo_hz / hi_hz parameters bound the y-axis only; the FFT itself is full-bandwidth.

function decipher.audio.bandpass
def bandpass(
    audio: np.ndarray,
    sr: int,
    lo_hz: float,
    hi_hz: float,
    order: int = 4,
) -> np.ndarray

Zero-phase Butterworth bandpass. Output remains float32 and writable to a WAV via write_wav.

function decipher.audio.resample_to
def resample_to(
    audio: np.ndarray,
    sr_in: int,
    sr_out: int,
) -> np.ndarray

Resample with torchaudio's Kaiser-windowed sinc filter (the same parameters HuBERT uses for preprocessing). Operates on raw arrays — wrap the result in a new AudioSource if you want to preserve provenance through the resample.

function decipher.audio.detect_units
def detect_units(
    audio: np.ndarray,
    sr: int,
    *,
    min_dur_s: float = 0.08,
    max_dur_s: float = 8.0,
    merge_gap_s: float = 0.05,
    envelope: str = "hilbert",       # or "rms"
    smooth_ms: float = 25.0,
    rms_win_ms: float = 25.0,
    rms_hop_ms: float = 5.0,
    threshold_db: float = 6.0,
    noise_floor: str = "global",     # or "local"
    local_win_s: float = 20.0,
    local_pctile: float = 20.0,
    absolute_silence_db: float = -60.0,
) -> list[Unit]

Energy-based unit detection. Pipeline: envelope → noise-floor estimation → threshold → merge nearby → duration filter → extract listenable Unit objects.

The input should already be bandpassed to the species' vocal range. See the segmentation-tuning guide for parameter choices.