Skip to content

Commit

Permalink
Implmeent base for linearization
Browse files Browse the repository at this point in the history
  • Loading branch information
v-dvorak committed Feb 14, 2025
1 parent c3466a2 commit 0db714b
Show file tree
Hide file tree
Showing 9 changed files with 373 additions and 102 deletions.
126 changes: 126 additions & 0 deletions app/Linearize/GraphToLMX.py
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 ""
88 changes: 88 additions & 0 deletions app/Linearize/MXMLSimplifier.py
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)
18 changes: 18 additions & 0 deletions app/Linearize/Tokens.py
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 added app/Linearize/__init__.py
Empty file.
35 changes: 35 additions & 0 deletions app/Linearize/__main__.py
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()
20 changes: 20 additions & 0 deletions app/Linearize/lmx_to_musicxml.py
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
Loading

0 comments on commit 0db714b

Please sign in to comment.