Direct Formula Markov Synthesis — User Guide

Markov chain-controlled synthesis: generates evolving sequences using stochastic state transitions, where each state defines distinct sonic characteristics (frequency, duration, amplitude) with direct formula construction for maximum efficiency.

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

What this does

This script implements Markov chain synthesis — generating structured yet unpredictable sequences using stochastic state transitions. Process: (1) Creates N states (typically 8) with defined sonic properties (frequency, duration, amplitude, bandwidth), (2) Starts in random state, (3) For each event: generates sound based on current state properties, (4) Transitions to next state based on Markov rules (4 types available), (5) Builds single massive formula containing all events, (6) Synthesizes sound directly from formula, (7) Applies spectral filtering for polish. The result is structured sequences with controlled randomness, where state transitions follow probabilistic rules rather than pure randomness.

Key Features:

What are Markov chains in synthesis? Markov chain: stochastic process where next state depends only on current state (memoryless property). In music/sound: states = sonic configurations (pitches, timbres, rhythms), transitions = probabilities between configurations. Advantages: (1) Structured randomness: Not pure random, follows rules. (2) Controlled evolution: Can design likely/unlikely transitions. (3) Reproducible: Same parameters = same sequence (deterministic with random seed). (4) Musical: Creates patterns with repetition and variation. This script: states are defined by frequency, duration, amplitude, bandwidth. Transitions follow one of four rule sets. Each state transition generates a sound event. Result: sequences that sound composed but are algorithmically generated.

Technical Implementation: (1) State initialization: Create arrays for state properties: frequency (logarithmic spread from base), duration (increasing with state), amplitude (increasing with state), bandwidth (increasing with state). (2) Formula building: Start with formula$ = "0". While time < duration: get current state properties, build event formula with conditionals (if x >= time and x < time+duration), add harmonics if enabled, add envelope (Hanning or exponential), concatenate to formula$. (3) State transition: Apply Markov rules based on markov_type. (4) Synthesis: Create Sound from formula with the complete formula string. (5) Post-processing: Filter with Hann bandpass, normalize amplitude. Key innovation: Direct formula construction avoids object creation overhead, though formula length can become extreme for long durations/high densities.

Quick start

  1. In Praat, ensure no objects selected (generates from scratch).
  2. Run script…direct_formula_markov_synthesis.praat.
  3. Set Duration (12 seconds typical, shorter for testing).
  4. Set Number_states (8 recommended for first try).
  5. Set Base_frequency (100Hz for bass, 440Hz for mid-range).
  6. Set Event_density (5 events/sec for sparse, 15+ for dense).
  7. Choose Markov_type (Simple_chain recommended for first try).
  8. Enable Enable_harmonics for richer sounds.
  9. Enable Enable_envelopes for smoother events.
  10. Adjust Transition_randomness (0.3 for balanced).
  11. Click OK — formula builds with progress updates.
  12. Output named "Markov_Synthesis" appears in Objects window.
  13. Output automatically played when complete.
Quick tip: Start with Simple_chain Markov type to hear clear state transitions. Use 8 states for good variety without complexity. Base_frequency 100-200Hz creates audible pitch differences between states. Event_density 3-8 events/sec allows clear perception of individual states. Enable Enable_harmonics for richer, more musical sounds. Enable Enable_envelopes for smooth event boundaries. Transition_randomness 0.2-0.4 balances structure and surprise. Monitor echo output during synthesis — high event counts may cause formula length issues. For longer durations, use lower event density.
Important: FORMULA LENGTH EXPLOSION — each event adds ~200-500 characters to formula. Event count = duration × density — 60 seconds × 10 events/sec = 600 events × 300 chars = 180,000 characters (may be okay). Praat formula limit ~1,000,000 characters practical. Very long/high density combinations may crash. NO CHUNKING — single formula for entire duration. State history stored but not used in synthesis. Deterministic with random seed — same parameters produce same sequence. Filtering applied post-synthesis — raw output may have extreme frequencies. Progress updates every 50 events — monitor for performance.

Markov Chain Theory

Markov Process Fundamentals

What is a Markov Chain?

⛓️ Markov Chain Properties

Memoryless property: P(next state | all past states) = P(next state | current state)

State space: Finite set of states S = {s₁, s₂, ..., sₙ}

Transition probabilities: Pᵢⱼ = P(stateⱼ at t+1 | stateᵢ at t)

Transition matrix: Square matrix P where Pᵢⱼ ≥ 0 and Σⱼ Pᵢⱼ = 1

Initial distribution: Probability distribution over initial state

Musical Markov Chains

Musical interpretation:
States = musical parameters (pitch, rhythm, timbre, etc.)
Transitions = compositional rules

Example: Pitch Markov Chain
States: C, D, E, F, G, A, B
Transition probabilities:
C → D: 0.3, C → E: 0.2, C → G: 0.4, C → C: 0.1
D → E: 0.4, D → F: 0.3, D → A: 0.2, D → D: 0.1
etc.

Result: Melodies that follow probabilistic rules
Not purely random, not purely deterministic
"Composed" feeling with controlled variation

Script Implementation

Discrete-Time Markov Process

Script as discrete-time Markov chain: Time: t = 0, 1, 2, ... (event indices) State space: S = {1, 2, ..., number_states} Current state: s_t ∈ S Transition function depends on markov_type: 1. Simple_chain: Probabilities based on randomness parameter 2. Circular_chain: Deterministic s_{t+1} = (s_t mod n) + 1 3. Random_walk: s_{t+1} = s_t + randomInteger(-2, 2) 4. Biased_chain: Moves toward center state State duration: Each state lasts for its defined duration (state_dur#) Time advances by current_dur after each event Not fixed time steps, but event-based Initial state: randomUniform(1, number_states)

State-Dependent Sound Generation

🎵 State to Sound Mapping

Each state defines:

  • Frequency (pitch center)
  • Duration (event length)
  • Amplitude (loudness)
  • Bandwidth (spectral width, used in filtering)

Per-event variations:

  • Duration: ±30% random variation
  • Amplitude: ±20% random variation
  • Harmonics: optional 3 harmonics
  • Envelope: Hanning or exponential

Result: Each state has characteristic sound but with natural variation

Mathematical Formulation

State Property Calculation

State properties (for state i = 1 to n): Frequency: f_i = base_frequency × 2^((i-1)/n) Interpretation: Logarithmic spread over one octave Example: n=8, base=100Hz i=1: 100 × 2^(0/8) = 100Hz i=2: 100 × 2^(1/8) ≈ 109Hz (semitone up) i=3: 100 × 2^(2/8) ≈ 119Hz (whole tone up) ... i=8: 100 × 2^(7/8) ≈ 188Hz i=9 would be: 100 × 2^(8/8) = 200Hz (octave) Duration: d_i = 0.15 + (i/n) × 0.2 seconds Range: 0.15-0.35 seconds Higher states = longer durations Amplitude: a_i = 0.4 + (i/n) × 0.4 Range: 0.4-0.8 Higher states = louder Bandwidth: bw_i = 0.1 + (i/n) × 0.4 Range: 0.1-0.5 Higher states = wider bandwidth (filter parameter)

Event Count Calculation

Expected event count: total_events = round(duration × event_density) But script uses: while current_time < duration and event_count < total_events × 3 The ×3 factor is safety margin because: - Events have varying durations - May need more events to fill duration - Prevents infinite loops Actual event count determination: Events placed sequentially in time Event i starts at time t_i, lasts d_i Next event starts at t_{i+1} = t_i + d_i Continues until t_i ≥ duration or count limit reached Because durations vary, number of events ≠ duration × density exactly But approximately proportional

State System Design

State Property Ranges

🎚️ State Parameter Ranges (for n=8 states)

Frequency (logarithmic spread):

1
100Hz
2
109Hz
3
119Hz
4
130Hz
5
142Hz
6
155Hz
7
170Hz
8
188Hz

Duration: 0.15s → 0.35s (linear increase)

Amplitude: 0.4 → 0.8 (linear increase)

Bandwidth: 0.1 → 0.5 (linear increase)

State Property Calculations

StateFrequency (Hz)
base=100, n=8
Duration (s)AmplitudeBandwidthMusical Interval
from State 1
1100.00.1500.4000.100Unison
2109.00.1750.4500.163Minor 2nd
3119.20.2000.5000.225Major 2nd
4130.00.2250.5500.288Minor 3rd
5141.80.2500.6000.350Major 3rd
6154.80.2750.6500.413Perfect 4th
7169.00.3000.7000.475Tritone
8188.00.3250.7500.538Perfect 5th
9*200.00.3500.8000.600Octave

*State 9 shown for comparison (would require n=9 states)

Per-Event Variations

Random variations applied to each event: Duration variation: current_dur = state_dur × (0.7 + 0.6 × randomUniform(0,1)) Example: state_dur = 0.2s → current_dur ∈ [0.14, 0.26]s ±30% variation around state duration Amplitude variation: current_amp = state_amp × (0.8 + 0.4 × randomUniform(0,1)) Example: state_amp = 0.6 → current_amp ∈ [0.48, 0.72] ±20% variation around state amplitude Frequency: no per-event variation (except harmonics) Pure state frequency (for pitch clarity) Can add variation by increasing number_states Purpose of variations: - Prevents mechanical repetition - Creates natural, organic feel - Maintains state identity while adding nuance - Essential for avoiding robotic sound

Harmonic Content Generation

When Enable_harmonics = yes: For each event, add 3 harmonics: Harmonic 1: fundamental (state frequency × 1) Harmonic 2: octave (state frequency × 2) Harmonic 3: octave+fifth (state frequency × 3) Amplitudes: harmonic_amp = current_amp / (harmonic × 1.5) Harmonic 1: full amplitude (divided by 1.5) Harmonic 2: half amplitude (divided by 3) Harmonic 3: one-third amplitude (divided by 4.5) Formula construction: harmonic_content$ = amp1 + "*sin(2*pi*" + f1 + "*x)" + " + " + amp2 + "*sin(2*pi*" + f2 + "*x)" + " + " + amp3 + "*sin(2*pi*" + f3 + "*x)" wave_formula$ = "(" + harmonic_content$ + ")" Result: Richer, more musical tone Especially effective for lower states (bass frequencies) Adds spectral evolution as states change

Envelope Options

Two envelope types: 1. Hanning envelope (Enable_envelopes = yes): envelope$ = " * (1 - cos(2*pi*(x-start)/dur))/2" Properties: - Smooth fade in from zero - Smooth fade out to zero - Peak at center - No clicks at boundaries - Classic granular envelope 2. Exponential decay (Enable_envelopes = no): envelope$ = " * exp(-3*(x-start)/dur)" Properties: - Immediate start at full amplitude - Exponential decay to near zero - More percussive character - Natural decay like plucked string Musical differences: Hanning: smoother, more legato, pad-like Exponential: sharper, more staccato, plucked-like Implementation: Envelope multiplies the oscillator waveform Applied within time bounds: start ≤ x < start+dur

Transition Types

Type 1: Simple Chain

🔗 Simple Chain (Markov_type = 1)

Transition logic:

r = randomUniform(0,1) if r < 0.6 - transition_randomness/2: current_state = current_state # Stay in same state (60%) elsif r < 0.9 - transition_randomness/3: direction = randomInteger(0,1)*2 - 1 # -1 or +1 current_state = current_state + direction # Adjacent state (30%) current_state = max(1, min(number_states, current_state)) else: current_state = randomInteger(1, number_states) # Random jump (10%)

Probability structure:

  • Stay: 0.6 - randomness/2 (default 0.45 with randomness=0.3)
  • Adjacent: 0.3 - randomness/3 (default 0.2 with randomness=0.3)
  • Random jump: 0.1 + randomness/6 (default 0.15 with randomness=0.3)

Character: Mostly stays in same state, sometimes moves to neighbors, rarely jumps randomly

Sonic result: Stable with occasional small changes, rare surprises

Best for: Stable textures, minimal evolution, background patterns

Type 2: Circular Chain

🔄 Circular Chain (Markov_type = 2)

Transition logic:

current_state = current_state + 1 if current_state > number_states: current_state = 1

Deterministic cycle: 1 → 2 → 3 → ... → n → 1 → 2 → ...

No randomness: Completely predictable sequence

Musical pattern: Ascending frequency pattern that cycles

Duration effect: Events get progressively longer (state duration increases)

Amplitude effect: Events get progressively louder

Character: Predictable, cyclical, pattern-based

Sonic result: Rising sequence that cycles, getting longer and louder each cycle

Best for: Pattern-based music, ostinatos, rhythmic sequences

Type 3: Random Walk

🎲 Random Walk (Markov_type = 3)

Transition logic:

step = randomInteger(-2, 2) current_state = current_state + step current_state = max(1, min(number_states, current_state))

Step distribution: -2, -1, 0, 1, 2 with equal probability

Boundary handling: Clamped to [1, number_states]

Movement character: Can stay, move ±1, or jump ±2

Random walk properties: Drifts through state space, tends to explore all states

No attraction: No tendency to return to center or previous states

Character: Exploratory, wandering, unpredictable but bounded

Sonic result: Meandering through frequencies, unpredictable but not extreme

Best for: Exploratory textures, wandering melodies, abstract patterns

Type 4: Biased Chain

⚖️ Biased Chain (Markov_type = 4)

Transition logic:

center_state = round(number_states/2) if current_state < center_state: current_state = current_state + 1 # Move toward center elsif current_state > center_state: current_state = current_state - 1 # Move toward center else: if randomUniform(0,1) < 0.7: current_state = current_state # Stay at center (70%) else: current_state = current_state + randomInteger(-1,1) # Small perturbation current_state = max(1, min(number_states, current_state))

Center attraction: Always moves toward center state when not at center

Center behavior: Mostly stays at center, occasionally perturbs

Mathematical form: Ornstein-Uhlenbeck process (mean-reverting random walk)

Stability property: Returns to center after perturbations

Character: Stable with excursions, home-seeking, centered

Sonic result: Mostly centered around middle frequency, with occasional excursions that return

Best for: Stable backgrounds with variation, centered textures, tension-release patterns

Transition Type Comparison

TypeRandomnessPredictabilityMovement PatternState CoverageSonic Character
Simple ChainMediumMediumMostly static, small stepsLocalStable, occasional changes
Circular ChainNoneHighDeterministic cycleAll (systematic)Patterned, cyclical
Random WalkHighLowWandering, unbounded*All (eventually)Explor