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: int) -> str:
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])