diff --git a/maple-py/CLAUDE.md b/maple-py/CLAUDE.md index 0205596..6a8d3b0 100644 --- a/maple-py/CLAUDE.md +++ b/maple-py/CLAUDE.md @@ -20,10 +20,12 @@ is an `if TYPE_CHECKING:` block to break a circular type-only reference. "Don't swallow into None" is a general principle at every level, including call sites. An operation that fails must **let the exception propagate** — never catch -it and return `None` / an empty / degraded result. BPM/beat analysis is integral: -`_mixxx_analyze` raises rather than returning None, and every `Analysis.analyze` -call site (in `anlz.py`, `cli.py`, `collection/core.py`) lets failures abort the -run (including not catching around `ThreadPoolExecutor` `fut.result()`). +it and return `None` / an empty / degraded result. BPM/beat analysis and waveform +extraction are integral: `_mixxx_analyze` raises rather than returning None, +`_decode_pcm` raises (`RuntimeError`) if ffmpeg is missing or fails to decode +rather than returning an empty array, and every `Analysis.analyze` call site (in +`anlz.py`, `cli.py`, `collection/core.py`) lets failures abort the run (including +not catching around `ThreadPoolExecutor` `fut.result()`). Reserve `None` returns for *legitimate absence* (e.g. a cache miss in `Analysis.load`), never for *failure*. The one tolerated catch is a genuinely diff --git a/maple-py/src/maple/analyze.py b/maple-py/src/maple/analyze.py index 8cce00d..d73245e 100644 --- a/maple-py/src/maple/analyze.py +++ b/maple-py/src/maple/analyze.py @@ -123,11 +123,12 @@ def _project_pqtz(beatgrid: list[float], bpm: float) -> list[Beat]: def _decode_pcm(path: Path) -> np.ndarray: """Decode `path` to a 1D float32 numpy array of mono PCM at SAMPLE_RATE. - Returns an empty array on ffmpeg failure. + Raises `RuntimeError` if ffmpeg is missing or fails to decode — waveform + extraction is integral, so a failure here is fatal rather than swallowed + into an empty array (which would yield a waveform-less ANLZ). """ if not shutil.which("ffmpeg"): - logger.warning("ffmpeg not on PATH; cannot extract waveform") - return np.empty(0, dtype=np.float32) + raise RuntimeError("ffmpeg not on PATH; cannot extract waveform") proc = subprocess.run( ["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), @@ -135,9 +136,10 @@ def _decode_pcm(path: Path) -> np.ndarray: capture_output=True, ) if proc.returncode != 0: - logger.warning("ffmpeg failed for {}: {}", path.name, - proc.stderr.decode("utf-8", "replace").strip()) - return np.empty(0, dtype=np.float32) + raise RuntimeError( + f"ffmpeg failed for {path.name}: " + f"{proc.stderr.decode('utf-8', 'replace').strip()}" + ) return np.frombuffer(proc.stdout, dtype=np.float32)