def get_min_period() -> int: """ Avoid picking periods shorter than `max_freq`. - Yamaha FM feedback produces nearly inaudible high frequencies, which tend to produce erroneously short period estimates, causing correlation to fail. - Most music does not go this high. - Overestimating period of high notes is mostly harmless. """ max_cyc_s = max_freq min_s_cyc = 1 / max_cyc_s min_subsmp_cyc = subsmp_s * min_s_cyc return iround(min_subsmp_cyc)
def _calc_slope_finder(self, period: float) -> np.ndarray: """ Called whenever period changes substantially. Returns a kernel to be correlated with input data, to find positive slopes.""" N = self._buffer_nsamp halfN = N // 2 slope_finder = np.zeros(N) cfg = self.cfg slope_width = max(iround(cfg.slope_width * period), 1) slope_strength = cfg.slope_strength * cfg.buffer_falloff slope_finder[halfN - slope_width:halfN] = -slope_strength slope_finder[halfN:halfN + slope_width] = slope_strength return slope_finder
def spectrum_rescale_buffer(self, data: np.ndarray) -> None: """ - Cross-correlate the log-frequency spectrum of `data` with `buffer`. - Rescale `buffer` until its pitch matches `data`. """ # Setup scfg = self.scfg Ntrigger = self._corr_buffer.size if self._frames_since_spectrum < self.scfg.min_frames_between_recompute: return self._frames_since_spectrum = 0 calc_spectrum = self._spectrum_calc.calc_spectrum # Compute log-frequency spectrum of `data`. spectrum = calc_spectrum(data) normalize_buffer(spectrum) assert not np.any(np.isnan(spectrum)) # Compute log-frequency spectrum of `self._buffer`. prev_spectrum = calc_spectrum(self._corr_buffer) # Don't normalize self._spectrum. It was already normalized when being assigned. # Rescale `self._buffer` until its pitch matches `data`. resample_notes = correlate_spectrum(spectrum, prev_spectrum, scfg.max_notes_to_resample).peak if resample_notes != 0: # If we want to double pitch, we must divide data length by 2. new_len = iround(Ntrigger / 2**(resample_notes / scfg.notes_per_octave)) def rescale_mut(corr_kernel_mut): buf = np.interp( np.linspace(0, 1, new_len), np.linspace(0, 1, Ntrigger), corr_kernel_mut, ) # assert len(buf) == new_len buf = midpad(buf, Ntrigger) corr_kernel_mut[:] = buf # Copy+resample self._buffer. rescale_mut(self._corr_buffer)
def _calc_lag_prevention(self) -> np.ndarray: """ Returns input-data window, which zeroes out all data older than 1-ish frame old. See https://github.com/jimbo1qaz/corrscope/wiki/Correlation-Trigger """ N = self._buffer_nsamp halfN = N // 2 # - Create a cosine taper of `width` <= 1 frame # - Right-pad(value=1, len=1 frame) # - Place in left half of N-sample buffer. # To avoid cutting off data, use a narrow transition zone (invariant to stride). lag_prevention = self.cfg.lag_prevention tsamp_frame = self._tsamp_frame transition_nsamp = round(tsamp_frame * lag_prevention.transition_frames) # Left half of a Hann cosine taper # Width (type=subsample) = min(frame * lag_prevention, 1 frame) assert transition_nsamp <= tsamp_frame width = transition_nsamp taper = windows.hann(width * 2)[:width] # Right-pad=1 taper to lag_prevention.max_frames long [t-#*f, t] taper = rightpad(taper, iround(tsamp_frame * lag_prevention.max_frames)) # Left-pad=0 taper to left `halfN` of data_taper [t-halfN, t] taper = leftpad(taper, halfN) # Generate left half-taper to prevent correlating with 1-frame-old data. # Right-pad=1 taper to [t-halfN, t-halfN+N] # TODO switch to rightpad()? Does it return FLOAT or not? data_taper = np.ones(N, dtype=FLOAT) data_taper[:halfN] = np.minimum(data_taper[:halfN], taper) return data_taper
def on_begin(self, begin_time, end_time): self.setRange(iround(begin_time), iround(end_time))