Feature-Driven Sidechain Examples

May 25, 2026 ยท View on GitHub

pvx logo

Feature-Driven Sidechain Examples

This guide shows copy-paste command-line interface (CLI) recipes where one file (the guide) controls another file (the target) using tracked audio features.

  • Guide file: guide.wav (feature source).
  • Target file: target.wav (sound being transformed).
  • Output folder in examples: out/.
  • Most recipes use pvx follow (shortest path) and --route formulas.

Create an output folder once:

mkdir -p out maps

Quick built-in recipe discovery:

pvx follow --example
pvx follow --example all
pvx follow --example mfcc_flux

1) Quick Start

Basic pitch-to-stretch sidechain:

pvx follow guide.wav target.wav --emit pitch_to_stretch --pitch-conf-min 0.75 --output out/follow_basic.wav

Direct pitch-follow (guide pitch contour drives target pitch contour):

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 --output out/follow_pitch.wav

Track first, render later (decoupled analysis/render):

pvx pitch-track guide.wav --feature-set all --mfcc-count 13 --output maps/guide_features.csv
pvx voc target.wav --pitch-map maps/guide_features.csv \
  --route stretch=pitch_ratio \
  --route pitch_ratio=const(1.0) \
  --output out/track_then_render.wav

2) Inspect Feature Columns Before Routing

Emit all tracked columns:

pvx pitch-track guide.wav --feature-set all --mfcc-count 20 --output maps/guide_all.csv

Print the CSV header as one column per line:

head -n 1 maps/guide_all.csv | tr ',' '\n'

Useful columns include:

  • pitch/voicing: f0_hz, pitch_ratio, confidence, voicing_prob, pitch_stability
  • spectral: spectral_centroid_hz, spectral_flux, spectral_flatness, rolloff_hz
  • dynamics: rms, rms_db, short_lufs_db, transientness, crest_factor_db
  • timbre vectors: mfcc_01..mfcc_N, mpeg7_*, mpeg7_audio_spectrum_envelope_01..10

3) Single-Feature Recipes

3.1 f0_hz -> pitch ratio

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(f0_hz,0.002272727,0.0) \
  --route pitch_ratio=clip(pitch_ratio,0.5,2.0) \
  --output out/f0_to_pitch.wav

3.2 pitch_ratio -> stretch (inverse motion)

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=inv(pitch_ratio) \
  --route stretch=clip(stretch,0.7,1.8) \
  --route pitch_ratio=const(1.0) \
  --output out/pitch_to_stretch_inverse.wav

3.3 confidence -> subtle pitch bend depth

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(confidence,0.25,0.9) \
  --route pitch_ratio=clip(pitch_ratio,0.85,1.15) \
  --output out/confidence_to_pitch.wav

3.4 voicing_prob -> stretch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(voicing_prob,0.6,0.8) \
  --route stretch=clip(stretch,0.8,1.4) \
  --route pitch_ratio=const(1.0) \
  --output out/voicing_to_stretch.wav

3.5 rms_norm -> stretch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(rms_norm,1.2,0.6) \
  --route stretch=clip(stretch,0.8,1.8) \
  --route pitch_ratio=const(1.0) \
  --output out/rms_to_stretch.wav

3.6 spectral_flux -> stretch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(spectral_flux,0.04,1.0) \
  --route stretch=clip(stretch,0.85,1.6) \
  --route pitch_ratio=const(1.0) \
  --output out/flux_to_stretch.wav

3.7 onset_strength -> attack-reactive stretch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(onset_strength,0.05,0.95) \
  --route stretch=clip(stretch,0.75,1.7) \
  --route pitch_ratio=const(1.0) \
  --output out/onset_to_stretch.wav

3.8 transientness -> transient-safe stretch variation

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(transientness,-0.4,1.2) \
  --route stretch=clip(stretch,0.8,1.2) \
  --route pitch_ratio=const(1.0) \
  --output out/transientness_to_stretch.wav

3.9 spectral_centroid_hz -> brightness-linked pitch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(spectral_centroid_hz,0.00025,0.7) \
  --route pitch_ratio=clip(pitch_ratio,0.7,1.6) \
  --output out/centroid_to_pitch.wav

3.10 spectral_flatness -> noisy/tonal pitch response

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(spectral_flatness,-0.5,1.25) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.25) \
  --output out/flatness_to_pitch.wav

3.11 harmonic_ratio -> harmonicity-linked pitch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(harmonic_ratio,0.3,0.85) \
  --route pitch_ratio=clip(pitch_ratio,0.85,1.2) \
  --output out/harmonic_ratio_to_pitch.wav

3.12 inharmonicity -> anti-harmonic pitch detune

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(inharmonicity,-0.8,1.2) \
  --route pitch_ratio=clip(pitch_ratio,0.75,1.2) \
  --output out/inharmonicity_to_pitch.wav

3.13 rolloff_norm -> pitch

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(rolloff_norm,0.45,0.8) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.3) \
  --output out/rolloff_to_pitch.wav

3.14 pitch_stability -> micro-variation amount

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(pitch_stability,0.2,0.9) \
  --route pitch_ratio=clip(pitch_ratio,0.9,1.12) \
  --output out/pitch_stability_to_pitch.wav

4) Multi-Feature Recipes (Different Features -> Different Targets)

4.1 MFCC + MPEG-7 spectral flux

pvx follow guide.wav target.wav --feature-set all --mfcc-count 13 --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mfcc_01,0.002,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.5,2.0) \
  --route stretch=affine(mpeg7_spectral_flux,0.05,1.0) \
  --route stretch=clip(stretch,0.85,1.6) \
  --output out/mfcc_flux_dual.wav

4.2 Formant + onset

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(formant_f1_hz,0.0016,0.2) \
  --route pitch_ratio=clip(pitch_ratio,0.7,1.5) \
  --route stretch=affine(onset_norm,-0.35,1.2) \
  --route stretch=clip(stretch,0.8,1.3) \
  --output out/formant_onset_dual.wav

4.3 Beat phase + downbeat phase

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(beat_phase,0.12,0.94) \
  --route pitch_ratio=clip(pitch_ratio,0.88,1.12) \
  --route stretch=affine(downbeat_phase,0.3,0.85) \
  --route stretch=clip(stretch,0.85,1.25) \
  --output out/beat_downbeat_dual.wav

4.4 Tempo + centroid

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route stretch=affine(tempo_bpm,0.003,0.6) \
  --route stretch=clip(stretch,0.8,1.4) \
  --route pitch_ratio=affine(centroid_norm,0.4,0.8) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.3) \
  --output out/tempo_centroid_dual.wav

4.5 Stereo cues: interaural level difference + interaural time difference

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(ild_db,0.02,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.9,1.1) \
  --route stretch=affine(itd_ms,0.12,1.0) \
  --route stretch=clip(stretch,0.9,1.15) \
  --output out/ild_itd_dual.wav

4.6 Noise-aware: hiss + hum

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route stretch=affine(hiss_ratio,-0.6,1.2) \
  --route stretch=clip(stretch,0.8,1.2) \
  --route pitch_ratio=affine(hum_60_ratio,-0.4,1.15) \
  --route pitch_ratio=clip(pitch_ratio,0.9,1.2) \
  --output out/noise_aware_dual.wav

4.7 Speech vs music probabilities

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route stretch=affine(speech_prob,0.5,0.8) \
  --route stretch=clip(stretch,0.8,1.35) \
  --route pitch_ratio=affine(music_prob,0.2,0.9) \
  --route pitch_ratio=clip(pitch_ratio,0.9,1.2) \
  --output out/speech_music_dual.wav

4.8 Formant 2 + spectral spread

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(formant_f2_hz,0.0008,0.5) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.5) \
  --route stretch=affine(spectral_spread_hz,0.00008,0.85) \
  --route stretch=clip(stretch,0.8,1.4) \
  --output out/formant2_spread_dual.wav

4.9 Loudness proxy + transient mask

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route stretch=affine(short_lufs_db,0.02,1.4) \
  --route stretch=clip(stretch,0.75,1.4) \
  --route pitch_ratio=affine(transient_mask,0.1,0.95) \
  --route pitch_ratio=clip(pitch_ratio,0.95,1.08) \
  --output out/lufs_transientmask_dual.wav

4.10 MPEG-7 centroid + MPEG-7 spread

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mpeg7_spectral_centroid_hz,0.00022,0.75) \
  --route pitch_ratio=clip(pitch_ratio,0.7,1.5) \
  --route stretch=affine(mpeg7_spectral_spread_hz,0.00008,0.9) \
  --route stretch=clip(stretch,0.8,1.5) \
  --output out/mpeg7_centroid_spread.wav

4.11 MPEG-7 attack-time + MFCC driver

pvx follow guide.wav target.wav --feature-set all --mfcc-count 13 --emit pitch_map --stretch 1.0 \
  --route stretch=affine(mpeg7_log_attack_time_s,0.4,1.0) \
  --route stretch=clip(stretch,0.85,1.3) \
  --route pitch_ratio=affine(mfcc_03,0.0015,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.25) \
  --output out/mpeg7_attack_mfcc.wav

5) Route Operator Patterns (const, inv, pow, mul, add, affine, clip)

5.1 Start from a feature, then scale and bias

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=pitch_ratio \
  --route pitch_ratio=mul(pitch_ratio,0.85) \
  --route pitch_ratio=add(pitch_ratio,0.15) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.4) \
  --output out/route_mul_add.wav

5.2 Exaggerate movement with power law

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=affine(flux_norm,1.0,0.0) \
  --route stretch=pow(stretch,1.8) \
  --route stretch=affine(stretch,0.9,0.7) \
  --route stretch=clip(stretch,0.8,1.8) \
  --route pitch_ratio=const(1.0) \
  --output out/route_pow_flux.wav

5.3 Invert and clamp

pvx follow guide.wav target.wav --emit pitch_map --stretch 1.0 \
  --route stretch=inv(rms_norm) \
  --route stretch=clip(stretch,0.8,1.5) \
  --route pitch_ratio=const(1.0) \
  --output out/route_inv_rms.wav

6) Feature-Vector Recipes (MFCC + MPEG-7)

6.1 MFCC second coefficient driver

pvx follow guide.wav target.wav --feature-set all --mfcc-count 13 --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mfcc_02,0.0018,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.7,1.4) \
  --output out/mfcc02_pitch.wav

6.2 MFCC + MPEG-7 audio spectrum envelope band

pvx follow guide.wav target.wav --feature-set all --mfcc-count 13 --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mfcc_04,0.0015,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.75,1.35) \
  --route stretch=affine(mpeg7_audio_spectrum_envelope_03,0.08,1.0) \
  --route stretch=clip(stretch,0.85,1.4) \
  --output out/mfcc_envband_dual.wav

6.3 MPEG-7 spectral crest and decrease

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mpeg7_spectral_crest,0.03,0.9) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.25) \
  --route stretch=affine(mpeg7_spectral_decrease,-3.0,1.0) \
  --route stretch=clip(stretch,0.85,1.2) \
  --output out/mpeg7_crest_decrease.wav

6.4 Increase MFCC dimensionality to 20

pvx follow guide.wav target.wav --feature-set all --mfcc-count 20 --emit pitch_map --stretch 1.0 \
  --route pitch_ratio=affine(mfcc_10,0.0012,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.25) \
  --route stretch=affine(mfcc_12,0.03,1.0) \
  --route stretch=clip(stretch,0.9,1.2) \
  --output out/mfcc10_mfcc12_dual.wav

6.5 Build custom vector features, then route them

pvx pitch-track guide.wav --feature-set all --mfcc-count 20 --output maps/guide_vec.csv
python3 - <<'PY'
import csv
from pathlib import Path
src = Path('maps/guide_vec.csv')
dst = Path('maps/guide_vec_aug.csv')
with src.open() as f_in, dst.open('w', newline='') as f_out:
    r = csv.DictReader(f_in)
    fn = list(r.fieldnames or []) + ['mfcc_energy_1_4', 'mfcc_tilt_1_8']
    w = csv.DictWriter(f_out, fieldnames=fn)
    w.writeheader()
    for row in r:
        m1 = float(row.get('mfcc_01', 0.0) or 0.0)
        m2 = float(row.get('mfcc_02', 0.0) or 0.0)
        m3 = float(row.get('mfcc_03', 0.0) or 0.0)
        m4 = float(row.get('mfcc_04', 0.0) or 0.0)
        m8 = float(row.get('mfcc_08', 0.0) or 0.0)
        row['mfcc_energy_1_4'] = 0.25 * (m1 + m2 + m3 + m4)
        row['mfcc_tilt_1_8'] = m1 - m8
        w.writerow(row)
PY
pvx voc target.wav --pitch-map maps/guide_vec_aug.csv \
  --route pitch_ratio=affine(mfcc_tilt_1_8,0.0018,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.75,1.4) \
  --route stretch=affine(mfcc_energy_1_4,0.05,1.0) \
  --route stretch=clip(stretch,0.85,1.4) \
  --output out/vector_augmented.wav

7) Multiple Guide Files (A and B both influence target)

7.1 Blend pitch behavior from two guides

pvx pitch-track guide_A.wav --feature-set all --output maps/guide_A.csv
pvx pitch-track guide_B.wav --feature-set all --output maps/guide_B.csv
python3 - <<'PY'
import csv
from pathlib import Path

a = Path('maps/guide_A.csv')
b = Path('maps/guide_B.csv')
out = Path('maps/guide_blend.csv')
with a.open() as fa, b.open() as fb, out.open('w', newline='') as fo:
    ra = csv.DictReader(fa)
    rb = csv.DictReader(fb)
    rows_b = list(rb)
    fields = list(ra.fieldnames or [])
    if 'pitch_ratio' not in fields:
        fields.append('pitch_ratio')
    w = csv.DictWriter(fo, fieldnames=fields)
    w.writeheader()
    rows_b_len = max(1, len(rows_b))
    for i, row_a in enumerate(ra):
        row_b = rows_b[min(i, rows_b_len - 1)]
        pa = float(row_a.get('pitch_ratio', 1.0) or 1.0)
        pb = float(row_b.get('pitch_ratio', 1.0) or 1.0)
        row_a['pitch_ratio'] = 0.6 * pa + 0.4 * pb
        w.writerow(row_a)
PY
pvx voc target.wav --pitch-map maps/guide_blend.csv \
  --route pitch_ratio=clip(pitch_ratio,0.7,1.5) \
  --route stretch=const(1.0) \
  --output out/two_guide_pitch_blend.wav

7.2 Guide A controls pitch, guide B controls stretch

pvx pitch-track guide_A.wav --feature-set all --output maps/guide_A.csv
pvx pitch-track guide_B.wav --feature-set all --output maps/guide_B.csv
python3 - <<'PY'
import csv
from pathlib import Path

a = Path('maps/guide_A.csv')
b = Path('maps/guide_B.csv')
out = Path('maps/guide_Apitch_Bstretch.csv')
with a.open() as fa, b.open() as fb, out.open('w', newline='') as fo:
    ra = csv.DictReader(fa)
    rb = csv.DictReader(fb)
    rows_b = list(rb)
    fields = list(dict.fromkeys((ra.fieldnames or []) + ['stretch']))
    w = csv.DictWriter(fo, fieldnames=fields)
    w.writeheader()
    rows_b_len = max(1, len(rows_b))
    for i, row_a in enumerate(ra):
        row_b = rows_b[min(i, rows_b_len - 1)]
        flux_b = float(row_b.get('spectral_flux', 0.0) or 0.0)
        row_a['stretch'] = max(0.8, min(1.5, 1.0 + 0.03 * flux_b))
        w.writerow(row_a)
PY
pvx voc target.wav --pitch-map maps/guide_Apitch_Bstretch.csv \
  --route pitch_ratio=clip(pitch_ratio,0.75,1.35) \
  --route stretch=clip(stretch,0.8,1.5) \
  --output out/two_guide_split_roles.wav

8) Manual Pipe Variants

Pitch-track to pvx voc in one pipe:

pvx pitch-track guide.wav --feature-set all --mfcc-count 13 --output - \
  | pvx voc target.wav --control-stdin \
      --route pitch_ratio=affine(mfcc_01,0.002,1.0) \
      --route pitch_ratio=clip(pitch_ratio,0.5,2.0) \
      --route stretch=affine(mpeg7_spectral_flux,0.05,1.0) \
      --route stretch=clip(stretch,0.85,1.6) \
      --output out/manual_pipe_feature_follow.wav

Time-varying control map followed by denoise/deverb in one chain:

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --route stretch=affine(spectral_flux,0.04,1.0) \
  --route stretch=clip(stretch,0.9,1.4) \
  --route pitch_ratio=affine(mfcc_01,0.0015,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.85,1.2) \
  --output out/follow_stage.wav
pvx denoise out/follow_stage.wav --reduction-db 8 --output out/follow_stage_dn.wav
pvx deverb out/follow_stage_dn.wav --strength 0.35 --output out/follow_stage_dn_dr.wav

9) Reuse a Saved Feature Map for Fast Iteration

pvx pitch-track guide.wav --feature-set all --mfcc-count 13 --output maps/guide_full.csv
pvx voc target.wav --pitch-map maps/guide_full.csv \
  --route pitch_ratio=affine(mfcc_01,0.002,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.5,2.0) \
  --route stretch=affine(spectral_flux,0.04,1.0) \
  --route stretch=clip(stretch,0.85,1.6) \
  --output out/reused_map_follow.wav

pvx voc target.wav --pitch-map maps/guide_full.csv \
  --route pitch_ratio=affine(formant_f1_hz,0.0012,0.6) \
  --route pitch_ratio=clip(pitch_ratio,0.8,1.4) \
  --route stretch=affine(onset_norm,-0.3,1.2) \
  --route stretch=clip(stretch,0.8,1.3) \
  --output out/reused_map_alt_formula.wav

10) Compact Feature Set (Smaller CSV)

pvx pitch-track guide.wav --feature-set basic --output maps/guide_basic.csv
pvx voc target.wav --pitch-map maps/guide_basic.csv \
  --route pitch_ratio=affine(spectral_centroid_hz,0.0002,0.8) \
  --route pitch_ratio=clip(pitch_ratio,0.75,1.4) \
  --output out/basic_features_follow.wav

11) Safe Starting Ranges

When designing formulas, start conservative and widen only if needed:

  • pitch_ratio: 0.85 .. 1.15
  • stretch: 0.8 .. 1.4
  • confidence floor: 0.65 .. 0.85
  • map smoothing: --pitch-map-smooth-ms 10..40

Example conservative profile:

pvx follow guide.wav target.wav --feature-set all --emit pitch_map --stretch 1.0 \
  --pitch-conf-min 0.8 --pitch-lowconf-mode hold --pitch-map-smooth-ms 20 \
  --route pitch_ratio=affine(mfcc_01,0.001,1.0) \
  --route pitch_ratio=clip(pitch_ratio,0.9,1.1) \
  --route stretch=affine(spectral_flux,0.02,1.0) \
  --route stretch=clip(stretch,0.9,1.2) \
  --output out/conservative_sidechain.wav

Attribution