cuthbertLab / music21

music21 is a Toolkit for Computational Musicology
https://www.music21.org/
Other
2.11k stars 400 forks source link

TimeSignature reduction method(s) #974

Open Luke-Poeppel opened 3 years ago

Luke-Poeppel commented 3 years ago

Motivation

I'm working on a project that requires reduction of meter.TimeSignature objects to the lowest possible denominator (by removing all powers of two). The music21.meter module provides a number of mathematical tools for reduction –– this feature involves applying it directly to meter.TimeSignature objects (which hasn't been done yet, as far as I can tell).

Feature summary I see two possible new methods for meter.TimeSignature. Firstly, one that works similar to key.alternateInterpretations that returns a list of all possible reductions. For instance,

>>> from music21.meter import TimeSignature
>>> t = TimeSignature("6/4")
>>> t.alternateInterpretations(include_self=True)
[<music21.meter.TimeSignature 6/4>, <music21.meter.TimeSignature 3/2]

A second possible feature is a method for reducing a TimeSignature object to a given denominator (or maximally by default):

>>> t = TimeSignature("14/16")
>>> reduced = t.reduce(minDenominator=8)
>>> reduced
<music21.meter.TimeSignature 7/8>
>>> TimeSignature("24/32").reduce() # maximal by default, relying on `validDenominators` in music21.meter.tools
<music21.meter.TimeSignature 3/4>

If it seems limiting to only allow reduction (and not increases of powers of 2, e.g. 2/4 -> 4/8), another option is to instead provide, for instance, a newDenominator that instead increases the values by powers of 2. The method might then be called reframe or something similar.

Proposed implementation The implementation is very simple. Use meter.tools.divisionOptionsFractionsDownward to generate the appropriate the tuples for ratioStrings. Use all tuples for the first implementation; use the matching tuple for the second. If it seems useful not to limit to reduction you can simply join all downward tuples with tools.divisionOptionsFractionsUpward.

Intent

jacobtylerwalls commented 3 years ago

Hi, Luke, yes, this sounds useful and low-maintenance to me.

Upward sounds just as useful as down, e.g. to get from 6/8 back "up" to 3/4. Maybe all of the cases you describe are simply one method?

    def rebar(self, shorterDenom=True, limitDenom=8, all=False):

(shorter_denom=True and all=True would require a limit_denom on this idea.)

Not certain of this, though. The list/all functionality could be broken out into a second method. What do you think?

UPDATE: maybe in a future enhancement Stream could eventually have a rebar() also that flattened the stream, rebarred time signatures using this method and made new measures? 🤔

mscuthbert commented 3 years ago

Ooh... It does sound like it could be useful, but I don't know that rebar() is the right name, because that seems to imply that measures will be changed (which would be a GREAT function).

My concern is that while 4/4 to 2/2 or vice-versa sounds good, and there's no problem with 10/2 going to 5/1 for people who want that, but 6/8 to 3/4 isn't just a change in fraction -- it's instead a change in the underlying metrical structure completely. (fast 6/8 is generally a type of 2/[dotted-quarter]).

I think that a method call that returns a new TS (or changes the current one) is the best approach. I think that the alternativeInterpretations would be used by something like, "Stream.analyze('meter')" -- which would be REALLY useful. :-) but not something to use with this.

Note that for doing the simplest version of this, you could do:

from fractions import Fraction
ts = meter.TimeSignature('6/4')
frac = Fraction(ts.numerator, ts.denominator)
new_ts = meter.TimeSignature(f'{frac.numerator}/{frac.denominator}')
new_ts  # returns <music21.meter.TimeSignature 3/2>

Is this better as a method in TS or as a demonstration in the docs?

Luke-Poeppel commented 3 years ago

Thanks for the interesting responses @jacobtylerwalls and @mscuthbert! The comment on changing the metrical structure was a concern that I also had. I'm not quite sure how this interacts with beaming, for example. I ended up using a simple solution (not integrated into m21):

valid_denominators = [1, 2, 4, 8, 16, 32, 64, 128]  # in order 
def reframe_ts(ts, new_denominator=None):
    """
    Function for 'reframing' a ``music21.meter.TimeSignature`` object to a given denominator (or maximally). 

        :param ts: A music21 TimeSignature object. 
    :param int new_denominator: The desired new denominator used in the TimeSignature. If not provided, reduces maximally. 
    :return: A new time signature object with the desired denominator. 
    :rtype: music21.meter.TimeSignature

    >>> from music21.meter import TimeSignature
    >>> reframe_ts(TimeSignature("4/16"), new_denominator=8)
    <music21.meter.TimeSignature 2/8>
    >>> reframe_ts(TimeSignature("4/4"), new_denominator=None)
    <music21.meter.TimeSignature 1/1>
    >>> reframe_ts(TimeSignature("7/16"))
    <music21.meter.TimeSignature 7/16>
    """
    numerator = ts.numerator
    denominator = ts.denominator
    if new_denominator is None:
        new_denominator = valid_denominators[0]
    else:
        assert new_denominator in set(valid_denominators) # Could raise MeterException here. 

    while numerator % 2 == 0 and denominator > new_denominator:
        numerator = numerator / 2
        denominator = denominator / 2

    reduced_ts_str = f"{int(numerator)}/{int(denominator)}"
    return TimeSignature(reduced_ts_str)

I'd personally prefer something a bit more explicit than the Fractions option, only because it provides the flexibility to choose a new denominator. I'd be happy to submit a PR with a method similar to the above, but if you feel that this would be better as a note in the docs, no problem by me! 😄