#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
This module contains methods for parsing matchfiles
"""
import os
from typing import Union, Tuple, Optional, Callable, List
import warnings
from functools import partial
import numpy as np
from partitura import score
from partitura.score import Part, Score
from partitura.performance import PerformedPart, Performance
from partitura.musicanalysis import estimate_voices, estimate_key
from partitura.io.matchlines_v0 import (
FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV0,
parse_matchline as parse_matchlinev0,
MatchInfo as MatchInfoV0,
MatchMeta as MatchMetaV0,
MatchSnote as MatchSnoteV0,
MatchNote as MatchNoteV0,
MatchSnoteNote as MatchSnoteNoteV0,
MatchSnoteDeletion as MatchSnoteDeletionV0,
MatchSnoteTrailingScore as MatchSnoteTrailingScoreV0,
MatchInsertionNote as MatchInsertionNoteV0,
MatchHammerBounceNote as MatchHammerBounceNoteV0,
MatchTrailingPlayedNote as MatchTrailingPlayedNoteV0,
MatchSustainPedal as MatchSustainPedalV0,
MatchSoftPedal as MatchSoftPedalV0,
MatchTrillNote as MatchTrillNoteV0,
)
from partitura.io.matchlines_v1 import (
FROM_MATCHLINE_METHODS as FROM_MATCHLINE_METHODSV1,
MatchInfo as MatchInfoV1,
MatchScoreProp as MatchScorePropV1,
MatchSection as MatchSectionV1,
MatchStime as MatchStimeV1,
MatchPtime as MatchPtimeV1,
MatchStimePtime as MatchStimePtimeV1,
MatchSnote as MatchSnoteV1,
MatchNote as MatchNoteV1,
MatchSnoteNote as MatchSnoteNoteV1,
MatchSnoteDeletion as MatchSnoteDeletionV1,
MatchInsertionNote as MatchInsertionNoteV1,
MatchSustainPedal as MatchSustainPedalV1,
MatchSoftPedal as MatchSoftPedalV1,
MatchOrnamentNote as MatchOrnamentNoteV1,
)
from partitura.io.matchfile_base import (
MatchError,
MatchFile,
MatchLine,
BaseSnoteLine,
BaseSnoteNoteLine,
BaseStimePtimeLine,
BaseDeletionLine,
BaseInsertionLine,
BaseOrnamentLine,
BaseSustainPedalLine,
BaseSoftPedalLine,
)
from partitura.io.matchfile_utils import (
Version,
number_pattern,
MatchTimeSignature,
MatchKeySignature,
format_pnote_id,
)
from partitura.utils.music import (
midi_ticks_to_seconds,
pitch_spelling_to_midi_pitch,
ensure_pitch_spelling_format,
key_name_to_fifths_mode,
estimate_clef_properties,
note_array_from_note_list,
)
from partitura.utils.misc import (
deprecated_alias,
deprecated_parameter,
PathLike,
get_document_name,
)
from partitura.utils.generic import interp1d, partition, iter_current_next
__all__ = ["load_match"]
def get_version(line: str) -> Version:
"""
Get version from the first line. Since the
first version of the format did not include this line,
we assume that the version is 0.1.0 if no version is
found.
Parameters
----------
line: str
The first line of the match file.
Returns
-------
version : Version
The version of the match file
"""
version = Version(0, 1, 0)
for parser in (MatchInfoV1, MatchInfoV0):
try:
ml = parser.from_matchline(line)
if isinstance(getattr(ml, "Value", None), Version):
version = ml.Value
return version
except MatchError:
pass
return version
def parse_matchline(
line: str,
from_matchline_methods: List[Callable[[str], MatchLine]],
version: Version,
debug: bool = False,
) -> Optional[MatchLine]:
"""
Return objects representing the line as one of:
* hammer_bounce-PlayedNote.
* info(Attribute, Value).
* insertion-PlayedNote.
* ornament(Anchor)-PlayedNote.
* ScoreNote-deletion.
* ScoreNote-PlayedNote.
* ScoreNote-trailing_score_note.
* trailing_played_note-PlayedNote.
* trill(Anchor)-PlayedNote.
* meta(Attribute, Value, Bar, Beat).
* sustain(Time, Value)
* soft(Time, Value)
or None if none can be matched
Parameters
----------
line : str
Line of the match file
from_matchline_methods : List[Callable[[str], MatchLine]]
Returns
-------
matchline : subclass of `MatchLine`
Object representing the line.
"""
matchline = None
for from_matchline in from_matchline_methods:
try:
matchline = from_matchline(line, version=version)
break
except Exception as e:
if not isinstance(e, MatchError):
print(line, e, version) # pragma: no cover
continue
return matchline
@deprecated_alias(fn="filename", create_part="create_score")
def load_matchfile(
filename: PathLike,
) -> MatchFile:
"""
Load a Matchfile as a `MatchFile` instance
"""
if not os.path.exists(filename):
raise ValueError("Filename does not exist") # pragma: no cover
with open(filename) as f:
raw_lines = f.read().splitlines()
version = get_version(raw_lines[0])
from_matchline_methods = FROM_MATCHLINE_METHODSV1
if version < Version(1, 0, 0):
from_matchline_methods = FROM_MATCHLINE_METHODSV0
parsed_lines = list()
# Functionality to remove duplicate lines
len_raw_lines = len(raw_lines)
np_lines = np.array(raw_lines, dtype=str)
# Remove empty lines
np_lines = np_lines[np_lines != ""]
# Remove duplicate lines
_, idx = np.unique(np_lines, return_index=True)
np_lines = np_lines[np.sort(idx)]
# Parse lines
f = partial(
parse_matchline, version=version, from_matchline_methods=from_matchline_methods
)
f_vec = np.vectorize(f)
parsed_lines = f_vec(np_lines).tolist()
# Create MatchFile instance
mf = MatchFile(lines=parsed_lines)
# Validate match for duplicate snote_ids or pnote_ids
validate_match_ids(mf)
return mf
[docs]@deprecated_alias(fn="filename", create_part="create_score")
def load_match(
filename: PathLike,
create_score: bool = False,
pedal_threshold: int = 64,
first_note_at_zero: bool = False,
offset_duration_whole: bool = True,
) -> Tuple[Union[Performance, list, Score]]:
"""
Load a matchfile.
Parameters
----------
filename : str
The matchfile
create_score : bool, optional
When True create a Part object from the snote information in
the match file. Defaults to False.
pedal_threshold : int, optional
Threshold for adjusting sound off of the performed notes using
pedal information. Defaults to 64.
first_note_at_zero : bool, optional
When True the note_on and note_off times in the performance
are shifted to make the first note_on time equal zero.
Returns
-------
performance : :class:partitura.performance.Performance
alignment : list
The score--performance alignment, a list of dictionaries
scr : :class:partitura.score.Score
The score. This item is only returned when `create_score` = True.
"""
# Parse Matchfile
mf = load_matchfile(filename)
# Generate PerformedPart
ppart = performed_part_from_match(mf, pedal_threshold, first_note_at_zero)
performance = Performance(
id=get_document_name(filename),
performedparts=ppart,
)
# Generate Part
if create_score:
spart = part_from_matchfile(
mf,
match_offset_duration_in_whole=offset_duration_whole,
)
scr = score.Score(id=get_document_name(filename), partlist=[spart])
# Alignment
alignment = alignment_from_matchfile(mf)
if create_score:
return performance, alignment, scr
else:
return performance, alignment
def note_alignment_from_matchfile(mf: MatchFile) -> List[dict]:
"""
Get a note-level alignment from a MatchFile instance
Parameters
----------
mf : MatchFile
A score-to-performance alignment
Returns
-------
results : List[dict]
An alignmnet as a list of dictionaries for each note.
"""
result = []
for line in mf.lines:
if isinstance(line, BaseSnoteNoteLine):
result.append(
dict(
label="match",
score_id=str(line.snote.Anchor),
performance_id=format_pnote_id(line.note.Id),
)
)
elif isinstance(
line,
BaseDeletionLine,
):
if "leftOutTied" in line.snote.ScoreAttributesList:
continue
else:
result.append(dict(label="deletion", score_id=str(line.snote.Anchor)))
elif isinstance(
line,
BaseInsertionLine,
):
result.append(
dict(label="insertion", performance_id=format_pnote_id(line.note.Id))
)
elif isinstance(line, BaseOrnamentLine):
if isinstance(line, MatchTrillNoteV0):
ornament_type = "trill"
elif isinstance(line, MatchOrnamentNoteV1):
ornament_type = line.OrnamentType
else:
ornament_type = "generic_ornament"
result.append(
dict(
label="ornament",
score_id=str(line.Anchor),
performance_id=format_pnote_id(line.note.Id),
type=ornament_type,
)
)
return result
# alias
alignment_from_matchfile = note_alignment_from_matchfile
def performed_part_from_match(
mf: MatchFile,
pedal_threshold: int = 64,
first_note_at_zero: bool = False,
) -> PerformedPart:
"""
Make PerformedPart from performance info in a MatchFile
Parameters
----------
mf : MatchFile
A MatchFile instance
pedal_threshold : int, optional
Threshold for adjusting sound off of the performed notes using
pedal information. Defaults to 64.
first_note_at_zero : bool, optional
When True the note_on and note_off times in the performance
are shifted to make the first note_on time equal zero.
Returns
-------
ppart : PerformedPart
A performed part
"""
# Get midi time units
mpq = mf.info("midiClockRate") # 500000 -> microseconds per quarter
ppq = mf.info("midiClockUnits") # 500 -> parts per quarter
# PerformedNote instances for all MatchNotes
notes = []
notes = list()
note_onsets_in_secs = np.array(np.zeros(len(mf.notes)), dtype=float)
note_onsets_in_tick = np.array(np.zeros(len(mf.notes)), dtype=int)
for i, note in enumerate(mf.notes):
n_onset_sec = midi_ticks_to_seconds(note.Onset, mpq, ppq)
note_onsets_in_secs[i] = n_onset_sec
note_onsets_in_tick[i] = note.Onset
notes.append(
dict(
id=format_pnote_id(note.Id),
midi_pitch=note.MidiPitch,
note_on=n_onset_sec,
note_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
note_on_tick=note.Onset,
note_off_tick=note.Offset,
sound_off=midi_ticks_to_seconds(note.Offset, mpq, ppq),
velocity=note.Velocity,
track=getattr(note, "Track", 0),
channel=getattr(note, "Channel", 1),
)
)
# Set first note_on to zero in ticks and seconds if first_note_at_zero
if first_note_at_zero and len(note_onsets_in_secs) > 0:
offset = note_onsets_in_secs.min()
offset_tick = note_onsets_in_tick.min()
if offset > 0 and offset_tick > 0:
for note in notes:
note["note_on"] -= offset
note["note_off"] -= offset
note["sound_off"] -= offset
note["note_on_tick"] -= offset_tick
note["note_off_tick"] -= offset_tick
# SustainPedal instances for sustain pedal lines
sustain_pedal = [
dict(
number=64,
time=midi_ticks_to_seconds(ped.Time, mpq, ppq),
value=ped.Value,
)
for ped in mf.sustain_pedal
]
# SoftPedal instances for soft pedal lines
soft_pedal = [
dict(
number=67,
time=midi_ticks_to_seconds(ped.Time, mpq, ppq),
value=ped.Value,
)
for ped in mf.soft_pedal
]
# Make performed part
ppart = PerformedPart(
id="P1",
part_name=mf.info("piece"),
notes=notes,
controls=sustain_pedal + soft_pedal,
sustain_pedal_threshold=pedal_threshold,
)
return ppart
def sort_snotes(snotes: List[BaseSnoteLine]) -> List[BaseSnoteLine]:
"""
Sort s(core)notes.
Parameters
----------
snotes : list
The score notes
Returns
-------
snotes_sorted : list
The sorted score notes
"""
sidx = np.lexsort(
list(zip(*[(float(n.Offset), float(n.Beat), float(n.Measure)) for n in snotes]))
)
return [snotes[i] for i in sidx if snotes[i].NoteName.lower() != "r"]
def part_from_matchfile(
mf: MatchFile,
match_offset_duration_in_whole: bool = True,
) -> Part:
"""
Create a score part from a matchfile.
Parameters
----------
mf : MatchFile
An instance of `MatchFile`
match_offset_duration_in_whole: Boolean
A flag for the type of offset and duration given in the matchfile.
When true, the function expects the values to be given in whole
notes (e.g. 1/4 for a quarter note) independet of time signature.
Returns
-------
part : partitura.score.Part
An instance of `Part` containing score information.
"""
part = score.Part("P1", mf.info("piece"))
snotes = sort_snotes(mf.snotes)
ts = mf.time_signatures
min_time = snotes[0].OnsetInBeats # sorted by OnsetInBeats
max_time = max(n.OffsetInBeats for n in snotes)
_, beats_map, _, beat_type_map, min_time_q, max_time_q = make_timesig_maps(
ts, max_time
)
# compute necessary divs based on the types of notes in the
# match snotes (only integers)
divs_arg = [
max(int((beat_type_map(note.OnsetInBeats) / 4)), 1)
* note.Offset.denominator
* (note.Offset.tuple_div or 1)
for note in snotes
]
divs_arg += [
max(int((beat_type_map(note.OnsetInBeats) / 4)), 1)
* note.Duration.denominator
* (note.Duration.tuple_div or 1)
for note in snotes
]
onset_in_beats = np.array([note.OnsetInBeats for note in snotes])
unique_onsets, inv_idxs = np.unique(onset_in_beats, return_inverse=True)
# unique_onset_idxs = [np.where(onset_in_beats == u) for u in unique_onsets]
iois_in_beats = np.diff(unique_onsets)
beat_to_quarter = 4 / beat_type_map(onset_in_beats)
iois_in_quarters_offset = np.r_[
beat_to_quarter[0] * onset_in_beats[0],
(4 / beat_type_map(unique_onsets[:-1])) * iois_in_beats,
]
onset_in_quarters = np.cumsum(iois_in_quarters_offset)
iois_in_quarters = np.diff(onset_in_quarters)
# ___ these divs are relative to quarters;
divs = np.lcm.reduce(np.unique(divs_arg))
onset_in_divs = np.r_[0, np.cumsum(divs * iois_in_quarters)][inv_idxs]
onset_in_quarters = onset_in_quarters[inv_idxs]
# duration_in_beats = np.array([note.DurationInBeats for note in snotes])
# duration_in_quarters = duration_in_beats * beat_to_quarter
# duration_in_divs = duration_in_quarters * divs
part.set_quarter_duration(0, divs)
bars = np.unique([n.Measure for n in snotes])
t = min_time
t = t * 4 / beat_type_map(min_time)
offset = t
bar_times = {}
if t > 0:
# if we have an incomplete first measure that isn't an anacrusis
# measure, add a rest (dummy)
# t = t-t%beats_map(min_time)
# if starting beat is above zero, add padding
rest = score.Rest()
part.add(rest, start=0, end=t * divs)
onset_in_divs += t * divs
offset = 0
t = t - t % beats_map(min_time)
for b0, b1 in iter_current_next(bars, end=bars[-1] + 1):
bar_times.setdefault(b0, t)
if t < 0:
t = 0
else:
# multiply by diff between consecutive bar numbers
n_bars = b1 - b0
if t <= max_time_q:
t += (n_bars * 4 * beats_map(t)) / beat_type_map(t)
for ni, note in enumerate(snotes):
# start of bar in quarter units
bar_start = bar_times[note.Measure]
on_off_scale = 1
# on_off_scale = 1 means duration and beat offset are given in
# whole notes, else they're given in beats (as in the KAIST data)
if not match_offset_duration_in_whole:
on_off_scale = beat_type_map(bar_start)
# offset within bar in quarter units adjusted for different
# time signatures -> 4 / beat_type_map(bar_start)
bar_offset = (note.Beat - 1) * 4 / beat_type_map(bar_start)
# offset within beat in quarter units adjusted for different
# time signatures -> 4 / beat_type_map(bar_start)
beat_offset = (
4
/ on_off_scale
* note.Offset.numerator
/ (note.Offset.denominator * (note.Offset.tuple_div or 1))
)
# check anacrusis measure beat counting type for the first note
if bar_start < 0 and (bar_offset != 0 or beat_offset != 0) and ni == 0:
# in case of fully counted anacrusis we set the bar_start
# to -bar_duration (in quarters) so that the below calculation is correct
# not active for shortened anacrusis measures
bar_start = -beats_map(bar_start) * 4 / beat_type_map(bar_start)
# reset the bar_start for other notes in the anacrusis measure
bar_times[note.Bar] = bar_start
# convert the onset time in quarters (0 at first barline) to onset
# time in divs (0 at first note)
onset_divs = int(round(divs * (bar_start + bar_offset + beat_offset - offset)))
if not np.isclose(onset_divs, onset_in_divs[ni], atol=divs * 0.01):
warnings.warn(
"Calculated `onset_divs` does not match `OnsetInBeats` " "information!."
)
onset_divs = onset_in_divs[ni]
assert onset_divs >= 0
assert np.isclose(onset_divs, onset_in_divs[ni], atol=divs * 0.01)
is_tied = False
articulations = set()
if "staccato" in note.ScoreAttributesList or "stac" in note.ScoreAttributesList:
articulations.add("staccato")
if "accent" in note.ScoreAttributesList:
articulations.add("accent")
if "leftOutTied" in note.ScoreAttributesList:
is_tied = True
# dictionary with keyword args with which the Note
# (or GraceNote) will be instantiated
note_attributes = dict(
step=note.NoteName,
octave=note.Octave,
alter=note.Modifier,
id=note.Anchor,
articulations=articulations,
)
staff_nr = next(
(a[-1] for a in note.ScoreAttributesList if a.startswith("staff")), None
)
try:
note_attributes["staff"] = int(staff_nr)
except (TypeError, ValueError):
# no staff attribute, or staff attribute does not end with a number
note_attributes["staff"] = None
if "s" in note.ScoreAttributesList:
note_attributes["voice"] = 1
else:
note_attributes["voice"] = next(
(int(a) for a in note.ScoreAttributesList if number_pattern.match(a)),
None,
)
# get rid of this if as soon as we have a way to iterate over the
# duration components. For now we have to treat the cases simple
# and compound durations separately.
if note.Duration.add_components:
prev_part_note = None
for i, (num, den, tuple_div) in enumerate(note.Duration.add_components):
# when we add multiple notes that are tied, the first note will
# get the original note id, and subsequent notes will get a
# derived note id (by appending, 'a', 'b', 'c',...)
if i > 0:
# tnote_id = 'n{}_{}'.format(note.Anchor, i)
note_attributes["id"] = score._make_tied_note_id(
note_attributes["id"]
)
part_note = score.Note(**note_attributes)
# duration_divs from local beats --> 4/beat_type_map(bar_start)
duration_divs = int(
(4 / on_off_scale) * divs * num / (den * (tuple_div or 1))
)
assert duration_divs > 0
offset_divs = onset_divs + duration_divs
part.add(part_note, onset_divs, offset_divs)
if prev_part_note:
prev_part_note.tie_next = part_note
part_note.tie_prev = prev_part_note
prev_part_note = part_note
onset_divs = offset_divs
else:
num = note.Duration.numerator
den = note.Duration.denominator
tuple_div = note.Duration.tuple_div
# duration_divs from local beats --> 4/beat_type_map(bar_start)
duration_divs = int(
divs * 4 / on_off_scale * num / (den * (tuple_div or 1))
)
offset_divs = onset_divs + duration_divs
# notes with duration 0, are also treated as grace notes, even if
# they do not have a 'grace' score attribute
if "grace" in note.ScoreAttributesList or note.Duration.numerator == 0:
part_note = score.GraceNote(
grace_type="appoggiatura", **note_attributes
)
else:
part_note = score.Note(**note_attributes)
part.add(part_note, onset_divs, offset_divs)
# Check if the note is tied and if so, add the tie information
if is_tied:
found = False
# iterate over all notes in the Timeline that end at the starting point.
for el in part_note.start.iter_ending(score.Note):
if isinstance(el, score.Note):
condition = (
el.step == note_attributes["step"]
and el.octave == note_attributes["octave"]
and el.alter == note_attributes["alter"]
)
if condition:
el.tie_next = part_note
part_note.tie_prev = el
found = True
break
if not found:
warnings.warn(
"Tie information found, but no previous note found to tie to for note {}.".format(
part_note.id
)
)
# add time signatures
for ts_beat_time, ts_bar, tsg in ts:
ts_beats = tsg.numerator
ts_beat_type = tsg.denominator
# check if time signature is in a known measure (from notes)
if ts_bar in bar_times.keys():
bar_start_divs = int(divs * (bar_times[ts_bar] - offset)) # in quarters
bar_start_divs = max(0, bar_start_divs)
else:
bar_start_divs = 0
part.add(score.TimeSignature(ts_beats, ts_beat_type), bar_start_divs)
# add key signatures
for ks_beat_time, ks_bar, keys in mf.key_signatures:
if ks_bar in bar_times.keys():
bar_start_divs = int(divs * (bar_times[ks_bar] - offset)) # in quarters
bar_start_divs = max(0, bar_start_divs)
else:
bar_start_divs = 0
# TODO
# * use key estimation if there are multiple defined keys
# fifths, mode = key_name_to_fifths_mode(key_name)
part.add(score.KeySignature(keys.fifths, keys.mode), ks_bar)
add_staffs(part)
# add_clefs(part)
# add incomplete measure if necessary
if offset < 0:
part.add(score.Measure(number=0), 0, int(-offset * divs))
# add the rest of the measures automatically
score.add_measures(part)
score.tie_notes(part)
score.find_tuplets(part)
if not all([n.voice for n in part.notes_tied]):
for note in part.notes_tied:
if note.voice is None:
note.voice = 1
return part
def make_timesig_maps(
ts_orig: List[Tuple[float, int, MatchTimeSignature]],
max_time: float,
) -> (Callable, Callable, Callable, Callable, float, float):
"""
Create time signature (interpolation) maps
Parameters
----------
ts_orig : List[Tuple[float, int, MatchTimeSignature]]
A list of tuples containing position in beats, measure and
MatchTimeSignature instances
max_time : float
Maximal time of the time signatures
Returns
-------
beats_map: callable
qbeats_map: callable
beat_type_map: callable
qbeat_type_map: callable
start_q: float
end_q: float
"""
# TODO: make sure that ts_orig covers range from min_time
# return two functions that map score times (in quarter units) to time sig
# beats, and time sig beat_type respectively
ts = list(ts_orig)
assert len(ts) > 0
ts.append((max_time, None, ts[-1][2]))
x = np.array([t for t, _, _ in ts])
y = np.array([(x.numerator, x.denominator) for _, _, x in ts])
start_q = x[0] * 4 / y[0, 1]
x_q = np.cumsum(np.r_[start_q, 4 * np.diff(x) / y[:-1, 1]])
end_q = x_q[-1]
# TODO: fix error with bounds
qbeats_map = interp1d(
x_q,
y[:, 0],
kind="previous",
bounds_error=False,
fill_value=(y[0, 0], y[-1, 0]),
)
qbeat_type_map = interp1d(
x_q,
y[:, 1],
kind="previous",
bounds_error=False,
fill_value=(y[0, 1], y[-1, 1]),
)
beats_map = interp1d(
x,
y[:, 0],
kind="previous",
bounds_error=False,
fill_value=(y[0, 0], y[-1, 0]),
)
beat_type_map = interp1d(
x,
y[:, 1],
kind="previous",
bounds_error=False,
fill_value=(y[0, 1], y[-1, 1]),
)
return beats_map, qbeats_map, beat_type_map, qbeat_type_map, start_q, end_q
def add_staffs(part: Part, split: int = 55, only_missing: bool = True) -> None:
"""
Method to add staff information to a part
Parameters
----------
part: Part
Part to add staff information to.
split: int
MIDI pitch to split staff into upper and lower. Default is 55
only_missing: bool
If True, only add staff to those notes that do not have staff info already.
x"""
# assign staffs using a hard limit
notes = part.notes_tied
for n in notes:
if only_missing and n.staff:
continue
if n.midi_pitch > split:
staff = 1
else:
staff = 2
n.staff = staff
n_tied = n.tie_next
while n_tied:
n_tied.staff = staff
n_tied = n_tied.tie_next
part.add(score.Clef(staff=1, sign="G", line=2, octave_change=0), 0)
part.add(score.Clef(staff=2, sign="F", line=4, octave_change=0), 0)
def validate_match_ids(mf):
"""
Check a matchfile for duplicate snote and note IDs.
This function will:
- remove all deletions with a score ID that occurs in multiple lines.
- remove all insertions with a performance ID that occurs in multiple lines.
Handles cases with conflicting match/insertion(s) and match/deletion(s) tuples
with any number of insertions or deletions and a single match by keeping
only the match.
Unhandled cases:
- multiple conflicting matches: all are kept.
- multiple insertions with the same ID: all are deleted.
- multiple deletions with the same ID: all are deleted.
Parameters
----------
mf: MatchFile
MatchFile to validate
Returns
-------
Updates the representation of the matchfile by removing match lines.
"""
# Check if the matchfile is valid (i.e. check for snote duplicates)
sids = np.array([n.Anchor for n in mf.snotes])
# First check if score ids are unique
sids_unique, counts = np.unique(sids, return_counts=True)
sids_to_check = sids_unique[np.where(counts > 1)[0]]
if len(sids_to_check) > 0:
indices_to_del = []
for i, line in enumerate(mf.lines):
if isinstance(line, BaseDeletionLine):
if line.Anchor in sids_to_check:
indices_to_del.append(i)
warnings.warn(
"Matchfile contains duplicate score notes. "
"Removing {} deletions.".format(len(indices_to_del))
)
mf.lines = np.delete(mf.lines, indices_to_del)
# Check if the matchfile is valid (i.e. check for performance note duplicates)
pids = np.array([n.Id for n in mf.notes])
pids_unique, counts = np.unique(pids, return_counts=True)
pids_to_check = pids_unique[np.where(counts > 1)[0]]
if len(pids_to_check) > 0:
indices_to_del = []
for i, line in enumerate(mf.lines):
if isinstance(line, BaseInsertionLine):
if line.Id in pids_to_check:
indices_to_del.append(i)
warnings.warn(
"Matchfile contains duplicate performance notes. "
"Removing {} insertions.".format(len(indices_to_del))
)
mf.lines = np.delete(mf.lines, indices_to_del)
if __name__ == "__main__":
pass