Module music_df.humdrum_export.dur_to_kern

Functions

def attempt_to_represent_as_tuple(inp: float)
Expand source code
def attempt_to_represent_as_tuple(inp: float):
    """
    >>> attempt_to_represent_as_tuple(0.14406779661015978)
    """
    # TODO: (Malcolm 2024-02-29)
    pass
>>> attempt_to_represent_as_tuple(0.14406779661015978)
def dur_to_kern(inp: float | int | fractions.Fraction,
offset: float | int | fractions.Fraction,
meter: str | metricker.meter.Meter,
raise_exception_on_unrecognized_duration: bool = False) ‑> list[tuple[float, str]]
Expand source code
def dur_to_kern(
    inp: float | int | Fraction,
    offset: float | int | Fraction,
    meter: t.Union[str, Meter],
    raise_exception_on_unrecognized_duration: bool = False,
) -> list[tuple[float, str]]:
    """

    >>> dur_to_kern(10.25, 0.0, "4/4")
    [(4.0, '1'), (4.0, '1'), (2.0, '2'), (0.25, '16')]
    >>> dur_to_kern(10.0, 0.25, "4/4")
    [(0.25, '16'), (0.5, '8'), (1.0, '4'), (2.0, '2'), (4.0, '1'), (2.0, '2'), (0.25, '16')]
    >>> dur_to_kern(5.5, 2.0, "3/4")
    [(1.0, '4'), (3.0, '2.'), (1.5, '4.')]
    >>> dur_to_kern(9.25, 0.5, "9/8")
    [(0.5, '8'), (0.5, '8'), (1.5, '4.'), (1.5, '4.'), (4.5, '8%9'), (0.75, '8.')]
    >>> dur_to_kern(0.3333, 0.0, "4/4")
    [(0.3333, '12')]
    >>> dur_to_kern(0.1666, 0.0, "4/4")
    [(0.1666, '24')]
    >>> dur_to_kern(0.1667, 0.3333, "2/4")
    [(0.16670000000000001, '24')]
    >>> dur_to_kern(0.1667, 1.6667, "2/4")
    [(0.16670000000000007, '24')]
    >>> dur_to_kern(0.1667, 1.75, "2/4")
    [(0.16670000000000007, '24')]
    >>> dur_to_kern(0.1670000000000016, 1.5, "2/4")
    [(0.1670000000000016, '24')]

    Following used to give a recursion error, fixed by `split_fives_hack()`
    >>> dur_to_kern(5.0, 6.0, "6/4")
    [(3.0, '2.'), (2.0, '2')]
    """
    if isinstance(meter, str):
        meter = Meter(meter)
    split_durs = meter.split_at_metric_strong_points(
        [_Dur(offset, offset + inp)], min_split_dur=0.5  # type:ignore
    )
    # (Malcolm 2023-10-10) I'm not precisely sure what the rationale for applying
    #   split_odd_duration is
    split_durs = split_durs[:-1] + meter.split_odd_duration(
        split_durs[-1], min_split_dur=1.0  # type:ignore
    )
    # Hack
    split_durs = split_durs[:-1] + split_fives_hack(split_durs[-1])

    durs = [float(dur.release - dur.onset) for dur in split_durs]
    # It's possible these will be different
    output_durs = []
    output_kern_durs = []
    for d in durs:
        kern_dur = duration_float_to_recip(d)
        if kern_dur.startswith("q"):
            remainder = d
            temp_offset = offset
            for divisor in [1, 2, 4, 8, 16, 24, 32, 48, 64, 96]:
                whole, remainder = divmod(remainder, 1 / divisor)
                whole /= divisor
                if whole <= 0.0:
                    continue
                result1 = dur_to_kern(
                    whole,
                    temp_offset,
                    meter,
                    raise_exception_on_unrecognized_duration,
                )
                output_durs.extend([r[0] for r in result1])
                output_kern_durs.extend([r[1] for r in result1])
                if remainder < 1e-6:
                    break
                temp_offset += whole
            if remainder > 1e-6 and raise_exception_on_unrecognized_duration:
                raise KernDurError(
                    f"Unrecognized duration {inp} with {offset=} in {meter=}"
                )
            # if d > 1:
            #     whole, frac = divmod(d, 1.0)
            #     if whole <= 0:
            #         continue
            #     result1 = dur_to_kern(
            #         whole,
            #         offset,
            #         meter,
            #         raise_exception_on_unrecognized_duration,
            #     )
            #     result2 = dur_to_kern(
            #         frac,
            #         offset + whole,
            #         meter,
            #         raise_exception_on_unrecognized_duration,
            #     )
            #     output_durs.extend([r[0] for r in result1])
            #     output_durs.extend([r[0] for r in result2])
            #     output_kern_durs.extend([r[1] for r in result1])
            #     output_kern_durs.extend([r[1] for r in result2])

            # TODO: (Malcolm 2024-02-29) restore
            # elif raise_exception_on_unrecognized_duration:
            #     raise KernDurError(
            #         f"Unrecognized duration {inp} with {offset=} in {meter=}"
            #     )
            # else:
            #     # TODO: (Malcolm 2024-02-28) remove?
            #     output_durs.append(d)
            #     output_kern_durs.append(kern_dur)
        else:
            output_durs.append(d)
            output_kern_durs.append(kern_dur)

    # kern_durs = [duration_float_to_recip(d) for d in durs]

    # if raise_exception_on_unrecognized_duration and any(
    #     d.startswith("q") for d in kern_durs
    # ):
    #     raise KernDurError(f"Unrecognized duration {inp} with {offset=} in {meter=}")
    return list(zip(output_durs, output_kern_durs))
>>> dur_to_kern(10.25, 0.0, "4/4")
[(4.0, '1'), (4.0, '1'), (2.0, '2'), (0.25, '16')]
>>> dur_to_kern(10.0, 0.25, "4/4")
[(0.25, '16'), (0.5, '8'), (1.0, '4'), (2.0, '2'), (4.0, '1'), (2.0, '2'), (0.25, '16')]
>>> dur_to_kern(5.5, 2.0, "3/4")
[(1.0, '4'), (3.0, '2.'), (1.5, '4.')]
>>> dur_to_kern(9.25, 0.5, "9/8")
[(0.5, '8'), (0.5, '8'), (1.5, '4.'), (1.5, '4.'), (4.5, '8%9'), (0.75, '8.')]
>>> dur_to_kern(0.3333, 0.0, "4/4")
[(0.3333, '12')]
>>> dur_to_kern(0.1666, 0.0, "4/4")
[(0.1666, '24')]
>>> dur_to_kern(0.1667, 0.3333, "2/4")
[(0.16670000000000001, '24')]
>>> dur_to_kern(0.1667, 1.6667, "2/4")
[(0.16670000000000007, '24')]
>>> dur_to_kern(0.1667, 1.75, "2/4")
[(0.16670000000000007, '24')]
>>> dur_to_kern(0.1670000000000016, 1.5, "2/4")
[(0.1670000000000016, '24')]

Following used to give a recursion error, fixed by split_fives_hack()

>>> dur_to_kern(5.0, 6.0, "6/4")
[(3.0, '2.'), (2.0, '2')]
def duration_float_to_recip(input: float, threshold=0.01) ‑> str
Expand source code
def duration_float_to_recip(input: float, threshold=0.01) -> str:
    """
    A Python implementation of humlib's Convert::durationFloatToRecip function.

    Differences: omits timebase parameter (for now, at least).

    >>> duration_float_to_recip(4.0)
    '1'
    >>> duration_float_to_recip(6.0)
    '1.'
    >>> duration_float_to_recip(1 / 3)
    '12'
    >>> duration_float_to_recip(3 / 4)
    '8.'
    >>> duration_float_to_recip(9 / 2)
    '8%9'
    >>> duration_float_to_recip(0.3333)
    '12'
    >>> duration_float_to_recip(0.333)
    '12'
    >>> duration_float_to_recip(0.334)
    '12'

    If threshold is set too high the next tests fail.
    >>> duration_float_to_recip(0.1667)
    '24'
    >>> duration_float_to_recip(0.1666)
    '24'
    >>> duration_float_to_recip(0.1670)
    '24'
    >>> duration_float_to_recip(0.0833)
    '48'
    >>> duration_float_to_recip(0.04165)
    '96'
    """
    if input == 0.0625:
        output = "64"
        return output
    if input == 0.125:
        output = "32"
        return output
    if input == 0.25:
        output = "16"
        return output
    if input == 0.5:
        output = "8"
        return output
    if input == 1.0:
        output = "4"
        return output
    if input == 2.0:
        output = "2"
        return output
    if input == 4.0:
        output = "1"
        return output
    if input == 8.0:
        output = "0"
        return output
    if input == 12.0:
        output = "0."
        return output
    if input == 16.0:
        output = "00"
        return output
    if input == 24.0:
        output = "00."
        return output
    if input == 32.0:
        output = "000"
        return output
    if input == 48.0:
        output = "000."
        return output

    # special case for triplet whole notes:
    if abs(input - (4.0 * 2.0 / 3.0)) < 0.0001:
        return "3%2"

    # special case for triplet breve notes:
    if abs(input - (4.0 * 4.0 / 3.0)) < 0.0001:
        return "3%4"

    # special case for 9/8 full rests
    if abs(input - (4.0 * 9.0 / 8.0)) < 0.0001:
        return "8%9"

    # special case for 9/2 full-measure rest
    if abs(input - 18.0) < 0.0001:
        return "2%9"

    # handle special rounding cases primarily for SCORE which
    # only stores 4 digits for a duration
    if math.isclose(input, 1 / 3, abs_tol=0.01):
        return "12"
    if math.isclose(input, 1 / 6, abs_tol=0.01):
        return "24"
    if math.isclose(input, 1 / 12, abs_tol=0.01):
        # triplet 32nd note, which has a real duration of 0.0833333 etc.
        return "48"
    if math.isclose(input, 1 / 24, abs_tol=0.01):
        # triplet 64th note, which has a real duration of 0.0833333 etc.
        return "96"

    basic = 4.0 / input
    diff = basic - int(basic)
    if diff > (1 - threshold):
        diff = 1.0 - diff
        basic += diff

    output = []
    if diff < threshold:
        output.append(str(int(basic)))
    else:
        testinput = input / 3.0 * 2.0
        basic = 4.0 / testinput
        diff = basic - int(basic)
        if diff < threshold:
            output.append(str(int(basic)))
            output.append(".")
        else:
            testinput = input / 7.0 * 4.0
            basic = 4.0 / testinput
            diff = basic - int(basic)
            if diff < threshold:
                output.append(str(int(basic)))
                output.append("..")
            else:
                testinput = input / 15.0 * 4.0
                basic = 2.0 / testinput
                diff = basic - int(basic)
                if diff < threshold:
                    output.append(str(int(basic)))
                    output.append("...")
                else:
                    # Don't know what it could be so echo as a grace note.
                    output.append("q")
                    output.append(str(input))

    return "".join(output)

A Python implementation of humlib's Convert::durationFloatToRecip function.

Differences: omits timebase parameter (for now, at least).

>>> duration_float_to_recip(4.0)
'1'
>>> duration_float_to_recip(6.0)
'1.'
>>> duration_float_to_recip(1 / 3)
'12'
>>> duration_float_to_recip(3 / 4)
'8.'
>>> duration_float_to_recip(9 / 2)
'8%9'
>>> duration_float_to_recip(0.3333)
'12'
>>> duration_float_to_recip(0.333)
'12'
>>> duration_float_to_recip(0.334)
'12'

If threshold is set too high the next tests fail.

>>> duration_float_to_recip(0.1667)
'24'
>>> duration_float_to_recip(0.1666)
'24'
>>> duration_float_to_recip(0.1670)
'24'
>>> duration_float_to_recip(0.0833)
'48'
>>> duration_float_to_recip(0.04165)
'96'
def split_fives_hack(dur: music_df.humdrum_export.dur_to_kern._Dur) ‑> list[music_df.humdrum_export.dur_to_kern._Dur]
Expand source code
def split_fives_hack(dur: _Dur) -> list[_Dur]:
    """
    There is an issue where durations of 5, (or of 5/16, etc.), which cannot be
    represented in notation without a tie, make it through when they start on a
    downbeat in certain meters,
    e.g.:

    >>> six_four = Meter("6/4")
    >>> six_four.split_at_metric_strong_points([_Dur(0.0, 5.0)])
    [_Dur(onset=0.0, release=5.0)]

    To avoid this circumstance, we just hackily divide them into 3 + 2.
    >>> split_fives_hack(_Dur(0.0, 5.0))
    [_Dur(onset=0.0, release=3.0), _Dur(onset=3.0, release=5.0)]
    >>> split_fives_hack(_Dur(0.0, 10.0))
    [_Dur(onset=0.0, release=6.0), _Dur(onset=6.0, release=10.0)]
    >>> split_fives_hack(_Dur(0.0, 1.25))
    [_Dur(onset=0.0, release=0.75), _Dur(onset=0.75, release=1.25)]
    """
    # TODO: (Malcolm 2023-12-18) better solution than this hack
    for divisor in [1, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 96]:
        if math.isclose(math.log(divisor * dur.dur / 5, 2) % 1, 0, abs_tol=1e-9):
            dur1 = _Dur(dur.onset, dur.onset + 3 / 5 * dur.dur)
            dur2 = _Dur(dur1.release, dur.release)
            return [dur1, dur2]
    return [dur]

There is an issue where durations of 5, (or of 5/16, etc.), which cannot be represented in notation without a tie, make it through when they start on a downbeat in certain meters, e.g.:

>>> six_four = Meter("6/4")
>>> six_four.split_at_metric_strong_points([_Dur(0.0, 5.0)])
[_Dur(onset=0.0, release=5.0)]

To avoid this circumstance, we just hackily divide them into 3 + 2.

>>> split_fives_hack(_Dur(0.0, 5.0))
[_Dur(onset=0.0, release=3.0), _Dur(onset=3.0, release=5.0)]
>>> split_fives_hack(_Dur(0.0, 10.0))
[_Dur(onset=0.0, release=6.0), _Dur(onset=6.0, release=10.0)]
>>> split_fives_hack(_Dur(0.0, 1.25))
[_Dur(onset=0.0, release=0.75), _Dur(onset=0.75, release=1.25)]

Classes

class KernDurError (*args, **kwargs)
Expand source code
class KernDurError(Exception):
    pass

Common base class for all non-exit exceptions.

Ancestors

  • builtins.Exception
  • builtins.BaseException