-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
373 additions
and
102 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
from decimal import Decimal, ROUND_HALF_UP | ||
|
||
from .Tokens import (G_CLEF_ZERO_PITCH_INDEX, F_CLEF_ZERO_PITCH_INDEX, PITCH_TOKENS, NOTE_TOKEN, INDENTATION, | ||
CHORD_TOKEN, GS_CLEF_TOKEN, BASE_TIME_BEAT_TOKEN, STAFF_TOKEN, DEFAULT_STEM_TOKEN, MEASURE_TOKEN, | ||
DEFAULT_KEY_TOKEN) | ||
from ..Reconstruction.Graph.Names import NodeName | ||
from ..Reconstruction.Graph.Node import Node, VirtualNode | ||
from ..Reconstruction.Graph.Tags import (NOTEHEAD_TYPE_TAG, ACCIDENTAL_TYPE_TAG, SYMBOL_GS_INDEX_TAG, SYMBOL_PITCH_TAG) | ||
|
||
|
||
def _symbol_pitch_to_str(note: Node) -> int: | ||
# skip python default rounding (0.5 should be rounded to 1) | ||
return int(Decimal(note.get_tag(SYMBOL_PITCH_TAG)).to_integral(ROUND_HALF_UP)) | ||
|
||
|
||
def _notehead_to_string(note: Node) -> str: | ||
gs_index = note.get_tag(SYMBOL_GS_INDEX_TAG) | ||
pitch = _symbol_pitch_to_str(note) | ||
if gs_index is not None: | ||
return f"{gs_index}{note.get_tag(NOTEHEAD_TYPE_TAG)}{pitch}" | ||
else: | ||
return f"{note.get_tag(NOTEHEAD_TYPE_TAG)}{pitch}" | ||
|
||
|
||
def _accident_to_string(note: Node) -> str: | ||
gs_index = note.get_tag(SYMBOL_GS_INDEX_TAG) | ||
pitch = _symbol_pitch_to_str(note) | ||
if gs_index is not None: | ||
return f"{gs_index}{note.get_tag(ACCIDENTAL_TYPE_TAG)}{pitch}" | ||
else: | ||
return f"{note.get_tag(ACCIDENTAL_TYPE_TAG)}{pitch}" | ||
|
||
|
||
def symbol_to_str(note: Node) -> str: | ||
match note.name: | ||
case NodeName.NOTEHEAD: | ||
return _notehead_to_string(note) | ||
case NodeName.ACCIDENTAL: | ||
return _accident_to_string(note) | ||
case _: | ||
raise ValueError(f"Unknown symbol type {note.name}") | ||
|
||
|
||
def get_note_pitch(note: Node) -> str: | ||
gs_index = note.get_tag(SYMBOL_GS_INDEX_TAG) | ||
pitch = round(note.get_tag(SYMBOL_PITCH_TAG)) | ||
|
||
if gs_index is None or gs_index == 1: | ||
pitch_index = G_CLEF_ZERO_PITCH_INDEX + pitch | ||
elif gs_index == 2: | ||
pitch_index = F_CLEF_ZERO_PITCH_INDEX + pitch | ||
else: | ||
raise ValueError(f"Unknown value of {SYMBOL_GS_INDEX_TAG}: {gs_index}") | ||
|
||
return PITCH_TOKENS[pitch_index] | ||
|
||
|
||
def _note_to_lmx(note: Node) -> str: | ||
gs_tag = note.get_tag(SYMBOL_GS_INDEX_TAG) | ||
pitch_token = get_note_pitch(note) | ||
|
||
return " ".join( | ||
[pitch_token, NOTE_TOKEN, DEFAULT_STEM_TOKEN, f"{STAFF_TOKEN}:{gs_tag if gs_tag is not None else 1}"]) | ||
|
||
|
||
def _linearize_note_event_to_lmx(event: VirtualNode, human_readable: bool = True) -> str: | ||
if human_readable: | ||
output: str = "" | ||
first = True | ||
for note in event.children(): | ||
output += INDENTATION | ||
if first: | ||
output += (len(CHORD_TOKEN) + 1) * " " | ||
first = False | ||
else: | ||
output += CHORD_TOKEN + " " | ||
|
||
output += _note_to_lmx(note) | ||
output += "\n" | ||
return output | ||
|
||
else: | ||
output: list[str] = [] | ||
first = True | ||
for note in event.children(): | ||
note: Node | ||
|
||
if first: | ||
first = False | ||
else: | ||
output.append(CHORD_TOKEN) | ||
|
||
output.append(_note_to_lmx(note)) | ||
|
||
return " ".join(output) | ||
|
||
|
||
def linearize_note_events_to_lmx(measure_groups: list[list[VirtualNode]], human_readable: bool = True) -> str: | ||
note_written = False | ||
output: list[str] = [] | ||
first = True | ||
for row in measure_groups: | ||
|
||
for measure in row: | ||
|
||
output.append(MEASURE_TOKEN) | ||
if first: | ||
output.append(DEFAULT_KEY_TOKEN) | ||
output.append(INDENTATION + BASE_TIME_BEAT_TOKEN if human_readable else BASE_TIME_BEAT_TOKEN) | ||
output.append(INDENTATION + GS_CLEF_TOKEN if human_readable else GS_CLEF_TOKEN) | ||
first = False | ||
|
||
for child in measure.children(): | ||
child: VirtualNode | ||
if child.name == NodeName.NOTE_EVENT: | ||
output.append(_linearize_note_event_to_lmx(child, human_readable=human_readable)) | ||
note_written = True | ||
|
||
if note_written: | ||
if human_readable: | ||
return "\n".join(output) | ||
else: | ||
return " ".join(output) | ||
else: | ||
print("Warning: No note events were written.") | ||
return "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
from pathlib import Path | ||
|
||
import smashcima as sc | ||
from smashcima import Clef, Event, Note, Score, StaffSemantic | ||
|
||
from .Tokens import G_CLEF_ZERO_PITCH_INDEX, F_CLEF_ZERO_PITCH_INDEX | ||
from .Tokens import (NOTE_TOKEN, CHORD_TOKEN, GS_CLEF_TOKEN, BASE_TIME_BEAT_TOKEN, STAFF_TOKEN, MEASURE_TOKEN, | ||
DEFAULT_KEY_TOKEN, DEFAULT_STEM_TOKEN, PITCH_TOKENS) | ||
from .lmx_to_musicxml import lmx_to_musicxml | ||
|
||
|
||
def _get_note_relative_pitch_to_first_staff_line(note: Note) -> int: | ||
event = Event.of_durable(note) | ||
staff_sem = StaffSemantic.of_durable(note) | ||
clef: Clef = event.attributes.clefs[staff_sem.staff_number] | ||
|
||
# get absolute position of notehead on staff | ||
pitch_position = clef.pitch_to_pitch_position(note.pitch) + 4 | ||
# +4 -> smashcima indexes from the middle staff line, this project indexes from the bottom staff line | ||
|
||
return pitch_position | ||
|
||
|
||
def _note_to_lmx(note: Note) -> str: | ||
# get absolute position of notehead on staff | ||
pitch_position = _get_note_relative_pitch_to_first_staff_line(note) | ||
|
||
# get staff index grand staff | ||
staff_index = StaffSemantic.of_durable(note).staff_number | ||
|
||
# simplify note pitch: G clef at first staff, F clef at second staff | ||
if staff_index == 1: | ||
pitch_index = G_CLEF_ZERO_PITCH_INDEX + pitch_position | ||
elif staff_index == 2: | ||
pitch_index = F_CLEF_ZERO_PITCH_INDEX + pitch_position | ||
else: | ||
raise NotImplementedError(f"Unsupported staff index \"{staff_index}\"") | ||
|
||
return " ".join([PITCH_TOKENS[pitch_index], NOTE_TOKEN, DEFAULT_STEM_TOKEN, | ||
f"{STAFF_TOKEN}:{staff_index}"]) | ||
|
||
|
||
def _event_to_lmx(event: Event) -> list[str]: | ||
sequence: list[str] = [] | ||
is_chord = False | ||
notes = [durable for durable in event.durables if isinstance(durable, Note)] | ||
notes: list[Note] | ||
notes = sorted(notes, key=lambda n: n.pitch.get_linear_pitch()) | ||
for note in notes: | ||
if isinstance(note, Note): | ||
if is_chord: | ||
sequence.append(CHORD_TOKEN) | ||
sequence.append(_note_to_lmx(note)) | ||
is_chord = True | ||
|
||
return sequence | ||
|
||
|
||
def scene_to_lmx(score: Score) -> str: | ||
sequence: list[str] = [] | ||
|
||
sequence.append(MEASURE_TOKEN) | ||
sequence.append(DEFAULT_KEY_TOKEN) | ||
sequence.append(BASE_TIME_BEAT_TOKEN) | ||
sequence.append(GS_CLEF_TOKEN) | ||
first = True | ||
for part in score.parts: | ||
for measure in part.measures: | ||
if not first: | ||
sequence.append(MEASURE_TOKEN) | ||
first = False | ||
for event in measure.events: | ||
sequence.extend(_event_to_lmx(event)) | ||
|
||
return " ".join(sequence) | ||
|
||
|
||
def complex_musicxml_file_to_lmx(file_path: Path) -> str: | ||
score = sc.loading.load_score(file_path) | ||
return scene_to_lmx(score) | ||
|
||
|
||
def simplify_musicxml_file(input_path: Path, output_path: Path): | ||
output_lmx = complex_musicxml_file_to_lmx(input_path) | ||
output_xml = lmx_to_musicxml(output_lmx) | ||
|
||
with open(output_path, "w", encoding="utf8") as f: | ||
f.write(output_xml) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from lmx.linearization.vocabulary import PITCH_TOKENS | ||
|
||
BASE_TIME_BEAT_TOKEN = "time beats:4 beat-type:4" | ||
GS_CLEF_TOKEN = "clef:G2 staff:1 clef:F4 staff:2" | ||
DEFAULT_KEY_TOKEN = "key:fifths:0" | ||
|
||
INDENTATION = 4 * " " | ||
|
||
G_CLEF_ZERO_PITCH_INDEX = PITCH_TOKENS.index("E4") | ||
F_CLEF_ZERO_PITCH_INDEX = PITCH_TOKENS.index("G2") | ||
|
||
CHORD_TOKEN = "chord" | ||
NOTE_TOKEN = "quarter" | ||
STAFF_TOKEN = "staff" | ||
MEASURE_TOKEN = "measure" | ||
|
||
_STEM_TOKEN = "stem" | ||
DEFAULT_STEM_TOKEN = f"{_STEM_TOKEN}:up" |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from argparse import ArgumentParser | ||
from pathlib import Path | ||
|
||
from .MXMLSimplifier import simplify_musicxml_file | ||
|
||
|
||
def main(): | ||
parser = ArgumentParser( | ||
|
||
) | ||
subparsers = parser.add_subparsers(dest="command", help="Jobs") | ||
simp_parser = subparsers.add_parser("simplify") | ||
|
||
simp_parser.add_argument("input", help="Path to input file") | ||
simp_parser.add_argument("-o", "--output", help="Path to output file") | ||
|
||
args = parser.parse_args() | ||
|
||
if args.command == "simplify": | ||
|
||
input_file = Path(args.input) | ||
if args.output is None: | ||
output_file = input_file.parent / (input_file.stem + "_simple" + input_file.suffix) | ||
else: | ||
output_file = Path(args.output) | ||
|
||
simplify_musicxml_file(input_file, output_file) | ||
print(f"Saved at: {output_file.absolute()}") | ||
|
||
else: | ||
parser.print_help() | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import xml.etree.ElementTree as ET | ||
|
||
from lmx.linearization.Delinearizer import Delinearizer | ||
from lmx.symbolic.part_to_score import part_to_score | ||
|
||
|
||
def lmx_to_musicxml(linearized: str) -> str: | ||
ln = Delinearizer() | ||
|
||
ln.process_text(linearized) | ||
|
||
score_etree = part_to_score(ln.part_element) | ||
output_xml = str( | ||
ET.tostring( | ||
score_etree.getroot(), | ||
encoding="utf-8", | ||
xml_declaration=True | ||
), "utf-8") | ||
|
||
return output_xml |
Oops, something went wrong.