Esempio n. 1
0
    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)
Esempio n. 2
0
    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)
Esempio n. 3
0
    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)
Esempio n. 4
0
    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)