feat(audio-sequences): beat phase sync and aligned playback

Add bar-phase tracking, audio reset/anchor APIs, BPM holdover, beat-phase
sequence switching, sync-phase endpoint, and sample sequence data.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-17 18:32:10 +12:00
parent 7ecb5c3b3e
commit 964cfc6d91
14 changed files with 1117 additions and 292 deletions

View File

@@ -12,11 +12,15 @@ def _settings_path():
return "settings.json"
_settings_singleton: "Settings | None" = None
class Settings(dict):
SETTINGS_FILE = None # Set in __init__ from _settings_path()
def __init__(self):
def __init__(self, *, quiet: bool = False):
super().__init__()
self._quiet = quiet
if Settings.SETTINGS_FILE is None:
Settings.SETTINGS_FILE = _settings_path()
self.load() # Load settings from file during initialization
@@ -79,13 +83,22 @@ class Settings(dict):
# Zone UI global brightness (0255); shared across browsers/devices.
if 'global_brightness' not in self:
self['global_brightness'] = 255
# Sequence tile start: wait for beat or downbeat (server-owned).
if 'sequence_switch_wait' not in self:
self['sequence_switch_wait'] = 'beat'
elif str(self.get('sequence_switch_wait', '')).strip().lower() == 'phrase':
self['sequence_switch_wait'] = 'beat'
# Beat flash alignment delay (ms); applied by all UI clients polling audio status.
if 'audio_beat_phase_ms' not in self:
self['audio_beat_phase_ms'] = 0
def save(self):
try:
j = json.dumps(self)
with open(self.SETTINGS_FILE, 'w') as file:
file.write(j)
print("Settings saved successfully.")
if not getattr(self, "_quiet", False):
print("Settings saved successfully.")
except Exception as e:
print(f"Error saving settings: {e}")
@@ -96,9 +109,11 @@ class Settings(dict):
loaded_settings = json.load(file)
self.update(loaded_settings)
loaded_from_file = True
print("Settings loaded successfully.")
if not getattr(self, "_quiet", False):
print("Settings loaded successfully.")
except Exception as e:
print(f"Error loading settings")
if not getattr(self, "_quiet", False):
print(f"Error loading settings: {e}")
self.clear()
finally:
# Ensure defaults are set even if file exists but is missing keys
@@ -106,3 +121,18 @@ class Settings(dict):
# Only save if file didn't exist or was invalid
if not loaded_from_file:
self.save()
def get_settings() -> Settings:
"""Process-wide settings instance (avoid re-reading settings.json on every request)."""
global _settings_singleton
if _settings_singleton is None:
_settings_singleton = Settings()
return _settings_singleton
def reload_settings() -> Settings:
"""Re-read settings.json (e.g. after external file edit)."""
global _settings_singleton
_settings_singleton = Settings(quiet=True)
return _settings_singleton