Spaces:
Running on Zero
Running on Zero
| # post_process.py | |
| ## Vibe-coded with Z AI | |
| ### Project Los Angeles | |
| ### Tegridy Code 2026 | |
| ### License CC BY-NC-SA 4.0 | |
| import math | |
| # --------------------------------------------------------------------------- # | |
| # Helpers | |
| # --------------------------------------------------------------------------- # | |
| def _ioi_mode(onsets, res_ms=5.0): | |
| """Most common inter-onset interval (binned), ignoring <40 ms gaps.""" | |
| us = sorted(set(onsets)) | |
| iois = [us[i + 1] - us[i] for i in range(len(us) - 1) if us[i + 1] - us[i] > 1e-6] | |
| if not iois: | |
| return None | |
| bins = {} | |
| for d in iois: | |
| b = int(round(d / res_ms)) * res_ms | |
| if b >= 40.0: | |
| bins[b] = bins.get(b, 0) + 1 | |
| if not bins: | |
| return None | |
| return max(bins, key=bins.get) | |
| def _detect_beat_period_ms(onsets, res_ms=5.0, lo_bpm=50.0, hi_bpm=210.0): | |
| """ | |
| Estimate the quarter-note period (ms) from absolute note-onset times | |
| using the autocorrelation of an onset impulse train. Peak is searched | |
| inside the 70-180 BPM preferred range and parabolically refined. | |
| """ | |
| if len(onsets) < 2: | |
| return 500.0 | |
| t_max = max(onsets) | |
| if t_max <= 0: | |
| return 500.0 | |
| n = int(t_max / res_ms) + 2 | |
| env = [0.0] * n | |
| for t in onsets: | |
| idx = int(round(t / res_ms)) | |
| if 0 <= idx < n: | |
| env[idx] += 1.0 | |
| lag_min = max(1, int(round((60000.0 / hi_bpm) / res_ms))) | |
| lag_max = max(lag_min + 1, int(round((60000.0 / lo_bpm) / res_ms))) | |
| acf = [0.0] * (lag_max + 2) | |
| for lag in range(lag_min, lag_max + 1): | |
| s = 0.0 | |
| for i in range(n - lag): | |
| s += env[i] * env[i + lag] | |
| acf[lag] = s | |
| # preferred tempo window: 70-180 BPM -> 333-858 ms | |
| pref_lo = max(lag_min, int(round((60000.0 / 180.0) / res_ms))) | |
| pref_hi = min(lag_max, int(round((60000.0 / 70.0) / res_ms))) | |
| best_lag, best_val = None, -1.0 | |
| for lag in range(pref_lo, pref_hi + 1): | |
| if acf[lag] > best_val: | |
| best_val, best_lag = acf[lag], lag | |
| if best_lag is None or best_val <= 0: # fallback: global max | |
| for lag in range(lag_min, lag_max + 1): | |
| if acf[lag] > best_val: | |
| best_val, best_lag = acf[lag], lag | |
| if best_lag is None: | |
| return 500.0 | |
| # parabolic interpolation around the peak | |
| y0 = acf[best_lag - 1] if best_lag - 1 >= 0 else acf[best_lag] | |
| y1 = acf[best_lag] | |
| y2 = acf[best_lag + 1] if best_lag + 1 < len(acf) else acf[best_lag] | |
| denom = (y0 - 2 * y1 + y2) | |
| offset = 0.0 | |
| if abs(denom) > 1e-12: | |
| offset = max(-0.5, min(0.5, 0.5 * (y0 - y2) / denom)) | |
| beat = (best_lag + offset) * res_ms | |
| # light octave correction toward the preferred range | |
| while beat < 333.0: | |
| beat *= 2.0 | |
| while beat > 858.0: | |
| beat /= 2.0 | |
| return beat | |
| def _level_of(g, spacings_asc): | |
| """Coarsest grid value v whose spacing divides g (within tol).""" | |
| for v, s in spacings_asc: # coarse (small v) first | |
| r = g / s | |
| if abs(r - round(r)) * s < 1e-3 + 1e-3 * s: | |
| return v | |
| return spacings_asc[-1][0] | |
| def _duration_level(dur, spacings_asc): | |
| """Grid value v whose spacing makes the note last ~1-4 units.""" | |
| best_v, best_err = None, None | |
| for v, s in spacings_asc: | |
| if s <= 0: | |
| continue | |
| units = dur / s | |
| k = max(1, min(4, int(round(units)))) | |
| err = abs(dur - k * s) / max(dur, s) | |
| if best_err is None or err < best_err: | |
| best_err, best_v = err, v | |
| return best_v | |
| def _level_distance(lv, v_star, spacings_asc): | |
| vs = [v for v, _ in spacings_asc] | |
| try: | |
| return abs(vs.index(lv) - vs.index(v_star)) | |
| except ValueError: | |
| return 99 | |
| def _snap_one(t, finest, spacings_asc, back_window, forward_bonus, | |
| duration_hint, dur=None): | |
| """Forward-biased nearest-grid snap for a single (chord-reference) onset.""" | |
| if finest <= 0: | |
| return t | |
| k0 = math.floor(t / finest + 1e-9) | |
| g0 = k0 * finest | |
| g1 = (k0 + 1) * finest | |
| if abs(t - g0) < 1e-6: # already on grid | |
| return g0 | |
| d_bwd = t - g0 # > 0 | |
| d_fwd = g1 - t # > 0 | |
| v_star = None | |
| if duration_hint and dur is not None and dur > 0: | |
| v_star = _duration_level(dur, spacings_asc) | |
| best, best_score = None, None | |
| for g, d, tag in ((g0, d_bwd, 'back'), (g1, d_fwd, 'fwd')): | |
| if tag == 'back' and d > back_window: # backward only when mild | |
| continue | |
| score = -d | |
| if tag == 'fwd': | |
| score += forward_bonus * finest # forward preferred on ties | |
| if v_star is not None: | |
| lv = _level_of(g, spacings_asc) | |
| if lv == v_star: | |
| score += 0.30 * finest | |
| elif _level_distance(lv, v_star, spacings_asc) == 1: | |
| score += 0.10 * finest | |
| if best_score is None or score > best_score + 1e-9: | |
| best_score, best = score, g | |
| if best is None: # fallback (shouldn't happen) | |
| best = g0 if d_bwd <= d_fwd else g1 | |
| return best | |
| # --------------------------------------------------------------------------- # | |
| # Main function | |
| # --------------------------------------------------------------------------- # | |
| def snap_score_to_grid( | |
| score, | |
| grid_values, | |
| beat_ms=None, | |
| tempo_bpm=None, | |
| chord_tol_ms=None, | |
| back_factor=0.40, | |
| max_backward_ms=45.0, | |
| forward_bonus=0.10, | |
| duration_hint=True, | |
| preserve_order=True, | |
| resort=True, | |
| verbose=False, | |
| ): | |
| """ | |
| Snap MIDI.py note onsets (millisecond, single flat track) to a musical grid. | |
| score flat list of MIDI.py events in ms absolute time | |
| (midi2ms_score output, ticks removed). Note events: | |
| ['note', start_ms, dur_ms, channel, note, velocity, extra] | |
| The 7th element (index 6) is a no-op and is passed through. | |
| Non-note events are preserved unchanged. | |
| grid_values note-value denominators, e.g. [1,2,4,8,16,32] | |
| (1=whole, 2=half, 4=quarter, 8=eighth, 16=sixteenth, | |
| 32=thirty-second). Spacing for value v = (4*beat_ms)/v. | |
| beat_ms quarter-note period in ms (auto-detected if None). | |
| tempo_bpm overrides beat_ms (beat_ms = 60000/tempo_bpm). | |
| chord_tol_ms max onset spread to group asynchronous chord notes | |
| (default min(45.0, finest*0.5)). | |
| back_factor backward snaps allowed only within back_factor*finest | |
| of a grid line (default 0.40). | |
| max_backward_ms absolute cap on any backward shift (default 45.0). | |
| forward_bonus fraction of finest added to forward candidate score | |
| so forward wins near-ties (default 0.10). | |
| duration_hint use note durations to bias toward the correct grid level. | |
| preserve_order enforce non-decreasing snapped onsets. | |
| resort re-sort output events by time. | |
| verbose print detected tempo + statistics. | |
| Returns: new score (same structure), onsets snapped, durations + 7th | |
| element preserved. | |
| """ | |
| if score is None: | |
| return [] | |
| # tolerate a leading ticks integer, just in case it wasn't stripped | |
| leading_ticks = None | |
| events = score | |
| if len(events) > 0 and isinstance(events[0], int) and not isinstance(events[0], bool): | |
| leading_ticks = events[0] | |
| events = events[1:] | |
| # collect note events | |
| note_idxs = [i for i, e in enumerate(events) | |
| if isinstance(e, list) and len(e) >= 6 and e[0] == 'note'] | |
| if not note_idxs: | |
| return list(score) | |
| notes = [events[i] for i in note_idxs] | |
| onsets = [float(n[1]) for n in notes] | |
| durs = [float(n[2]) if len(n) > 2 else 0.0 for n in notes] | |
| # ---- 1. beat period -------------------------------------------------- # | |
| if tempo_bpm is not None: | |
| beat = 60000.0 / float(tempo_bpm) | |
| elif beat_ms is not None: | |
| beat = float(beat_ms) | |
| else: | |
| beat = _detect_beat_period_ms(onsets) | |
| # ---- 2. grid --------------------------------------------------------- # | |
| gvs = sorted(set(int(v) for v in grid_values if int(v) > 0)) | |
| whole = 4.0 * beat | |
| spacings_asc = [(v, whole / v) for v in gvs] # coarse (small v) first | |
| spacings_by_v = dict(spacings_asc) | |
| finest = spacings_by_v[gvs[-1]] | |
| if finest <= 0: | |
| finest = beat | |
| if chord_tol_ms is None: | |
| chord_tol_ms = min(45.0, finest * 0.5) | |
| back_window = min(max_backward_ms, finest * back_factor) | |
| # ---- 3. chord grouping (async onsets -> one reference) --------------- # | |
| order = sorted(range(len(notes)), key=lambda k: (onsets[k], k)) | |
| groups, cur = [], [order[0]] | |
| for k in order[1:]: | |
| if onsets[k] - onsets[cur[0]] <= chord_tol_ms: | |
| cur.append(k) | |
| else: | |
| groups.append(cur); cur = [k] | |
| groups.append(cur) | |
| group_ref = [] | |
| for g in groups: | |
| gs = sorted(onsets[k] for k in g) | |
| group_ref.append(gs[len(gs) // 2]) # median, robust | |
| # ---- 4. snap each group reference ------------------------------------ # | |
| snapped = [None] * len(notes) | |
| prev_snapped = -float('inf') | |
| n_back = n_fwd = n_exact = 0 | |
| for gi, g in enumerate(groups): | |
| t = group_ref[gi] | |
| # duration hint = longest note in the chord (defines its rhythmic level) | |
| gdur = max((durs[k] for k in g), default=None) | |
| st = _snap_one(t, finest, spacings_asc, back_window, | |
| forward_bonus, duration_hint, dur=gdur) | |
| if preserve_order and st < prev_snapped: | |
| st = prev_snapped # never slip backwards | |
| if abs(st - t) < 1e-6: | |
| n_exact += 1 | |
| elif st < t: | |
| n_back += 1 | |
| else: | |
| n_fwd += 1 | |
| group_ref[gi] = st | |
| prev_snapped = st | |
| for k in g: | |
| snapped[k] = st | |
| # ---- 5. rebuild events ---------------------------------------------- # | |
| new_events = [list(e) if isinstance(e, list) else e for e in events] | |
| for pos, nk in enumerate(note_idxs): | |
| new_events[nk][1] = int(round(snapped[pos])) # onset only | |
| # duration (idx 2), channel/note/velocity, and the 7th element (idx 6) | |
| # are left untouched. | |
| if resort: | |
| def time_key(e): | |
| if isinstance(e, list) and len(e) >= 2: | |
| return e[1] | |
| return 0 | |
| indexed = list(enumerate(new_events)) | |
| indexed.sort(key=lambda p: (time_key(p[1]), p[0])) | |
| new_events = [e for _, e in indexed] | |
| if verbose: | |
| print(f"[snap_score_to_grid] beat={beat:.2f} ms " | |
| f"({60000.0/beat:.1f} BPM), finest grid={finest:.2f} ms, " | |
| f"chord_tol={chord_tol_ms:.2f} ms, back_window={back_window:.2f} ms") | |
| print(f"[snap_score_to_grid] snaps: exact={n_exact}, " | |
| f"forward={n_fwd}, backward={n_back}, groups={len(groups)}") | |
| if leading_ticks is not None: | |
| return [leading_ticks] + new_events | |
| return new_events |