def _update_buffer(self, data: np.ndarray, cache: PerFrameCache) -> None: """ Update self._buffer by adding `data` and a step function. Data is reshaped to taper away from the center. :param data: Wave data. WILL BE MODIFIED. """ assert cache.mean is not None assert cache.period is not None buffer_falloff = self.cfg.buffer_falloff responsiveness = self.cfg.responsiveness N = len(data) if N != self._buffer_nsamp: raise ValueError(f"invalid data length {len(data)} does not match " f"CorrelationTrigger {self._buffer_nsamp}") # New waveform data -= cache.mean normalize_buffer(data) window = gaussian_or_zero(N, std=(cache.period / self._stride) * buffer_falloff) data *= window # Old buffer normalize_buffer(self._buffer) self._buffer = lerp(self._buffer, data, responsiveness)
def _update_buffer(self, data: np.ndarray, cache: PerFrameCache) -> None: """ Update self._buffer by adding `data` and a step function. Data is reshaped to taper away from the center. :param data: Wave data. WILL BE MODIFIED. """ assert cache.mean is not None assert cache.period is not None responsiveness = self.cfg.responsiveness if self.cfg.buffer_strength and responsiveness: # N should equal self.A + self.B. N = len(data) if N != self._corr_buffer.size: raise ValueError( f"invalid data length {len(data)} does not match " f"CorrelationTrigger {self._corr_buffer.size}") # New waveform data -= cache.mean normalize_buffer(data) window = gaussian_or_zero(N, std=self.calc_buffer_std(cache.period / self._stride)) data *= window self._prev_window = window # Old buffer normalize_buffer(self._corr_buffer) self._corr_buffer = lerp(self._corr_buffer, data, responsiveness)
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 get_trigger(self, pos: int, cache: "PerFrameCache") -> TriggerResult: cfg = self.cfg stride = self._stride # Convert sizes to full samples (not trigger subsamples) when indexing into # _wave. # _trigger_diameter is defined as inclusive. The length of find_peak()'s # corr variable is (A + _trigger_diameter + B) - (A + B) + 1, or # _trigger_diameter + 1. This gives us a possible triggering range of # _trigger_diameter inclusive, which is what we want. data_nsubsmp = self.A + self._trigger_diameter + self.B trigger_begin = max(pos - self._smp_per_frame, pos - self._trigger_diameter // 2) data_begin = trigger_begin - stride * self.A # Get subsampled data (1D, downmixed to mono) # [data_nsubsmp = A + _trigger_diameter + B] Amplitude data = self._wave.get_padded(data_begin, data_begin + stride * data_nsubsmp, stride) assert data.size == data_nsubsmp, (data.size, data_nsubsmp) if cfg.sign_strength != 0: signs = sign_times_peak(data) data += cfg.sign_strength * signs # Remove mean from data, if enabled. mean = np.add.reduce(data) / data.size period_data = data - mean if cfg.mean_responsiveness: self._prev_mean += cfg.mean_responsiveness * (mean - self._prev_mean) if cfg.mean_responsiveness != 1: data -= self._prev_mean else: data = period_data # Use period to recompute slope finder (if enabled) and restrict trigger # diameter. period = get_period(period_data, self.subsmp_per_s, cfg.max_freq, self) cache.period = period * stride semitones = self._is_window_invalid(period) # If pitch changed... if semitones: slope_finder = self._calc_slope_finder(period) # If pitch tracking enabled, rescale buffer to match data's pitch. if self.scfg and (data != 0).any(): # Mutates self._buffer. self.spectrum_rescale_buffer(data) self._prev_period = period self._prev_slope_finder = slope_finder else: slope_finder = cast(np.ndarray, self._prev_slope_finder) corr_enabled = bool(cfg.buffer_strength) and bool(cfg.responsiveness) # Buffer sizes: # data_nsubsmp = A + _trigger_diameter + B kernel_size = self.A + self.B corr_nsamp = self._trigger_diameter + 1 assert corr_nsamp == data_nsubsmp - kernel_size + 1 # Check if buffer still lines up well with data. if corr_enabled: # array[corr_nsamp] Amplitude corr_quality = signal.correlate_valid(data, self._corr_buffer) assert len(corr_quality) == corr_nsamp if cfg.reset_below > 0: peak_idx = np.argmax(corr_quality) peak_quality = corr_quality[peak_idx] data_slice = data[peak_idx:peak_idx + kernel_size] # Keep in sync with _update_buffer()! windowed_slice = data_slice - mean normalize_buffer(windowed_slice) windowed_slice *= self._prev_window self_quality = np.add.reduce(data_slice * windowed_slice) relative_quality = peak_quality / (self_quality + 0.001) should_reset = relative_quality < cfg.reset_below if should_reset: corr_quality[:] = 0 self._corr_buffer[:] = 0 corr_enabled = False else: corr_quality = np.zeros(corr_nsamp, f32) # array[A+B] Amplitude corr_kernel = slope_finder del slope_finder if corr_enabled: corr_kernel += self._corr_buffer * cfg.buffer_strength # `corr[x]` = correlation of kernel placed at position `x` in data. # `corr_kernel` is not allowed to move past the boundaries of `data`. corr = signal.correlate_valid(data, corr_kernel) assert len(corr) == corr_nsamp peaks = corr_quality del corr_quality peaks *= cfg.buffer_strength if cfg.edge_strength: # I want a half-open cumsum, where edge_score[0] = 0, [1] = data[A], [2] = # data[A] + data[A+1], etc. But cumsum is inclusive, which causes tests to # fail. So subtract 1 from the input range. edge_score = np.cumsum(data[self.A - 1:len(data) - self.B]) # The optimal edge alignment is the *minimum* cumulative sum, so invert # the cumsum so the minimum amplitude maps to the highest score. edge_score *= -cfg.edge_strength peaks += edge_score # Don't pick peaks more than `period * trigger_radius_periods` away from the # center. if cfg.trigger_radius_periods and period != UNKNOWN_PERIOD: trigger_radius = round(period * cfg.trigger_radius_periods) else: trigger_radius = None def find_peak(corr: np.ndarray, peaks: np.ndarray, radius: Optional[int]) -> int: """If radius is set, the returned offset is limited to ±radius from the center of correlation. """ assert len(corr) == len(peaks) == corr_nsamp # returns double, not single/f32 begin_offset = 0 if radius is not None: Ncorr = len(corr) mid = Ncorr // 2 left = max(mid - radius, 0) right = min(mid + radius + 1, Ncorr) corr = corr[left:right] peaks = peaks[left:right] begin_offset = left min_corr = np.min(corr) # Only permit local maxima. This fixes triggering errors where the edge # of the allowed range has higher correlation than edges in-bounds, # but isn't a rising edge itself (a local maximum of alignment). corr[:-1][peaks[:-1] < peaks[1:]] = min_corr corr[1:][peaks[1:] < peaks[:-1]] = min_corr corr[0] = corr[-1] = min_corr # Find optimal offset peak_offset = np.argmax(corr) + begin_offset # type: int return peak_offset # Find correlation peak. peak_offset = find_peak(corr, peaks, trigger_radius) trigger = trigger_begin + stride * (peak_offset) del data if self.post: new_data = self._wave.get_around(trigger, kernel_size, stride) cache.mean = np.add.reduce(new_data) / kernel_size # Apply post trigger (before updating correlation buffer) trigger = self.post.get_trigger(trigger, cache) # Avoid time traveling backwards. self._prev_trigger = trigger = max(trigger, self._prev_trigger) # Update correlation buffer (distinct from visible area) aligned = self._wave.get_around(trigger, kernel_size, stride) if cache.mean is None: cache.mean = np.add.reduce(aligned) / kernel_size self._update_buffer(aligned, cache) self._frames_since_spectrum += 1 # period: subsmp/cyc freq_estimate = self.subsmp_per_s / period if period else None # freq_estimate: cyc/s # If period is 0 (unknown), freq_estimate is None. return TriggerResult(trigger, freq_estimate)