Repository navigation
Conversation
|
If i set via --bpm range, in web, it is writen as "Default". |
Bug: the web BPM-range control never renders the persisted value — and every save wipes the rest of the settingsFound while testing this PR. Two related bugs, both in the new 1. Render always shows "Default"
const bpmFull = (cfg && cfg.full) || {}; // undefined on the poll pathSo 2. Saving from the web destructively truncates the whole settings config
const full = JSON.parse(JSON.stringify((currentSettings && currentSettings.full) || {}));The POST then sends (Side note, design rather than bug: FixAccept both shapes when reading, and base the POST on the full config: 1|diff --git a/api/web/index.html b/api/web/index.html
2|index 1aabd23..a34b4f6 100644
3|--- a/api/web/index.html
4|+++ b/api/web/index.html
5|@@ -7269,7 +7269,9 @@ function renderSettings(cfg) {
6|
7| // LIBRARY SOURCE: import from rekordbox + bulk path fix (not a CDJ setting).
8| if (section.key === 'source') {
9|- const bpmFull = (cfg && cfg.full) || {};
10|+ // cfg is either the API response (has .full) or the full config itself
11|+ // (1 Hz poll passes settings.full) — accept both shapes.
12|+ const bpmFull = (cfg && (cfg.full || cfg)) || {};
13| const bpmCurMin = Number(bpmFull.bpm_range_min) || 0;
14| const bpmCurMax = Number(bpmFull.bpm_range_max) || 0;
15| const bpmCustomActive = bpmCurMin > 0 && bpmCurMax > bpmCurMin &&
16|@@ -7412,7 +7414,7 @@ async function handleSettingChange(select) {
17| // when the persisted range matches no preset (e.g. a custom value entered here
18| // or via --bpm-range on the CLI).
19| function bpmRangeOptionsHTML(cfg) {
20|- const full = (cfg && cfg.full) || {};
21|+ const full = (cfg && (cfg.full || cfg)) || {};
22| const curMin = Number(full.bpm_range_min) || 0;
23| const curMax = Number(full.bpm_range_max) || 0;
24| let matched = false;
25|@@ -7429,15 +7431,19 @@ function bpmRangeOptionsHTML(cfg) {
26| // postBPMRange persists a range via the full-config POST (the same path every
27| // settings field uses) and reflects it immediately.
28| async function postBPMRange(min, max, label) {
29|- const full = JSON.parse(JSON.stringify((currentSettings && currentSettings.full) || {}));
30|+ // currentSettings is the full config itself (renderSettings receives
31|+ // settings.full in the poll loop) — base the POST on it, not .full, or the
32|+ // full-config POST would wipe every other settings field.
33|+ const full = JSON.parse(JSON.stringify(currentSettings || {}));
34| full.bpm_range_min = min;
35| full.bpm_range_max = max;
36|+ delete full.full; // in case currentSettings was the API response shape
37| const r = await fetch('/api/settings', {
38| method: 'POST', headers: {'Content-Type': 'application/json'},
39| body: JSON.stringify({ full }),
40| });
41| if (!r.ok) { toast('SAVE FAILED', 'error'); return; }
42|- if (currentSettings) { currentSettings.full = full; lastSettingsKey = ''; }
43|+ if (currentSettings) { currentSettings.bpm_range_min = min; currentSettings.bpm_range_max = max; lastSettingsKey = ''; }
44| toast('BPM RANGE · ' + label, 'success');
45| }
46| ```
Verified locally: dropdown now shows the persisted preset, web changes survive the 1 Hz poll and a page reload, and `settings.json` keeps its other sections after a BPM save. Applied on top of the PR as a verification patch (`git apply` on the PR head, no commits pushed). |
…eview)
renderSettings receives the full config itself on the 1 Hz poll
(renderSettings(settings.full)), not the API-response shape, so the BPM
code's cfg.full was always undefined: the dropdown always showed Default
even with a persisted range, and postBPMRange built its POST from
currentSettings.full (undefined) and sent {full: {only the two BPM
fields}}. Since the settings POST does a SetConfig replace, every BPM
change rewrote settings.json with just those two fields, wiping
my_setting, dev_setting, and the rest.
Accept both config shapes (x.full || x) at the read sites, and base the
POST on the whole config so the other sections are preserved. Verified
both shapes read the persisted range and the POST payload retains the
other settings.
Found with a byte-exact root-cause analysis and a verification patch in
the review of #55.
Co-authored-by: kayrozen <kayrozen@users.noreply.github.com>
…-up) --bpm-range overrides the analysis range for the session without writing settings.json, so the settings GET (which returned only the persisted config) showed Default while a flag-set range was actually in force. Report the effective analysis range instead, so the UI shows a flag-set value. Editing it in the UI still persists to settings.json as before, and the flag stays non-persistent. Verified live: --bpm-range 90-180 now reads back 90/180 while settings.json keeps no persisted range. Raised as a side note in the #55 review by kayrozen.
|
Great catch, and thank you for the root-cause write-up, looks good on both counts. The settings-wipe one especially, that's destructive. Pushed the fix. I went with the symmetric form of your read fix on the write side too, basing the POST on On your side note: yes, Give it a shot and let me know. Thanks again! |
…eview)
renderSettings receives the full config itself on the 1 Hz poll
(renderSettings(settings.full)), not the API-response shape, so the BPM
code's cfg.full was always undefined: the dropdown always showed Default
even with a persisted range, and postBPMRange built its POST from
currentSettings.full (undefined) and sent {full: {only the two BPM
fields}}. Since the settings POST does a SetConfig replace, every BPM
change rewrote settings.json with just those two fields, wiping
my_setting, dev_setting, and the rest.
Accept both config shapes (x.full || x) at the read sites, and base the
POST on the whole config so the other sections are preserved. Verified
both shapes read the persisted range and the POST payload retains the
other settings.
Found with a byte-exact root-cause analysis and a verification patch in
the review of #55.
Co-authored-by: kayrozen <kayrozen@users.noreply.github.com>
…-up) --bpm-range overrides the analysis range for the session without writing settings.json, so the settings GET (which returned only the persisted config) showed Default while a flag-set range was actually in force. Report the effective analysis range instead, so the UI shows a flag-set value. Editing it in the UI still persists to settings.json as before, and the flag stays non-persistent. Verified live: --bpm-range 90-180 now reads back 90/180 while settings.json keeps no persisted range. Raised as a side note in the #55 review by kayrozen.
Fast genres like drum & bass analyze at half (~87) or a third (~58) of their true tempo. The cause is the detector's candidate window, hard-coded to 80-170 BPM: a real 174 is above the ceiling, so it is rejected and the in-window half wins. A perceptual prior centred on 130 reinforces the pull. Add an opt-in --bpm-range MIN-MAX (e.g. 160-185). When set it becomes the candidate window and re-centres the prior on the range's geometric mean, so the true tempo is the only harmonic that lands in-window and wins. The default (0) keeps the historical 80-170 window and 130 prior unchanged, so nothing changes for anyone who does not set it. Changing the range changes analysis output, so each Result records the range it was computed under and the cache re-analyzes a track when the setting differs. Global setting for now; a per-batch option in the web Add Files flow is a likely follow-up for mixed-genre libraries. Resolves the request in issue #52.
Expose the --bpm-range setting in the web UI so it isn't startup-only. The Library Source settings section gets a BPM Range dropdown: Default (shown with its actual 80-170 window) plus octave-wide presets 60-120, 70-140, 80-160, 90-180, 100-200, and a Custom option with min/max inputs. The presets and the custom inputs stay inside the beat detector's real 60-200 BPM search range (autocorrelation + peak spacing) rather than copying rekordbox's list, whose 48-95 and 108-255 entries would have dead zones here. Custom values are clamped to 60-200 with a hint. The range persists in settings.json alongside the other settings (SettingsConfig.BPMRangeMin/Max, like TrackDetail) and is read at startup before analysis runs, with --bpm-range overriding it for that session. Changing it in the UI applies to the analyzer live and drops cached analyses so affected tracks re-analyze as they're next loaded (lazy). The analyzer's tempo range is now behind a mutex (SetTempoRange/TempoRange) since the settings handler can change it at runtime while analysis workers read it. Adds Store.InvalidateAll for the runtime cache drop, a settings persistence test, and verified the HTTP round-trip (GET/POST/persist/sync).
Note the configurable Analysis BPM range (web + --bpm-range) in the feature list, and that the detector covers 60-200 BPM — tracks above 200 analyze at a sub-harmonic (usually half-tempo), the current ceiling.
…eview)
renderSettings receives the full config itself on the 1 Hz poll
(renderSettings(settings.full)), not the API-response shape, so the BPM
code's cfg.full was always undefined: the dropdown always showed Default
even with a persisted range, and postBPMRange built its POST from
currentSettings.full (undefined) and sent {full: {only the two BPM
fields}}. Since the settings POST does a SetConfig replace, every BPM
change rewrote settings.json with just those two fields, wiping
my_setting, dev_setting, and the rest.
Accept both config shapes (x.full || x) at the read sites, and base the
POST on the whole config so the other sections are preserved. Verified
both shapes read the persisted range and the POST payload retains the
other settings.
Found with a byte-exact root-cause analysis and a verification patch in
the review of #55.
Co-authored-by: kayrozen <kayrozen@users.noreply.github.com>
…-up) --bpm-range overrides the analysis range for the session without writing settings.json, so the settings GET (which returned only the persisted config) showed Default while a flag-set range was actually in force. Report the effective analysis range instead, so the UI shows a flag-set value. Editing it in the UI still persists to settings.json as before, and the flag stays non-persistent. Verified live: --bpm-range 90-180 now reads back 90/180 while settings.json keeps no persisted range. Raised as a side note in the #55 review by kayrozen.
What & why
Fixes #52.
Fast genres, drum & bass especially, were analyzing at half (~87) or a third (~58) of their true tempo. The cause is in the detector: a hard-coded candidate window of 80-170 BPM, reinforced by a perceptual tempo prior centered on 130. A real 174 BPM track is above the 170 ceiling, so it gets rejected and its strong half-tempo autocorrelation peak wins instead.
This adds an opt-in BPM range. When set, it replaces the candidate window and re-centres the prior on the range's geometric mean, so the true tempo becomes the only harmonic that lands in-window and wins. The default (unset) keeps the historical 80-170 window and 130 prior exactly, so nothing changes for anyone who doesn't opt in.
Two ways to set it:
--bpm-range 90-180on the command line.The presets and the custom inputs are tuned to the detector's real 60-200 BPM search range (autocorrelation + peak spacing), rather than copying rekordbox's Analysis Setting list verbatim, whose 48-95 and 108-255 entries would have dead zones here. Custom values are clamped to 60-200 with a hint. Tracks above 200 BPM remain out of range (they analyze at a sub-harmonic); that's documented as the current ceiling, with a comment tying the Go bounds to the web mirror so they don't drift.
The range persists in
settings.json(SettingsConfig.BPMRangeMin/Max, likeTrackDetail) and is read at startup before analysis;--bpm-rangeoverrides it for that session. Changing it in the UI applies to the analyzer live and drops cached analyses (Store.InvalidateAll) so affected tracks re-analyze as they're next loaded. EachResultrecords the range it was computed under, and the cache treats a mismatch as stale, so changing the range re-analyzes instead of serving BPMs fit under the old one. The analyzer's range is behind a mutex (SetTempoRange/TempoRange) because the settings handler can change it at runtime while analysis workers read it.Out of scope for this PR (possible follow-ups): a per-batch option in the web Add Files flow for mixed-genre libraries. A global range biases non-matching tracks, which is inherent to a global setting and the reason the per-batch option is noted for later.
Hardware testing
TestBPMRangeForcesTrueTempo), the settings round-trip is unit-tested, the analyzer setter is race-tested, and the fullmake checkincluding e2e passes. The live HTTP settings round-trip (GET/POST/persist/sync/invalidate) was verified against a running server. Real drum & bass tracks on a CDJ are queued for the next decks session before merge.Checklist
go build ./...,go vet ./..., andgo test ./...passgofmt -l .is cleanGPL-3.0-or-later)