Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
freeze_hold.hpp File Reference

Spectral freeze / infinite hold for phase-vocoder frame groups. More...

#include <pulp/signal/checked_allocation.hpp>
#include <pulp/signal/rt_work_counter.hpp>
#include <pulp/signal/staged_transition.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <cmath>
#include <complex>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <span>
#include <utility>
#include <vector>

Go to the source code of this file.

Classes

struct  pulp::signal::FreezeHoldSnapshotT< SampleType >
 Value image of a FreezeHold's held state: everything needed to reproduce the held output bit-for-bit on a hold prepared with the same geometry. More...
struct  pulp::signal::FreezeHoldReferenceTiming
 Time-based FreezeHold timing that reproduces the frame-count defaults (8 capture frames, 6 crossfade frames, 0.015 rad/frame walk) at the reference geometry they were tuned for: a 512-sample analysis hop at 48 kHz (RealtimePitchTimeProcessor's quality mode, 4096/512). More...
class  pulp::signal::FreezeHoldT< SampleType >
struct  pulp::signal::FreezeHoldT< SampleType >::Config
 Two sizing modes. More...

Namespaces

namespace  pulp
namespace  pulp::signal

Typedefs

using pulp::signal::FreezeHoldSnapshot = FreezeHoldSnapshotT<float>
using pulp::signal::FreezeHoldSnapshot64 = FreezeHoldSnapshotT<double>
using pulp::signal::FreezeHold = FreezeHoldT<float>
using pulp::signal::FreezeHold64 = FreezeHoldT<double>

Enumerations

enum class  pulp::signal::FreezeHoldSynthesis : std::uint8_t { pulp::signal::polar , pulp::signal::rotor }
 How held frames are synthesised (see the class notes on per-callback cost). More...
enum class  pulp::signal::FreezeHoldFrequency : std::uint8_t { pulp::signal::newest , pulp::signal::energy_weighted }
 Where a held bin's instantaneous frequency comes from. More...
enum class  pulp::signal::FreezeHoldAverage : std::uint8_t { pulp::signal::at_latch , pulp::signal::running }
 When the capture window is averaged. More...
enum class  pulp::signal::FreezeHoldPhaseLock : std::uint8_t { pulp::signal::none , pulp::signal::peak_lobe }
 Phase structure of a hold as it starts (see the class notes on stationarity). More...
enum class  pulp::signal::FreezeRestoreEngage : std::uint8_t { pulp::signal::crossfade , pulp::signal::immediate , pulp::signal::as_captured }
 How a restored hold enters the output. More...

Detailed Description

Spectral freeze / infinite hold for phase-vocoder frame groups.

Captures a rolling window of recent analysis frames (preallocated at prepare) and, when engaged, replaces live analysis frames with a synthetic steady-state hold: per-channel magnitudes averaged over the captured window, per-bin phases advancing at the instantaneous frequency estimated from the captured frames, plus a small bounded random walk that de-periodicizes the hold (spectral freezing per J.-F. Charles, "A Tutorial on Spectral Sound Processing Using Max/MSP and Jitter," CMJ 32(3), 2008; the random-walk de-looping follows the granular-hold contrast family, Roads, Microsound, 2001).

Designed to sit at the HEAD of a vocoder chain (before phase propagation and formant processing): held frames look like live steady-state input, so downstream pitch/formant control keeps working over frozen audio. Per-channel phase offsets are initialized from the latched frame and every channel advances by the same per-bin amount, preserving the spatial image. The per-bin advance is estimated from all channels together (magnitude-weighted), so an anti-phase bin — whose channel sum cancels — still gets its true frequency.

Engage policy (explicit no-mute rule): freezing latches only once the capture window is full; until then live frames pass through and the latch arms. The hold averages exactly the last capture_frames consecutive analysis frames before the latch. A release is committed: the old hold fades out even if a freeze is requested again during the fade, and the capture window restarts at the release, so the next hold contains only input analyzed after it. Engage/release crossfade over a fixed number of frames in the spectral domain; engage_progress() reports the fade position. Consumers that align their own (e.g. time-domain) crossfade can instead render the hold directly through the public pipeline: write_hold(), advance_hold(), rewind_hold_phases(), and the held-state accessors.

Deterministic (xorshift PRNG seeded at prepare); no allocation or locks after prepare(). All members except stage_restore() and restore_pending() belong to the thread that runs process_group(); state such as is_latched() must reach a UI through the owner's own publication, not by reading this object from another thread.

A held state can be captured with snapshot() into caller-prepared storage, serialized to a versioned, size-bounded byte image, and recalled with stage_restore() from a non-audio thread. The audio owner adopts a staged restore at the start of the next frame group.

Per-callback cost. The defaults reproduce the original hold bit for bit, and three of their costs land in single callbacks: every held frame evaluates one std::polar per channel per bin; a latch averages capture_frames × channels × bins magnitudes in the latching frame group (about 1.5 M adds for a 2 s hold at 8192 points, stereo); and the latch derives every bin's phase with an atan2. A host drops a buffer whose callback overruns, so these are opt-in Config fields that bound them:

  • synthesis = rotor keeps each held bin as a unit phasor advanced by a per-bin rotor and a 256-entry jitter table: two complex multiplies per bin per hop and no transcendental. The walk is quantised to 256 steps over ±phase_jitter (inaudible at the default rates) and phasors are renormalised every 64 hops, so output differs from polar in the last bits and in the walk's fine structure, not in character.
  • engage_bins_per_hop > 0 (with rotor) stages the latch-side rotor construction — and, with energy_weighted, the per-bin atan2 — across the following frame groups through StagedTransition; a bin whose rotor is not ready yet holds its phase for those hops, inaudible at fade-in gain.
  • average = running keeps per-bin running sums over the capture window as frames arrive, so the latch divides instead of summing the window.

Level and frequency over long holds. With frequency = newest (the default) every bin rotates at the increment of the NEWEST captured frame. Over a window that spans a change of material, a bin whose energy came from the older material is then rotated at whatever frequency the newest frame's leakage gave it, so the older sound is not heard and the hold plays well below the input. frequency = energy_weighted takes each bin's increment as the circular mean over the whole window, weighted by the per-frame product of consecutive magnitudes (the argument of sum_t sum_ch X_t conj(X_{t-1})), so a bin follows the content that dominates it.

Stationarity. With phase_lock = none a hold starts phase-coherent (the latched frame's phases) and each bin then advances at its own frequency, so the bins of one partial's main lobe drift apart and the hold decoheres over its first few hundred milliseconds: its level moves after it is already audible. phase_lock = peak_lobe keeps each peak's lobe rotating at the peak's frequency, so a partial stays coherent, and starts every other bin at a random phase, so noise-like content has its steady-state statistics from the first held frame. The inter-channel phase offsets are kept in both cases.

Expensive work is reported to pulp::signal::rt counters (trig for each polar/atan2, bins for per-bin accumulation), which is what a transition-cost gate reads.