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:
Sin
2026-06-16 19:36:53 +01:00
parent bc0e83c345
commit 4148b0e72e
49 changed files with 397 additions and 392 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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