Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
fb9264c
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 1, 2026
5a4adb1
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
c4f1b50
test(chords): prove vectorized probability and Viterbi parity
seonghobae Aug 3, 2026
327eb9c
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
8cc2634
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
232fa07
chore(ci): revalidate against current coverage workflow
seonghobae Aug 3, 2026
f1b2cec
test(analysis): prove vectorized chord parity and rolling Viterbi
seonghobae Aug 3, 2026
b6b4fdc
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
024bb11
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
eecae60
test(chords): stage similarity alignment regression
seonghobae Aug 3, 2026
4a45e75
ci(chords): apply frame-alignment bootstrap
seonghobae Aug 3, 2026
278fca9
ci(chords): trigger one-shot alignment on PR updates
seonghobae Aug 3, 2026
f82512d
test(chords): cover padded similarity confidence alignment
seonghobae Aug 3, 2026
07db6a0
ci(chords): verify padded-frame alignment before commit
seonghobae Aug 3, 2026
e4bd67c
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
231da8c
test(chords): require one reusable padded-frame buffer
seonghobae Aug 3, 2026
1a551f4
chore(chords): add one-shot fallback buffer finalizer
seonghobae Aug 3, 2026
94e640a
ci(chords): verify reusable fallback buffer
seonghobae Aug 3, 2026
07bd35a
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
06af335
test(chords): restore reusable fallback allocation contract
seonghobae Aug 3, 2026
0d63479
chore(chords): add final reviewed fallback reuse step
seonghobae Aug 3, 2026
16b40ef
ci(chords): finalize and verify fallback allocation reuse
seonghobae Aug 3, 2026
44f8226
ci(chords): make finalizer visible to pull-request verification
seonghobae Aug 3, 2026
820f5ca
perf(chords): reuse padded-frame fallback allocation
github-actions[bot] Aug 3, 2026
a9db88f
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
44e3ebd
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
2a08fd4
ci: stage neutral chord fallback fix
seonghobae Aug 3, 2026
86b87a4
ci: apply neutral chord fallback fix
seonghobae Aug 3, 2026
e379479
test(chords): cover normalized mismatch fallbacks
seonghobae Aug 3, 2026
b94e54b
fix(chords): keep missing-frame fallback neutral
github-actions[bot] Aug 3, 2026
5bd1631
ci(chords): restore strict formatting for chord fallback vector tests
seonghobae Aug 3, 2026
e759965
test(chords): verify observation padding values
seonghobae Aug 3, 2026
b61d2e5
perf(backend): optimize viterbi decoding and observation probability …
seonghobae Aug 3, 2026
58b8856
test(chords): assert observed and padded frame probabilities
seonghobae Aug 3, 2026
f80b6dd
chore(ci): stage chord observation review fixes
seonghobae Aug 3, 2026
f584d94
chore(ci): run chord observation review fixes
seonghobae Aug 3, 2026
fd05ae8
chore(ci): trigger chord observation finalizer
seonghobae Aug 3, 2026
c4d0ebb
fix(chords): keep missing observation frames neutral
github-actions[bot] Aug 3, 2026
dc64482
test(chords): clarify neutral metadata fallback
seonghobae Aug 3, 2026
7f1a195
fix(chords): conform fallback fixes to standard format
seonghobae Aug 3, 2026
65e667d
chore: retrigger ci without push
seonghobae Aug 3, 2026
de493d1
test(chords): verify observation frame alignment
seonghobae Aug 3, 2026
bdfdf70
test(chords): assert mismatch probability values
seonghobae Aug 3, 2026
a8e5248
chore: retrigger ci
seonghobae Aug 4, 2026
1caa712
test(chords): restore semantic edge-case assertions
seonghobae Aug 4, 2026
5f7244b
fix(deps): update undici to 7.29.0
seonghobae Aug 4, 2026
c98eca8
chore: ci retrigger
seonghobae Aug 4, 2026
82a8cbf
fix(deps): update undici to resolve high severity vulnerability
seonghobae Aug 4, 2026
3135945
chore: ci retrigger
seonghobae Aug 5, 2026
a03f3cc
style: conform line lengths to format guidelines and fix lint errors
seonghobae Aug 5, 2026
f3e34b2
chore: ci retrigger
seonghobae Aug 6, 2026
5686a2e
ci(pr732): verify bounded chord optimization cleanup
seonghobae Aug 6, 2026
83dd337
fix(chords): validate sampling rate to prevent excessive resource exh…
seonghobae Aug 6, 2026
3deab33
fix(chords): Viterbi bounded optimization and removed scope pollution
seonghobae Aug 7, 2026
aa3e399
test(chords): add independent scalar and dense numerical oracles
seonghobae Aug 7, 2026
eb10fa2
docs(changelog): record chord reference optimization
seonghobae Aug 7, 2026
31780c4
fix(test): sort numerical oracle imports
seonghobae Aug 7, 2026
1bc3b24
fix(chords): Viterbi bounded optimization and removed scope pollution
seonghobae Aug 7, 2026
9b5f19f
fix(chords): handle nn_filter crash for short audio and fix package v…
seonghobae Aug 7, 2026
b672697
fix(chords): handle nn_filter crash for short audio, limit RMS fallba…
seonghobae Aug 7, 2026
af13d8c
fix(chords): handle nn_filter crash for short audio, limit RMS fallba…
seonghobae Aug 7, 2026
c1cc5f4
fix(security): require patched PDF.js
seonghobae Aug 11, 2026
b34cc6d
fix(security): refresh vulnerable npm transitive pins
seonghobae Aug 11, 2026
ac94282
fix(deps): restore manifest-lock consistency
seonghobae Aug 11, 2026
99d5c64
fix(chords): handle nn_filter crash for short audio, limit RMS fallba…
seonghobae Aug 11, 2026
3fa0d7f
fix(chords): handle nn_filter crash for short audio, limit RMS fallba…
seonghobae Aug 11, 2026
564b675
chore(chords): restore reviewed reference-only scope
seonghobae Aug 14, 2026
ccda9fc
fix(chords): handle nn_filter crash for short audio, limit RMS fallba…
seonghobae Aug 14, 2026
a246be8
fix(chords): restore reviewed scope and organize oracle imports
seonghobae Aug 14, 2026
9c108c3
chore(chords): restore reviewed reference-only scope and fix ruff for…
seonghobae Aug 14, 2026
6dfef20
chore(chords): restore reviewed reference-only scope and fix ruff for…
seonghobae Aug 14, 2026
51a4b8d
chore(chords): restore reviewed reference-only scope and fix ruff for…
seonghobae Aug 14, 2026
847d310
fix(scope): restore protected package lock
seonghobae Aug 14, 2026
220fa6c
fix(chords): prevent resource exhaustion from unbounded audio input
seonghobae Aug 14, 2026
735c6ed
fix(chords): restore bounded numerical PR scope
seonghobae Aug 14, 2026
fb514d8
test(chords): pin relative-key transition prior
seonghobae Aug 15, 2026
e3988a2
fix(chords): correct relative-key transition prior
seonghobae Aug 15, 2026
ded30d0
docs(doctoring): ground chord transition prior repair
seonghobae Aug 15, 2026
7247e6b
docs(changelog): record relative-key accuracy fix
seonghobae Aug 15, 2026
801fb3e
fix(chords): prevent resource exhaustion from unbounded audio input
seonghobae Aug 15, 2026
51d13f7
fix(chords): remove unrelated duration-limit drift
seonghobae Aug 15, 2026
de492c1
test(chords): keep non-finite observations neutral
seonghobae Aug 16, 2026
459d52e
fix(chords): neutralize non-finite DSP metadata
seonghobae Aug 16, 2026
fd73ce8
style(chords): format non-finite guard
seonghobae Aug 16, 2026
e0b430c
style(chords): match ruff formatter
seonghobae Aug 16, 2026
e004959
fix(chords): prevent resource exhaustion from unbounded audio input
seonghobae Aug 16, 2026
0701cf8
test(chords): restore non-finite observation regression
seonghobae Aug 16, 2026
591a3c4
test(chords): keep resource policy out of feature decoder
seonghobae Aug 16, 2026
7408f1c
fix(chords): restore feature-neutral resource policy and finite obser…
seonghobae Aug 16, 2026
d5fc507
fix(chords): prevent resource exhaustion from unbounded audio input
seonghobae Aug 16, 2026
eee6f0f
test(chords): restore canonical resource and non-finite regressions
seonghobae Aug 16, 2026
ab9b508
fix(chords): restore canonical resource ownership and finite observat…
seonghobae Aug 16, 2026
904b97a
⚡ Bolt: [performance improvement] Vectorize chord recognizer observat…
seonghobae Aug 16, 2026
342695f
fix(chords): restore finite observation neutrality after Bolt drift
cursoragent Aug 16, 2026
213e330
test(chords): reject non-finite confidence arithmetic
seonghobae Aug 16, 2026
b106a16
fix(chords): keep confidence arithmetic finite
seonghobae Aug 16, 2026
830dd4c
docs(chords): align confidence with finite observation contract
seonghobae Aug 16, 2026
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
- Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace.
- 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함.

### Changed

- Vectorized chord observation construction and reduced the NumPy Viterbi parity oracle to rolling scores while preserving exact dense-oracle and Rust numerical behavior.

### Fixed

- Corrected the HMM relative-key transition prior so major chords favor their true relative minor and minor chords favor their true relative major instead of unrelated pitch-class targets.
- Neutralize non-finite chromagram, similarity, and RMS metadata in the chord observation path so corrupt DSP frames stay finite and do not get treated as silence.

## [0.1.3] - 2026-04-29

### Fixed
Expand Down
39 changes: 39 additions & 0 deletions docs/doctoring/chord-observation-finiteness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Chord observation finiteness

## Status

**Active Draft PR evidence.** This record documents the observation-probability robustness contract on PR #732. It is not protected-`develop` shipped truth until the implementation is merged and revalidated on the protected branch.

## Buyer-visible problem

The chord HMM consumes per-frame observation probabilities. A single non-finite chromagram bin, template-similarity score, or RMS value turns the corresponding Viterbi column into `NaN`. The decoder then emits an arbitrary or empty rehearsal chord where the player needed either a labeled chord or an honest unknown frame.

Missing metadata and invalid metadata are different events:

- missing similarity or RMS is unknown evidence and must stay neutral;
- non-finite similarity invalidates the whole frame and must fall back to a uniform chord distribution;
- non-finite RMS or chromagram variance is unknown, not evidence of silence; and
- confidence computation treats non-finite similarity as unknown evidence and returns low confidence without performing invalid entropy arithmetic.

The next action for a player is unchanged: read the surviving chord label, or treat a neutralized frame as “listen again” rather than as a forced no-chord cut.

## Research rationale

Rabiner (1989) defines HMM decoding over valid observation probabilities; a non-finite column is outside that model. Lee and Slaney (2006) treat frame-level chord observations as the input to Viterbi, so observation integrity is an accuracy precondition rather than a presentation detail. Mauch and Dixon (2010) likewise keep the observation model separate from the transition prior, which is why this repair stays inside probability construction and confidence reporting and does not change the 25-state vocabulary or the Rust production decoder.

## Test-first verification contract

- a frame with any non-finite similarity stays finite and column-normalized;
- that frame uses a uniform 24-chord fallback and does not raise the no-chord state above the chord mass;
- non-finite RMS or chromagram variance does not satisfy the silence thresholds;
- confidence on NaN or infinite similarity falls back to `low` without emitting a NumPy `RuntimeWarning` from invalid arithmetic;
- the vectorized implementation matches an independent framewise scalar oracle on the same corrupt inputs; and
- `recognize()` still forwards the full audio array to the harmonic separator, so duration policy stays upstream.

## References

Lee, K., & Slaney, M. (2006). Automatic chord recognition from audio using an HMM with supervised learning. In *Proceedings of the 7th International Conference on Music Information Retrieval (ISMIR 2006)* (pp. 133–137). University of Victoria. https://ismir.net/conferences/ismir-2006/

Mauch, M., & Dixon, S. (2010). Approximate note transcription for the improved identification of difficult chords. In *Proceedings of the 11th International Society for Music Information Retrieval Conference (ISMIR 2010)* (pp. 135–140). International Society for Music Information Retrieval. https://ismir.net/conferences/ismir-2010/

Rabiner, L. R. (1989). A tutorial on hidden Markov models and selected applications in speech recognition. *Proceedings of the IEEE, 77*(2), 257–286. https://doi.org/10.1109/5.18626
48 changes: 48 additions & 0 deletions docs/doctoring/chord-transition-prior.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Chord-transition prior evidence

## Status

**Active Draft PR evidence.** This record documents the chord-transition correctness repair developed on PR #732. It is not protected-`develop` shipped truth until the implementation is merged and revalidated on the protected branch.

## Buyer-visible problem

BandScope smooths frame-level chord evidence with a 25-state major/minor/no-chord HMM before emitting rehearsal chord segments. The transition matrix therefore influences which chord label survives when acoustic evidence is ambiguous. A wrong harmonic prior can turn a plausible rehearsal progression into an unrelated chord change even when the frame observations themselves are sound.

The existing relative-key calculation was reversed:

- a major chord root used `+3` semitones for its relative minor target; and
- a minor chord root used `+9` semitones for its relative major target.

In pitch-class arithmetic that maps C major toward D♯ minor instead of A minor, and A minor toward F♯ major instead of C major. The corrected invariant is:

- major root → relative minor root: `+9 mod 12` semitones;
- minor root → relative major root: `+3 mod 12` semitones.

The repair changes only the relation used to populate the existing heuristic transition prior. It does not change the 24-major/minor-plus-no-chord vocabulary, observation model, Viterbi algorithm, Rust/NumPy parity boundary, filesystem/network authority, or persistence format.

## Research rationale

Lee and Slaney (2006) established supervised-HMM automatic chord recognition as a sequence-decoding problem in which transition probabilities are part of the model rather than presentation-only metadata. Masada and Bunescu (2019) likewise identify chord-transition information as useful harmonic-syntax evidence and note that transition distributions depend on tonal context. Gotham et al. (2023) provide a large, reproducible functional-harmony meta-corpus suitable for measuring repertoire-dependent transition behavior instead of assuming that chord transitions are equiprobable.

These sources support two engineering conclusions. First, a transition-prior defect is an accuracy defect because the prior participates directly in decoding. Second, the current hand-authored prior should be treated as a bounded baseline rather than a universal model of harmony: future accuracy work should estimate or calibrate transition probabilities against documented corpora and stratify by musical context instead of silently increasing heuristic complexity.

## Test-first verification contract

The regression test names musically recognizable relative pairs and compares them with the exact unrelated pairs favored by the old formula:

- `C → Am` must have greater transition probability than `C → D#m`;
- `Am → C` must have greater transition probability than `Am → F#`.

This catches the old formula in both directions without depending on an implementation constant such as the raw `related_prob`. The production correction is then one bounded pitch-class formula change. Because both the native Rust Viterbi path and the NumPy reference consume the same Python-built transition matrix, the relation is shared across the two numerical paths and remains subject to the repository's exact Rust-to-NumPy parity tests.

## Accuracy follow-up

A commercial-quality next step is corpus-backed calibration rather than additional unvalidated hand rules. Candidate evaluation should compare the current heuristic prior with learned/key-conditioned priors on an openly documented chord corpus and report sequence metrics such as chord-symbol recall/weighted chord symbol recall together with transition-specific confusion. Any learned prior must retain deterministic fixtures for common progressions and must not reduce the existing native/reference numerical parity or fail-closed behavior.

## References

Gotham, M., Micchi, G., López, N. N., & Sailor, M. (2023). When in Rome: A meta-corpus of functional harmony. *Transactions of the International Society for Music Information Retrieval, 6*(1), 150–166. https://doi.org/10.5334/tismir.165

Lee, K., & Slaney, M. (2006). Automatic chord recognition from audio using an HMM with supervised learning. In *Proceedings of the 7th International Conference on Music Information Retrieval (ISMIR 2006)* (pp. 133–137). University of Victoria. https://ismir.net/conferences/ismir-2006/

Masada, K., & Bunescu, R. C. (2019). Chord recognition in symbolic music: A segmental CRF model, segment-level features, and comparative evaluations on classical and popular music. *Transactions of the International Society for Music Information Retrieval, 2*(1), 1–13. https://doi.org/10.5334/tismir.18
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def _build_transition_matrix(self) -> np.ndarray:
# Fifth relationship (e.g. C -> G, Am -> Em)
fifth = (root + 7) % 12
fourth = (root + 5) % 12
relative = (root + 3) % 12 if i < 12 else (root + 9) % 12
# Relative minor is nine semitones above a major root (C -> A),
# while relative major is three semitones above a minor root (A -> C).
relative = (root + 9) % 12 if i < 12 else (root + 3) % 12

# Same root major/minor interchange
parallel = (i + 12) % 24
Expand Down Expand Up @@ -168,25 +170,19 @@ def _viterbi_decode_reference(self, observation_probs: np.ndarray) -> np.ndarray
log_trans = np.log(self._transition_matrix + 1e-12)
log_obs = np.log(observation_probs + 1e-12)

# Uniform initial probability
log_pi = np.full(n_states, np.log(1.0 / n_states))

# Viterbi tables
viterbi = np.zeros((n_states, n_frames))
# Viterbi tables initialized with uniform initial probability
viterbi = np.full(n_states, np.log(1.0 / n_states)) + log_obs[:, 0]
backpointer = np.zeros((n_states, n_frames), dtype=np.intp)

# Initialization
viterbi[:, 0] = log_pi + log_obs[:, 0]

# Forward pass (Vectorized over states for ~7x speedup)
# Forward pass (Vectorized over states)
for t in range(1, n_frames):
trans_probs = viterbi[:, t - 1, np.newaxis] + log_trans
trans_probs = viterbi[:, np.newaxis] + log_trans
backpointer[:, t] = np.argmax(trans_probs, axis=0)
viterbi[:, t] = np.max(trans_probs, axis=0) + log_obs[:, t]
viterbi = np.max(trans_probs, axis=0) + log_obs[:, t]

# Backtrace
states = np.zeros(n_frames, dtype=np.intp)
states[-1] = int(np.argmax(viterbi[:, -1]))
states[-1] = int(np.argmax(viterbi))
for t in range(n_frames - 2, -1, -1):
states[t] = backpointer[states[t + 1], t + 1]

Expand All @@ -206,6 +202,8 @@ def _compute_confidence(self, similarity: np.ndarray, best_state: int) -> str:
"""
if best_state == _NO_CHORD_STATE:
return "low"
if similarity.size == 0 or not np.all(np.isfinite(similarity)):
return "low"

# Normalize similarities to probability distribution
sim_shifted = similarity - similarity.max()
Expand Down Expand Up @@ -296,27 +294,70 @@ def _build_observation_probs(
n_frames = chromagram.shape[1]
obs_probs = np.zeros((_NUM_CHORD_STATES, n_frames))

# Chord observation likelihoods from template similarity
# Normalize similarity per frame to get valid probability-like values
sim_max = similarity.max(axis=0, keepdims=True)
sim_shifted = similarity - sim_max
exp_sim = np.exp(sim_shifted * 2.0)
sim_sum = exp_sim.sum(axis=0, keepdims=True) + 1e-12
obs_probs[:24, :] = exp_sim / sim_sum
# Chord observation likelihoods from template similarity. A frame with
# any non-finite similarity is unknown evidence: neutralize the entire
# frame rather than allowing partial/corrupt DSP metadata into Viterbi.
n_sim_frames = similarity.shape[1]
valid_similarity_frames = np.all(np.isfinite(similarity), axis=0)
safe_similarity = np.where(
valid_similarity_frames[np.newaxis, :],
similarity,
0.0,
)

if n_sim_frames > 0:
sim_max = safe_similarity.max(axis=0, keepdims=True)
sim_shifted = safe_similarity - sim_max
exp_sim = np.exp(sim_shifted * 2.0)
sim_sum = exp_sim.sum(axis=0, keepdims=True) + 1e-12
if n_sim_frames >= n_frames:
obs_probs[:24, :] = (exp_sim / sim_sum)[:, :n_frames]
else:
obs_probs[:24, :n_sim_frames] = exp_sim / sim_sum

# Default uniform probability for any missing frames
if n_sim_frames < n_frames:
obs_probs[:24, n_sim_frames:] = 1.0 / 24.0

# N (no-chord) observation probability based on noise indicators
# N (no-chord) observation probability based on noise indicators.
# Non-finite chroma variance is unknown, not evidence of a flat signal.
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
chroma_vars = np.where(np.isfinite(chroma_vars), chroma_vars, 1.0)

# Missing or non-finite RMS is unknown rather than evidence of silence.
rms_vals = (
rms[:n_frames]
if len(rms) >= n_frames
else np.pad(
rms,
(0, n_frames - len(rms)),
constant_values=1.0,
)
)
rms_vals = np.where(np.isfinite(rms_vals), rms_vals, 1.0)

# Max similarity per frame: handle array length mismatches explicitly.
# Invalid similarity frames remain neutral for no-chord detection.
if n_sim_frames == 0:
max_sims = np.full(n_frames, 1.0)
else:
sim_max_raw = safe_similarity.max(axis=0)
sim_max_raw = np.where(valid_similarity_frames, sim_max_raw, 1.0)
if n_sim_frames >= n_frames:
max_sims = sim_max_raw[:n_frames]
else:
obs_probs[_NO_CHORD_STATE, i] = 0.05
max_sims = np.pad(
sim_max_raw,
(0, n_frames - n_sim_frames),
constant_values=1.0,
)

# Vectorized condition for high N probability when signal is low/flat
mask = (max_sims < 0.3) | (rms_vals < 0.01) | (chroma_vars < 0.02)

obs_probs[:24, mask] *= 0.1
obs_probs[_NO_CHORD_STATE, mask] = 0.9
obs_probs[_NO_CHORD_STATE, ~mask] = 0.05
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Normalize columns
col_sums = obs_probs.sum(axis=0, keepdims=True) + 1e-12
Expand Down Expand Up @@ -344,12 +385,15 @@ def _create_chord_segments(
current_confidence = "low"
start_frame = 0

n_sim_frames = similarity.shape[1]
missing_similarity = np.zeros(similarity.shape[0], dtype=similarity.dtype)
for i in range(n_frames):
state = int(decoded_states[i])
chord_label = self.chord_labels[state]

# Compute per-frame confidence from the similarity distribution
frame_confidence = self._compute_confidence(similarity[:, i], state)
sim_frame = similarity[:, i] if i < n_sim_frames else missing_similarity
frame_confidence = self._compute_confidence(sim_frame, state)
Comment thread
seonghobae marked this conversation as resolved.

if current_chord is None:
current_chord = chord_label
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Regression tests for finite-only chord-confidence arithmetic."""

from __future__ import annotations

import warnings

import numpy as np
import pytest

from bandscope_analysis.chords.chord_recognizer import ChordRecognizer


@pytest.mark.parametrize(
"invalid_value",
[np.nan, np.inf, -np.inf],
)
def test_non_finite_similarity_confidence_falls_back_without_runtime_warning(
invalid_value: float,
) -> None:
"""Unknown similarity evidence must yield low confidence without invalid arithmetic."""
recognizer = ChordRecognizer()
similarity = np.linspace(0.1, 0.9, 24, dtype=float)
similarity[5] = invalid_value

with warnings.catch_warnings():
warnings.simplefilter("error", RuntimeWarning)
assert recognizer._compute_confidence(similarity, best_state=0) == "low"
14 changes: 14 additions & 0 deletions services/analysis-engine/tests/test_chord_recognizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,20 @@ def test_chord_recognizer_empty_audio() -> None:
assert result == []


def test_chord_recognizer_does_not_apply_feature_local_duration_policy() -> None:
"""Delegate duration policy upstream instead of silently truncating one feature."""
recognizer = ChordRecognizer()
sample_rate = 1
audio = np.arange(601, dtype=np.float32)

with patch.object(recognizer, "_separate_harmonic", return_value=np.array([])) as separate:
result = recognizer.recognize(audio, sr=sample_rate)

separate.assert_called_once()
np.testing.assert_array_equal(separate.call_args.args[0], audio)
assert result == []


def test_chord_recognizer_unvoiced_audio() -> None:
"""Test chord recognition with noise."""
recognizer = ChordRecognizer()
Expand Down
Loading
Loading