"""Interpolate f0 through unvoiced frames exactly as training did. The conformer's `f0_key` is `f0_interp`, which is pYAAPT's own interpolation of the raw pitch track -- not a straight line through the gap. It median filters, fits a shape-preserving PCHIP spline over the voiced frames, fills the zeros from that, median filters again, and extends the first and last voiced values outward. Nothing here reimplements any of that. The real `pYAAPT.PitchObj` is used, with the same monkey-patch the training pipeline applies, so every constant and helper comes from the library rather than from a copy that can drift. This is for contours built here -- drawn strokes with gaps where the pen was lifted. It cannot reproduce a stored `f0_interp` from a stored raw `f0`: the training pipeline interpolates first and *then* truncates (`f0 = f0[6:]`, then both arrays cut to the shorter), so the original interpolation saw neighbouring frames that no longer exist. Differences of up to ~14 Hz appear at the end of the contour for exactly that reason. Where a stored `f0_interp` exists, use it rather than recomputing. """ from __future__ import annotations import numpy as np def _blank_pitch_obj(f0: np.ndarray): """A real PitchObj carrying `f0`, without running pitch tracking on audio. PitchObj.__init__ wants a SignalObj to analyse. Interpolation only reads `samp_values` and `nframes` (plus class constants and `edges_finder`), so the object is built uninitialised and those two fields are set directly -- every other attribute and method is the library's own. """ from amfm_decompy import pYAAPT obj = pYAAPT.PitchObj.__new__(pYAAPT.PitchObj) obj.samp_values = np.asarray(f0, dtype=np.float64) obj.nframes = len(obj.samp_values) return obj def interpolate_f0(f0: np.ndarray) -> np.ndarray: """Raw f0 (zeros at unvoiced frames) -> the contour the model expects. Returns the input unchanged when there is nothing to interpolate. """ f0 = np.asarray(f0, dtype=np.float64) if f0.size == 0: return f0 if not (f0 == 0).any(): return f0 # already continuous from ._yaapt_patch import patched_interpolate obj = _blank_pitch_obj(f0) patched_interpolate(obj) return np.asarray(obj.samp_interp, dtype=np.float64)