+81
-21
@@ -6,6 +6,8 @@ import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from generator.styles import canonical_style
|
||||
|
||||
WHOLE_TICKS = 16384
|
||||
HALF_TICKS = WHOLE_TICKS // 2
|
||||
QUARTER_TICKS = WHOLE_TICKS // 4
|
||||
@@ -28,13 +30,6 @@ RENDERABLE_TICKS = (
|
||||
|
||||
REST_PITCH = {"letter": "b", "accidental": "n", "octave": 4}
|
||||
FORBIDDEN_OUTPUT_FIELDS = {
|
||||
"tab_positions",
|
||||
"tab_frets",
|
||||
"finger",
|
||||
"fingers",
|
||||
"fingering",
|
||||
"left_hand_finger",
|
||||
"right_hand_finger",
|
||||
"chord_optional",
|
||||
}
|
||||
|
||||
@@ -91,6 +86,35 @@ def score_field(document: dict[str, Any], metadata: dict[str, Any], *names: str,
|
||||
return fallback
|
||||
|
||||
|
||||
def canonicalize_score_metadata(metadata: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
|
||||
output = sanitize_json(metadata)
|
||||
if not isinstance(output, dict):
|
||||
output = {}
|
||||
style_source = output.get("style")
|
||||
try:
|
||||
output["style"] = canonical_style(style_source)
|
||||
except ValueError as error:
|
||||
location = f"{path}: " if path is not None else ""
|
||||
raise CompactScoreError(f"{location}{error}") from error
|
||||
return output
|
||||
|
||||
|
||||
def normalize_full_score_document(document: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
|
||||
output = copy.deepcopy(document)
|
||||
output.pop("version", None)
|
||||
metadata_value = output.get("metadata", {})
|
||||
if metadata_value is not None and not isinstance(metadata_value, dict):
|
||||
location = f"{path}: " if path is not None else ""
|
||||
raise CompactScoreError(f"{location}metadata must be an object")
|
||||
metadata = copy.deepcopy(metadata_value) if isinstance(metadata_value, dict) else {}
|
||||
for key in ("style",):
|
||||
if key in output and key not in metadata:
|
||||
metadata[key] = output[key]
|
||||
output.pop(key, None)
|
||||
output["metadata"] = canonicalize_score_metadata(metadata, path)
|
||||
return output
|
||||
|
||||
|
||||
def parse_duration_token(text: str, label: str) -> int:
|
||||
dotted = text.endswith(".")
|
||||
base_text = text[:-1] if dotted else text
|
||||
@@ -352,9 +376,31 @@ class CompactMeasureParser:
|
||||
return notes, self.tuplets
|
||||
|
||||
|
||||
def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str]]:
|
||||
COMPACT_MEASURE_CONTROL_FIELDS = {
|
||||
"notes",
|
||||
"notation",
|
||||
"section",
|
||||
"section_label",
|
||||
"key",
|
||||
"key_signature",
|
||||
"time",
|
||||
"time_signature",
|
||||
"id",
|
||||
"voices",
|
||||
}
|
||||
|
||||
GENERATED_MEASURE_FIELDS = {
|
||||
"id",
|
||||
"section_label",
|
||||
"time_signature",
|
||||
"key_signature",
|
||||
"voices",
|
||||
}
|
||||
|
||||
|
||||
def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str], dict[str, Any]]:
|
||||
if isinstance(value, str):
|
||||
return value, {}
|
||||
return value, {}, {}
|
||||
if isinstance(value, dict):
|
||||
text = value.get("notes", value.get("notation", ""))
|
||||
if not isinstance(text, str):
|
||||
@@ -371,16 +417,21 @@ def compact_measure_text(value: Any, index: int) -> tuple[str, dict[str, str]]:
|
||||
field = value.get(source)
|
||||
if isinstance(field, str):
|
||||
overrides[target] = field
|
||||
return text, overrides
|
||||
extra_fields = {
|
||||
key: sanitize_json(extra_value)
|
||||
for key, extra_value in value.items()
|
||||
if key not in COMPACT_MEASURE_CONTROL_FIELDS
|
||||
}
|
||||
return text, overrides, extra_fields
|
||||
raise CompactScoreError(f"measure {index + 1}: compact measure must be a string or object")
|
||||
|
||||
|
||||
def normalize_score_document(document: dict[str, Any], path: Path | None = None) -> dict[str, Any]:
|
||||
if isinstance(document.get("staves"), list):
|
||||
return document
|
||||
return normalize_full_score_document(document, path)
|
||||
measures_value = document.get("measures")
|
||||
if not isinstance(measures_value, list):
|
||||
return document
|
||||
return normalize_full_score_document(document, path)
|
||||
metadata_value = document.get("metadata", {})
|
||||
metadata = metadata_value if isinstance(metadata_value, dict) else {}
|
||||
title = score_field(document, metadata, "title", fallback=human_label(path.stem if path is not None else "score"))
|
||||
@@ -401,18 +452,19 @@ def normalize_score_document(document: dict[str, Any], path: Path | None = None)
|
||||
"time_signature": time_signature,
|
||||
})
|
||||
for key, value in document.items():
|
||||
if key in {"title", "composer", "key", "key_signature", "time", "time_signature", "clef", "part", "name", "role", "measures", "metadata"}:
|
||||
if key in {"version", "title", "composer", "key", "key_signature", "time", "time_signature", "clef", "part", "name", "role", "measures", "metadata"}:
|
||||
continue
|
||||
output_metadata[key] = sanitize_json(value)
|
||||
output_metadata = canonicalize_score_metadata(output_metadata, path)
|
||||
|
||||
measures: list[dict[str, Any]] = []
|
||||
for index, value in enumerate(measures_value):
|
||||
text, overrides = compact_measure_text(value, index)
|
||||
text, overrides, extra_fields = compact_measure_text(value, index)
|
||||
measure_time = overrides.get("time_signature", time_signature)
|
||||
measure_key = overrides.get("key_signature", key_signature)
|
||||
parser = CompactMeasureParser(text, measure_number=index + 1, time_signature=measure_time)
|
||||
notes, tuplets = parser.parse()
|
||||
measures.append({
|
||||
measure = {
|
||||
"id": f"measure-{index + 1}",
|
||||
"section_label": overrides.get("section_label", ""),
|
||||
"time_signature": measure_time,
|
||||
@@ -422,10 +474,11 @@ def normalize_score_document(document: dict[str, Any], path: Path | None = None)
|
||||
"notes": notes,
|
||||
"tuplets": tuplets,
|
||||
}],
|
||||
})
|
||||
}
|
||||
measure.update(extra_fields)
|
||||
measures.append(measure)
|
||||
|
||||
return {
|
||||
"version": int(document.get("version", 1)) if isinstance(document.get("version", 1), int) else 1,
|
||||
"metadata": output_metadata,
|
||||
"staves": [{
|
||||
"id": "staff-source",
|
||||
@@ -706,6 +759,14 @@ def copy_source_voice(measure: dict[str, Any], *, strip_lyrics: bool, include_op
|
||||
}
|
||||
|
||||
|
||||
def measure_notation_metadata(source: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: sanitize_json(value)
|
||||
for key, value in source.items()
|
||||
if key not in GENERATED_MEASURE_FIELDS
|
||||
}
|
||||
|
||||
|
||||
def make_measure(measure_id: str, source: dict[str, Any], notes: list[dict[str, Any]], *, section_label: str = "", tuplets: list[Any] | None = None) -> dict[str, Any]:
|
||||
measure: dict[str, Any] = {
|
||||
"id": measure_id,
|
||||
@@ -719,15 +780,14 @@ def make_measure(measure_id: str, source: dict[str, Any], notes: list[dict[str,
|
||||
}
|
||||
if section_label:
|
||||
measure["section_label"] = section_label
|
||||
measure.update(measure_notation_metadata(source))
|
||||
return measure
|
||||
|
||||
|
||||
def make_arranged_document(source_document: dict[str, Any], staff: dict[str, Any], *, staff_id: str, staff_name: str, role: str, arrangement_part_type: str, measures: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
metadata = sanitize_json(source_document.get("metadata", {}))
|
||||
if not isinstance(metadata, dict):
|
||||
metadata = {}
|
||||
source_metadata = source_document.get("metadata", {})
|
||||
metadata = canonicalize_score_metadata(source_metadata if isinstance(source_metadata, dict) else {})
|
||||
output: dict[str, Any] = {
|
||||
"version": int(source_document.get("version", 1)) if isinstance(source_document.get("version", 1), int) else 1,
|
||||
"metadata": metadata,
|
||||
"staves": [{
|
||||
"id": staff_id,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
DEFAULT_STYLE = "Medium Swing"
|
||||
|
||||
BUILT_IN_STYLES = (
|
||||
# Jazz
|
||||
"Afro 12/8",
|
||||
"Ballad Double Time Feel",
|
||||
"Ballad Even",
|
||||
"Ballad Melodic",
|
||||
"Ballad Swing",
|
||||
"Blue Note",
|
||||
"Bossa Nova",
|
||||
"Doo Doo Cats",
|
||||
"Double Time Swing",
|
||||
"Even 8ths",
|
||||
"Even 8ths Open",
|
||||
"Even 16ths",
|
||||
"Guitar Trio",
|
||||
"Gypsy Jazz",
|
||||
"Latin",
|
||||
"Latin/Swing",
|
||||
"Long Notes",
|
||||
"Medium Swing",
|
||||
"Medium Up Swing",
|
||||
"Medium Up Swing 2",
|
||||
"New Orleans Swing",
|
||||
"Second Line",
|
||||
"Slow Swing",
|
||||
"Swing Two/Four",
|
||||
"Trad Jazz",
|
||||
"Up Tempo Swing",
|
||||
"Up Tempo Swing 2",
|
||||
# Latin
|
||||
"Argentina: Tango",
|
||||
"Brazil: Bossa Acoustic",
|
||||
"Brazil: Bossa Electric",
|
||||
"Brazil: Samba",
|
||||
"Cuba: Bolero",
|
||||
"Cuba: Cha Cha Cha",
|
||||
"Cuba: Son Montuno 2-3",
|
||||
"Cuba: Son Montuno 3-2",
|
||||
# Pop
|
||||
"Bluegrass",
|
||||
"Country",
|
||||
"Disco",
|
||||
"Funk",
|
||||
"Glam Funk",
|
||||
"House",
|
||||
"Reggae",
|
||||
"RnB",
|
||||
"Rock",
|
||||
"Rock 12/8",
|
||||
"Shuffle",
|
||||
"Slow Rock",
|
||||
"Smooth",
|
||||
"Soul",
|
||||
"Virtual Funk",
|
||||
)
|
||||
|
||||
BUILT_IN_STYLE_SET = frozenset(BUILT_IN_STYLES)
|
||||
|
||||
STYLE_ALIASES = {
|
||||
"Med.": DEFAULT_STYLE,
|
||||
"Med. Swing": DEFAULT_STYLE,
|
||||
"Medium": DEFAULT_STYLE,
|
||||
}
|
||||
|
||||
|
||||
def canonical_style(value: object) -> str:
|
||||
if value is None:
|
||||
return DEFAULT_STYLE
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("style must be a string")
|
||||
|
||||
style = value.strip()
|
||||
if not style:
|
||||
return DEFAULT_STYLE
|
||||
|
||||
style = STYLE_ALIASES.get(style, style)
|
||||
if style not in BUILT_IN_STYLE_SET:
|
||||
raise ValueError(f"style {value!r} is not a supported player style")
|
||||
|
||||
return style
|
||||
Reference in New Issue
Block a user