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.
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.
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
→ 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.
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.
Feature space — 11 dimensions
Target and corpus material are analysed through a consistent mono signal. Multichannel inputs are downmixed before feature extraction.
| Dimensions | Measurement | Weight control |
|---|---|---|
| MFCC 1–6 | The first six coefficients from a 12-coefficient Praat MFCC object, sampled at the grain midpoint. | Mfcc_weight, shared by all six coefficients |
| Spectral centroid | Centre of gravity of the grain's power spectrum. | Spectral_centroid_weight |
| Pitch | F0 at the grain midpoint from a 75–600 Hz Praat Pitch analysis; unvoiced values become 0. | Pitch_weight |
| Intensity | Mean Praat Intensity over the grain, using energy averaging; values are in Praat's dB SPL scale. | Intensity_weight |
| HNR | Mean Harmonicity (cc) over the grain; undefined values become −200. | Hnr_weight |
| ZCR | Zero-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.
weighted_z = z × feature_weight
distance = sqrt(Σ weighted_z²)
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
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:
| Voice | Gain | Pan | Delay |
|---|---|---|---|
| Voice 1 | 0.90 | Centre | 0 ms |
| Voice 2 | 0.65 | Alternates ±0.5 by target-grain index | 20 ms |
| Voice 3 | 0.45 | Alternates ±0.8 in the opposite left/right pattern | 45 ms |
| Voice 4+ | 0.30 | Independent random position from −1 to +1 | 80 ms |
Pan uses the constant-power law:
left gain = cos(angle)
right gain = sin(angle)
Presets
Presets are partial overrides. Any field not listed below retains the value currently entered in the form.
| Preset | Fields overridden |
|---|---|
| Custom | No overrides. |
| Strict Doppelgänger | Ranks = 1,2,3,4; Randomness = 0.05; MFCC weight = 1.5; Intensity weight = 1.0. |
| Spectral Counterpoint | Ranks = 1,3,8,20; Randomness = 0.20. All feature weights remain as entered. |
| Ghost Choir | Ranks = 5,12,25,50; Randomness = 0.50. All feature weights remain as entered. |
| Orchestral Shadow | Randomness = 0.20; Pitch weight = 1.5; Spectral-centroid weight = 1.2. Ranks and the other weights remain as entered. |
| Noise Doppelgänger | Randomness = 0.20; HNR/ZCR weight = 2.0; Spectral-centroid weight = 1.5. Ranks and the other weights remain as entered. |
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:
This is not a pairwise crossfade operation between adjacent grains.
Scheduling
Each corpus grain is scheduled at:
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.
| Mode | Behavior |
|---|---|
| Off | No target-derived gain shaping. |
| Pauses only | Detects target silence at −35 dB and multiplies the result by a gate. Silence edges use cosine ramps up to 15 ms. |
| Amplitude envelope only | Builds 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 envelope | Multiplies the output by both the pause gate and the intensity-derived gain curve. |
Outputs and final level
| Output mode | Objects retained |
|---|---|
| Mixdown | KDTC_mix only. |
| Separate voices | Individual stereo voice Sounds; the temporary mix is removed after it has been used for trimming and safety calculations. |
| Both | KDTC_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
- All reconstructed outputs are stereo.
- All selected corpus grains are rendered from mono before panning.
- Every reconstructed grain is resampled to the selected target Sound's sampling frequency when necessary.
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:
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.
| Panel | What it shows |
|---|---|
| A — Timbral distance per grain | Weighted 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 provenance | One block per scheduled grain, coloured by corpus file. Consecutive grains alternate half-lanes so overlapping grain density remains visible. |
| C — Voice separation | Minimum, 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 usage | Number of selected grains per corpus file, sorted by use count; when more than 12 files are used, the panel displays the top 12. |
| Summary bar | Effective preset/weights, voice and grain counts, corpus coverage, duration, randomness, repetition penalty, edge-fade setting, envelope mode, overall distance range, and mean MFCC contribution. |
Controls
| Control | Meaning |
|---|---|
| Corpus_folder | Folder containing corpus .wav files. Only top-level WAV files are scanned. |
| Grain_size_ms | Analysis and reconstruction grain duration in milliseconds; must be greater than zero. |
| Grain_overlap_percent | Controls grain hop; valid range 0 to less than 100%. |
| Number_of_voices | Number of independently matched layers; minimum 1. |
| Neighbor_ranks | Comma-separated positive integer ranks. The final rank is repeated if the list is shorter than the voice count. |
| Preset | Applies the partial overrides listed above. |
| Feature weights | Non-negative scaling factors in standardized feature space. At least one must be greater than zero. |
| Randomness_amount | 0…1 rank jitter. Some presets override it. |
| Repetition_penalty | Avoids recently used corpus-grain indices during matching. |
| Output_mode | Mixdown, separate voices, or both. |
| Crossfade_duration | Per-grain cosine edge-fade duration in seconds; clamped to half the actual grain duration. |
| Envelope_shaping | Off, pause gate, intensity-derived envelope, or both. |
| Draw_visualization | Draws the result-oriented match visualization in Praat Picture. |
| Play_result | Plays the retained mix automatically when a mix is part of the chosen output mode. |
Dependencies and files
- Required: Praat, Python 3, NumPy.
- Optional: SciPy. When available,
cKDTreeperforms the neighbour queries; without it, NumPy performs exact exhaustive matching. - Corpus format: top-level
.wavfiles. - Python companion:
kd_tree_timbral_counterpoint.py, normally underplugin_AudioTools/py/or next to the Praat script.