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:
@@ -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 (0–255); 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
|
||||
|
||||
Reference in New Issue
Block a user