ms3 icon indicating copy to clipboard operation
ms3 copied to clipboard

Excerpts allowing for mc_onset positions [FEATURE]

Open johentsch opened this issue 10 months ago • 0 comments

Is your feature request related to a problem? Please describe. Currently (v2.1.1) excerpts can only be created to include entire MC units. It would be great to be able to

  • cut off the beginning of the first MC before a given mc_onset and/or
  • cut off the ending of the last MC starting from a given mc_onset.

Describe the solution you'd like

Additional optional arguments start_mc_onset and end_mc_onset for the bs4_parser.Excerpt class which can be any number, most typically a Fraction.

For the start_mc_onset, it is not trivial to remove XML elements while maintaining correct positions of the remaining ones (can be done but much more convoluted because positions are relative and implicit). So the easiest solution might be to replace all note events before the given onset with rests. It remains to be seen how we would deal with the remaining score elements (harmony labels, dynamic marks, staff text, etc.). One solution might lie in making them invisible.

In terms of the end_mc_onset, the ideal solution would be if we could emulate the functionality in MuseScore that lets you select a measure and set the "Actual duration" in the Measure Properties to the desired value, which correctly truncates it (although it removes existing notes overlapping the new end of the bar rather than shortening them). In principle, it should be unproblematic to remove events after a given point because it does not affect any relevant positions. But for consistency we might as well use the same mechanism as for the start_mc_onset (replace notes with rests), which also would be adventageous for the following consideration.

In both cases we should decide how to deal with overlapping note events, which may, or may not, lead to more "organic" excerpts, e.g. by

  • having an excerpt begin with the one note that starts earlier but overlaps "into it" or
  • letting notes that overlap the end_mc_onset "ring" rather than cutting them or, which would be worse, removing/replacing them.

This should probably become a boolean argument keep_overlapping to let the user decide.

Mechanics, avenues, pointers

  • The new arguments go into Excerpt and into the _MSCX_bs4.make_excerpt() method that creates and returns it.

  • The manipulation of the excerpt will be triggered, depending on the given arguments, by calling

    • Excerpt.replace_notes_before_onset()
    • Excerpt.replace_notes_after_onset()
  • both of which might be recurring to the same underlying method under the hood.

  • An example of how to access the relevant tags in the XML tree can be seen here in the method _MSCX_bs4.color_notes(), with the difference that we can address the MC in question right away instead of iterating over all MCs. self.tags is a nested dictionary/JSON thingy with the following structure:

    {MC -> 
       {staff -> 
          {voice -> 
            {mc_onset -> 
               [{"name" -> str,
                 "duration" -> Fraction,
                 "tag" -> bs4.Tag
                 },
                 ...
               ]
            } 
          } 
       } 
     }
    
  • This means we can

    • get the dict for the given start or end MC
    • iterate over all staves
    • iterate over all voice
    • iterate over all mc_onsets before or starting from the given values (with continue for the other values).
    • iterate over the 3-item info dicts for all tags at a given onset
    • act on the tags according to their names
      • bs4.Tag.decompose() deletes a tag
      • self.new_tag() (a method inherited from _MSCX_bs4) conveniently inserts a new tag by specifying a parent or sibling for reference, BUT
  • maybe the easiest approach for replacing the tag is by simply modifying the existing one (otherwise one would have to store one of its siblings first to pass it to .new_tag()). Let's consider the task: In MuseScore, every note is contained in a <Chord> and all notes in the same chord have the same onset and duration:

    <Chord>
      <durationType>value</durationType>
      <Note>...</Note>
      ...
    </Chord>
    

    The goal is to replace this whole structure with

    <Rest>
      <durationType>value</durationType>
    </Rest>
    
  • To achieve this, we can simply change transform the <Chord> tag via bs4.Tag.name = "Rest" and then either

    • iterate over all children (bs4.Tag.find_all()) removing all except the durationType, or
    • store the durationType value, remove all children (bs4.Tag.clear()) and create a new durationType tag with the stored value (using self.new_tag("durationType", value, append_within=rest_tag))

Future

Suggestion to keep the high-level interface in score.py (to be added later) simpler by restricting the mechanism to mc_onset values, too, as opposed to alternatively allowing for mn_onset.

johentsch avatar Aug 31 '23 10:08 johentsch