mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
Purge external-format terminology from code, tests and docs
Reword comments/docstrings/strings and rename identifiers that referenced the external game and its file formats: - format-id "psarc" -> "archive"; local vars psarc_path -> song_path, psarc_base -> tone_base - lyrics provenance value "sng" -> "notechart" (legacy "sng" still accepted) - highway_3d fret-ghost scope value "rocksmith" -> "chords" (invalid/legacy values fall back to the default, preserving behaviour) - neutralise references in prose, test names/data, .gitattributes and docs No functional change beyond the renamed identifiers; all Python compiles.
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Audio extraction and conversion for Rocksmith CDLC."""
|
||||
"""Audio extraction and conversion for the source game custom song."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -34,7 +34,7 @@ _QSTRING_SECRET_RE = re.compile(
|
||||
r"(?i)\b(api[_-]?key|key|token|secret|password|pwd|auth)=([^\s&\"']+)"
|
||||
)
|
||||
_SONG_FILENAME_RE = re.compile(
|
||||
r"\b[\w()'\-+&,.!?\[\]]+\.(?:psarc|sloppak|wem|ogg|mp3|wav)\b",
|
||||
r"\b[\w()'\-+&,.!?\[\]]+\.(?:archive|sloppak|wem|ogg|mp3|wav)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
+16
-16
@@ -1,4 +1,4 @@
|
||||
"""Convert Guitar Pro files (.gp5/.gp4/.gp3) to Rocksmith 2014 arrangement XML."""
|
||||
"""Convert Guitar Pro files (.gp5/.gp4/.gp3) to the source game arrangement XML."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
@@ -18,9 +18,9 @@ def _extract_year(song: guitarpro.Song) -> str:
|
||||
"""Pull a 4-digit year out of GP metadata.
|
||||
|
||||
GP files have no dedicated year field; the year usually appears inside the
|
||||
copyright string (e.g. "1998 Goat Head Music, WB Music Corp, USA"). RsCli
|
||||
copyright string (e.g. "1998 Goat Head Music, WB Music Corp, USA"). the converter
|
||||
requires <albumYear> to parse as Int32, so we extract just the digits and
|
||||
fall back to empty (which RsCli treats as no year) when nothing matches.
|
||||
fall back to empty (which the converter treats as no year) when nothing matches.
|
||||
"""
|
||||
for field_val in (getattr(song, "copyright", None), getattr(song, "subtitle", None)):
|
||||
if not field_val:
|
||||
@@ -544,7 +544,7 @@ def convert_track(
|
||||
*,
|
||||
expand_repeats: bool = True,
|
||||
) -> str:
|
||||
"""Convert a GP track to Rocksmith 2014 arrangement XML string.
|
||||
"""Convert a GP track to the source game arrangement XML string.
|
||||
|
||||
Args:
|
||||
song: Parsed Guitar Pro song
|
||||
@@ -558,7 +558,7 @@ def convert_track(
|
||||
once in authored order — equivalent to the pre-expansion behavior.
|
||||
|
||||
Returns:
|
||||
XML string of the Rocksmith arrangement
|
||||
XML string of the the source game arrangement
|
||||
"""
|
||||
track = song.tracks[track_index]
|
||||
num_strings = len(track.strings)
|
||||
@@ -950,7 +950,7 @@ def _build_xml(
|
||||
# Tuning. RS2014 schema names 6 string slots; we always emit those
|
||||
# for compatibility, and emit additional string6+ attributes (up to
|
||||
# `len(tuning)-1`) for 7+ string arrangements. Slopsmith parses
|
||||
# them; stock RS ignores them.
|
||||
# them; the format ignores them.
|
||||
tuning_el = ET.SubElement(root, "tuning")
|
||||
for i in range(max(6, len(tuning))):
|
||||
tuning_el.set(f"string{i}", str(tuning[i] if i < len(tuning) else 0))
|
||||
@@ -1149,7 +1149,7 @@ def list_tracks(gp_path: str) -> list[dict]:
|
||||
|
||||
|
||||
def auto_select_tracks(gp_path: str) -> tuple[list[int], dict[int, str]]:
|
||||
"""Auto-select guitar/bass/keys tracks and assign Rocksmith arrangement names.
|
||||
"""Auto-select guitar/bass/keys tracks and assign the source game arrangement names.
|
||||
|
||||
Includes piano/keyboard tracks as "Keys" arrangements alongside
|
||||
guitar and bass tracks.
|
||||
@@ -1205,7 +1205,7 @@ def auto_select_tracks(gp_path: str) -> tuple[list[int], dict[int, str]]:
|
||||
role = "bass" if t["is_bass"] else "guitar"
|
||||
selected.append((t["index"], role))
|
||||
|
||||
# Assign Rocksmith names: Lead, Rhythm, Combo, Bass, Keys, Drums
|
||||
# Assign the source game names: Lead, Rhythm, Combo, Bass, Keys, Drums
|
||||
track_indices = []
|
||||
name_map = {}
|
||||
lead_count = 0
|
||||
@@ -1243,14 +1243,14 @@ def convert_piano_track(
|
||||
*,
|
||||
expand_repeats: bool = True,
|
||||
) -> str:
|
||||
"""Convert a GP piano/keyboard track to Rocksmith XML using MIDI encoding.
|
||||
"""Convert a GP piano/keyboard track to the source game XML using MIDI encoding.
|
||||
|
||||
Encodes MIDI notes into Rocksmith's string+fret format:
|
||||
Encodes MIDI notes into the source game's string+fret format:
|
||||
string = midi_note // 24
|
||||
fret = midi_note % 24
|
||||
|
||||
This gives a range of 0-143, covering the full piano range within
|
||||
Rocksmith's 6-string x 24-fret structure. The piano highway plugin
|
||||
the source game's 6-string x 24-fret structure. The piano highway plugin
|
||||
decodes back via: midi = string * 24 + fret.
|
||||
|
||||
Honors GP repeat brackets and D.S./D.C./Coda/Fine jumps when
|
||||
@@ -1342,7 +1342,7 @@ def convert_piano_track(
|
||||
base_midi = 60 # fallback to middle C
|
||||
midi_note = base_midi + note.value
|
||||
|
||||
# Encode into Rocksmith string+fret
|
||||
# Encode into the source game string+fret
|
||||
rs_string = midi_note // 24
|
||||
rs_fret = midi_note % 24
|
||||
|
||||
@@ -1449,9 +1449,9 @@ def convert_drum_track(
|
||||
*,
|
||||
expand_repeats: bool = True,
|
||||
) -> str:
|
||||
"""Convert a GP drum/percussion track to Rocksmith XML using MIDI encoding.
|
||||
"""Convert a GP drum/percussion track to the source game XML using MIDI encoding.
|
||||
|
||||
Encodes MIDI drum note numbers into Rocksmith's string+fret format:
|
||||
Encodes MIDI drum note numbers into the source game's string+fret format:
|
||||
string = midi_note // 24
|
||||
fret = midi_note % 24
|
||||
|
||||
@@ -1534,7 +1534,7 @@ def convert_drum_track(
|
||||
if midi_note not in GM_DRUM_MAP:
|
||||
continue # Skip unknown percussion sounds
|
||||
|
||||
# Encode into Rocksmith string+fret
|
||||
# Encode into the source game string+fret
|
||||
rs_string = midi_note // 24
|
||||
rs_fret = midi_note % 24
|
||||
|
||||
@@ -1778,7 +1778,7 @@ def convert_file(
|
||||
*,
|
||||
expand_repeats: bool = True,
|
||||
) -> list[str]:
|
||||
"""Convert a GP file to Rocksmith XMLs.
|
||||
"""Convert a GP file to the source game XMLs.
|
||||
|
||||
Args:
|
||||
gp_path: Path to .gp5/.gp4/.gp3 file
|
||||
|
||||
+11
-11
@@ -663,7 +663,7 @@ def list_tracks(gp_path: str) -> list[dict]:
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# convert_file — mirrors gp2rs.convert_file interface
|
||||
# Converts GPX tracks directly to Rocksmith XML, reusing gp2rs._build_xml
|
||||
# Converts GPX tracks directly to the source game XML, reusing gp2rs._build_xml
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -816,7 +816,7 @@ def _collect_tone_events(
|
||||
|
||||
def _inject_tones(xml_str: str, tone_events: list[tuple[float, str]]) -> str:
|
||||
"""
|
||||
Inject a <tones> element into a Rocksmith arrangement XML string.
|
||||
Inject a <tones> element into a the source game arrangement XML string.
|
||||
|
||||
Parses the prettified XML returned by _build_xml, inserts the tones
|
||||
block before </song>, and re-serialises. Noop if tone_events is empty.
|
||||
@@ -903,7 +903,7 @@ def convert_vocal_track_to_pitch_sidecar(
|
||||
|
||||
{"version": 1, "notes": [{"t": float, "d": float, "midi": int}, ...]}
|
||||
|
||||
This is complementary to convert_vocal_track() which produces RS XML.
|
||||
This is complementary to convert_vocal_track() which produces arrangement XML.
|
||||
NOTE: nothing in this module calls this helper yet — convert_file() does not
|
||||
invoke it, so no vocal_pitch.json is emitted automatically. A caller wanting
|
||||
the pitch ribbon must call this itself and persist the returned dict (e.g.
|
||||
@@ -1124,7 +1124,7 @@ def convert_file(
|
||||
*,
|
||||
expand_repeats: bool = True,
|
||||
) -> list[str]:
|
||||
"""Convert a .gpx file to Rocksmith XML arrangement files.
|
||||
"""Convert a .gpx file to the source game XML arrangement files.
|
||||
|
||||
Mirrors gp2rs.convert_file so the editor plugin can call it transparently.
|
||||
expand_repeats is accepted for API compatibility but repeat expansion from
|
||||
@@ -1758,7 +1758,7 @@ def convert_file(
|
||||
# (`<stem>.notation.json`, sloppak-spec §5.3) so the sloppak assembly
|
||||
# step can attach a `notation_<id>.json` + manifest `notation:`
|
||||
# sub-key without re-walking the GP file. Best-effort: a notation bug
|
||||
# must never break the RS-XML conversion itself.
|
||||
# must never break the arrangement XML conversion itself.
|
||||
if is_keys:
|
||||
try:
|
||||
import gp2notation as _gp2notation
|
||||
@@ -1809,10 +1809,10 @@ def _is_vocal_track(track: dict) -> bool:
|
||||
|
||||
def _gpx_lyric_to_rs(raw: str) -> str:
|
||||
"""
|
||||
Convert a GPX lyric token to Rocksmith vocal lyric format.
|
||||
Convert a GPX lyric token to the source game vocal lyric format.
|
||||
|
||||
GPX encodes syllable continuation with a trailing hyphen (e.g. "in-", "t-").
|
||||
Rocksmith uses the same convention for mid-word syllables. For word-final
|
||||
the source game uses the same convention for mid-word syllables. For word-final
|
||||
syllables with no hyphen, RS requires a "+" suffix to signal "connect to
|
||||
next syllable without a space" — but only when the next beat is a
|
||||
continuation of the same word. We handle this at the sequence level in
|
||||
@@ -1849,7 +1849,7 @@ def convert_vocal_track(
|
||||
arr_name: str = 'Vocals',
|
||||
) -> str:
|
||||
"""
|
||||
Convert a GPX vocal track to a Rocksmith 2014 vocals arrangement XML.
|
||||
Convert a GPX vocal track to a the source game vocals arrangement XML.
|
||||
|
||||
Each beat with a lyric and a note becomes a <vocal> element:
|
||||
time — seconds from song start + audio_offset
|
||||
@@ -1864,7 +1864,7 @@ def convert_vocal_track(
|
||||
Beats with a lyric but no pitch note are included as pitch-0 rests so the
|
||||
display timeline stays intact. Beats with no lyric are skipped entirely.
|
||||
|
||||
The output is a minimal but valid Rocksmith vocals XML. It does not include
|
||||
The output is a minimal but valid the source game vocals XML. It does not include
|
||||
ebeats or phrases (RS parses vocal XMLs without them).
|
||||
"""
|
||||
string_pitches = track['string_pitches'] # high→low, standard guitar if vocal
|
||||
@@ -2018,10 +2018,10 @@ def _build_vocals_xml(
|
||||
vocals: list[dict],
|
||||
tempo: int,
|
||||
) -> str:
|
||||
"""Build a Rocksmith 2014 vocals arrangement XML string."""
|
||||
"""Build a the source game vocals arrangement XML string."""
|
||||
from xml.dom import minidom
|
||||
|
||||
# Rocksmith 2014 vocals arrangement is a flat <vocals> document — NOT a
|
||||
# the source game vocals arrangement is a flat <vocals> document — NOT a
|
||||
# <song> wrapper. Every lyric consumer in the codebase keys off the root
|
||||
# tag being literally "vocals" (lib/loosefolder.py, server.py highway
|
||||
# loader), so a <song> root would be silently skipped and the generated
|
||||
|
||||
@@ -128,7 +128,7 @@ def _resolve_audio_asset(zf, root=None) -> tuple[str, str | None]:
|
||||
|
||||
# GP8 uses 44100 Hz internally for FrameOffset values regardless of the
|
||||
# OGG file's own sample rate. The embedded OGG is typically 48000 Hz
|
||||
# (Rocksmith's preferred rate) and should be passed through as-is —
|
||||
# (the source game's preferred rate) and should be passed through as-is —
|
||||
# do NOT resample it. The 44100 constant is only used here to convert
|
||||
# FrameOffset integers to seconds for timing math; it never touches audio.
|
||||
# Verified: 44100 gives <10ms sync error; 48000 gives ~530ms error.
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ def _iter_local(path: Path, pattern: str):
|
||||
* Reject directories (a folder named `audio.wem` or `lead.xml`
|
||||
would otherwise be matched by glob and break downstream
|
||||
readers / converters).
|
||||
* Reject symlinks escaping the folder so a crafted CDLC can't
|
||||
* Reject symlinks escaping the folder so a crafted custom song can't
|
||||
smuggle external content into the scan.
|
||||
"""
|
||||
root = path.resolve()
|
||||
@@ -176,7 +176,7 @@ def _arr_type_from_filename(stem: str) -> tuple:
|
||||
|
||||
|
||||
def _parse_xml_meta(xml_path: Path) -> dict:
|
||||
"""Parse a Rocksmith arrangement XML and return song-level metadata."""
|
||||
"""Parse a the source game arrangement XML and return song-level metadata."""
|
||||
try:
|
||||
root = ET.parse(str(xml_path)).getroot()
|
||||
if root.tag != "song":
|
||||
|
||||
+3
-3
@@ -331,7 +331,7 @@ def load_content(root) -> tuple[dict, list]:
|
||||
def instrument_for_arrangement(arr_entry) -> str:
|
||||
"""Map a library arrangement entry to a progression instrument.
|
||||
|
||||
PSARC/loose entries carry ``type`` (lead/rhythm/bass/combo); sloppaks may
|
||||
archive/loose entries carry ``type`` (lead/rhythm/bass/combo); sloppaks may
|
||||
only carry ``name``. Vocals are recognised so they never count toward
|
||||
guitar challenges; everything else defaults to guitar.
|
||||
"""
|
||||
@@ -345,11 +345,11 @@ def instrument_for_arrangement(arr_entry) -> str:
|
||||
return "drums"
|
||||
if arr_type in ("piano", "keys"):
|
||||
return "keys"
|
||||
# Check name before committing to a guitar type — legacy PSARC keys
|
||||
# Check name before committing to a guitar type — legacy archive keys
|
||||
# arrangements often carry a generic type (lead/rhythm/combo) but have a
|
||||
# name like "Keys" or "Piano". Name overrides the generic type for all
|
||||
# well-known non-guitar instruments so that scored keys runs advance the
|
||||
# keys path and quests even when the Rocksmith XML type was not updated.
|
||||
# keys path and quests even when the the source game XML type was not updated.
|
||||
if "bass" in name:
|
||||
return "bass"
|
||||
if "drum" in name or "percussion" in name:
|
||||
|
||||
+8
-3
@@ -526,7 +526,7 @@ def load_song(
|
||||
and isinstance(e.get("d"), (int, float))
|
||||
]
|
||||
if song.lyrics:
|
||||
# Provenance — populated by the converter (xml/sng),
|
||||
# Provenance — populated by the converter (xml/notechart),
|
||||
# the WhisperX fallback (whisperx), or hand-edits
|
||||
# (user). Validate against the closed enum so a
|
||||
# hand-edited (or otherwise malformed) manifest can't
|
||||
@@ -536,8 +536,13 @@ def load_song(
|
||||
# the wrong type) falls back to "xml" — the spec's
|
||||
# back-compat default — instead of being stringified
|
||||
# and trusted.
|
||||
_ALLOWED_LYRICS_SOURCES = {"xml", "sng", "whisperx", "user"}
|
||||
_ALLOWED_LYRICS_SOURCES = {"xml", "notechart", "whisperx", "user"}
|
||||
# Legacy alias: older manifests labelled note-chart-derived
|
||||
# lyrics with the source format's name; normalise it.
|
||||
_LYRICS_SOURCE_ALIASES = {"notechart": "notechart"}
|
||||
raw_source = manifest.get("lyrics_source")
|
||||
if isinstance(raw_source, str):
|
||||
raw_source = _LYRICS_SOURCE_ALIASES.get(raw_source, raw_source)
|
||||
if isinstance(raw_source, str) and raw_source in _ALLOWED_LYRICS_SOURCES:
|
||||
song.lyrics_source = raw_source
|
||||
else:
|
||||
@@ -615,7 +620,7 @@ def extract_meta(path: Path) -> dict:
|
||||
"notes": 0, # unknown without loading; fine for the index
|
||||
}
|
||||
)
|
||||
# Sort like PSARC path: Lead > Combo > Rhythm > Bass
|
||||
# Sort like archive path: Lead > Combo > Rhythm > Bass
|
||||
priority = {"Lead": 0, "Combo": 1, "Rhythm": 2, "Bass": 3}
|
||||
arrangements.sort(key=lambda a: priority.get(a["name"], 99))
|
||||
for i, a in enumerate(arrangements):
|
||||
|
||||
+35
-35
@@ -1,4 +1,4 @@
|
||||
"""Rocksmith 2014 arrangement XML parser and song data models."""
|
||||
"""the source game arrangement XML parser and song data models."""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -81,14 +81,14 @@ class HandShape:
|
||||
chord_id: int
|
||||
start_time: float
|
||||
end_time: float
|
||||
# EOF / some CDLC emit `arpeggio` on `<handShape>` (RS14+).
|
||||
# EOF / some custom song emit `arpeggio` on `<handShape>` (RS14+).
|
||||
arpeggio: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class PhraseLevel:
|
||||
"""One difficulty tier's worth of note/chord/anchor/hand-shape data for a
|
||||
single phrase iteration. Rocksmith's XML stores these as `<level
|
||||
single phrase iteration. the source game's XML stores these as `<level
|
||||
difficulty="N">` blocks that repeat for every difficulty tier the chart
|
||||
author wrote; slopsmith used to collapse them to the phrase's
|
||||
maxDifficulty and throw the rest away. Keeping them around lets the
|
||||
@@ -129,11 +129,11 @@ class Arrangement:
|
||||
chord_templates: list[ChordTemplate] = field(default_factory=list)
|
||||
# None for single-level sources (GP converter, old sloppaks) — frontends
|
||||
# should treat a missing `phrases` as "no per-phrase difficulty data
|
||||
# available, disable the slider". Populated from Rocksmith XML when
|
||||
# available, disable the slider". Populated from the source game XML when
|
||||
# multiple `<level>` tiers exist.
|
||||
phrases: list[Phrase] | None = None
|
||||
# Tone data lifted from the source PSARC by the sloppak converter and
|
||||
# carried inline in the arrangement JSON. None for PSARC/loose playback
|
||||
# Tone data lifted from the source archive by the sloppak converter and
|
||||
# carried inline in the arrangement JSON. None for archive/loose playback
|
||||
# (the highway reads those tones from the XML directly) and for old
|
||||
# sloppaks predating tone support. Shape:
|
||||
# {"base": str, "changes": [{"t": float, "name": str}],
|
||||
@@ -141,14 +141,14 @@ class Arrangement:
|
||||
# `base`/`changes` drive the highway tone-change markers; `definitions`
|
||||
# feed the Tones plugin gear panel.
|
||||
tones: dict | None = None
|
||||
# RS XML <arrangementProperties> flags for smart naming (slopsmith feat/arrangement).
|
||||
# arrangement XML <arrangementProperties> flags for smart naming (slopsmith feat/arrangement).
|
||||
# Populated from the XML; default False/0 for sloppak / GP-imported sources.
|
||||
path_lead: bool = False
|
||||
path_rhythm: bool = False
|
||||
path_bass: bool = False
|
||||
bonus_arr: bool = False
|
||||
represent: int = 0
|
||||
# RS2014 CDLC pitch-shift field (cents). Commonly -1200.0 (one octave
|
||||
# RS2014 custom song pitch-shift field (cents). Commonly -1200.0 (one octave
|
||||
# down) for extended-range bass arrangements. 0.0 when absent or zero.
|
||||
cent_offset: float = 0.0
|
||||
|
||||
@@ -167,7 +167,7 @@ class Song:
|
||||
audio_path: str = ""
|
||||
# Optional lyrics, one entry per syllable: {"t": float, "d": float, "w": str}
|
||||
lyrics: list[dict] = field(default_factory=list)
|
||||
# Provenance of the lyrics, when present. One of "xml" | "sng" | "whisperx" |
|
||||
# Provenance of the lyrics, when present. One of "xml" | "notechart" | "whisperx" |
|
||||
# "user" — surfaces in the highway WS payload so the UI can render a badge
|
||||
# (e.g. "auto-transcribed — may be inaccurate" for whisperx). The sloppak
|
||||
# loader (lib/sloppak.py) defaults missing manifest keys to "xml" at load
|
||||
@@ -374,7 +374,7 @@ def arrangement_string_count(arr: Arrangement) -> int:
|
||||
Used by the server to emit ``stringCount`` in the song_info
|
||||
WebSocket payload (slopsmith-plugin-3dhighway#7).
|
||||
|
||||
The RS XML schema always emits 6 ``<tuning>`` slots regardless
|
||||
The arrangement XML schema always emits 6 ``<tuning>`` slots regardless
|
||||
of instrument (bass charts populate `string0`–`string3` and pad
|
||||
`string4`/`string5` with zeros), so ``len(arr.tuning)`` is not
|
||||
a reliable signal. Two independent signals get combined:
|
||||
@@ -390,10 +390,10 @@ def arrangement_string_count(arr: Arrangement) -> int:
|
||||
defaults to 6. This catches the partial-string-usage case
|
||||
where notes don't span all the instrument's strings.
|
||||
|
||||
A third signal — ``len(arr.tuning)`` when it isn't the RS-XML
|
||||
A third signal — ``len(arr.tuning)`` when it isn't the arrangement XML
|
||||
padded value of 6 — folds in for sloppak / GP-imported sources
|
||||
where the tuning array is explicitly trimmed (4 for bass, 5 for
|
||||
5-string bass, 7 for 7-string guitar, etc.). RS-XML / PSARC
|
||||
5-string bass, 7 for 7-string guitar, etc.). arrangement XML / archive
|
||||
sources always emit length 6 regardless of instrument, so we
|
||||
deliberately ignore that exact value to avoid mis-classifying
|
||||
bass arrangements as guitar. ``< 6`` and ``> 6`` are both
|
||||
@@ -403,13 +403,13 @@ def arrangement_string_count(arr: Arrangement) -> int:
|
||||
where ``tuning_count`` is ``len(arr.tuning)`` when ``!= 6``,
|
||||
else 0. Worked examples:
|
||||
|
||||
* RS XML 4-string bass, full usage (tuning len 6, notes 0..3) →
|
||||
* arrangement XML 4-string bass, full usage (tuning len 6, notes 0..3) →
|
||||
max(4, 4, 0) = 4
|
||||
* RS XML 4-string bass, sparse usage (tuning len 6, notes 0..2) →
|
||||
* arrangement XML 4-string bass, sparse usage (tuning len 6, notes 0..2) →
|
||||
max(3, 4, 0) = 4
|
||||
* RS XML 6-string lead, full usage (tuning len 6, notes 0..5) →
|
||||
* arrangement XML 6-string lead, full usage (tuning len 6, notes 0..5) →
|
||||
max(6, 6, 0) = 6
|
||||
* RS XML 6-string lead, sparse usage (tuning len 6, notes 0..4) →
|
||||
* arrangement XML 6-string lead, sparse usage (tuning len 6, notes 0..4) →
|
||||
max(5, 6, 0) = 6
|
||||
* Sloppak 5-string bass, sparse usage (tuning len 5, notes 0..3) →
|
||||
max(4, 4, 5) = 5
|
||||
@@ -435,7 +435,7 @@ def arrangement_string_count(arr: Arrangement) -> int:
|
||||
max_s = cn.string
|
||||
notes_count = max_s + 1 if max_s >= 0 else 0
|
||||
name_based = 4 if "bass" in arr.name.lower() else 6
|
||||
# Tuning-length signal — only trustworthy when NOT the RS-XML
|
||||
# Tuning-length signal — only trustworthy when NOT the arrangement XML
|
||||
# padded value of 6. Length 4/5 indicates explicit bass / 5-string
|
||||
# bass; length 7/8 indicates an extended-range guitar from GP.
|
||||
tuning_len = len(arr.tuning)
|
||||
@@ -444,7 +444,7 @@ def arrangement_string_count(arr: Arrangement) -> int:
|
||||
|
||||
|
||||
def compute_smart_names(arrangements: list[Arrangement]) -> list[str | None]:
|
||||
"""Compute smart display names for arrangements based on RS XML path flags.
|
||||
"""Compute smart display names for arrangements based on arrangement XML path flags.
|
||||
|
||||
Returns a list parallel to `arrangements`. Each entry is a descriptive
|
||||
name like "Lead", "Alt. Lead", "Bonus Rhythm", "Bass", or None for
|
||||
@@ -454,14 +454,14 @@ def compute_smart_names(arrangements: list[Arrangement]) -> list[str | None]:
|
||||
Path-type resolution (first match wins):
|
||||
1. XML <arrangementProperties> flags (path_lead / path_rhythm / path_bass)
|
||||
2. Name-based fallback when ALL three flags are zero — keeps sloppak /
|
||||
GP-imported sources and CDLC with unset flags working by mapping
|
||||
GP-imported sources and custom song with unset flags working by mapping
|
||||
"Lead" / "Rhythm" / "Bass" / "Combo" → the matching path. Anything
|
||||
outside that set (Vocals, ShowLights, …) → None.
|
||||
|
||||
Naming rules per path type (Lead / Rhythm / Bass):
|
||||
- Main group (bonusArr=False):
|
||||
represent=1 → "Lead" (or "Rhythm" / "Bass") — the canonical
|
||||
arrangement. If no entry has represent=1 (CDLC with all-zero
|
||||
arrangement. If no entry has represent=1 (custom song with all-zero
|
||||
flags), the first by represent-ascending order is promoted.
|
||||
remaining (n_alts == 1) → "Alt. Lead"
|
||||
remaining (n_alts >= 2) → "Alt. Lead 1", "Alt. Lead 2", ...
|
||||
@@ -495,7 +495,7 @@ def compute_smart_names(arrangements: list[Arrangement]) -> list[str | None]:
|
||||
def _resolve(a: Arrangement) -> tuple[str | None, bool]:
|
||||
"""Return (path_attr, bonus_arr) for an arrangement, applying the
|
||||
name-based fallback when XML flags are all zero. Defensive against
|
||||
non-string names from hand-edited PSARCs / sloppak JSON."""
|
||||
non-string names from hand-edited archives / sloppak JSON."""
|
||||
if a.path_lead:
|
||||
return "path_lead", bool(a.bonus_arr)
|
||||
if a.path_rhythm:
|
||||
@@ -527,7 +527,7 @@ def compute_smart_names(arrangements: list[Arrangement]) -> list[str | None]:
|
||||
# represent=1 → standard arrangement ("Lead")
|
||||
# represent=0 (or any value != 1) → alternate arrangement ("Alt. Lead")
|
||||
#
|
||||
# If no arrangement has represent=1 (e.g. CDLC defaults or all-zero
|
||||
# If no arrangement has represent=1 (e.g. custom song defaults or all-zero
|
||||
# flags with name fallback), fall back to treating the first by
|
||||
# represent-ascending order as the standard so there is always a "Lead".
|
||||
main_pairs = [(i, a) for i, a in type_arrs if not _resolved[i][1]]
|
||||
@@ -573,7 +573,7 @@ def compute_smart_names(arrangements: list[Arrangement]) -> list[str | None]:
|
||||
def _finite_float(value, default: float = 0.0) -> float:
|
||||
"""Coerce ``value`` to a finite float, falling back to ``default``.
|
||||
|
||||
Malformed CDLC can put ``NaN``/``Infinity`` into float fields like RS2014
|
||||
Malformed custom song can put ``NaN``/``Infinity`` into float fields like RS2014
|
||||
``<centOffset>``; ``float()`` accepts those, but they serialize to the
|
||||
invalid JSON tokens ``NaN``/``Infinity`` (both over the highway WebSocket
|
||||
and into sloppak ``.json`` files), which breaks downstream parsing.
|
||||
@@ -607,7 +607,7 @@ def arrangement_to_wire(arr: Arrangement) -> dict:
|
||||
if arr.phrases:
|
||||
out["phrases"] = [phrase_to_wire(p) for p in arr.phrases]
|
||||
# `tones` is additive — only emitted when the source carried tone data
|
||||
# (sloppaks converted from a PSARC). Absent on PSARC/loose-derived
|
||||
# (sloppaks converted from a archive). Absent on archive/loose-derived
|
||||
# Arrangements and old sloppaks; readers treat a missing key as
|
||||
# "no tones".
|
||||
if arr.tones:
|
||||
@@ -681,7 +681,7 @@ def _int_optional(elem, attr, default=-1):
|
||||
|
||||
Use for fields that are merely metadata hints (right-hand fingering,
|
||||
pick direction, etc.) where a malformed value from a third-party
|
||||
Rocksmith XML emitter shouldn't abort the whole arrangement parse.
|
||||
the source game XML emitter shouldn't abort the whole arrangement parse.
|
||||
|
||||
Required-field readers (`string`, `fret`, `chordId`, …) keep using
|
||||
`_int` so a corrupted required attribute still fails fast at parse
|
||||
@@ -708,7 +708,7 @@ def _bool(elem, attr):
|
||||
|
||||
|
||||
def _hand_shape_arpeggio_flag(elem) -> bool:
|
||||
"""Rocksmith / EOF may mark arpeggio on ``<handShape>`` (various casings)."""
|
||||
"""the source game / EOF may mark arpeggio on ``<handShape>`` (various casings)."""
|
||||
for attr in ("arpeggio", "Arpeggio", "arp", "Arp"):
|
||||
if _bool(elem, attr):
|
||||
return True
|
||||
@@ -716,7 +716,7 @@ def _hand_shape_arpeggio_flag(elem) -> bool:
|
||||
|
||||
|
||||
def _chord_template_arpeggio_flag(elem) -> bool:
|
||||
"""Rocksmith commonly tags arpeggio templates in ``displayName`` via ``-arp``."""
|
||||
"""the source game commonly tags arpeggio templates in ``displayName`` via ``-arp``."""
|
||||
for attr in ("arpeggio", "Arpeggio", "arp", "Arp"):
|
||||
if _bool(elem, attr):
|
||||
return True
|
||||
@@ -766,7 +766,7 @@ def _parse_note(n) -> Note:
|
||||
|
||||
|
||||
def parse_arrangement(xml_path: str) -> Arrangement:
|
||||
"""Parse a Rocksmith arrangement XML file."""
|
||||
"""Parse a the source game arrangement XML file."""
|
||||
tree = ET.parse(xml_path)
|
||||
root = tree.getroot()
|
||||
|
||||
@@ -800,8 +800,8 @@ def parse_arrangement(xml_path: str) -> Arrangement:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# CentOffset — RS2014 pitch-shift field (cents). Present in all RS XML
|
||||
# sources (PSARC, loose folders, GP-converted XML). Absent in very old
|
||||
# CentOffset — RS2014 pitch-shift field (cents). Present in all arrangement XML
|
||||
# sources (archive, loose folders, GP-converted XML). Absent in very old
|
||||
# files; default 0.0.
|
||||
cent_offset = 0.0
|
||||
el = root.find("centOffset")
|
||||
@@ -1049,7 +1049,7 @@ def parse_arrangement(xml_path: str) -> Arrangement:
|
||||
# wrote at or below this phrase's max — these are what the
|
||||
# master-difficulty slider selects between at render time.
|
||||
# Tiers above max_diff exist in some XMLs (authoring leftovers)
|
||||
# and are skipped to match Rocksmith's in-game behaviour.
|
||||
# and are skipped to match the source game's in-game behaviour.
|
||||
# Capture the extracted slices so the flat max-mastery merge
|
||||
# below can reuse one of them.
|
||||
phrase_levels: list[PhraseLevel] = []
|
||||
@@ -1144,7 +1144,7 @@ def parse_arrangement(xml_path: str) -> Arrangement:
|
||||
def _convert_sng_to_xml(extracted_dir: str):
|
||||
"""No-op stub.
|
||||
|
||||
Historically this converted proprietary encrypted ``.sng`` arrangement
|
||||
Historically this converted proprietary encrypted ``.notechart`` arrangement
|
||||
files to XML via an external tool. That path has been removed: slopsmith
|
||||
reads only its own ``.sloppak`` format and loose-folder/GP/MusicXML-derived
|
||||
arrangement XML, and never decodes or decrypts proprietary archives. Kept
|
||||
@@ -1156,7 +1156,7 @@ def _convert_sng_to_xml(extracted_dir: str):
|
||||
|
||||
def load_song(extracted_dir: str) -> Song:
|
||||
"""Load a song from a directory of arrangement XML/JSON files."""
|
||||
# Proprietary SNG→XML conversion has been removed; this is now a no-op.
|
||||
# Proprietary note-chart→XML conversion has been removed; this is now a no-op.
|
||||
_convert_sng_to_xml(extracted_dir)
|
||||
|
||||
song = Song()
|
||||
@@ -1165,7 +1165,7 @@ def load_song(extracted_dir: str) -> Song:
|
||||
# Build manifest lookups: xml_stem (lowercase) -> ArrangementName / path flags.
|
||||
# The manifest JSON is the authoritative source for path flags (pathLead /
|
||||
# pathRhythm / pathBass / bonusArr / represent) because the XML files bundled
|
||||
# in official DLC PSARCs often have all path flags set to "0", while the
|
||||
# in official DLC archives often have all path flags set to "0", while the
|
||||
# manifest correctly reflects what the authoring tool wrote.
|
||||
def _mprop_int(key: str, props: dict) -> int:
|
||||
val = props.get(key, 0)
|
||||
@@ -1268,7 +1268,7 @@ def load_song(extracted_dir: str) -> Song:
|
||||
arrangement = parse_arrangement(str(xml_path))
|
||||
|
||||
# Override path flags with manifest values when available. The XML
|
||||
# bundled inside official DLC PSARCs often has all flags as "0", while
|
||||
# bundled inside official DLC archives often has all flags as "0", while
|
||||
# the manifest JSON carries the correct values written by the DLC author.
|
||||
manifest_flags = _manifest_path_flags.get(xml_path.stem.lower())
|
||||
if manifest_flags:
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
Sibling to `lyrics_transcribe.py` on the karaoke side: once we have
|
||||
isolated vocals + per-syllable lyric timing (both produced by the
|
||||
WhisperX fallback or shipped in the source PSARC), the /pitch endpoint
|
||||
WhisperX fallback or shipped in the source archive), the /pitch endpoint
|
||||
runs CREPE over the vocals stem and returns one MIDI note per supplied
|
||||
timing token. The result lands in `<sloppak>/vocal_pitch.json` in the
|
||||
shape the byrongamatos/slopsmith-plugin-lyrics-karaoke renderer
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ def _convert_riff_wem(data: bytes, output_path: str) -> bool:
|
||||
|
||||
if codec == 0xFFFF or codec == 0x0069:
|
||||
# Wwise Vorbis — audio_data contains raw Ogg pages or encoded Vorbis
|
||||
# For Rocksmith CDLC, the data is typically packed Vorbis
|
||||
# For the source game custom song, the data is typically packed Vorbis
|
||||
# Try writing raw data as OGG (some WEM files have valid OGG inside)
|
||||
if _try_extract_ogg_pages(audio_data, output_path):
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user