CWT Granular Resampler — Wavelet-Driven Cross-Synthesis

Takes two Sounds: A (analysis) and B (grain source). Significant pooled |CWT(t,f)| cells from A generate an expected number of grains from B; fractional counts are realised stochastically. Each grain is read from B at a playback-rate ratio derived from the firing wavelet bin, with duration scaled by that bin's frequency. The result maps A's time–frequency morphology onto B's timbral material.

Author: Shai Cohen Affiliation: Department of Music, Bar-Ilan University, Israel Version: 1.2 (2026) License: MIT License Repo: https://github.com/ShaiCohen-ops/Praat-plugin_AudioTools
Contents:

What this does

This script implements a continuous wavelet transform (CWT) driven granular resynthesizer for Praat AudioTools. It takes two Sounds: Sound A provides the analysis — its time–frequency structure is extracted via a complex Morlet CWT. Sound B provides the raw audio material. Each trigger-grid cell that survives peak selection and thresholding produces an expected grain count from its dB level. That count can become zero, one, or several grains after stochastic fractional rounding. Each grain is read from Sound B at a playback-rate ratio derived from the firing wavelet frequency and given a duration inversely related to that frequency.

Concatenative cross-synthesis: This is not a filter‑bank resynthesis. A's time‑frequency morphology triggers grains from B, but each grain preserves B's full spectral content — it is not band‑limited to the bin's frequency. The result is a morphological mapping: A's rhythm, density, and spectral envelope drive a timbral interpretation from B.

Key Features:

How |CWT| drives density: A cell's expected grain count is (dB − threshold) / (0 − threshold) × Grain_density_multiplier. This is a linear function of dB, not of magnitude or energy. At a −40 dB threshold, a −20 dB cell scores 0.5, where its magnitude ratio is 0.1 and its energy ratio 0.01. Quiet cells therefore keep far more presence than a linear reconstruction would give them.

Signal path

A (analysis) → mono → complex Morlet CWT → |CWT(t,f)| Matrix
→ pooled onto coarse trigger grid (Trigger_step_ms × Trigger_bands_per_octave)
→ dB re global peak → optional 4‑neighbour local‑maximum mask → threshold → grain list

B (grain source) → mono → oversampled (1–8×) → band‑limited per rate group
→ read by each grain at its own rate → windowed, panned, summed into output buffer

How each CWT cell maps to a grain

Time: onset = cell_time ± jitter (up to half a trigger step).

Pitch: rate = f_k / Reference_frequency_Hz, clamped to ±Transposition_range_st semitones.

Duration: d_k = Base_grain_duration_ms × fGeo / f_k, clamped to 3–400 ms. This is the 1/f relation of the Morlet scale itself.

Density: expected = ((dB − threshold) / (0 − threshold)) × Grain_density_multiplier, fractional part realised stochastically.

Position: Sequential: a normalised position u is computed over the analysed duration of A, warped by Position_warp, then mapped into the valid read range of B: approximately position = u × (durB − grain_read_duration) before position jitter. Energy‑weighted random: drawn from B's RMS profile.

Pan: constant‑power pan with per‑grain uniform draw scaled by Pan_jitter.

Level: amp = 10^(dB/20) re the trigger‑grid peak.

Important — transposition semantics: The playback rate is a ratio relative to Reference_frequency_Hz. It does not transpose B to the bin's frequency. With a 220 Hz tone in B, reference 440 Hz, and a bin at 880 Hz, the rate is 2 and the tone lands on 440 Hz, not 880 Hz. The grain also stays as broadband as B is — there is no bandpass around f_k.

4 Presets

PresetCharacterFreq range (Hz)Voices/octTrigger step (ms)Threshold (dB)Density mult.Transposition range
Balanced cloudDefault, versatile80 – 60001220−321.5User setting (default ±24 st)
Sparse pointillistFew, well‑separated grains80 – 60001230−201.0User setting (default ±24 st)
Dense washThick, continuous texture60 – 80001612−402.5User setting (default ±24 st)
ShimmerHigh, short grains300 – 100001610−362.0User setting (default ±24 st)
Custom and editable presets: Selecting Custom opens the Analysis and Engine settings dialogs. Built-in presets provide starting values, and the same dialogs can also be opened for a preset using the edit-settings controls before rendering. Parameters such as Transposition_range_st remain user controls rather than fixed preset values. Shimmer uses a higher reference frequency (880 Hz); the other built-in presets use 440 Hz. In batch mode (--run), pause blocks auto-continue with the current values.

Grain mapping — where in B the grain reads from

Sequential

A playhead walks through B. The grain's position is ((t − t0) / durA)^Position_warp. warp = 1 is linear; warp > 1 holds the read head near the start; warp < 1 pushes it towards the end.

Energy‑weighted random

Positions are drawn from B's RMS profile — probability proportional to RMS (not RMS²). Dense sections of B are sampled more often, creating a timbral density that follows B's own energy envelope.

Random seed: The Engine settings include Random_seed. A non-zero seed makes stochastic fractional grain counts, onset jitter, position jitter, and pan draws reproducible.
Position jitter: Each grain's position is perturbed by a uniform draw of ±Position_jitter_ms. This decorrelates grains that fire from the same cell, preventing a hard, repetitive stutter.

Implementation notes

Vectorised CWT: Uses the same core as CWT_Scalogram v1.1: one "Create Sound from formula" per frequency bin, then Concatenate → Down to Matrix → one indexed Formula to assemble the magnitude Matrix. No per‑time‑step script loop.

Trigger pooling: v1.2 performs explicit 2D max pooling: each coarse trigger cell takes the maximum of all fine CWT cells that fall inside its time–frequency bounds. This is not bilinear or point resampling. v1.1 point-sampled the fine grid, so a transient between trigger centres could be missed.

Anti‑aliased source copies: Reading B at rate r can fold source content above outputNyquist/r back into the audible band. v1.2 band-limits B once per half-octave rate group, using Filter (pass Hann band): 0, cutoff. This prevents the predictable pitch-up fold-over within the grouped-rate design while keeping render cost manageable.

Single-pass overlap-add: Each grain is rendered with one Formula (part) command:
self + gain * window(x) * object(srcB, pos + rate*(x - onset))
object() returns 0 outside the referenced Sound's domain, so no range guard is needed. In one reference benchmark this measured about 0.58 ms per grain for 30 ms grains at 44.1 kHz; actual performance depends on the machine and parameter set.

Windows: Hanning or Gaussian (truncated at 2.5σ, offset‑corrected so edges reach exactly zero). Both reach zero at the grain edges, so overlap‑add introduces no step discontinuity.

Oversampling: Sound B is resampled up front (1×, 2×, 4×, or 8×). Oversampling reduces interpolation error; the grouped low-pass source copies address pitch-up aliasing. In one reference test on a 5 kHz tone, measured interpolation error improved from about −37 dB at 1× to −50 dB at 4× and −79 dB at 8×.
Limits and safeguards: Trigger_threshold_dB values at or above 0 are forced below 0 dB; Position_warp values at or below 0 are clamped to a small positive value. Maximum_frequency_Hz is constrained below Nyquist. The analysis grid is bounded for practicality (up to 400 frequency bins and 20,000 time positions); if the requested time grid exceeds the limit, only the initial analysed portion of A is used and a warning is reported.
Maximum_grains: If the expected total grain count exceeds Maximum_grains, v1.2 first reduces density uniformly across the whole trigger map rather than simply stopping at the end of A. A final hard cap still protects the render if stochastic rounding reaches the limit.
Aliasing — solved in v1.2: v1.1 had a severe aliasing problem on pitch‑up. With an 8 kHz tone in B at rate 4, 99.92% of the output energy sat at the 12.1 kHz alias, identically at 1×, 4×, and 8× oversampling. Oversampling fixes interpolation error, not aliasing. v1.2's band‑limited source copies eliminate this.

Applications

Speech → Granular texture

Use case: Analyse a spoken phrase (A) and use a synth pad or environmental recording (B) as the grain source. The output follows the speech's rhythmic and spectral contour but sounds like the pad.

Settings: Balanced cloud preset, Sequential mapping, warp = 1. Time-frequency energy associated with speech formants, frication, and plosive transients can concentrate triggers in corresponding regions, creating a "talking pad" effect.

Drum loop → Rhythmic re‑synthesis

Use case: Analyse a drum loop (A) and use a sustained synth tone (B). The drum loop's transient structure triggers grains from the synth, turning a rhythmic pattern into a pitch‑based texture.

Settings: Sparse pointillist preset, Energy‑weighted random. Low-frequency-dominant events may concentrate triggers in lower CWT bands, while brighter transients may activate higher bands, preserving much of the loop's rhythmic timing in a different timbral domain.

Environmental recording → Morphological mapping

Use case: Analyse a bird call or field recording (A) and use a musical instrument (B). The output carries the time‑frequency morphology of the environment but sounds like the instrument.

Settings: Shimmer preset, Sequential mapping with warp = 2 (holds the read head near B's start). The result is a dense, shimmering texture that follows the contour of the environmental recording.

Workflow: Speech → Pad (talking pad)

A: A spoken sentence.
B: A sustained synth pad.
Settings: Balanced cloud, Sequential, warp = 1, dry/wet = 0.8.
Result: The speech's time-frequency concentrations generate grains from the pad. The pad timbre can therefore follow the speech's rhythmic and spectral motion — a "talking pad" effect.

Workflow: Drum loop → Synth (rhythmic re‑synthesis)

A: A breakbeat drum loop.
B: A synthesizer with a rich harmonic spectrum.
Settings: Sparse pointillist, Energy‑weighted random, transposition range = ±12 st.
Result: Each drum hit triggers a grain from the synth. Low-frequency-dominant drum events tend to favour lower trigger bands, while brighter transients can favour higher ones. The rhythmic timing of the loop can remain recognisable while the audible material comes from the synth.

Workflow: Bird call → Piano (morphological mapping)

A: A bird call or field recording.
B: A piano recording.
Settings: Shimmer, Sequential, warp = 2, grain window = Gaussian.
Result: The bird call's fast frequency modulations and rhythmic structure map onto the piano. The output is a shimmering, piano‑based texture that follows the bird's contour.

Troubleshooting:
No grains fire: The threshold may be too high, peak selection may reject most cells, or the trigger grid may be too coarse. Lower Trigger_threshold_dB, reduce Trigger_step_ms, or try a less restrictive Peak_selection mode.
Output is aliased / distorted: Higher Source_oversampling can reduce interpolation error, while v1.2's rate-grouped low-pass source copies protect pitch-up grains from predictable fold-over. Very wide transposition ranges can still produce demanding edge cases near Nyquist.
Grains run past the end of B: If B is shorter than the mean grain read length, grains tail into silence. Use a longer B or shorten Base_grain_duration_ms.
Output is silent but grains fired: Check Dry_wet — if it is 0, the output is pure dry (Sound A) and the wet grains are muted. If it is 1, the wet signal may be very quiet — check Output_peak and the grain amplitudes.
Trigger map shows no dots: The trigger grid cells are below the threshold. Lower the threshold or choose a different Peak_selection mode.
Very few grains are generated: Expected counts below 1 are not simply rounded down: their fractional parts fire probabilistically. Increase Grain_density_multiplier or lower the threshold if the overall expected density is too low.

Output & report

Output Sound

Named granular_<A>_x_<B>. Stereo or mono, then peak-scaled to Output_peak. When dry/wet mixing is used, Sound A is first gain-matched to the wet peak before the dry and wet contributions are combined. The output buffer is slightly longer than the analysed duration to accommodate onset jitter and grain tails.

Visualisation

When Draw_trigger_map is enabled: scalogram of A (dB) with generated grain positions overlaid as coloured circles (radius ∝ amplitude, colour ∝ amplitude), plus the output waveform. For very large grain counts the plotted markers may be visually subsampled for rendering speed. The title/summary bands report analysis and engine settings.

Info window

Reports grain distribution by octave (count, share, mean duration, transposition span), synthesis statistics (including mean duration, overlap, grain rate, playback/transposition statistics, wet peak/RMS, and B consumption), and analysis/render statistics such as CWT grid, trigger grid, headroom, grain-budget scaling, and elapsed time.

Playback: If Play is enabled, the script plays the output Sound after rendering. This is wrapped in nocheck, so a machine without audio device will not abort the run.
Trigger‑grid headroom: The dB scale is referenced to the trigger grid's own peak, not the fine analysis grid's peak. Pooling onto a coarser trigger grid can change which fine-cell maxima are represented, so referencing to the fine peak would put 0 dB out of reach — the threshold would bite harder than its number suggests, and the normalised energy driving grain density could never reach 1. The report shows how much headroom the trigger grid has below the fine‑grid peak.