mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 06:14:29 +00:00
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>
This commit is contained in:
+60
-2
@@ -855,18 +855,46 @@ def _load_rigs_file(source_dir: Path, rel: str) -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
def _entry_tones(entry: dict) -> dict | None:
|
||||
"""A manifest entry's `tones` binding, or None when it doesn't carry one.
|
||||
|
||||
Spec §5.2: a manifest arrangement entry's `tones` overrides the arrangement
|
||||
JSON's `tones` **wholesale** — no field-level merge. This normalizes the
|
||||
"does it carry one" test for both the arrangement path and the drum path.
|
||||
|
||||
An empty dict reads as *absent*, not as "override to silence": it is what a
|
||||
Writer emits by accident, `arrangement_from_wire` already normalizes the
|
||||
in-JSON `{}` to None the same way, and treating it as an override would let
|
||||
a stray empty object silently unbind a part's sound.
|
||||
"""
|
||||
tones = entry.get("tones")
|
||||
return tones if isinstance(tones, dict) and tones else None
|
||||
|
||||
|
||||
def _resolve_drum_parts(
|
||||
source_dir: Path,
|
||||
drum_tab_rel: object,
|
||||
drum_tab_data: dict | None,
|
||||
drum_pointer_entries: list[dict],
|
||||
drum_tones: dict | None = None,
|
||||
) -> tuple[dict | None, list[dict] | None]:
|
||||
"""Resolve drum pointers into a primary-first list with unique ids."""
|
||||
"""Resolve drum pointers into a primary-first list with unique ids.
|
||||
|
||||
Also binds each part's sound (feedpak 1.18.0). The precedence mirrors the
|
||||
`drum_tab` alias rule this function already implements: a `type: drums`
|
||||
entry's own `tones` wins for that part, and the song-level `drum_tones` is
|
||||
the fallback for the **primary** part only. A Reader MUST NOT apply both to
|
||||
the same part (spec §5.1/§5.2), which is why the primary picks one or the
|
||||
other here rather than merging them.
|
||||
"""
|
||||
if drum_tab_data is None and not drum_pointer_entries:
|
||||
return drum_tab_data, None
|
||||
|
||||
primary_id = "drums"
|
||||
primary_name = None
|
||||
# The primary's own binding, lifted from its alias pointer entry when it has
|
||||
# one. Stays None if no entry claims the primary — `drum_tones` fills in.
|
||||
primary_tones = None
|
||||
extra_parts: list[dict] = []
|
||||
seen_rels: set[str] = set()
|
||||
# Use the same canonical, traversal-safe identity as zip member lookup so
|
||||
@@ -890,6 +918,11 @@ def _resolve_drum_parts(
|
||||
primary_id = entry_id
|
||||
if entry_name:
|
||||
primary_name = entry_name
|
||||
# This entry IS the primary (an alias pointer at the same file), so
|
||||
# its binding is the primary's — and it outranks `drum_tones`.
|
||||
_alias_tones = _entry_tones(entry)
|
||||
if _alias_tones is not None:
|
||||
primary_tones = _alias_tones
|
||||
continue
|
||||
tab = _load_drum_tab_file(source_dir, rel, f"drum part {entry_id or rel}")
|
||||
if tab is None:
|
||||
@@ -900,6 +933,9 @@ def _resolve_drum_parts(
|
||||
"name": entry_name
|
||||
or (tab_name if isinstance(tab_name, str) and tab_name else "Drums"),
|
||||
"drum_tab": tab,
|
||||
# Non-primary parts bind through their own entry only; `drum_tones`
|
||||
# is explicitly the primary's fallback, never theirs.
|
||||
"tones": _entry_tones(entry),
|
||||
})
|
||||
|
||||
parts: list[dict] = []
|
||||
@@ -908,7 +944,14 @@ def _resolve_drum_parts(
|
||||
if primary_name is None:
|
||||
tab_name = drum_tab_data.get("name")
|
||||
primary_name = tab_name if isinstance(tab_name, str) and tab_name else "Drums"
|
||||
parts.append({"id": primary_id, "name": primary_name, "drum_tab": drum_tab_data})
|
||||
parts.append({
|
||||
"id": primary_id,
|
||||
"name": primary_name,
|
||||
"drum_tab": drum_tab_data,
|
||||
# Entry `tones` takes precedence; `drum_tones` is the fallback. One
|
||||
# or the other, never both on the same part (spec §5.1).
|
||||
"tones": primary_tones if primary_tones is not None else drum_tones,
|
||||
})
|
||||
used_ids.add(primary_id)
|
||||
|
||||
next_generated_id = 2
|
||||
@@ -1027,6 +1070,14 @@ def load_song(
|
||||
# _finite_float keeps a malformed manifest NaN/Infinity from
|
||||
# poisoning the song_info JSON (same guard as the wire path).
|
||||
arr.cent_offset = _finite_float(entry["centOffset"])
|
||||
# `tones` overrides WHOLESALE, unlike the field-level overrides above:
|
||||
# the entry's object replaces the arrangement JSON's entirely, with no
|
||||
# per-field merge (spec §5.2). A Writer SHOULD NOT emit both, but when
|
||||
# one does, a half-merged sound — this pack's base with that pack's
|
||||
# changes — would be worse than either source alone.
|
||||
_entry_tone_block = _entry_tones(entry)
|
||||
if _entry_tone_block is not None:
|
||||
arr.tones = _entry_tone_block
|
||||
|
||||
# Beats/sections can live on the arrangement itself in the wire format.
|
||||
# If the manifest-level arrangement JSON carries them, pull them onto
|
||||
@@ -1099,8 +1150,15 @@ def load_song(
|
||||
|
||||
# Keep the dense compatibility logic independently testable and guarantee
|
||||
# ids are unique before the highway exposes them as selectors.
|
||||
# Top-level `drum_tones` (spec §5.1) binds the song-level drum part — the
|
||||
# fallback for packs without `type: drums` arrangements. Same shape as an
|
||||
# arrangement entry's `tones`; `_resolve_drum_parts` owns the precedence.
|
||||
_raw_drum_tones = manifest.get("drum_tones")
|
||||
drum_tones_data = _raw_drum_tones if isinstance(_raw_drum_tones, dict) and _raw_drum_tones else None
|
||||
|
||||
drum_tab_data, drum_parts = _resolve_drum_parts(
|
||||
source_dir, drum_tab_rel, drum_tab_data, drum_pointer_entries,
|
||||
drum_tones_data,
|
||||
)
|
||||
|
||||
# Drum-only sloppak: every GP track was percussion, so it ships a
|
||||
|
||||
Reference in New Issue
Block a user