User:Contribution/Ups & downs notation in Python

From Xenharmonic Wiki
Jump to navigation Jump to search
def upsdowns(degree: int, steps_per_octave: int, steps_per_fifth: int, steps_per_degree: int = 1):
    sharpness = 7 * steps_per_fifth - 4 * steps_per_octave
    POS_ALTS, NEG_ALTS = ({(i if i % 2 else -i) for i in range(1, 6)}, {(-i if i % 2 else i) for i in range(1, 6)})

    naturals = []
    for a in range(-5, 6):
        pos = (a * steps_per_fifth) % steps_per_octave
        oadj = -1 if (2 * pos < steps_per_octave and a in POS_ALTS) else (1 if (2 * pos > steps_per_octave and a in NEG_ALTS) else 0)
        naturals.append([pos, [0, a, oadj]])
    naturals.sort(key=lambda x: (x[0], -x[1][1], x[1][2]))
    first_pos, (zero, a0, o0) = naturals[0][0], naturals[0][1]
    naturals.append([first_pos + steps_per_octave, [zero, a0, o0 + 1]])

    ups_and_downs = [[i, []] for i in range(steps_per_octave)]
    for (lpos, ltri), (rpos, rtri) in zip(naturals, naturals[1:]):
        ups_and_downs[lpos][1].append([ltri[0], ltri[1], ltri[2]])
        for b in range(lpos + 1, rpos):
            L, R = b - lpos, rpos - b
            ups_and_downs[b][1] = [[L, ltri[1], ltri[2]]] if L < R else ([[L, ltri[1], ltri[2]], [-R, rtri[1], rtri[2]]] if L == R else [[-R, rtri[1], rtri[2]]])

    ups_step = degree * steps_per_degree
    octave_shift, base_step = divmod(ups_step, steps_per_octave)
    elems = [[e[0], e[1], e[2] + octave_shift] for e in ups_and_downs[base_step][1]]
    is_half_float = lambda x: isinstance(x, float) and (x % 1 == 0.5)

    if sharpness:
        sgn = 1 if sharpness > 0 else -1
        abs_sh, s2, new = abs(sharpness), abs(sharpness) / 2, []
        for off, fi, octv in elems:
            while off > s2: off, fi = off - abs_sh, fi + 7 * sgn
            while off < -s2: off, fi = off + abs_sh, fi - 7 * sgn
            nt_off, nt_fi = off, fi
            while nt_off > 0 and abs(nt_off) >= s2: nt_off, nt_fi = nt_off - s2, nt_fi + 3.5 * sgn
            while nt_off < 0 and abs(nt_off) >= s2: nt_off, nt_fi = nt_off + s2, nt_fi - 3.5 * sgn
            if nt_off == 0 and is_half_float(nt_fi) and abs(nt_fi) <= 3: off = "neutral"
            new.append([off, fi, octv])
        elems = new

    def arrow_str(a):
        if a == "neutral": return "~"
        sym, n = ("v" if a < 0 else "^"), abs(int(a))
        return sym * n if n <= 3 else f"{sym}<sup>{n}</sup>"

    def label_str(fi, a):
        if a == "neutral": letter = ""
        elif abs(fi) <= 1: letter = "" if a != 0 else "P"
        elif abs(fi) <= 5: letter = "m" if fi < 0 else "M"
        else: letter = ("d" if fi < 0 else "A") * ((abs(fi) + 1) // 7)
        return f"{letter}{int((fi * 4) % 7 + 1)}"

    def octave_str(o):
        return "" if o == 0 else f" {'+' if o > 0 else '-'}{abs(o)}"

    def dedupe(seq):
        seen, out = set(), []
        for x in seq:
            if x not in seen: seen.add(x); out.append(x)
        return out

    return dedupe([arrow_str(off) + label_str(fi, off) + octave_str(octv) for off, fi, octv in elems])