Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@
## 2026-07-13 - Array.from mapping optimization
**Learning:** Using `Array.from({ length: N }).map(...)` creates an intermediate array of `undefined` values which requires memory allocation and garbage collection, adding O(N) unnecessary overhead in frequently re-rendered UI components.
**Action:** Use `Array.from({ length: N }, (_, index) => ...)` to map elements directly during array creation, avoiding intermediate allocations.

## 2026-08-23 - Vectorize Python loop over audio frames in probability matrix construction
**Learning:** Computing observation probabilities per frame using a `for i in range(n_frames)` loop in pure Python generates significant overhead when handling audio segments (e.g. 50k+ frames), creating a slow bottleneck before Viterbi decoding.
**Action:** Replace sequential per-frame condition checks with fully vectorized NumPy operations (`np.pad`, `|` boolean mask, and array slicing assignments) to compute logic globally over the time axis, changing slow Python loops into fast C-level operations.
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,13 @@ def _build_observation_probs(
Observation probabilities of shape (25, n_frames).
"""
n_frames = chromagram.shape[1]

if similarity.shape != (24, n_frames):
raise ValueError(
f"similarity shape {similarity.shape} does not match "
f"expected shape (24, {n_frames})"
)

obs_probs = np.zeros((_NUM_CHORD_STATES, n_frames))

# Chord observation likelihoods from template similarity
Expand All @@ -306,17 +313,20 @@ def _build_observation_probs(

# N (no-chord) observation probability based on noise indicators
chroma_vars = np.var(chromagram, axis=0)
for i in range(n_frames):
rms_val = rms[i] if i < len(rms) else 0.0
chroma_var = chroma_vars[i]
max_sim = similarity[:, i].max() if similarity.shape[1] > i else 0.0

# High N probability when signal is low/flat
if max_sim < 0.3 or rms_val < 0.01 or chroma_var < 0.02:
obs_probs[:24, i] *= 0.1
obs_probs[_NO_CHORD_STATE, i] = 0.9
else:
obs_probs[_NO_CHORD_STATE, i] = 0.05

rms_vals = np.pad(rms, (0, max(0, n_frames - len(rms))))[:n_frames]
max_sims = (
similarity[:, :n_frames].max(axis=0)
if similarity.shape[1] >= n_frames
else np.pad(similarity[:, :].max(axis=0), (0, max(0, n_frames - similarity.shape[1])))
)

condition = (max_sims < 0.3) | (rms_vals < 0.01) | (chroma_vars[:n_frames] < 0.02)

# High N probability when signal is low/flat
obs_probs[:24, condition] *= 0.1
obs_probs[_NO_CHORD_STATE, condition] = 0.9
obs_probs[_NO_CHORD_STATE, ~condition] = 0.05
Comment thread
seonghobae marked this conversation as resolved.

# Normalize columns
col_sums = obs_probs.sum(axis=0, keepdims=True) + 1e-12
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Test observation probability contracts for the chord recognizer."""

import numpy as np
import pytest

from bandscope_analysis.chords.chord_recognizer import ChordRecognizer


def test_observation_probs_reject_similarity_frame_mismatch() -> None:
"""Reject similarity arrays that would otherwise broadcast across frames."""
recognizer = ChordRecognizer()
chromagram = np.ones((12, 5), dtype=np.float64)
similarity = np.ones((24, 1), dtype=np.float64)
rms = np.ones(5, dtype=np.float64)

with pytest.raises(ValueError, match="similarity shape"):
recognizer._build_observation_probs(chromagram, similarity, rms)


def test_observation_probs_reject_similarity_chord_state_mismatch() -> None:
"""Reject similarity arrays that do not contain all 24 chord templates."""
recognizer = ChordRecognizer()
chromagram = np.ones((12, 5), dtype=np.float64)
similarity = np.ones((23, 5), dtype=np.float64)
rms = np.ones(5, dtype=np.float64)

with pytest.raises(ValueError, match="similarity shape"):
recognizer._build_observation_probs(chromagram, similarity, rms)
Loading