Harmonic Tension Sorted Grains — User Guide

Randomly samples grains, reduces each grain's spectrum to a set of unique 12-TET pitch classes, assigns a user-defined interval-class tension score, and reorders the grains from higher to lower score or vice versa.

Author: Shai Cohen Affiliation: Department of Music, Bar-Ilan University, Israel Version: 1.4 (2026) License: MIT License Repo: GitHub
Contents:

What this does

Harmonic Tension Sorted Grains is a granular reordering tool. It draws grain locations randomly from the selected Sound, analyzes each grain as a mono spectral snapshot, derives a set of pitch classes, computes one interval-class score, and then sorts the original-channel-count grains by that score.

The processing chain is:

  1. Choose how many random grain samples to draw.
  2. Extract a Hanning-windowed mono analysis grain and the matching multichannel render grain.
  3. Divide the requested analysis range into logarithmic frequency bands.
  4. Take at most one sufficiently strong spectral maximum from each band.
  5. Map those maxima to 12-TET pitch classes and remove duplicates.
  6. Average fixed interval-class weights over all unique pitch-class pairs.
  7. Sort the grains by score and concatenate them, optionally inserting digital-silence gaps.
Channel behavior: spectral analysis uses a mono fold only. The grains used in the output are extracted from the original Sound, so mono, stereo and higher channel counts are preserved.

What “harmonic tension” means here

The score is a compositional pitch-class interval metric. It is not a psychoacoustic roughness model, not a sensory-dissonance calculation, and not a prediction of perceived consonance for arbitrary audio.

The script assigns fixed weights to interval classes:

Interval classSemitone-class relationWeight
IC 0unison / octave class0.00
IC 1m2 / M71.00
IC 2M2 / m70.50
IC 3m3 / M60.50
IC 4M3 / m60.10
IC 5P4 / P50.10
IC 6tritone1.00
Interpret the labels compositionally: the visualization calls scores below 0.35 “Consonant”, 0.35–0.75 “Medium”, and 0.75 or above “High”. These are fixed categories of this script's score, not universal perceptual thresholds.

Quick start

  1. Select exactly one Sound object.
  2. Run Harmonic_Tension_Sorted_Grains.praat.
  3. Choose Custom or one of the eight named presets.
  4. Choose Chaos to Clarity for descending score or Clarity to Chaos for ascending score.
  5. For Custom, set grain size, sampling density, spectral-band count and analysis range.
  6. Set Gap_between_grains_ms to 0 for butt concatenation or a positive value for digital silence between grains.
  7. Run the script. The result is named <source>_HTsorted_<preset>.

Grain sampling

How Sampling_overlap works

Sampling_overlap_(0-0.8) controls only the number of random grain draws. It does not create overlap in the final timeline.

baseDuration = Grain_size_ms / 1000 hopTime = max(baseDuration × (1 - Sampling_overlap), 1/sampleRate) numberOfGrains = round(sourceDuration / hopTime) numberOfGrains = min(numberOfGrains, Max_grains)

The parameter therefore acts like a density control: larger values request more random samples from the source. All named presets use this rule.

Grain duration

With Fixed mode, every grain uses the base duration, limited to the source duration and a minimum of two samples. With Random mode:

grainDuration = baseDuration + uniform(-variation, +variation)

The result is then clamped to the legal range from two samples up to the source duration.

Source position

Every grain start is drawn independently and uniformly from the legal source-start range. Sampling_overlap is not a source-hop relation; two consecutive draws can come from anywhere in the source.

Spectral peaks → unique pitch classes

For analysis only, the source is converted to mono. Each random grain is extracted with a Hanning window, converted to a Spectrum, then to Ltas (1-to-1) so the spectral frequency resolution is retained.

Logarithmic analysis bands

The effective upper frequency is:

analysisMax = min(Max_frequency_Hz, 0.98 × Nyquist)

The range Min_frequency_Hz ... analysisMax is divided into Number_of_peaks non-overlapping logarithmic bands:

bandRatio = (analysisMax / analysisMin)^(1 / Number_of_peaks)

Each band contributes at most one local spectral maximum. A candidate is accepted only when its level is no more than Peak_relative_threshold_dB below the strongest maximum anywhere in the full analysis range:

accept if bandPeak_dB >= globalPeak_dB - Peak_relative_threshold_dB

Pitch-class mapping

MIDI = 69 + 12 × log2(frequency / 440) pitchClass = round(MIDI) mod 12

The frequency is therefore quantized to the nearest 12-TET semitone class. If several bands map to the same pitch class, that class is stored once. The score is computed on a true pitch-class set, not a multiset of repeated detections.

Interval-class tension score

For every unordered pair of unique pitch classes, the pitch-class distance is folded into interval class 0–6:

raw = abs(pc[i] - pc[j]) mod 12 IC = raw if raw <= 6 else 12 - raw

The grain score is the arithmetic mean of the corresponding IC weights:

score = sum(weight[IC(i,j)]) / numberOfPairs

Because all weights lie between 0 and 1, the resulting score also lies between 0 and 1.

Fewer than two valid pitch classes: there is no pitch-class pair to score, so the script assigns 0. This means “no pairwise tension evidence under this metric”; it should not be read as proof that a noisy, weak or spectrally ambiguous grain is perceptually consonant.

Sorting & assembly

Sort direction

The insertion sort is stable, so grains with equal scores retain their original random-draw order.

Final timeline

The sorted Hanning-windowed grains are copied in the desired order and concatenated once. If Gap_between_grains_ms is positive, a matching-channel digital-silence object is inserted between successive grains.

There is no output-grain overlap or crossfade stage. Sampling_overlap affects the number of sampled grains only. The final renderer uses butt concatenation plus the optional explicit silence gap.
outputDuration = sum(rendered grain durations) + (grainCount - 1) × gapDuration

Eight presets

PresetGrainSampling overlapMax grainsPeak bandsMax HzSortGap
Chaos Purge80 ± 20 ms0.3020045000Chaos → Clarity0 ms
Dawn Resolve150 ± 50 ms0.5015044000Chaos → Clarity20 ms
Tritone Hell60 ± 15 ms0.2030056000Clarity → Chaos5 ms
Perfect Fifth Cloud200 ± 60 ms0.6012033000Chaos → Clarity30 ms
Micro Tension30 ± 10 ms0.1040038000Chaos → Clarity0 ms
Slow Meditation400 ± 100 ms0.708043000Chaos → Clarity50 ms
Reverse Resolve100 ± 30 ms0.3020045000Clarity → Chaos10 ms
Extreme Purge50 ± 20 ms0.0050058000Chaos → Clarity0 ms

All named presets set Random grain-size mode. They overwrite grain size/variation, Sampling_overlap, Max_grains, Number_of_peaks, Max_frequency_Hz, sort direction and gap. They do not overwrite Min_frequency_Hz, Peak_relative_threshold_dB, Draw_visualization or Play_result.

Parameters & limits

ParameterDefaultBehavior
PresetCustomCustom plus eight named strategies.
Grain_size_ms100 msBase grain duration; must be > 0.
Grain_size_variation_ms20 msUniform ± duration variation; must be ≥ 0.
Grain_size_modeFixedFixed ignores the variation value; Random applies it.
Sampling_overlap0.300–0.8. Controls sample count, not final overlap.
Max_grains2002–5000.
Number_of_peaks42–12 logarithmic analysis bands.
Min_frequency_Hz80 HzLower analysis bound; must be > 0.
Max_frequency_Hz5000 HzRequested upper bound; internally limited to 98% of Nyquist.
Peak_relative_threshold_dB35 dBRejects band maxima more than this amount below the full-range spectral maximum.
Sort_directionChaos → ClarityDescending or ascending score.
Gap_between_grains_ms10 msDigital silence between sorted grains; may be 0.
Draw_visualizationOnDraw analysis summary.
Play_resultOnPlay the created Sound.

Visualization

The Picture window shows the transformation and score distribution:

The tension axis is intentionally the script's absolute 0–1 metric rather than an autoscaled range, so the 0.35 and 0.75 class boundaries retain the same meaning from one run to another.

Output behavior

Normalization and edge fades

After concatenation, every non-silent result is peak-normalized to 0.90 using Sinc70 peak measurement. The script then applies a short raised-cosine fade at both ends:

edgeFade = min(20 ms, outputDuration / 4)

Because the fades are applied after normalization and there is no second normalization pass, the final peak can end below 0.90 if the previous maximum lies inside an edge fade.

Duration, channels and naming

Further Reading