+43
-17
@@ -35,6 +35,7 @@ FORBIDDEN_OUTPUT_FIELDS = {
|
||||
"fingering",
|
||||
"left_hand_finger",
|
||||
"right_hand_finger",
|
||||
"chord_optional",
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +117,7 @@ class CompactMeasureParser:
|
||||
self.sequence = 0
|
||||
self.tuplet_sequence = 0
|
||||
self.pending_chord = ""
|
||||
self.pending_chord_optional = False
|
||||
self.last_note: dict[str, Any] | None = None
|
||||
self.last_octave = 4
|
||||
self.tuplets: list[dict[str, Any]] = []
|
||||
@@ -155,9 +157,14 @@ class CompactMeasureParser:
|
||||
|
||||
def parse_chord(self) -> None:
|
||||
self.index += 1
|
||||
self.pending_chord = self.parse_until("]", "chord symbol").strip()
|
||||
if not self.pending_chord:
|
||||
chord = self.parse_until("]", "chord symbol").strip()
|
||||
optional = chord.startswith("(") and chord.endswith(")")
|
||||
if optional:
|
||||
chord = chord[1:-1].strip()
|
||||
if not chord:
|
||||
raise self.error("chord symbol must not be empty")
|
||||
self.pending_chord = chord
|
||||
self.pending_chord_optional = optional
|
||||
|
||||
def parse_lyric(self) -> None:
|
||||
if self.last_note is None:
|
||||
@@ -288,7 +295,10 @@ class CompactMeasureParser:
|
||||
}
|
||||
if self.pending_chord:
|
||||
note["chord_symbol"] = self.pending_chord
|
||||
if self.pending_chord_optional:
|
||||
note["chord_optional"] = True
|
||||
self.pending_chord = ""
|
||||
self.pending_chord_optional = False
|
||||
if tie_from_previous:
|
||||
note["tie_from_previous"] = True
|
||||
if self.peek() == "~":
|
||||
@@ -485,16 +495,30 @@ def first_voice(measure: dict[str, Any]) -> dict[str, Any]:
|
||||
return {"id": "voice-empty", "tuplets": [], "notes": []}
|
||||
|
||||
|
||||
def first_chord_symbol(measure: dict[str, Any], fallback: str = "") -> str:
|
||||
def note_chord_symbol(note: dict[str, Any], *, include_optional: bool = True) -> str:
|
||||
chord = note.get("chord_symbol")
|
||||
if not isinstance(chord, str) or not chord:
|
||||
return ""
|
||||
if not include_optional and note.get("chord_optional") is True:
|
||||
return ""
|
||||
return chord
|
||||
|
||||
|
||||
def first_chord_symbol(measure: dict[str, Any], fallback: str = "", *, include_optional: bool = True) -> str:
|
||||
voice = first_voice(measure)
|
||||
notes = voice.get("notes")
|
||||
if not isinstance(notes, list):
|
||||
return fallback
|
||||
has_filtered_chord = False
|
||||
for note in notes:
|
||||
if isinstance(note, dict):
|
||||
chord = note.get("chord_symbol")
|
||||
if isinstance(chord, str) and chord:
|
||||
if isinstance(note.get("chord_symbol"), str) and note.get("chord_symbol"):
|
||||
has_filtered_chord = True
|
||||
chord = note_chord_symbol(note, include_optional=include_optional)
|
||||
if chord:
|
||||
return chord
|
||||
if not include_optional and has_filtered_chord:
|
||||
return ""
|
||||
return fallback
|
||||
|
||||
|
||||
@@ -513,25 +537,25 @@ def timed_measure_notes(measure: dict[str, Any]) -> list[tuple[int, dict[str, An
|
||||
return result
|
||||
|
||||
|
||||
def chord_markers(measure: dict[str, Any]) -> list[tuple[int, int, str]]:
|
||||
def chord_markers(measure: dict[str, Any], *, include_optional: bool = True) -> list[tuple[int, int, str]]:
|
||||
markers: list[tuple[int, int, str]] = []
|
||||
for start_ticks, note in timed_measure_notes(measure):
|
||||
chord = note.get("chord_symbol")
|
||||
if isinstance(chord, str) and chord:
|
||||
chord = note_chord_symbol(note, include_optional=include_optional)
|
||||
if chord:
|
||||
markers.append((start_ticks, note_duration(note), chord))
|
||||
return markers
|
||||
|
||||
|
||||
def chord_symbol_at_offset(measure: dict[str, Any], offset_ticks: int) -> str:
|
||||
for start_ticks, _, chord in chord_markers(measure):
|
||||
def chord_symbol_at_offset(measure: dict[str, Any], offset_ticks: int, *, include_optional: bool = True) -> str:
|
||||
for start_ticks, _, chord in chord_markers(measure, include_optional=include_optional):
|
||||
if start_ticks == offset_ticks:
|
||||
return chord
|
||||
return ""
|
||||
|
||||
|
||||
def active_chord_at_offset(measure: dict[str, Any], offset_ticks: int, fallback: str = "") -> str:
|
||||
def active_chord_at_offset(measure: dict[str, Any], offset_ticks: int, fallback: str = "", *, include_optional: bool = True) -> str:
|
||||
current = fallback
|
||||
for start_ticks, _, chord in chord_markers(measure):
|
||||
for start_ticks, _, chord in chord_markers(measure, include_optional=include_optional):
|
||||
if start_ticks > offset_ticks:
|
||||
break
|
||||
current = chord
|
||||
@@ -592,8 +616,8 @@ def renderable_rest_notes(note_id_prefix: str, duration_ticks: int, sequence: in
|
||||
return notes, sequence
|
||||
|
||||
|
||||
def apply_source_chord_markers(notes: list[dict[str, Any]], measure: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
markers = [(start, chord) for start, _, chord in chord_markers(measure)]
|
||||
def apply_source_chord_markers(notes: list[dict[str, Any]], measure: dict[str, Any], *, include_optional: bool = True) -> list[dict[str, Any]]:
|
||||
markers = [(start, chord) for start, _, chord in chord_markers(measure, include_optional=include_optional)]
|
||||
if not markers:
|
||||
result = [copy.deepcopy(note) for note in notes]
|
||||
for note in result:
|
||||
@@ -661,18 +685,20 @@ def sanitize_json(value: Any) -> Any:
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def sanitized_note(note: dict[str, Any], *, strip_lyrics: bool) -> dict[str, Any]:
|
||||
def sanitized_note(note: dict[str, Any], *, strip_lyrics: bool, include_optional: bool = True) -> dict[str, Any]:
|
||||
output = sanitize_json(note)
|
||||
if strip_lyrics:
|
||||
output.pop("lyric", None)
|
||||
if not include_optional and note.get("chord_optional") is True:
|
||||
output.pop("chord_symbol", None)
|
||||
output.setdefault("hidden", False)
|
||||
return output
|
||||
|
||||
|
||||
def copy_source_voice(measure: dict[str, Any], *, strip_lyrics: bool) -> dict[str, Any]:
|
||||
def copy_source_voice(measure: dict[str, Any], *, strip_lyrics: bool, include_optional: bool = True) -> dict[str, Any]:
|
||||
voice = first_voice(measure)
|
||||
tuplets = sanitize_json(voice.get("tuplets", []))
|
||||
notes = [sanitized_note(note, strip_lyrics=strip_lyrics) for note in measure_notes(measure)]
|
||||
notes = [sanitized_note(note, strip_lyrics=strip_lyrics, include_optional=include_optional) for note in measure_notes(measure)]
|
||||
return {
|
||||
"id": str(voice.get("id", "voice-source")),
|
||||
"tuplets": tuplets if isinstance(tuplets, list) else [],
|
||||
|
||||
Reference in New Issue
Block a user