mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
ship-ci / ci (push) Has been cancelled
* Carry rig bindings through the sloppak tone payload
`sloppak_tone_changes` emitted `{t, name}` only, so a chart's declared
sound never reached the client: `base_rig` was never read and each
change's `rig` was dropped at the wire boundary. Both survive load
intact (`Arrangement.tones` is an opaque passthrough) — the strip
happened here, at the last step before send.
That left the rig model (feedpak-spec 1.18.0 §6.9/§7.9) unreachable
from core: a pack could declare which rig voices a part, and nothing
downstream could ever see it. First step of the core reader for source
rigs; the rig library itself and the manifest precedence cascade follow.
Return `(base, base_rig, changes)` and keep `rig` on each change. Both
ids are validated as non-blank strings and stripped — anything else is
dropped rather than forwarded, so presence of the key means the change
binds a rig. Resolution against `rigs.json` deliberately does NOT happen
here: this builder preserves the declared binding, while realization
selection and the `intent.gm` fallback belong to whatever voices the
part.
On the wire `base_rig` is omitted entirely when empty, so packs that
bind no rig produce the byte-identical `tone_changes` message they
always did.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Load the pack's rig library from the manifest
feedpak 1.18.0 lets a chart declare what a MIDI part should sound like
by binding a rig id, but core had nothing to bind to: `rigs`, `base_rig`
and `drum_tones` appeared nowhere in lib/, server.py or static/. The
preceding commit carries the reference onto the wire; this adds the
library it references.
Read the manifest `rigs:` key into a new `LoadedSloppak.rigs`, alongside
the other side-files rather than on Song — every side-file (drum_tab,
song_timeline, keys, notation) hangs off the load result, and rigs is
pack-level, not per-arrangement. Same permissive posture as its
neighbours: missing, unreadable, malformed or traversing disables rigs
with a warning and never fails the pack, which §7.9 requires outright.
Rig objects pass through VERBATIM. §7.9 obliges a Reader to preserve
unknown role/engine/kind values and `ext` namespaces, so validating
block structure here would be wrong as well as premature — realization
selection and the `intent.gm` floor belong to whatever voices the part.
The only entries dropped are ones unreachable by construction: a rig is
addressable solely by `id`, so a non-dict entry or one without a usable
string id can never be referenced. Ids are stripped to match the
reference side, and a duplicate id resolves first-wins with a warning,
since ambiguity there would surface as the wrong sound rather than an
error.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Resolve which tones block binds a part
feedpak 1.18.0 lets a sound binding arrive from three places, and core
honoured none of them: the manifest arrangement entry, the arrangement
JSON, and the top-level drum_tones. Reading them needs a precedence
rule, because two of the three can be present at once.
Arrangement entries: the entry's `tones` replaces the arrangement JSON's
WHOLESALE (spec 5.2), unlike name/tuning/capo/centOffset beside it,
which override field by field. A merge would produce a sound nobody
authored -- one source's base under the other's changes -- which is
worse than either block alone. This is also what makes a notation-only
keys entry bindable at all, since it has no arrangement JSON to carry
tones in the first place.
Drums: the top-level drum_tones binds the song-level primary part, and
a `type: drums` entry's own tones takes precedence, with a Reader
forbidden from applying both to the same part (5.1). That is the same
shape as the drum_tab alias rule, so it lives inside
_resolve_drum_parts next to it rather than beside it -- one precedence
resolver, not two that drift. drum_tones is the PRIMARY's fallback
only: a second drummer with no binding gets None, never the primary's
kit.
An empty `tones: {}` reads as absent rather than as an override to
silence, matching how arrangement_from_wire already normalizes the
in-JSON empty dict, so a stray empty object cannot quietly unbind a
part.
Spec-conformance gate passes with drum_tones added to the keys core
reads (22 of the spec's 32, all declared).
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
* Document the rig bindings on the tone_changes wire message
CHANGELOG entry for the core rig reader, plus the WS protocol table in
CLAUDE.md, which described `tone_changes` as carrying only base + name.
While in that row: its time key was documented as `time`, but every
producer emits `t` — both the sloppak builder and the legacy XML path.
The 3D highway already carries a comment warning readers about exactly
this discrepancy. Corrected here rather than left sitting next to the
newly-added keys, where a reader would reasonably assume both were
equally reliable.
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
---------
Signed-off-by: gionnibgud <gionnibgud@gmail.com>
87 lines
3.5 KiB
Python
87 lines
3.5 KiB
Python
"""Tone helpers for sloppak playback.
|
|
|
|
A feedBack 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. FeedBack 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("feedBack.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, 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, base_rig, changes)`` where ``base`` is the initial tone
|
|
name, ``base_rig`` is the ``rigs.json`` rig id bound to it (feedpak-spec
|
|
§6.9; ``""`` when absent), and ``changes`` is a time-sorted
|
|
``[{"t", "name", "rig"?}]`` 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).
|
|
|
|
``rig`` / ``base_rig`` are carried through but NOT resolved against
|
|
``rigs.json`` here: this builder only preserves the binding the chart
|
|
declared. Realization selection and the ``intent.gm`` fallback (§7.9) belong
|
|
to the consumer that actually voices the part.
|
|
"""
|
|
if not isinstance(arr_tones, dict):
|
|
return "", "", []
|
|
base_val = arr_tones.get("base", "")
|
|
base = base_val.strip() if isinstance(base_val, str) else ""
|
|
base_rig_val = arr_tones.get("base_rig", "")
|
|
base_rig = base_rig_val.strip() if isinstance(base_rig_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
|
|
change = {"t": round(t, 3), "name": name}
|
|
# ponytail: `rig` only when it's a usable id — a non-string or blank
|
|
# value is dropped rather than forwarded, so a consumer can treat
|
|
# presence of the key as "this change binds a rig".
|
|
rig = c.get("rig")
|
|
if isinstance(rig, str) and rig.strip():
|
|
change["rig"] = rig.strip()
|
|
changes.append(change)
|
|
changes.sort(key=lambda x: x["t"])
|
|
return base, base_rig, changes
|