mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
71 lines
2.7 KiB
Python
71 lines
2.7 KiB
Python
"""Tone helpers for sloppak playback.
|
|
|
|
A slopsmith arrangement may carry a tone block — the initial tone name plus
|
|
in-song tone switches — embedded inline in the arrangement JSON (see
|
|
``lib/song.py`` ``arrangement_to_wire`` / the ``tones`` wire key). This module
|
|
turns that already-embedded block into the (base, changes) payload the highway
|
|
WebSocket sends to the client.
|
|
|
|
The proprietary-archive tone-extraction path (lifting tone definitions out of
|
|
an unpacked encrypted archive) has been removed. Slopsmith reads tones only
|
|
from its own ``.sloppak`` / arrangement JSON; it never reads or decrypts
|
|
proprietary archive formats.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import math
|
|
import re
|
|
|
|
log = logging.getLogger("slopsmith.lib.tones")
|
|
|
|
|
|
def tokens(s: str) -> set[str]:
|
|
"""Split a name or file stem into lowercased alphanumeric tokens.
|
|
|
|
Used for fuzzy arrangement↔XML matching: arrangement names carry spaces
|
|
("Bonus Lead") while file stems are underscored ("song_bonus_lead"), and a
|
|
plain substring check is ambiguous ("lead" is a substring of "bonuslead").
|
|
Shared with the playback path in `server.py` so the two stay consistent.
|
|
"""
|
|
return {t for t in re.split(r"[^a-z0-9]+", (s or "").lower()) if t}
|
|
|
|
|
|
def sloppak_tone_changes(arr_tones) -> tuple[str, list[dict]]:
|
|
"""Build the highway tone-change payload from an arrangement's tone block.
|
|
|
|
Given ``Arrangement.tones`` (the dict embedded in the sloppak, or ``None``),
|
|
returns ``(base, changes)`` where ``base`` is the initial tone name and
|
|
``changes`` is a time-sorted ``[{"t", "name"}]`` list. Non-string names,
|
|
non-dict entries, and non-numeric / non-finite times are skipped — a
|
|
hand-edited or third-party sloppak must not crash the highway WebSocket
|
|
or emit NaN/inf (which the client's ``JSON.parse`` rejects).
|
|
"""
|
|
if not isinstance(arr_tones, dict):
|
|
return "", []
|
|
base_val = arr_tones.get("base", "")
|
|
base = base_val.strip() if isinstance(base_val, str) else ""
|
|
|
|
changes: list[dict] = []
|
|
raw_changes = arr_tones.get("changes")
|
|
if not isinstance(raw_changes, list):
|
|
# A truthy non-list (e.g. `1`) would raise TypeError on iteration.
|
|
raw_changes = []
|
|
for c in raw_changes:
|
|
if not isinstance(c, dict):
|
|
continue
|
|
t = c.get("t")
|
|
name = c.get("name")
|
|
if t is None or not isinstance(name, str) or not name:
|
|
continue
|
|
try:
|
|
t = float(t)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if not math.isfinite(t):
|
|
continue
|
|
changes.append({"t": round(t, 3), "name": name})
|
|
changes.sort(key=lambda x: x["t"])
|
|
return base, changes
|