KD-Tree Timbral Counterpoint — User Guide

Builds a stereo contrapuntal texture from a selected target Sound by matching each target grain to corpus grains at controlled distances in an 11-dimensional acoustic feature space, then placing several independently ranked voices on the target timeline.

Author: Shai Cohen Affiliation: Department of Music, Bar-Ilan University, Israel Version: 1.3 (2026) License: MIT License Python: NumPy required; SciPy optional Repo: Praat AudioTools
Contents:

What this does

KD-Tree Timbral Counterpoint treats a selected target Sound as a sequence of overlapping analysis grains. Each target grain is compared with grains extracted from every .wav file in a corpus folder. The comparison uses MFCC, spectral, pitch, intensity, periodicity, and zero-crossing descriptors. For every target grain, each voice requests a different neighbour rank in that feature space, so the voices can range from close timbral matches to progressively more distant alternatives.

In practical terms:
the target supplies the timeline and the acoustic query. The corpus supplies the sounding material. The result is reconstructed entirely from selected corpus grains.

The default architecture uses four voices, but Number_of_voices can be any positive integer. Voices share the same target-grain timing while using different neighbour ranks, gains, delays, and panoramic rules.

Complete pipeline

Selected target Sound + corpus folder
→ target and corpus grain segmentation
→ mono acoustic analysis in Praat
→ 11-feature CSV tables
→ corpus-based standardization and feature weighting in Python
→ SciPy cKDTree search, or exact NumPy fallback
→ one corpus match per target grain × voice
→ corpus-grain extraction in Praat
→ edge fades + voice gain + mono downmix + constant-power stereo pan
→ resample each selected grain to the target sample rate
→ sample-grid scheduling, overlap-preserving voice assembly, voice summation
→ trailing-silence trim + 50 ms final fade
→ optional target-derived envelope shaping
→ attenuate-only 0.99 peak safety ceiling

Grain segmentation

The same grain size and overlap are used when analysing the target and each corpus file.

grain duration = Grain_size_ms / 1000
hop = grain duration × (1 − Grain_overlap_percent / 100)

Grain_overlap_percent must satisfy 0 ≤ overlap < 100. It controls the analysis hop and therefore how densely target and corpus grains are sampled. It is separate from Crossfade_duration.

For sounds at least one grain long, the regular grid contains complete grains only; a trailing remainder shorter than one full grain is not emitted as an extra grain. A sound shorter than the requested grain size still produces one grain spanning its available duration.

Very short sounds are zero-padded to a 0.1 s analysis-only signal so Praat's Pitch and Intensity objects can be created reliably. The original source duration is still used for grain timing and audio extraction.

Feature space — 11 dimensions

Target and corpus material are analysed through a consistent mono signal. Multichannel inputs are downmixed before feature extraction.

DimensionsMeasurementWeight control
MFCC 1–6The first six coefficients from a 12-coefficient Praat MFCC object, sampled at the grain midpoint.Mfcc_weight, shared by all six coefficients
Spectral centroidCentre of gravity of the grain's power spectrum.Spectral_centroid_weight
PitchF0 at the grain midpoint from a 75–600 Hz Praat Pitch analysis; unvoiced values become 0.Pitch_weight
IntensityMean Praat Intensity over the grain, using energy averaging; values are in Praat's dB SPL scale.Intensity_weight
HNRMean Harmonicity (cc) over the grain; undefined values become −200.Hnr_weight
ZCRZero-crossing count divided by grain duration.Hnr_weight also scales ZCR

MFCC values are taken from the MFCC frame corresponding to each grain midpoint. Spectral centroid and ZCR are measured from the complete grain interval rather than from a single midpoint frame.

Distance and corpus matching

Corpus-based standardization

Python calculates the mean and standard deviation of every feature dimension from the corpus. Both corpus and target feature vectors are standardized with those same corpus statistics. Constant corpus dimensions use a scale of 1 so they contribute no spurious infinities.

z = (feature − corpus_mean) / corpus_std
weighted_z = z × feature_weight
distance = sqrt(Σ weighted_z²)
Because the weight multiplies the standardized coordinate before Euclidean distance is calculated, its contribution is squared inside the sum. The five form weights expand to 11 dimensions: one MFCC weight for six MFCCs, then centroid, pitch, intensity, and one HNR weight shared by HNR and ZCR.

Neighbour rank

Rank 1 means the closest corpus grain under the current weighted distance. Larger ranks request progressively more distant candidates. If fewer rank values are supplied than the number of voices, Python repeats the final rank for the remaining voices.

Randomness

shift = integer(rank × Randomness_amount)
randomized rank = max(1, rank + random integer from −shift to +shift)

There is no random-seed control. Rank jitter and the random panorama used by Voice 4 and higher can therefore change between runs.

Repetition penalty

When enabled, the matcher avoids corpus-grain indices used within the 20 most recent selections across the complete matching sequence. This history is shared across voices rather than maintained separately for each voice. The search window is expanded so the matcher can look beyond recently used grains.

KD-tree and fallback

If SciPy is installed, the corpus is indexed with scipy.spatial.cKDTree. If SciPy is unavailable, the engine performs an exact NumPy distance calculation and sorts the candidates directly. The matching definition is therefore retained; only the search implementation changes.

Voice architecture

The neighbour rank comes from Neighbor_ranks or the active preset. Gain, pan, and delay follow fixed voice rules in the Python engine:

VoiceGainPanDelay
Voice 10.90Centre0 ms
Voice 20.65Alternates ±0.5 by target-grain index20 ms
Voice 30.45Alternates ±0.8 in the opposite left/right pattern45 ms
Voice 4+0.30Independent random position from −1 to +180 ms

Pan uses the constant-power law:

angle = (pan + 1) × π / 4
left gain = cos(angle)
right gain = sin(angle)
Before panning, every selected corpus grain is made mono. Mono grains pass through directly; stereo and other multichannel grains are downmixed. A fresh stereo image is then created by duplication and constant-power panning. The original corpus spatial image is therefore not preserved.

Presets

Presets are partial overrides. Any field not listed below retains the value currently entered in the form.

PresetFields overridden
CustomNo overrides.
Strict DoppelgängerRanks = 1,2,3,4; Randomness = 0.05; MFCC weight = 1.5; Intensity weight = 1.0.
Spectral CounterpointRanks = 1,3,8,20; Randomness = 0.20. All feature weights remain as entered.
Ghost ChoirRanks = 5,12,25,50; Randomness = 0.50. All feature weights remain as entered.
Orchestral ShadowRandomness = 0.20; Pitch weight = 1.5; Spectral-centroid weight = 1.2. Ranks and the other weights remain as entered.
Noise DoppelgängerRandomness = 0.20; HNR/ZCR weight = 2.0; Spectral-centroid weight = 1.5. Ranks and the other weights remain as entered.
The form opens with Strict Doppelgänger selected. Manual values remain visible even when a preset overrides some of them; the visualization and Info window report the effective values used by the run.

Resynthesis and timing

For every matched row, Praat extracts the selected corpus interval, applies the voice gain and edge fades, converts it to a controlled stereo image, and resamples it to the target Sound's sample rate when necessary.

Crossfade_duration is a per-grain edge fade

The form keeps the name Crossfade_duration, but the implementation applies separate cosine fade-in and fade-out envelopes to each selected grain:

effective edge fade = min(Crossfade_duration, grain duration / 2)

This is not a pairwise crossfade operation between adjacent grains.

Scheduling

Each corpus grain is scheduled at:

target grain start + voice delay

The start is quantized to the target sample grid. Overlapping scheduled grains are preserved: the wrapper assigns them to as many internal tracks as necessary, concatenates each non-overlapping track with explicit silence gaps, then sums the tracks into the voice. All voices are finally summed into the stereo mix.

Trailing trim

The mix is analysed for trailing silence with Praat's silence detector using a −35 dB threshold. The retained end includes the last sounding region plus room for a 50 ms final cosine fade and a 5 ms margin, within the script's target duration + 2 s work buffer. Retained separate voices are trimmed to the same final duration and receive the same 50 ms ending fade.

Envelope shaping

Envelope shaping is applied after reconstruction and trailing trim. Analysis is taken from a mono copy of the target.

ModeBehavior
OffNo target-derived gain shaping.
Pauses onlyDetects target silence at −35 dB and multiplies the result by a gate. Silence edges use cosine ramps up to 15 ms.
Amplitude envelope onlyBuilds a gain curve from target Praat Intensity. The maximum maps to 1; a point 60 dB below the maximum maps to 0; intermediate dB values are mapped linearly to 0…1.
Pauses + Amplitude envelopeMultiplies the output by both the pause gate and the intensity-derived gain curve.
The amplitude-envelope mode uses a linear mapping of Praat Intensity values across a 60 dB range. It is a control curve, not a dB-to-linear-amplitude conversion.

Outputs and final level

Output modeObjects retained
MixdownKDTC_mix only.
Separate voicesIndividual stereo voice Sounds; the temporary mix is removed after it has been used for trimming and safety calculations.
BothKDTC_mix plus the individual stereo voice Sounds.

The first four separate voices receive descriptive suffixes _close, _shadow, _cousin, and _ghost. Higher-numbered voices use their numeric voice name.

Channel and sample-rate behavior

Final peak safety

The completed mix is measured with Sinc70. If its peak exceeds 0.99, one common attenuation factor is applied to the mix and to every retained separate voice:

if mix_peak > 0.99:
  safety factor = 0.99 / mix_peak

This is an attenuate-only safety ceiling, not target peak normalization. Quieter results are left unchanged.

Play_result automatically plays the mix in Mixdown and Both modes. Separate-voices-only mode does not automatically choose one voice for playback.

Visualization — match results

When Draw_visualization is enabled, the 8 × 8 Praat Picture reports the matching decisions rather than waveform or spectrogram views.

PanelWhat it shows
A — Timbral distance per grainWeighted feature-space distance across target time, one trajectory per voice. This directly shows how close or distant the chosen corpus material is under the active feature weights.
B — Grain provenanceOne block per scheduled grain, coloured by corpus file. Consecutive grains alternate half-lanes so overlapping grain density remains visible.
C — Voice separationMinimum, maximum, and mean distance for every voice. The text also compares each base requested rank with the mean randomized rank used for that voice.
D — Corpus usageNumber of selected grains per corpus file, sorted by use count; when more than 12 files are used, the panel displays the top 12.
Summary barEffective preset/weights, voice and grain counts, corpus coverage, duration, randomness, repetition penalty, edge-fade setting, envelope mode, overall distance range, and mean MFCC contribution.
If the MFCC weight is non-zero but the measured mean MFCC contribution to the selected distances is exactly 0, the summary bar displays a warning that the MFCC columns are flat and matching is effectively being driven by the scalar features.

Controls

ControlMeaning
Corpus_folderFolder containing corpus .wav files. Only top-level WAV files are scanned.
Grain_size_msAnalysis and reconstruction grain duration in milliseconds; must be greater than zero.
Grain_overlap_percentControls grain hop; valid range 0 to less than 100%.
Number_of_voicesNumber of independently matched layers; minimum 1.
Neighbor_ranksComma-separated positive integer ranks. The final rank is repeated if the list is shorter than the voice count.
PresetApplies the partial overrides listed above.
Feature weightsNon-negative scaling factors in standardized feature space. At least one must be greater than zero.
Randomness_amount0…1 rank jitter. Some presets override it.
Repetition_penaltyAvoids recently used corpus-grain indices during matching.
Output_modeMixdown, separate voices, or both.
Crossfade_durationPer-grain cosine edge-fade duration in seconds; clamped to half the actual grain duration.
Envelope_shapingOff, pause gate, intensity-derived envelope, or both.
Draw_visualizationDraws the result-oriented match visualization in Praat Picture.
Play_resultPlays the retained mix automatically when a mix is part of the chosen output mode.

Dependencies and files