diff --git a/src/transcriber.py b/src/transcriber.py index a70b411..8071c44 100644 --- a/src/transcriber.py +++ b/src/transcriber.py @@ -155,7 +155,7 @@ def format_cents_deviation(freq): def format_dim_diff(dim_diff, ref): """Format dimensional difference markup.""" - if dim_diff is None or ref is None or ref < 0: + if dim_diff is None or ref is None or ref < 0 or dim_diff == 0: return "" diff_str = str(abs(dim_diff)) @@ -164,7 +164,8 @@ def format_dim_diff(dim_diff, ref): elif dim_diff < 0: diff_str += "↓" - ref_name = ["III", "II", "I"][ref] if 0 <= ref <= 2 else "" + ref_names = ["IV", "III", "II", "I"] + ref_name = ref_names[ref] if 0 <= ref <= 3 else "" return f'_\\markup {{ \\lower #3 \\pad-markup #0.2 \\concat{{ "{ref_name}"\\normal-size-super " {diff_str}" }} }}' @@ -332,6 +333,60 @@ def generate_parts(music_data, name, output_dir="lilypond"): print(f"Generated: {part_file}") +def _is_adjacent(hs1: tuple, hs2: tuple) -> bool: + """Check if two hs_arrays are adjacent (differ by ±1 in exactly one dimension, excluding dim 0).""" + diff_count = 0 + for i in range(1, len(hs1)): + diff = abs(hs1[i] - hs2[i]) + if diff > 1: + return False + if diff == 1: + diff_count += 1 + return diff_count == 1 + + +def _compute_dim_diff(current: tuple, prev: tuple) -> int: + """Compute dim_diff between two hs_arrays. Returns prime * direction.""" + primes = [3, 5, 7] + for i in range(1, 4): + diff = current[i] - prev[i] + if diff == 1: + return primes[i - 1] + if diff == -1: + return -primes[i - 1] + return 0 + + +def _find_ref_and_dim_diff( + current_hs: tuple, prev_chord: list, staying_voices: list +) -> tuple[int, int]: + """Find ref (staying voice index) and dim_diff for a changed pitch. + + Args: + current_hs: hs_array of current pitch + prev_chord: list of hs_arrays from previous chord + staying_voices: indices of voices that stay + + Returns: + (ref, dim_diff) tuple + """ + if not staying_voices: + return -1, 0 + + adjacent = [] + for idx in staying_voices: + prev_hs = prev_chord[idx] + if _is_adjacent(current_hs, prev_hs): + dim_diff = _compute_dim_diff(current_hs, prev_hs) + adjacent.append((idx, dim_diff)) + + if not adjacent: + return -1, 0 + + adjacent.sort(key=lambda x: abs(x[1])) + return adjacent[0] + + def output_chords_to_music_data(chords, fundamental=55, chord_duration=4): """Convert output_chords.json format to generic music data. @@ -350,20 +405,41 @@ def output_chords_to_music_data(chords, fundamental=55, chord_duration=4): music_data = [[] for _ in range(num_voices)] + prev_chord = None for chord in chords: + current_hs = [tuple(p["hs_array"]) for p in chord] + + if prev_chord is None: + staying_voices = [] + else: + staying_voices = [ + i for i in range(num_voices) if current_hs[i] == prev_chord[i] + ] + for voice_idx, pitch in enumerate(chord): if voice_idx >= num_voices: break frac = Fraction(pitch["fraction"]) freq = fundamental * float(frac) + current_hs_array = current_hs[voice_idx] - ref = None - dim_diff = 0 + if prev_chord is None: + ref = -1 + dim_diff = 0 + elif current_hs_array == prev_chord[voice_idx]: + ref = -1 + dim_diff = 0 + else: + ref, dim_diff = _find_ref_and_dim_diff( + current_hs_array, prev_chord, staying_voices + ) event = [freq, chord_duration, ref, dim_diff] music_data[voice_idx].append(event) + prev_chord = current_hs + return music_data