Skip to content

Commit

Permalink
New version for RNTXT import.
Browse files Browse the repository at this point in the history
  • Loading branch information
manoskary committed Dec 3, 2024
1 parent 578e41d commit d209908
Showing 1 changed file with 151 additions and 131 deletions.
282 changes: 151 additions & 131 deletions partitura/io/importrntxt.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,169 +48,189 @@ class RntxtParser:
https://github.com/MarkGotham/When-in-Rome/blob/master/syntax.md
"""
def __init__(self, score=None):
# Initialize parser state
self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp")
self.current_measure = None
self.current_position = 0
self.measure_beat_position = 1
self.current_time_signature = spt.TimeSignature(4, 4)
self.time_signature_style = 'Normal' # 'Normal', 'Slow', 'Fast'
self.key = 'C'
self.pedal = None
self.metadata = {}
self.measures = {}
# If a score is provided, copy relevant information
if score is not None:
self.ref_part = score.parts[0]
quarter_duration = self.ref_part._quarter_durations[0]
ref_measures = self.ref_part.measures
ref_time_sigs = self.ref_part.time_sigs
ref_keys = self.ref_part.key_sigs
self._initialize_from_score(score)
else:
quarter_duration = 4
ref_measures = []
ref_time_sigs = []
ref_keys = []
self.part = spt.Part(id="rn", part_name="Rn", part_abbreviation="rnp", quarter_duration=quarter_duration)
# include measures
for measure in ref_measures:
# Add default staff
self.part.add(spt.Staff(number=1, lines=5), 0)

def _initialize_from_score(self, score):
# Copy measures, time signatures, and key signatures from the reference score
self.ref_part = score.parts[0]
for measure in self.ref_part.measures:
self.part.add(measure, measure.start.t, measure.end.t)
# include time signatures
for time_sig in ref_time_sigs:
for time_sig in self.ref_part.time_sigs:
self.part.add(time_sig, time_sig.start.t)
# include key signatures
for key in ref_keys:
self.part.add(key, key.start.t)
for key_sig in self.ref_part.key_sigs:
self.part.add(key_sig, key_sig.start.t)
self.measures = {m.number: m for m in self.part.measures}
self.part.add(spt.Staff(number=1, lines=1), 0)
self.current_measure = None
self.current_position = 0
self.measure_beat_position = 1
self.current_voice = None
self.current_note = None
self.current_chord = None
self.current_tie = None
self.num_parsed_romans = 0
self.key = "C"

def parse(self, lines):
# np_lines = np.array(lines)
# potential_measure_lines = np.lines[np.char.startswith(np_lines, "m")]
# for line in potential_measure_lines:
# self._handle_measure(line)
for line in lines:
if line.startswith("Time Signature:"):
self.time_signature = line.split(":")[1].strip()
elif line.startswith("Pedal:"):
self.pedal = line.split(":")[1].strip()
elif line.startswith("m"):
self._handle_measure(line)

self.currate_ending_times()

def currate_ending_times(self):
romans = list(self.part.iter_all(spt.RomanNumeral))
starting_times = [rn.start.t for rn in romans]
argsort_start = np.argsort(starting_times)
for i, rn_idx in enumerate(argsort_start[:-1]):
rn = romans[rn_idx]
if rn.end is None:
rn.end = romans[argsort_start[i+1]].start if rn.start.t < romans[argsort_start[i+1]].start.t else rn.start.t + 1
for line_num, line in enumerate(lines, 1):
line = line.strip()
if not line or line.startswith('#'):
continue
try:
if ':' in line:
keyword = line.split(':', 1)[0].strip()
if keyword in ('Composer', 'Title', 'Analyst'):
self._handle_metadata(line)
elif keyword == 'Note':
self._handle_note_line(line)
elif keyword == 'Time Signature':
self._handle_time_signature(line)
elif keyword == 'Pedal':
self._handle_pedal(line)
else:
self._handle_line(line)
else:
self._handle_line(line)
except Exception as e:
print(f"Error parsing line {line_num}: {line}")
print(e)
self._calculate_ending_times()

def _handle_metadata(self, line):
key, value = line.split(':', 1)
self.metadata[key.strip()] = value.strip()

def _handle_note_line(self, line):
# Notes can be stored or logged as needed
pass

def _handle_measure(self, line):
if not self._validate_measure_line(line):
return
elements = line.split(" ")
measure_number = elements[0].strip("m")
if not measure_number.isnumeric():
# TODO: complete check for variation measures
if "var" in measure_number:
return
else:
def _handle_pedal(self, line):
# Parse pedal information
pass

def _handle_line(self, line):
if re.match(r'm\d+(-\d+)?\s*=', line):
self._handle_repeat(line)
elif line.startswith('m'):
self._handle_measure(line)
else:
raise ValueError(f"Unknown line format: {line}")

def _handle_repeat(self, line):
# Implement repeat logic
pass

raise ValueError(f"Invalid measure number: {measure_number}")
measure_number = int(measure_number)
if measure_number not in self.measures.keys():
def _handle_measure(self, line):
elements = line.strip().split()
measure_info = elements[0]
measure_match = re.match(r'm(\d+)(?:-(\d+))?', measure_info)
if not measure_match:
raise ValueError(f"Invalid measure number: {measure_info}")
measure_number = int(measure_match.group(1))
if measure_number not in self.measures:
# Check if previous measure is there
if measure_number - 1 in self.measures:
previous_measure_start = self.measures[measure_number - 1].start.t
# get the current time signature
current_time_signature_beats = self.current_time_signature.beats
self.current_position = self.part.beat_map(
self.part.inv_beat_map(previous_measure_start) + current_time_signature_beats)
self.current_measure = spt.Measure(number=measure_number)
self.measures[measure_number] = self.current_measure
self.part.add(self.current_measure, int(self.current_position))
self.part.add(self.current_measure, self.current_position)
else:
self.current_measure = self.measures[measure_number]

self.current_position = self.current_measure.start.t
# starts counting beats from 1
self.measure_beat_position = 1
for element in elements[1:]:
self._handle_element(element)

def _handle_element(self, element):
# if element starts with "b" followed by a number ("float" or "int") it is a beat
if element.startswith("b") and element[1:].replace(".", "").isnumeric():
self.measure_beat_position = float(element[1:])
if self.current_measure.number == 0:
if (self.current_position == 0 and self.num_parsed_romans == 0):
self.current_position = 0
else:
self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_position) + self.measure_beat_position - 1).item()
if element.startswith('b'):
beat_match = re.match(r'b(\d+(\.\d+)?)', element)
if beat_match:
self.measure_beat_position = float(beat_match.group(1))
self._update_current_position()
else:
self.current_position = self.part.inv_beat_map(self.part.beat_map(self.current_measure.start.t) + self.measure_beat_position - 1).item()

# if element starts with [A-G] and it includes : it is a key
elif len(re.findall(r"[A-Ga-g#b:]", element)) == len(element) and element[-1] == ":":
raise ValueError(f"Invalid beat format: {element}")
elif re.match(r'.*:', element):
self._handle_key(element)
# if element only contains "|" or ":" (and combinations) it is a barline
elif all(c in "|:" for c in element):
self._handle_barline(element)
# else it is a roman numeral
else:
self._handle_roman_numeral(element)

def _handle_key(self, element):
# key is in the format "C:" or "c:" for C major or c minor
# for alterations use "C#:" or "c#:" for C# major or c# minor
name = element[0]
mode = "major" if name.isupper() else "minor"
step = name.upper()
# handle alterations
alter = element[1:].strip(":")
key_name = f"{step}{alter}{('m' if mode == 'minor' else '')}"
# step and alter to fifths
fifths, mode = key_name_to_fifths_mode(key_name)
ks = spt.KeySignature(fifths=fifths, mode=mode)
self.key = element.strip(":")
self.part.add(ks, int(self.current_position))
def _update_current_position(self):
self.current_position = self.part.beat_map(self.part.inv_beat_map(self.current_measure.start.t) + self.measure_beat_position)

def _get_beat_duration(self):
# Calculate beat duration based on the time signature and style
nom, denom = self.current_time_signature.beats, self.current_time_signature.beat_type
quarter_note_duration = 1 # Assuming a quarter note duration of 1
beat_duration = (4 / denom) * quarter_note_duration
if self.time_signature_style == 'Fast' and nom % 3 == 0 and denom == 8:
beat_duration *= 3 # Compound meter counted in dotted quarters
elif self.time_signature_style == 'Slow' and nom % 3 == 0 and denom == 8:
beat_duration /= 3 # Compound meter counted in eighth notes
return beat_duration

def _handle_time_signature(self, line):
time_signature = line.split(':', 1)[1].strip()
style = 'Normal'
if 'Slow' in time_signature:
style = 'Slow'
time_signature = time_signature.replace('Slow', '').strip()
elif 'Fast' in time_signature:
style = 'Fast'
time_signature = time_signature.replace('Fast', '').strip()
if time_signature == 'C':
nom, denom = 4, 4
elif time_signature == 'Cut':
nom, denom = 2, 2
else:
nom, denom = map(int, time_signature.split('/'))
self.current_time_signature = spt.TimeSignature(nom, denom)
self.time_signature_style = style
self.part.add(self.current_time_signature, self.current_position)

def _handle_barline(self, element):
# Implement barline handling if needed
pass

def _handle_roman_numeral(self, element):
"""
The handling or roman numeral aims to translate rntxt notation to internal partitura notation.
Parameters
----------
element: txt
The element is a rntxt notation string
"""
# Remove line endings and spaces
element = element.strip()
# change strings such as RN6/5 to RN65 but keep RN65/RN for the secondary degree
if "/" in element:
# if all elements between "/" are either digits or one of [o, +] then remove "/" else leave it in place
el_list = element.split("/")
element = el_list[0]
for el in el_list[1:]:
if len(re.findall(r"[1-9\+o]", el)) == len(el):
element += el
else:
element += "/" + el
# Validity checks happen inside the Roman Numeral object
# The checks include 1 & 2 Degree, Root, Bass, Inversion, and Quality extraction.
rn = spt.RomanNumeral(text=element, local_key=self.key)

try:
self.part.add(rn, int(self.current_position))
except ValueError:
print(f"Could not add roman numeral {element} at position {self.current_position}")
return
# Set the end of the previous roman numeral
# if self.previous_rn is not None:
# self.previous_rn.end = spt.TimePoint(t=self.current_position)
self.num_parsed_romans += 1

def _validate_measure_line(self, line):
# does it have elements
if not len(line.split(" ")) > 1:
return False
return True
rn = spt.RomanNumeral(text=element, local_key=self.key)
self.part.add(rn, self.current_position)
except Exception as e:
raise ValueError(f"Error parsing Roman numeral '{element}': {e}")

def _handle_key(self, element):
match = re.match(r'([A-Ga-g])([#b]*):', element)
if not match:
raise ValueError(f"Invalid key signature: {element}")
note, accidental = match.groups()
mode = 'minor' if note.islower() else 'major'
key_name = note.upper() + accidental
key_str = f"{key_name}{('m' if mode == 'minor' else '')}"
fifths, mode = key_name_to_fifths_mode(key_str)
ks = spt.KeySignature(fifths=fifths, mode=mode)
self.key = element.strip(":")
self.part.add(ks, self.current_position)

def _calculate_ending_times(self):
romans = sorted(self.part.iter_all(spt.RomanNumeral), key=lambda rn: rn.start.t)
for i, rn in enumerate(romans[:-1]):
rn.end = spt.TimePoint(t=romans[i + 1].start.t)
if romans:
last_rn = romans[-1]
last_rn.end = self.part.end_time or (last_rn.start.t + 1)




0 comments on commit d209908

Please sign in to comment.