Thanks Cy! After scratching my head, I figured out that the score missed some rests and had note overlaps, which can cause troubles with score notation. I came up with these functions, the first to fill gaps with rests, the other to trim not durations which overlapped the next note. Thanks ChatGPT!
def fill_gaps_with_rests(s: stream.Stream, parent_offset: float = 0.0) -> stream.Stream:
"""
Recursively analyze a music21 stream (including Score, Part, Measure, Stream, etc.)
and insert rests to fill gaps between notes, ensuring that rests are inserted
at the correct offsets, considering the structure of the stream.
Args:
s (stream.Stream): The music21 stream to be processed.
parent_offset (float): The offset to consider from the parent stream, used in recursion.
Returns:
stream.Stream: The modified stream with gaps filled with rests.
"""
elements = list(s.elements)
last_offset = 0.0 # Keep track of the offset after the last note or rest
for element in elements:
if isinstance(element, (stream.Score, stream.Part, stream.Measure, stream.Stream)):
# If the element is a container, recursively process it
fill_gaps_with_rests(element, parent_offset + element.offset)
else:
current_offset = parent_offset + element.offset
if 'Note' in element.classes or 'Rest' in element.classes:
if current_offset > last_offset:
# There is a gap that needs to be filled with a rest
gap_duration = current_offset - last_offset
rest_to_insert = note.Rest(quarterLength=gap_duration)
s.insert(last_offset - parent_offset, rest_to_insert) # Adjust offset for insertion
element_end_offset = current_offset + element.duration.quarterLength
last_offset = max(last_offset, element_end_offset) # Update the last offset
# Handle the case where there's a gap at the end of the stream
if isinstance(s, stream.Measure) and last_offset < s.barDuration.quarterLength:
# If the stream is a Measure, ensure it's filled to its bar duration
gap_duration = s.barDuration.quarterLength - last_offset
rest_to_insert = note.Rest(quarterLength=gap_duration)
s.insert(last_offset - parent_offset, rest_to_insert)
return s
def adjust_note_durations_to_prevent_overlaps(s: stream.Stream) -> stream.Stream:
"""
Recursively adjust the durations of notes in a music21 stream (including scores, parts, measures, and any nested streams)
to prevent overlaps, while keeping their offsets intact.
Args:
s (stream.Stream): The music21 stream containing the notes or other streams to be adjusted.
Returns:
stream.Stream: The modified stream with adjusted note durations.
"""
if s.isStream:
s.sort() # Ensure the stream is sorted by offset
elements = list(s) # Work with a list of elements in the stream
notes = [e for e in elements if isinstance(e, note.Note)] # Get only Note objects
for i in range(len(notes) - 1): # Loop through all notes except the last one
current_note = notes[i]
next_note = notes[i + 1]
# Calculate the current end of the note
current_note_end = current_note.offset + current_note.duration.quarterLength
# If the current note ends after the next note starts, adjust its duration
if current_note_end > next_note.offset:
# Adjust duration to avoid overlap
new_duration = next_note.offset - current_note.offset
current_note.duration.quarterLength = new_duration
# Recursively adjust durations in nested streams (like Parts, Measures, Voices)
for el in elements:
if el.isStream:
adjust_note_durations_to_prevent_overlaps(el)
return s