"""Bounded-memory streaming primitives for full-track analysis. CPU, NO torch.

Producers upload whole tracks, not 60 s clips. Rather than 413-reject a big file
(or buffer the whole thing in RAM), the signal/loudness engines decode it in
fixed time windows and aggregate as they go — so peak memory is O(one window),
not O(file), and a large upload can't OOM the shared erable box.

soundfile (libsndfile 1.2) streams WAV/FLAC/OGG/MP3 with exact frame counts, so
we read at NATIVE sample rate and let each engine pass `sr` to librosa — no
per-block resampling (which would add seam artifacts at window boundaries).
"""
import numpy as np

# 30 s windows: at 96 kHz stereo float64 that is ~44 MB resident — comfortably
# under the container's 3 GB cap, with margin for the engine's own working set.
DEFAULT_WIN_S = 30.0


def probe(path) -> dict:
    """Header-only metadata (no decode): frames, sr, channels, duration_s.
    `frames <= 0` means the container can't frame-count this file (rare); callers
    fall back to a bounded whole-file decode."""
    import soundfile as sf
    i = sf.info(str(path))
    return {
        "frames": int(i.frames),
        "sr": int(i.samplerate),
        "channels": int(i.channels),
        "duration_s": (i.frames / i.samplerate) if i.samplerate else 0.0,
        "format": i.format,
    }


def windows(path, win_s=DEFAULT_WIN_S, *, mono=True, dtype="float32"):
    """Yield (block, sr, start_frame) over the WHOLE file in `win_s` windows.

    `mono=True` downmixes channels (mean); `mono=False` yields 2-D (n, channels).
    Non-overlapping and contiguous: start_frame of window k is the running sum of
    prior window lengths, so callers can map a block's samples to absolute time.
    Bounded memory: only one window is resident at a time."""
    import soundfile as sf
    with sf.SoundFile(str(path)) as f:
        sr = f.samplerate
        wf = max(1, int(win_s * sr))
        start = 0
        while True:
            blk = f.read(frames=wf, dtype=dtype, always_2d=not mono)
            if len(blk) == 0:
                return
            block = (blk if blk.ndim == 1 else blk.mean(axis=1)) if mono else blk
            yield block, sr, start
            start += len(block)
