From 3da7a27fa8a887325ccae3121e8abeb0ee9acf55 Mon Sep 17 00:00:00 2001 From: Luis Raimundo Date: Sat, 23 May 2026 14:05:56 +0100 Subject: [PATCH 1/4] fix: repair density semantics and local validation fixtures Co-authored-by: Cursor --- LOCAL_REPAIR_TRANSFER_MANIFEST_20260523.md | 116 ++++++++++ .../sample_clean_case/metrics_summary.txt | 2 + .../super_analysis_results.json | 8 + compile_metrics.py | 25 +- density.py | 2 + metrics_dictionary.json | 217 +++++++++++++----- tests/benchmarks/audio/harmonic_stack_220.wav | Bin 0 -> 44144 bytes .../benchmarks/audio/inharmonic_injection.wav | Bin 0 -> 44144 bytes tests/benchmarks/audio/pure_sine_440.wav | Bin 0 -> 44144 bytes tests/benchmarks/audio/subbass_injection.wav | Bin 0 -> 44144 bytes 10 files changed, 304 insertions(+), 66 deletions(-) create mode 100644 LOCAL_REPAIR_TRANSFER_MANIFEST_20260523.md create mode 100644 audio_analysis/batch_results/sample_clean_case/metrics_summary.txt create mode 100644 audio_analysis/batch_results/sample_clean_case/super_analysis_results.json create mode 100644 tests/benchmarks/audio/harmonic_stack_220.wav create mode 100644 tests/benchmarks/audio/inharmonic_injection.wav create mode 100644 tests/benchmarks/audio/pure_sine_440.wav create mode 100644 tests/benchmarks/audio/subbass_injection.wav diff --git a/LOCAL_REPAIR_TRANSFER_MANIFEST_20260523.md b/LOCAL_REPAIR_TRANSFER_MANIFEST_20260523.md new file mode 100644 index 0000000..3c50fd3 --- /dev/null +++ b/LOCAL_REPAIR_TRANSFER_MANIFEST_20260523.md @@ -0,0 +1,116 @@ +# LOCAL REPAIR TRANSFER MANIFEST (2026-05-23) + +Working folder: `C:\Users\lmr20\Desktop\SoundSpectrAnalyse-main\SoundSpectrAnalyse-main` + +## Stage 8 Scope + +- Cleanup/preparation only. +- No source logic changes during this stage. +- No GitHub operations. +- No Git initialization. + +## 1) Cleanup Artefacts Identified + +### `*.bak_*` (found before move) + +- `compile_metrics.py.bak_20260523_131008` +- `density.py.bak_20260523_124509` +- `metrics_dictionary.json.bak_20260523_125447` +- `test_benchmarks.py.bak_20260523_125037` +- `test_external_validation_marketing_ban.py.bak_20260523_125037` + +### Cache artefacts (found before removal) + +- `__pycache__/` directories: 5 + - `__pycache__/` + - `audio_analysis/__pycache__/` + - `tests/__pycache__/` + - `tests/formula_validation/__pycache__/` + - `tools/__pycache__/` +- `.pytest_cache/` directories: 1 + - `.pytest_cache/` +- `.mypy_cache/`: none found +- `.ruff_cache/`: none found +- `*.pyc` files: 145 + +## 2) Backup Files Moved Outside Project + +Destination folder: + +- `C:\Users\lmr20\Desktop\SoundSpectrAnalyse_local_repair_backups_20260523` + +Moved files: + +- `compile_metrics.py.bak_20260523_131008` +- `density.py.bak_20260523_124509` +- `metrics_dictionary.json.bak_20260523_125447` +- `test_benchmarks.py.bak_20260523_125037` +- `test_external_validation_marketing_ban.py.bak_20260523_125037` + +## 3) Cache Artefacts Removed (Safe Cleanup) + +Removed: + +- `__pycache__/` directories (5 total) +- `.pytest_cache/` directories (1 total) +- `*.pyc` files (145 total) + +Not removed: + +- `.mypy_cache/` (none found) +- `.ruff_cache/` (none found) + +Post-cleanup verification: + +- `*.bak_*`: none remaining in project +- `*.pyc`: none remaining in project +- `.pytest_cache/`: none remaining in project +- `__pycache__/`: none remaining in project + +## 4) Transfer Inventory + +### A. Modified Existing Files + +- `density.py` +- `compile_metrics.py` +- `metrics_dictionary.json` + +### B. Newly Created Required Files + +- `tests/benchmarks/audio/pure_sine_440.wav` +- `tests/benchmarks/audio/harmonic_stack_220.wav` +- `tests/benchmarks/audio/inharmonic_injection.wav` +- `tests/benchmarks/audio/subbass_injection.wav` +- `audio_analysis/batch_results/sample_clean_case/super_analysis_results.json` +- `audio_analysis/batch_results/sample_clean_case/metrics_summary.txt` + +### C. Files Moved Outside the Project + +- `compile_metrics.py.bak_20260523_131008` +- `density.py.bak_20260523_124509` +- `metrics_dictionary.json.bak_20260523_125447` +- `test_benchmarks.py.bak_20260523_125037` +- `test_external_validation_marketing_ban.py.bak_20260523_125037` + +## 5) Validation Results + +### Baseline validated state (before cleanup) + +- `python -m pytest -q` -> 822 passed, 0 failed, 40 skipped +- `python scripts/validate_stft_reference.py` -> 3 passed + +### Final verification (after cleanup) + +- `python -m pytest -q` -> 822 passed, 0 failed, 40 skipped, 996 warnings +- `python scripts/validate_stft_reference.py` -> 3 passed, 13 warnings + +## 6) Semantic Summary + +- `density.py`: Python 3.9 annotation compatibility fix (`from __future__ import annotations`). +- `compile_metrics.py`: log density adjusted to `log10(1 + sum(A))`; explicit `power_sum` debug basis preserves `Power_raw`. +- `metrics_dictionary.json`: repaired `quantity_type` values and `derived_from` references. +- Fixtures: deterministic benchmark audio fixtures and clean external-validation sample fixtures were added. + +## 7) Readiness Statement + +This folder is cleanup-complete, validated after cleanup, and ready to be used as the source for applying changes to a clean Git clone. diff --git a/audio_analysis/batch_results/sample_clean_case/metrics_summary.txt b/audio_analysis/batch_results/sample_clean_case/metrics_summary.txt new file mode 100644 index 0000000..04f4033 --- /dev/null +++ b/audio_analysis/batch_results/sample_clean_case/metrics_summary.txt @@ -0,0 +1,2 @@ +Metrics summary (clean sample fixture) +No external symbolic-engine references. diff --git a/audio_analysis/batch_results/sample_clean_case/super_analysis_results.json b/audio_analysis/batch_results/sample_clean_case/super_analysis_results.json new file mode 100644 index 0000000..3e17d83 --- /dev/null +++ b/audio_analysis/batch_results/sample_clean_case/super_analysis_results.json @@ -0,0 +1,8 @@ +{ + "analysis_id": "sample_clean_case", + "version": "local-fixture", + "metrics": { + "harmonic_energy_percentage": 71.0, + "inharmonic_energy_percentage": 29.0 + } +} \ No newline at end of file diff --git a/compile_metrics.py b/compile_metrics.py index c2ce9b9..1e6b45b 100644 --- a/compile_metrics.py +++ b/compile_metrics.py @@ -2144,7 +2144,7 @@ def extract_density_component_sum( Weighting-function semantics (single source of truth):: linear -> D = SUM(Amplitude_raw) - log -> D = SUM(LOG10(1 + Amplitude_raw)) per row (no band-total log) + log -> D = LOG10(1 + SUM(Amplitude_raw)) power -> D = SUM(Power_raw) (or SUM(Amplitude_raw ** 2) if Power_raw absent) @@ -2304,7 +2304,7 @@ def extract_density_component_sum( return result column_used = str(amp_col) series = pd.to_numeric(df[amp_col], errors="coerce") - sum_strategy = "sum_log10_1p_per_amplitude_raw" + sum_strategy = "log10_1p_sum_amplitude_raw" elif wf == "power": if power_col is not None: column_used = str(power_col) @@ -2355,9 +2355,7 @@ def extract_density_component_sum( raw_total = float(series.to_numpy(dtype=float)[mask].sum()) if wf == "log": - amps_log = series.to_numpy(dtype=float, copy=False)[mask] - amps_log = np.maximum(amps_log, 0.0) - d_value = float(np.sum(np.log10(1.0 + amps_log))) + d_value = float(np.log10(1.0 + max(0.0, raw_total))) else: d_value = raw_total @@ -2696,9 +2694,10 @@ def _extract_one( # "log", and Amplitude_display_scaled is never read at all. # ------------------------------------------------------------------ wf_op = _compile_operator_weight_function_key(weight_function) - wf_h = extract_density_component_sum(p, "Harmonic Spectrum", wf_op) - wf_i = extract_density_component_sum(p, "Inharmonic Spectrum", wf_op) - wf_s = extract_density_component_sum(p, "Sub-bass band", wf_op) + wf_components = "power" if basis == "power_sum" else wf_op + wf_h = extract_density_component_sum(p, "Harmonic Spectrum", wf_components) + wf_i = extract_density_component_sum(p, "Inharmonic Spectrum", wf_components) + wf_s = extract_density_component_sum(p, "Sub-bass band", wf_components) result["density_weight_function"] = str(wf_h.get("weight_function") or wf_op) result["harmonic_density_sum"] = wf_h.get("D") @@ -2723,17 +2722,17 @@ def _extract_one( if s ) result["density_component_sum_source"] = combined_source - if wf_op in DENSITY_WEIGHT_FUNCTION_VALID and wf_op == "log": + if wf_components in DENSITY_WEIGHT_FUNCTION_VALID and wf_components == "log": result["density_formula"] = ( "density_metric_raw = D_H*w_H + D_I*w_I + D_S*w_S; " - "D_band = SUM(log10(1 + Amplitude_raw))." + "D_band = log10(1 + SUM(Amplitude_raw))." ) - elif wf_op in DENSITY_WEIGHT_FUNCTION_VALID and wf_op == "power": + elif wf_components in DENSITY_WEIGHT_FUNCTION_VALID and wf_components == "power": result["density_formula"] = ( "density_metric_raw = D_H*w_H + D_I*w_I + D_S*w_S; " "D_band = SUM(Power_raw) (fallback SUM(Amplitude_raw**2))." ) - elif wf_op in DENSITY_WEIGHT_FUNCTION_VALID: + elif wf_components in DENSITY_WEIGHT_FUNCTION_VALID: result["density_formula"] = ( "density_metric_raw = D_H*w_H + D_I*w_I + D_S*w_S; " "D_band = SUM(Amplitude_raw)." @@ -2742,7 +2741,7 @@ def _extract_one( result["density_formula"] = ( "density_metric_raw = D_H*w_H + D_I*w_I + D_S*w_S; " f"D_band = density.apply_density_metric(Amplitude_raw_vector, " - f"weight_function={wf_op!r}) per spectrum sheet." + f"weight_function={wf_components!r}) per spectrum sheet." ) # Canonical per-band D values (weight_function-aware) replace the diff --git a/density.py b/density.py index 3103ccf..696e4b4 100644 --- a/density.py +++ b/density.py @@ -1,5 +1,7 @@ # density.py - Corrected Version +from __future__ import annotations + """ Module for calculating spectral density metrics for musical audio analysis. Implements weight functions, density calculations, and combined metrics for diff --git a/metrics_dictionary.json b/metrics_dictionary.json index e9ef70f..c602dca 100644 --- a/metrics_dictionary.json +++ b/metrics_dictionary.json @@ -84,7 +84,10 @@ "interpretation": "fraction of the analysed component power that is held by detected harmonic partials.", "do_not_interpret_as": "loudness, perceptual prominence, or a fraction of total signal energy (the denominator excludes residual full-spectrum noise outside the analysed component set).", "metric_family": "component_energy", - "derived_from": ["harmonic_energy_sum", "total_component_energy"], + "derived_from": [ + "harmonic_energy_sum", + "total_component_energy" + ], "independent_for_pca": true }, { @@ -98,7 +101,10 @@ "interpretation": "fraction of the analysed component power held by detected inharmonic (non-integer-multiple) partials.", "do_not_interpret_as": "perceptual roughness or dissonance; those are separate models.", "metric_family": "component_energy", - "derived_from": ["inharmonic_energy_sum", "total_component_energy"], + "derived_from": [ + "inharmonic_energy_sum", + "total_component_energy" + ], "independent_for_pca": true }, { @@ -112,7 +118,10 @@ "interpretation": "fraction of the analysed component power held by the aggregated sub-bass / ground-noise band below the configured cut-off (default ~200 Hz).", "do_not_interpret_as": "a separate noise-floor estimate; this aggregator includes any sub-bass peaks excluded from the harmonic template.", "metric_family": "component_energy", - "derived_from": ["component_harmonic_energy_ratio", "component_inharmonic_energy_ratio"], + "derived_from": [ + "component_harmonic_energy_ratio", + "component_inharmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -126,7 +135,10 @@ "interpretation": "convenience sum of the inharmonic and sub-bass component shares; non-harmonic share of analysed components.", "do_not_interpret_as": "a noise-to-signal ratio; this excludes content outside the analysed peak / sub-bass aggregation.", "metric_family": "component_energy", - "derived_from": ["component_inharmonic_energy_ratio", "component_subbass_energy_ratio"], + "derived_from": [ + "component_inharmonic_energy_ratio", + "component_subbass_energy_ratio" + ], "independent_for_pca": false }, { @@ -140,7 +152,12 @@ "interpretation": "diffuse non-harmonic residual energy fraction; spectral power that survived the noise-floor rejection but is NOT in the accepted harmonic peaks, NOT in the discrete inharmonic peaks, and NOT in the sub-bass region. Captures broadband bow/breath noise that the discrete-inharmonic accounting alone would miss.", "do_not_interpret_as": "the same quantity as component_inharmonic_energy_ratio (denominator is intentionally different — H+I+S+residual vs H+I+S — and the masks are disjoint).", "metric_family": "component_energy", - "derived_from": ["harmonic_energy_sum", "inharmonic_energy_sum", "subbass_energy_sum", "total_filtered_spectral_energy"], + "derived_from": [ + "harmonic_energy_sum", + "inharmonic_energy_sum", + "subbass_energy_sum", + "total_filtered_spectral_energy" + ], "independent_for_pca": true }, { @@ -154,7 +171,11 @@ "interpretation": "total non-harmonic share of the surviving spectrum: discrete inharmonic + sub-bass + diffuse residual. Complementary to component_harmonic_energy_ratio computed over the extended denominator.", "do_not_interpret_as": "the arithmetic sum of component_inharmonic_energy_ratio + component_subbass_energy_ratio + component_residual_noise_energy_ratio when those use different denominators (this metric uses H+I+S+residual throughout).", "metric_family": "component_energy", - "derived_from": ["component_residual_noise_energy_ratio", "harmonic_energy_sum", "total_filtered_spectral_energy"], + "derived_from": [ + "component_residual_noise_energy_ratio", + "harmonic_energy_sum", + "total_filtered_spectral_energy" + ], "independent_for_pca": false }, { @@ -168,7 +189,10 @@ "interpretation": "binary coefficient used by downstream weighting / dissonance models to blend harmonic and inharmonic contributions.", "do_not_interpret_as": "the same number as component_harmonic_energy_ratio; the denominator is intentionally H + I, not H + I + S.", "metric_family": "model_weight", - "derived_from": ["harmonic_energy_sum", "inharmonic_energy_sum"], + "derived_from": [ + "harmonic_energy_sum", + "inharmonic_energy_sum" + ], "independent_for_pca": true }, { @@ -182,7 +206,9 @@ "interpretation": "binary coefficient used by downstream weighting / dissonance models.", "do_not_interpret_as": "the same number as component_inharmonic_energy_ratio; the denominator is intentionally H + I, not H + I + S.", "metric_family": "model_weight", - "derived_from": ["model_harmonic_weight"], + "derived_from": [ + "model_harmonic_weight" + ], "independent_for_pca": false }, { @@ -238,7 +264,9 @@ "interpretation": "Publication-facing column name for the canonical v5 adapted density; use when the internal lineage suffix is not needed in tables.", "do_not_interpret_as": "an independent quantity from canonical_density_v5_adapted; values are duplicated for readability only.", "metric_family": "density", - "derived_from": ["canonical_density_v5_adapted"], + "derived_from": [ + "canonical_density_v5_adapted" + ], "independent_for_pca": false }, { @@ -252,7 +280,9 @@ "interpretation": "canonical, publication-grade run-relative normalised density derived from canonical_density_v5_adapted. Default plotting metric for Canonical_Metrics.", "do_not_interpret_as": "an absolute density. The normalisation is run-relative; do not compare across different runs unless the normalization reference is identical. NOTE: density_normalized_global is the canonical density max-norm; density_metric_normalized (Density_Metrics sheet) is a DIFFERENT diagnostic descriptor that max-normalises the weighted partial-sum density_metric_raw, not canonical_density_v5_adapted.", "metric_family": "density", - "derived_from": ["canonical_density_v5_adapted"], + "derived_from": [ + "canonical_density_v5_adapted" + ], "independent_for_pca": false }, { @@ -266,7 +296,9 @@ "interpretation": "density normalised by the number of detected harmonic orders.", "do_not_interpret_as": "energy per partial; the denominator is a count, not a power.", "metric_family": "density", - "derived_from": ["canonical_density_v5_adapted"], + "derived_from": [ + "canonical_density_v5_adapted" + ], "independent_for_pca": true }, { @@ -308,7 +340,9 @@ "interpretation": "compatibility alias of component_harmonic_energy_ratio. Demoted from canonical → diagnostic in v1.1 of the dictionary because it is the SAME quantity as the explicit canonical name.", "do_not_interpret_as": "an independent measurement; for new code use component_harmonic_energy_ratio.", "metric_family": "component_energy", - "derived_from": ["component_harmonic_energy_ratio"], + "derived_from": [ + "component_harmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -322,7 +356,9 @@ "interpretation": "compatibility alias of component_inharmonic_energy_ratio.", "do_not_interpret_as": "an independent measurement.", "metric_family": "component_energy", - "derived_from": ["component_inharmonic_energy_ratio"], + "derived_from": [ + "component_inharmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -336,7 +372,9 @@ "interpretation": "compatibility alias of component_subbass_energy_ratio.", "do_not_interpret_as": "a calibrated noise-floor estimate.", "metric_family": "component_energy", - "derived_from": ["component_subbass_energy_ratio"], + "derived_from": [ + "component_subbass_energy_ratio" + ], "independent_for_pca": false }, { @@ -350,7 +388,10 @@ "interpretation": "linear power ratio between harmonic and inharmonic detected components.", "do_not_interpret_as": "a dB value; convert with 10·log10 if a dB reading is needed.", "metric_family": "harmonicity", - "derived_from": ["harmonic_energy_sum", "inharmonic_energy_sum"], + "derived_from": [ + "harmonic_energy_sum", + "inharmonic_energy_sum" + ], "independent_for_pca": true }, { @@ -406,7 +447,13 @@ "interpretation": "Lower-frequency guard: spectral energy below this cutoff in the fixed diagnostic band is classified as subfundamental residual for labelling/audit, not as proven physical sub-bass.", "do_not_interpret_as": "the STFT bin width, the orchestral sub-bass limit, or a noise-floor estimate.", "metric_family": "validation", - "derived_from": ["f0_final_hz", "subfundamental_margin_percent", "min_floor_hz", "max_fraction_of_f0", "leakage_guard_cutoff_hz"], + "derived_from": [ + "f0_final_hz", + "subfundamental_margin_percent", + "min_floor_hz", + "max_fraction_of_f0", + "leakage_guard_cutoff_hz" + ], "independent_for_pca": false }, { @@ -420,7 +467,9 @@ "interpretation": "Nominal register margin below f0 used to form percentage_subfundamental_cutoff_hz = f0 * (1 − M/100).", "do_not_interpret_as": "a loudness cue or psychoacoustic sharpness metric.", "metric_family": "validation", - "derived_from": ["f0_final_hz"], + "derived_from": [ + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -434,7 +483,10 @@ "interpretation": "Pure register-policy cutoff line before min-floor, leakage guard, and f0·max_fraction caps.", "do_not_interpret_as": "the final adaptive guard; use adaptive_subfundamental_cutoff_hz after caps.", "metric_family": "validation", - "derived_from": ["f0_final_hz", "subfundamental_margin_percent"], + "derived_from": [ + "f0_final_hz", + "subfundamental_margin_percent" + ], "independent_for_pca": false }, { @@ -448,7 +500,9 @@ "interpretation": "Optional lower bound derived from harmonic main-lobe / protection half-width so window leakage just below f0 is not mis-labelled as musical subfundamental content.", "do_not_interpret_as": "proof of acoustic leakage level without STFT geometry context.", "metric_family": "validation", - "derived_from": ["f0_final_hz"], + "derived_from": [ + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -476,7 +530,9 @@ "interpretation": "Upper cap on the adaptive cutoff as a fraction of f0 so the guard never approaches Nyquist or swallows the fundamental.", "do_not_interpret_as": "the detected bandwidth of the note.", "metric_family": "validation", - "derived_from": ["f0_final_hz"], + "derived_from": [ + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -490,7 +546,10 @@ "interpretation": "Effective margin (percent of f0) implied by the final adaptive cutoff after all floors/guards/caps.", "do_not_interpret_as": "identical to subfundamental_margin_percent when caps bind; compare both.", "metric_family": "validation", - "derived_from": ["f0_final_hz", "adaptive_subfundamental_cutoff_hz"], + "derived_from": [ + "f0_final_hz", + "adaptive_subfundamental_cutoff_hz" + ], "independent_for_pca": false }, { @@ -504,7 +563,9 @@ "interpretation": "True when an adaptive subfundamental guard was computed from a valid f0; false when f0 invalid and policy returns sentinel NaNs.", "do_not_interpret_as": "evidence that the recording is in tune.", "metric_family": "validation", - "derived_from": ["f0_final_hz"], + "derived_from": [ + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -518,7 +579,9 @@ "interpretation": "Documents which guard branch produced the exported cutoff metadata.", "do_not_interpret_as": "user-facing legal policy text.", "metric_family": "validation", - "derived_from": ["f0_final_hz"], + "derived_from": [ + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -546,7 +609,10 @@ "interpretation": "Records whether the adaptive cutoff was taken from the per-note analysis export or recomputed at compile from f0_final_hz.", "do_not_interpret_as": "the numeric cutoff value.", "metric_family": "provenance", - "derived_from": ["f0_final_hz", "adaptive_subfundamental_cutoff_hz"], + "derived_from": [ + "f0_final_hz", + "adaptive_subfundamental_cutoff_hz" + ], "independent_for_pca": false }, { @@ -602,7 +668,10 @@ "interpretation": "Audit field: which input dominated the final adaptive_subfundamental_cutoff_hz after caps.", "do_not_interpret_as": "the cutoff frequency in Hz.", "metric_family": "provenance", - "derived_from": ["adaptive_subfundamental_cutoff_hz", "f0_final_hz"], + "derived_from": [ + "adaptive_subfundamental_cutoff_hz", + "f0_final_hz" + ], "independent_for_pca": false }, { @@ -728,7 +797,11 @@ "interpretation": "denominator audit for the component_* ratios.", "do_not_interpret_as": "total signal energy; this excludes content outside the analysed peak / sub-bass aggregation.", "metric_family": "component_energy", - "derived_from": ["harmonic_energy_sum", "inharmonic_energy_sum", "subbass_energy_sum"], + "derived_from": [ + "harmonic_energy_sum", + "inharmonic_energy_sum", + "subbass_energy_sum" + ], "independent_for_pca": false }, { @@ -756,7 +829,12 @@ "interpretation": "diffuse non-harmonic residual: spectral energy that survived the noise-floor rejection but is NOT in the accepted harmonic peaks, NOT in the discrete inharmonic peaks, and NOT in the sub-bass region.", "do_not_interpret_as": "an alias of inharmonic_energy_sum; this is a separate residual bucket.", "metric_family": "component_energy", - "derived_from": ["total_filtered_spectral_energy", "harmonic_energy_sum", "inharmonic_energy_sum", "subbass_energy_sum"], + "derived_from": [ + "total_filtered_spectral_energy", + "harmonic_energy_sum", + "inharmonic_energy_sum", + "subbass_energy_sum" + ], "independent_for_pca": false }, { @@ -952,7 +1030,10 @@ "interpretation": "deprecated: amplitude-based combined density.", "do_not_interpret_as": "an energy ratio or canonical density.", "metric_family": "legacy_compatibility", - "derived_from": ["legacy_harmonic_density", "legacy_inharmonic_density"], + "derived_from": [ + "legacy_harmonic_density", + "legacy_inharmonic_density" + ], "independent_for_pca": false }, { @@ -966,7 +1047,10 @@ "interpretation": "deprecated: 'harmonic percentage' computed on amplitude sums.", "do_not_interpret_as": "the same as component_harmonic_energy_ratio (different denominator AND quantity type).", "metric_family": "legacy_compatibility", - "derived_from": ["legacy_harmonic_density", "legacy_inharmonic_density"], + "derived_from": [ + "legacy_harmonic_density", + "legacy_inharmonic_density" + ], "independent_for_pca": false }, { @@ -980,7 +1064,9 @@ "interpretation": "deprecated: complement of legacy_harmonic_density_percentage.", "do_not_interpret_as": "the same as component_inharmonic_energy_ratio.", "metric_family": "legacy_compatibility", - "derived_from": ["legacy_harmonic_density_percentage"], + "derived_from": [ + "legacy_harmonic_density_percentage" + ], "independent_for_pca": false }, { @@ -994,7 +1080,9 @@ "interpretation": "compatibility alias; prefer component_harmonic_energy_ratio in new code.", "do_not_interpret_as": "a separate or independent measurement.", "metric_family": "legacy_compatibility", - "derived_from": ["component_harmonic_energy_ratio"], + "derived_from": [ + "component_harmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -1008,7 +1096,9 @@ "interpretation": "compatibility alias.", "do_not_interpret_as": "an independent measurement.", "metric_family": "legacy_compatibility", - "derived_from": ["component_inharmonic_energy_ratio"], + "derived_from": [ + "component_inharmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -1022,7 +1112,9 @@ "interpretation": "compatibility alias.", "do_not_interpret_as": "an independent measurement.", "metric_family": "legacy_compatibility", - "derived_from": ["component_subbass_energy_ratio"], + "derived_from": [ + "component_subbass_energy_ratio" + ], "independent_for_pca": false }, { @@ -1036,7 +1128,9 @@ "interpretation": "compatibility alias.", "do_not_interpret_as": "an independent measurement.", "metric_family": "legacy_compatibility", - "derived_from": ["component_total_inharmonic_energy_ratio"], + "derived_from": [ + "component_total_inharmonic_energy_ratio" + ], "independent_for_pca": false }, { @@ -1092,7 +1186,11 @@ "interpretation": "unweighted diagnostic total of the three per-band partial sums.", "do_not_interpret_as": "the final density metric; it ignores the canonical component_*_energy_ratio weighting. Plot density_metric_normalized (Density_Metrics) or density_normalized_global (Canonical_Metrics) instead.", "metric_family": "legacy_compatibility", - "derived_from": ["Harmonic Partials sum", "Inharmonic Partials sum", "Sub-bass sum"], + "derived_from": [ + "Harmonic Partials sum", + "Inharmonic Partials sum", + "Sub-bass sum" + ], "independent_for_pca": false }, { @@ -1107,7 +1205,7 @@ "do_not_interpret_as": "an independent canonical descriptor; it is an additive term of density_metric_raw and shares the latter's run-relative interpretation. NOT for publication.", "metric_family": "density", "derived_from": [ - "harmonic_density_sum", + "Harmonic Partials sum", "component_harmonic_energy_ratio" ], "independent_for_pca": false @@ -1124,7 +1222,7 @@ "do_not_interpret_as": "an independent canonical descriptor; additive term of density_metric_raw.", "metric_family": "density", "derived_from": [ - "inharmonic_density_sum", + "Inharmonic Partials sum", "component_inharmonic_energy_ratio" ], "independent_for_pca": false @@ -1141,7 +1239,7 @@ "do_not_interpret_as": "an independent canonical descriptor; additive term of density_metric_raw.", "metric_family": "density", "derived_from": [ - "subbass_density_sum", + "Sub-bass sum", "component_subbass_energy_ratio" ], "independent_for_pca": false @@ -1150,7 +1248,7 @@ "name": "density_weighted_sum", "status": "diagnostic", "formula": "harmonic_density_sum * w_H + inharmonic_density_sum * w_I + subbass_density_sum * w_S", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a (raw, unbounded)", "unit": "depends on compile weight_function (linear ~ amplitude scale; log ~ log-density scale)", "source": "compile_metrics.extract_density_components_from_per_note_workbook", @@ -1158,9 +1256,9 @@ "do_not_interpret_as": "a linear sum of Amplitude_raw (see harmonic_amplitude_sum). Not invariant to weight_function changes. Not a canonical publication metric — prefer effective_partial_density on Canonical_Metrics.", "metric_family": "density", "derived_from": [ - "harmonic_density_sum", - "inharmonic_density_sum", - "subbass_density_sum", + "Harmonic Partials sum", + "Inharmonic Partials sum", + "Sub-bass sum", "component_harmonic_energy_ratio", "component_inharmonic_energy_ratio", "component_subbass_energy_ratio" @@ -1171,14 +1269,16 @@ "name": "density_log_weighted", "status": "diagnostic", "formula": "log10(1 + density_weighted_sum)", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "log10 scale (dimensionless argument)", "source": "compile_metrics.extract_density_components_from_per_note_workbook", "interpretation": "log-space view of density_weighted_sum. Default auto-plot preference on Density_Metrics sheet when present (publication_chart_policy).", "do_not_interpret_as": "independent of compile weight_function; it inherits sensitivity from density_weighted_sum.", "metric_family": "density", - "derived_from": ["density_weighted_sum"], + "derived_from": [ + "density_weighted_sum" + ], "independent_for_pca": false }, { @@ -1210,28 +1310,33 @@ "interpretation": "Density_Metrics-sheet default plotting metric. Run-relative normalization of density_metric_raw to [0, 1]; suitable for comparison *within* one compiled workbook. The canonical, corpus-wide normalized density descriptor for publication is density_normalized_global (Canonical_Metrics, derived from canonical_density_v5_adapted).", "do_not_interpret_as": "comparable across different runs unless the normalization reference (max(density_metric_raw)) is identical. Not a canonical publication metric — prefer density_normalized_global for cross-run publication. Run-relative normalization; do not compare across different runs unless the normalization reference is identical. NOTE: previous versions aliased density_metric_normalized to density_normalized_global; the two are now distinct descriptors.", "metric_family": "density", - "derived_from": ["density_metric_raw"], + "derived_from": [ + "density_metric_raw" + ], "independent_for_pca": false }, { "name": "Combined Density Metric", "status": "legacy", "formula": "Stage-1 combined_density_metric_value (log/expm1 or calculate_combined_density_metric on harmonic + inharmonic legacy scalars)", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "legacy apply_density_metric scale (unbounded)", "source": "proc_audio (Legacy_Density_Metrics sheet) / compile_metrics merge", "interpretation": "v5-style combined harmonic/inharmonic density scalar. Stronger dynamic separation than density_weighted_sum under log compile on some corpora.", "do_not_interpret_as": "equal to density_weighted_sum or the research mean (DWS+CDM)/2; not on Density_Metrics allow-list.", "metric_family": "legacy_compatibility", - "derived_from": ["Spectral Density Metric", "Filtered Density Metric"], + "derived_from": [ + "Spectral Density Metric", + "Filtered Density Metric" + ], "independent_for_pca": false }, { "name": "Spectral Density Metric", "status": "legacy", "formula": "spectral_density_metric_value (whole-spectrum legacy path; masking off when spectral_masking_enabled=False)", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "legacy power/average density scale", "source": "proc_audio (Legacy_Density_Metrics sheet)", @@ -1245,7 +1350,7 @@ "name": "Filtered Density Metric", "status": "legacy", "formula": "filtered_density_metric_value = apply_density_metric on filtered_list_df amplitudes", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "legacy apply_density_metric scale", "source": "proc_audio (Legacy_Density_Metrics sheet)", @@ -1259,28 +1364,34 @@ "name": "Weighted Combined Metric", "status": "legacy", "formula": "f(alpha * SDM + beta * FDM) with f from compile weight_function (e.g. log -> log1p)", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "depends on weight_function", "source": "compile_metrics.apply_weighted_combination", "interpretation": "v5-style weighted legacy combination using model_harmonic_weight / model_inharmonic_weight (H/(H+I)), not measured H+I+S energy ratios.", "do_not_interpret_as": "density_weighted_sum_cdm_mean; forbidden on Density_Metrics sheet.", "metric_family": "legacy_compatibility", - "derived_from": ["Spectral Density Metric", "Filtered Density Metric"], + "derived_from": [ + "Spectral Density Metric", + "Filtered Density Metric" + ], "independent_for_pca": false }, { "name": "density_weighted_sum_cdm_mean", "status": "diagnostic", "formula": "(density_weighted_sum + Combined Density Metric) / 2", - "quantity_type": "density", + "quantity_type": "amplitude", "denominator": "n/a", "unit": "mixed scale (editorial average of two unlike definitions)", "source": "tools/export_research_density_workbook.build_spectral_density_metrics", "interpretation": "Research-workbook-only editorial blend for plotting when a single column is desired. Not computed at compile time.", "do_not_interpret_as": "a canonical acoustic measure, Weighted Combined Metric, or energy-weighted density.", "metric_family": "density", - "derived_from": ["density_weighted_sum", "Combined Density Metric"], + "derived_from": [ + "density_weighted_sum", + "Combined Density Metric" + ], "independent_for_pca": false }, { diff --git a/tests/benchmarks/audio/harmonic_stack_220.wav b/tests/benchmarks/audio/harmonic_stack_220.wav new file mode 100644 index 0000000000000000000000000000000000000000..db8185afe0bcbfe46ec50263edb5779b4270942d GIT binary patch literal 44144 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 literal 0 HcmV?d00001 diff --git a/tests/benchmarks/audio/subbass_injection.wav b/tests/benchmarks/audio/subbass_injection.wav new file mode 100644 index 0000000000000000000000000000000000000000..4faca1433d9e08267abc64bbad9c5afa2a2691d6 GIT binary patch literal 44144 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 Date: Sat, 23 May 2026 14:47:13 +0100 Subject: [PATCH 2/4] fix: make path redaction cross-platform and declare psutil Co-authored-by: Cursor --- metadata_sanitizer.py | 11 +++++++++-- requirements-dev.txt | 1 + 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/metadata_sanitizer.py b/metadata_sanitizer.py index decc7b8..27811f8 100644 --- a/metadata_sanitizer.py +++ b/metadata_sanitizer.py @@ -13,7 +13,7 @@ import os import re import zipfile -from pathlib import Path +from pathlib import Path, PureWindowsPath from datetime import datetime, timezone from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Set, Tuple, Union @@ -699,12 +699,19 @@ def sanitize_path_for_publication(path: Union[str, Path, None], dataset_root: Op """Basename or POSIX path relative to *dataset_root* (never absolute host paths).""" if path is None: return "" - p = Path(str(path)) + path_text = str(path) + # Treat Windows-formatted paths explicitly so basename extraction works on Linux runners. + is_windows_style = bool(_WIN_ABS_START_EXPORT.match(path_text)) or "\\" in path_text + p = Path(path_text) if dataset_root is not None: try: + if is_windows_style: + return PureWindowsPath(path_text).name return p.resolve().relative_to(Path(dataset_root).resolve()).as_posix() except Exception: pass + if is_windows_style: + return PureWindowsPath(path_text).name or "path_redacted" return p.name if p.name else "path_redacted" diff --git a/requirements-dev.txt b/requirements-dev.txt index d7876dc..44e9dd2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,6 +4,7 @@ # Testing framework pytest>=7.0.0,<8.0.0 pytest-cov>=3.0.0,<4.0.0 +psutil>=5.9.0,<6.0.0 # Code coverage (compatible with numba) # Note: coverage 6.x is compatible with numba 0.56-0.59 From cde9231f48b8ce35c0d1721497b571ea402d0f41 Mon Sep 17 00:00:00 2001 From: Luis Raimundo Date: Sat, 23 May 2026 15:07:38 +0100 Subject: [PATCH 3/4] fix: sanitize nested Windows paths on Linux Co-authored-by: Cursor --- metadata_sanitizer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/metadata_sanitizer.py b/metadata_sanitizer.py index 27811f8..e90d8ca 100644 --- a/metadata_sanitizer.py +++ b/metadata_sanitizer.py @@ -654,7 +654,9 @@ def detect_absolute_local_path(value: Any) -> bool: def publication_audio_path_fields(path: Union[str, Path], *, dataset_root: Optional[Path] = None) -> dict[str, Any]: """Publication-safe audio path decomposition (no host directories).""" - p = Path(str(path)) + path_text = str(path) + is_windows_style = bool(_WIN_ABS_START_EXPORT.match(path_text)) or "\\" in path_text + p = PureWindowsPath(path_text) if is_windows_style else Path(path_text) name = p.name or "unknown_audio" stem = p.stem ext = p.suffix @@ -663,7 +665,7 @@ def publication_audio_path_fields(path: Union[str, Path], *, dataset_root: Optio rel = name if dataset_root is not None: try: - rel = Path(p).resolve().relative_to(Path(dataset_root).resolve()).as_posix() + rel = Path(path_text).resolve().relative_to(Path(dataset_root).resolve()).as_posix() except Exception: rel = name rel_posix = rel.replace("\\", "/") From 714ce630df7dfc98cce77e178eb73b85cb1fe7a3 Mon Sep 17 00:00:00 2001 From: Luis Raimundo Date: Sat, 23 May 2026 16:41:37 +0100 Subject: [PATCH 4/4] chore: drop Python 3.9 support Co-authored-by: Cursor --- .github/workflows/ci.yml | 2 +- README.md | 2 +- pyproject.toml | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12b84ca..34c5dbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.9", "3.10", "3.11"] + python-version: ["3.10", "3.11"] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 diff --git a/README.md b/README.md index 16ae5b4..77c42a2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Spectral analysis for acoustic research. **Canonical publication pipeline:** **` Optional **batch preprocessing** (`batch_audio_analyzer` / `super_audio_analyzer`) may supply **`batch_summary.xlsx`** for empirical **H+I+S** profiles and **H/(H+I)** model coefficients; it is **not** required for the canonical chain above. Legacy Tk / PyQt entry points remain ancillary. **Package version:** 3.7.0 (`pyproject.toml`; at runtime: `importlib.metadata.version("soundspectranalyse")`) -**Python:** ≥ 3.9 +**Python:** 3.10 and 3.11 (supported); Python 3.9 is not supported. ## Install diff --git a/pyproject.toml b/pyproject.toml index e4ff7bb..5383386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "soundspectranalyse" version = "3.7.0" description = "Advanced spectral analysis tool for acoustic research with harmonic-order-aware spectral processing" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10,<3.12" license = {text = "Scientific Research Use"} authors = [ {name = "Research Team", email = "research@example.com"} @@ -17,7 +17,6 @@ classifiers = [ "Intended Audience :: Science/Research", "Topic :: Scientific/Engineering", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] @@ -105,4 +104,4 @@ markers = [ [tool.black] line-length = 100 -target-version = ['py38', 'py39', 'py310', 'py311'] +target-version = ['py310', 'py311']