From d20990882bb974accc4f1c7f459702fb9dc6214c Mon Sep 17 00:00:00 2001 From: melkisedeath Date: Tue, 3 Dec 2024 12:28:17 +0100 Subject: [PATCH] New version for RNTXT import. --- partitura/io/importrntxt.py | 282 +++++++++++++++++++----------------- 1 file changed, 151 insertions(+), 131 deletions(-) diff --git a/partitura/io/importrntxt.py b/partitura/io/importrntxt.py index b2828cf5..54cebe70 100644 --- a/partitura/io/importrntxt.py +++ b/partitura/io/importrntxt.py @@ -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) +