math-sonify 1.4.0

Real-time procedural audio from mathematical dynamical systems (Lorenz, Rossler, Double Pendulum, and more)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
# math-sonify

[![CI](https://github.com/Mattbusel/math-sonify/actions/workflows/ci.yml/badge.svg)](https://github.com/Mattbusel/math-sonify/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Rust 1.75+](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](https://www.rust-lang.org)

math-sonify is a real-time generative audio engine that runs mathematical dynamical systems (differential equations, maps, and coupled oscillators) and routes every variable of their evolving state directly into audio synthesis parameters. The Lorenz attractor is actually integrating at 120 Hz; the Kuramoto coupling constant is live; the Three-Body gravitational problem advances at each control frame. The result is not a preset synthesiser with math-themed names: the mathematics _is_ the music, and every parameter change propagates to sound within 8 ms.

It is a desktop app (egui GUI, cpal audio), a VST3/CLAP plugin built from the same code, and a headless WAV renderer. Useful if you make ambient or generative music, teach dynamical systems and want students to hear chaos onset, or just want a synth whose knobs are the parameters of the Lorenz equations.

| Lorenz phase portrait | Kuramoto math view | Waveform and spectrum |
| --- | --- | --- |
| ![Lorenz attractor phase portrait](screenshots/lorenz-phase-portrait.png) | ![Kuramoto oscillators in the math view](screenshots/kuramoto-math-view.png) | ![Waveform and FFT spectrum](screenshots/waveform-spectrum.png) |

---

## Feature highlights

1. **53 dynamical systems**: Lorenz, Rossler, Double Pendulum, Kuramoto, Three-Body, Hyperchaos (Chen-Li), WINDMI, Finance, all Sprott cases, Tinkerbell map, and more (full list below).
2. **9 sonification modes**: Direct, Orbital, Granular, Spectral, FM, AM, Vocal, Waveguide, Resonator.
3. **20 musical scales**: Pentatonic through Microtonal, EDO-19/24/31, Harmonic Series, Just Intonation.
4. **MIDI export**: trajectory-to-MIDI conversion; outputs Standard MIDI Files (SMF) importable into any DAW.
5. **Preset gallery**: 16+ named presets with mood tags, complexity ratings, favorites, and a discovery mode that surfaces less-played entries.
6. **Collaborative session mode**: real-time multi-user parameter control via a WebSocket server with per-participant colour highlights, conflict resolution, and full session replay log.
7. **Audio-driven ODE morphing**: reverse the sonification pipeline: incoming microphone audio extracts features (RMS, spectral centroid, flux, 8-band energy) and maps them to ODE parameters in real time. Can run simultaneously with the forward synthesis path (dual mode).
8. **Lyapunov exponent tracker**: real-time estimation of the maximal Lyapunov exponent; displayed in the MATH VIEW tab.
9. **FFT spectral overlay**: live FFT spectrum superimposed on the phase portrait and the WAVEFORM tab.
10. **Scene arranger**: 8-scene timeline with smooth parameter morphs; AUTO generator builds full arrangements from a mood pool.
11. **VST3 / CLAP plugin**: load inside Ableton, FL Studio, Logic Pro, Reaper, and any other NIH-plug-compatible DAW.
12. **Headless render**: `--headless --duration 60 --output clip.wav` with no display required.
13. **Live config reload**: edit `config.toml` while the engine runs; changes take effect without restart.

---

## 5-minute quickstart

### Pre-built binary

Download the file for your system from the [latest release](https://github.com/Mattbusel/math-sonify/releases/latest), unzip it and run `math-sonify`. Audio starts immediately on the system default output device. See [Installation](#installation) for which file to pick.

### Build from source

Requires [Rust](https://rustup.rs/) 1.75+ and a working audio output device.

```bash
git clone https://github.com/Mattbusel/math-sonify
cd math-sonify
cargo run --release
```

### Headless export (no GUI)

```bash
cargo run --release -- --headless --duration 60 --output clip.wav
```

---

## Architecture

```
ODE Solver (120 Hz, sim thread)
    |
    |  53 dynamical systems -- Lorenz, Rossler, Duffing, Kuramoto, Three-Body,
    |  Hyperchaos (4D), Finance, WINDMI, Liu, Genesio-Tesi, Shimizu-Morioka, ...
    |  RK4 integration per configured dt
    |
    v
Parameter Morphing (arrangement layer)
    |
    |  Scene arranger linearly interpolates all numeric config fields
    |  between named snapshots; string fields switch at midpoint
    |
    v
Sonification Mapper (sim thread, 120 Hz)
    |
    |  DirectMapping    -- state quantized to musical scale -> oscillator freqs
    |  OrbitalResonance -- angular velocity + Lyapunov exponent drive pitch
    |  GranularMapping  -- trajectory speed -> grain density and pitch
    |  SpectralMapping  -- state -> 32-partial additive envelope
    |  FmMapping        -- attractor drives carrier/modulator ratio and index
    |  AmMapping        -- amplitude modulation driven by state variables
    |  VocalMapping     -- state interpolates between vowel formant positions
    |  Waveguide        -- Karplus-Strong string with chaotic modulation
    |  Resonator        -- modal resonator bank driven by attractor
    |
    v  [crossbeam bounded channel, try_recv in audio callback]
    v
Audio Synthesis (audio thread, 44100 / 48000 Hz)
    |
    |  Per-layer DSP:
    |    Oscillator(s) [PolyBLEP anti-aliased] --> ADSR --> Waveshaper --> Bitcrusher
    |
    |  Master bus (shared across up to 4 layers):
    |    3-Band EQ --> LP BiquadFilter --> Stereo DelayLine --> Chorus
    |    --> FDN Reverb (8-channel, modulated) --> Lookahead Limiter
    |
    v
DAW (VST3 / CLAP plugin) or Desktop (standalone cpal output)
```

**Thread safety:** the sim thread and audio thread communicate through a bounded `crossbeam-channel` of capacity 16. The audio callback calls `try_recv` and renders silence on a miss, so it is never blocked. The UI thread reads shared state through `parking_lot::Mutex` on the control rate.

---

## Supported mathematical systems (53)

| System | Dim | Type | Notes |
|--------|-----|------|-------|
| Lorenz | 3 | chaos | Classic butterfly attractor; chaos onset near rho=24.74 |
| Rossler | 3 | chaos | Spiral attractor; period-doubling as c increases |
| Double Pendulum | 4 | chaos | Lagrangian mechanics (theta1, theta2, p1, p2); leapfrog integrator |
| Geodesic Torus | 4 | quasi-periodic | Ergodic irrational winding on a flat torus |
| Kuramoto | N | sync | N coupled oscillators; synchronization at critical K |
| Three-Body | 12 | chaos | Newtonian gravity, 3 point masses in 2D; figure-8 ICs |
| Duffing | 2 | chaos | Driven nonlinear oscillator; period-doubling cascade |
| Van der Pol | 2 | limit cycle | Self-sustaining limit cycle; relaxation oscillations |
| Halvorsen | 3 | chaos | Dense cyclic-symmetry spiral attractor |
| Aizawa | 3 | chaos | Six-parameter torus-like attractor |
| Chua | 3 | chaos | Piecewise-linear double-scroll circuit |
| Hindmarsh-Rose | 3 | chaos | Neuron firing model; bursting and spiking |
| Lorenz-96 | N | chaos | Weather model; spatiotemporal chaos at F > 8 |
| Mackey-Glass | DDE | chaos | Delay differential equation; history-dependent |
| Nose-Hoover | 3 | chaos | Thermostatted Hamiltonian; conservative chaos |
| Coupled Map Lattice | N | chaos | Logistic map on a 1D lattice with diffusive coupling |
| Henon Map | 2 | chaos | Discrete map; fractal strange attractor (dim ~1.26) |
| Custom ODE | 3-4 | user | User-defined equations via text input |
| Fractional Lorenz | 3 | chaos | Lorenz with derivative order alpha in (0.5, 1.0] |
| Logistic Map | 1 | chaos | Period-doubling route to chaos; bifurcation diagram classic |
| Standard Map | 2 | chaos | Area-preserving Chirikov map; KAM tori to global chaos |
| Arnold Cat | 2 | chaos | Ergodic linear torus map; hyperbolic fixed point |
| Stochastic Lorenz | 3 | chaos | Lorenz with additive Wiener noise per axis |
| Delayed Map | 1 | chaos | Logistic map with discrete delay tau |
| Oregonator | 3 | oscillation | Belousov-Zhabotinsky chemical reaction oscillator |
| Mathieu | 2 | parametric | Parametric resonance; stability tongues in a/q space |
| Kuramoto-Driven | N | sync | Kuramoto + external sinusoidal drive on first oscillator |
| Thomas | 3 | chaos | Conservative symmetric attractor; b~0.208 chaos boundary |
| Lorenz-84 | 3 | chaos | Low-order atmospheric circulation model |
| Dadras | 3 | chaos | Five-parameter attractor with rich bifurcation structure |
| Rucklidge | 3 | chaos | Double-scroll from a convection model |
| Chen | 3 | chaos | Lorenz-family; denser scroll than standard Lorenz |
| Burke-Shaw | 3 | chaos | Two-scroll; sigma/rho parameterization |
| Rabinovich-Fabrikant | 3 | chaos | Plasma wave instability model |
| Rikitake | 3 | chaos | Two coupled dynamos; geomagnetic reversal model |
| Bouali | 3 | chaos | Slow-manifold attractor; a/s parameterization |
| Newton-Leipnik | 3 | chaos | Two coupled rigid bodies; two coexisting attractors |
| Sprott B | 3 | chaos | Minimal 5-term polynomial system |
| Sprott C | 3 | chaos | Minimal polynomial; single quadratic term |
| Sprott D (Case I) | 3 | chaos | y^2 instability with -1.1z dissipation |
| Sprott E | 3 | chaos | Minimal chaos from a yz product |
| Sprott F | 3 | chaos | Slow-spiral; x^2 drives z |
| Sprott G | 3 | chaos | Linear + quadratic; minimal form |
| Sprott H | 3 | chaos | Single xz product nonlinearity |
| Sprott K | 3 | chaos | xy product; one of Sprott's simplest forms |
| Sprott L | 3 | chaos | Bounded strange attractor; yz coupling |
| Shimizu-Morioka | 3 | chaos | Two-scroll; x^2-driven z destabilizes y |
| Genesio-Tesi | 3 | chaos | Jerk circuit: one x^2 term is all the chaos needed |
| Liu | 3 | chaos | Single-band scroll; y^2 and xz/xy cross-coupling |
| WINDMI | 3 | chaos | Ionospheric substorm model; exponential nonlinearity |
| Finance | 3 | chaos | Macroeconomic chaos: interest rate, investment, price |
| Hyperchaos (Chen-Li) | 4 | hyperchaos | Two positive Lyapunov exponents; richer than ordinary chaos |
| Tinkerbell | 2 | chaos | Complex-plane map; orbit traps and fractal basins |

---

## Sonification modes (9)

| Mode | How math maps to audio |
|------|------------------------|
| Direct | State variables quantized to configured scale -> oscillator frequencies. Amplitude tracks normalized magnitude. |
| Orbital | State interpreted as polar coordinates. Angular velocity drives pitch; Lyapunov exponent modulates inharmonicity. |
| Granular | Trajectory speed controls grain spawn rate (0-50 grains/sec). Position in state space sets grain frequency. |
| Spectral | 32 additive partials. Each partial amplitude derived from a normalized component of the state vector. |
| FM | Two-operator FM synthesis. Carrier tracks first state variable; modulator ratio and index driven by remaining variables. |
| AM | Amplitude modulation. Carrier frequency from state; AM depth and rate driven by trajectory speed. |
| Vocal | State coordinates mapped to vowel formant positions (F1/F2). Trajectory wanders through /a/ /e/ /i/ /o/ /u/. |
| Waveguide | Karplus-Strong string model. Tension and damping modulated by the attractor in real time. |
| Resonator | Modal resonator bank. Attractor state excites a set of tuned resonant modes. |

---

## Generative Composition Engine (`src/composer.rs`)

The composition engine (`ComposerEngine`) turns the running attractor into a structured musical piece in real time, without any pre-written score.

### Musical forms

| Form | Description |
|------|-------------|
| ABA | Statement (A), contrast (B), varied return (A'). Section boundaries follow basin changes. |
| Theme & Variations | Theme derived from the initial attractor basin; each variation alters a different synthesis parameter. |
| Rondo | Refrain (A) alternates with episodes driven by bifurcation events. |
| Through-Composed | Linear succession of sections with no repeats; topology-driven. |
| Stochastic | Section boundaries placed by a pseudo-random walk seeded from the attractor's own bit-mixing. |

### Components

| Component | Role |
|-----------|------|
| `MotifGenerator` | Slides a window over recent pitches; finds the most-repeated sub-sequence as the current motif. |
| `HarmonicProgression` | Maps phase-space regions to chord progressions: Classical (I–IV–V–I), Jazz Turnaround (ii7–V7–Imaj7–VI7), Modal (scale-derived triads), Jazz Extended (9th/11th/13th tensions added proportional to chaos level). |
| `RhythmicQuantizer` | Snaps the continuous ODE output to a rhythmic grid. Supports 4/4, 3/4, 5/4, 6/8, and 7/8 time signatures with configurable sub-division. |
| `CompositionExporter` | Builds a 3-track MIDI SMF (melody, harmony, bass) and writes it to disk via the existing MIDI infrastructure. |

### MIDI export example

```rust
use math_sonify::composer::{ComposerEngine, MusicalForm, ProgressionStyle, TimeSig};

let mut engine = ComposerEngine::new(
    MusicalForm::Aba,
    ProgressionStyle::JazzTurnaround,
    TimeSig::FOUR_FOUR,
    120.0,   // BPM
    120.0,   // control rate Hz
    8,       // section length in bars
);

// Tick the engine each sim step:
let frame = engine.tick(&state, melody_pitch, velocity, chaos_level, lyapunov);
// frame.chord, frame.motif, frame.section_idx, frame.bar_position, …

// Export when done:
engine.export_midi("composition.mid", 120.0).ok();
```

---

## Fractal Dimension Analyzer (`src/fractal.rs`)

The fractal analyzer characterises the geometric and dynamical structure of the attractor in real time. All metrics are displayed in the **Math View** tab.

### Algorithms

| Algorithm | Struct | What it computes |
|-----------|--------|-----------------|
| Box-counting | `BoxCounting` | D₀ (Minkowski–Bouligand dimension). 2-D projection; slope of log N(ε) vs log(1/ε). Lorenz ≈ 2.05, Hénon ≈ 1.26. |
| Correlation dimension | `CorrelationDimension` | D₂ (Grassberger–Procaccia). Counts point pairs within distance r; slope of log C(r) vs log r. |
| Full Lyapunov spectrum | `LyapunovSpectrum` | All N exponents via QR/Gram–Schmidt on the tangent bundle. Kaplan–Yorke dimension D_KY = j + Σλᵢ/|λⱼ|. |

### `AttractorCharacterization` struct

```rust
pub struct AttractorCharacterization {
    pub fractal_dim: f64,                    // Box-counting D₀
    pub correlation_dim: f64,                // Grassberger-Procaccia D₂
    pub lyapunov_spectrum: Vec<f64>,         // Full spectrum λ₁ ≥ λ₂ ≥ ... ≥ λₙ
    pub kaplan_yorke_dim: f64,               // D_KY from the spectrum
    pub kolmogorov_entropy: f64,             // hKS = Σ positive λᵢ
    pub phase_space_volume_contraction: f64, // Σ all λᵢ (< 0 for dissipative)
    pub attractor_type: AttractorType,       // FixedPoint / LimitCycle / StrangeAttractor / Hyperchaos
    pub sample_size: usize,
    pub last_updated_ticks: u64,
}
```

`AttractorType` is derived automatically from the spectrum sign pattern:

| Type | Criterion |
|------|-----------|
| Fixed Point | All λᵢ < 0 |
| Limit Cycle | Exactly one zero exponent, rest negative |
| Quasi-Periodic (T²) | Two zero exponents |
| Strange Attractor | Exactly one positive exponent |
| Hyperchaos | Two or more positive exponents |

### Usage

```rust
use math_sonify::fractal::FractalAnalyzer;

let mut analyzer = FractalAnalyzer::new(3); // 3-D system

// Called periodically in the sim thread:
let ch = analyzer.analyze(&trajectory, &lorenz_deriv, dt, current_tick);
println!("{}", analyzer.lyapunov_spectrum().summary());
// λ = [+0.9053, -0.0001, -14.572]  D_KY=2.062  hKS=0.9053  div=-13.667
```

---

## Network of Coupled Oscillators (`src/network.rs`)

`OscillatorNetwork` places up to **16 coupled oscillators** on an arbitrary graph topology, with each oscillator mapped to a separate audio voice.

### Network topologies

| Topology | Description |
|----------|-------------|
| `Ring` | Each node connected to its two nearest neighbours (circular). |
| `StarGraph` | Central hub connected to all leaves; leaves only connect to the hub. |
| `SmallWorld(p)` | Watts–Strogatz: start from a ring and rewire each edge with probability p. |
| `RandomErdos(p)` | Each pair connected independently with probability p. |
| `FullyConnected` | All-to-all coupling; mean-field limit of the Kuramoto model. |

### Oscillator models

**Kuramoto network**: phase oscillators on an arbitrary graph:

```
dθᵢ/dt = ωᵢ + Σⱼ Kᵢⱼ sin(θⱼ − θᵢ)
```

Each oscillator's phase maps directly to an audio frequency. The order parameter r ∈ [0, 1] measures synchronisation.

**Stuart–Landau network**: complex-amplitude oscillators (normal form of the Hopf bifurcation):

```
dAᵢ/dt = (μᵢ + iωᵢ − |Aᵢ|²)·Aᵢ + Σⱼ Kᵢⱼ·Aⱼ
```

With diffusive coupling and heterogeneous μ, the network exhibits:
- **Amplitude death** (μ < 0 and coupling pushes all voices to zero).
- **Oscillation revival** (coupling restores oscillations suppressed by individual μ < 0).

### Audio voice mapping

Each tick, `OscillatorNetwork::state()` returns a `NetworkState`:

```rust
pub struct NetworkState {
    pub n: usize,
    pub frequencies: Vec<f64>,   // per-voice audio frequency (Hz)
    pub amplitudes: Vec<f64>,    // per-voice amplitude [0, 1]
    pub phases: Vec<f64>,        // oscillator phase (radians)
    pub order_parameter: f64,    // Kuramoto r or mean amplitude
    pub amplitude_death: bool,   // true if network collapsed to zero
    pub active_voices: usize,    // number of non-silent voices
}
```

`NetworkState::sorted_voices()` returns `(frequency, amplitude)` pairs sorted by amplitude, ready for polyphonic voice assignment.

### Example

```rust
use math_sonify::network::{OscillatorNetwork, NetworkTopology};

// 8 Kuramoto oscillators in a small-world graph.
let mut net = OscillatorNetwork::kuramoto(
    8,
    &NetworkTopology::SmallWorld { rewire_prob: 0.15 },
    2.0,   // coupling K
    0.4,   // natural frequency spread
    220.0, // base audio frequency (Hz)
    440.0, // frequency range (Hz)
    42,    // seed
);

// Sim loop (120 Hz):
net.step(1.0 / 120.0);
let st = net.state();
// Route st.frequencies[i] and st.amplitudes[i] to audio voice i.
println!("r = {:.3}  voices = {}", st.order_parameter, st.active_voices);
```

---

## Musical scales (20)

| Scale | Intervals (semitones) |
|-------|-----------------------|
| Pentatonic | 0, 2, 4, 7, 9 |
| Natural Minor (Aeolian) | 0, 2, 3, 5, 7, 8, 10 |
| Harmonic Minor | 0, 2, 3, 5, 7, 8, 11 |
| Dorian | 0, 2, 3, 5, 7, 9, 10 |
| Phrygian | 0, 1, 3, 5, 7, 8, 10 |
| Lydian | 0, 2, 4, 6, 7, 9, 11 |
| Mixolydian | 0, 2, 4, 5, 7, 9, 10 |
| Locrian | 0, 1, 3, 5, 6, 8, 10 |
| Whole Tone | 0, 2, 4, 6, 8, 10 |
| Blues | 0, 3, 5, 6, 7, 10 |
| Hirajoshi | 0, 2, 3, 7, 8 |
| Hungarian Minor | 0, 2, 3, 6, 7, 8, 11 |
| Octatonic (dim.) | 0, 2, 3, 5, 6, 8, 9, 11 |
| Chromatic | all 12 semitones |
| Just Intonation | pure-ratio tuning |
| Microtonal | 24 equal divisions per octave |
| EDO-19 | 19 equal divisions of the octave |
| EDO-31 | 31 equal divisions of the octave |
| EDO-24 | 24-TET (quarter-tones) |
| Harmonic Series | 16 partials of a fundamental |

---

## MIDI export guide

math-sonify can export attractor trajectories to Standard MIDI Files (SMF format 0) that import cleanly into Ableton Live, FL Studio, Logic Pro, Reaper, and any other DAW.

### Mapping

| Attractor coordinate | MIDI parameter |
|---|---|
| X | Note pitch -- quantised to the selected scale |
| Y | Velocity (64-127) |
| Z | Note duration (16th note to whole note, exponentially scaled) |
| Simulation speed | BPM written into the file tempo event |

### From the GUI

1. Open the **MIXER** tab.
2. Click **Record MIDI** to start capturing. The status bar shows the frame count.
3. Click **Stop + Export** to choose a filename and write the `.mid` file.

### From Rust code

```rust
use math_sonify::midi_export::{MidiExporter, SCALE_PENTATONIC_C4};

// trajectory is a Vec<(f64, f64, f64)> collected from the ODE solver
let exporter = MidiExporter::new();   // 480 ticks per quarter note
let track = exporter.trajectory_to_track(
    "Lorenz Take 1",
    &trajectory,
    SCALE_PENTATONIC_C4,
    120.0,   // BPM
);
exporter.export_to_file(&[track], "lorenz_take1.mid")?;
```

Multiple tracks can be passed to `export_smf` / `export_to_file`; they are merged into the single track required by SMF format 0, with each track's notes placed on a different MIDI channel (0-15) for DAW separation.

### Headless export

```bash
cargo run --release -- --headless --duration 30 --output take.wav --export-midi take.mid
```

---

## Preset gallery guide

math-sonify ships with 16 named presets organised in a browsable in-memory catalogue. Each preset carries:

- **System** -- which dynamical system it uses.
- **Mood tags** -- `atmospheric`, `rhythmic`, `experimental`, `meditative`, `melodic`, `percussive`, `drone`, `eerie`, `evolving`, `hypnotic`, `minimalist`, `complex`, `electronic`, `energetic`.
- **BPM range** -- the tempo window in which the preset sounds best.
- **Complexity** -- 1 (minimal) to 5 (dense).

### In the GUI

The **SYNTH** tab has a **Presets** panel with:

- A scrollable list filtered by mood or system.
- A search box for partial name/description match.
- A heart icon to toggle favorites.
- A **Discover** button that picks a random preset weighted toward entries you have played least.

### From Rust code

```rust
use math_sonify::preset_gallery::PresetGallery;

let mut gallery = PresetGallery::with_builtin_presets();

// Filter by mood
let drones = gallery.by_mood("drone");

// Search
let results = gallery.search("butterfly");

// Random discovery (weighted by inverse play count)
if let Some(preset) = gallery.random_discovery() {
    println!("Try: {} ({})", preset.name, preset.system);
    gallery.record_play(&preset.name.clone());
}

// Favorites
gallery.toggle_favorite("Lorenz Ambience");
let favs = gallery.favorites();
```

### Built-in presets

| Name | System | Moods | Complexity |
|------|--------|-------|-----------|
| Lorenz Ambience | Lorenz | atmospheric, meditative, melodic | 2 |
| Pendulum Rhythm | Double Pendulum | rhythmic, percussive, energetic | 3 |
| Torus Drone | Geodesic Torus | atmospheric, meditative, drone | 2 |
| Kuramoto Sync | Kuramoto | experimental, evolving, hypnotic | 3 |
| Three-Body Jazz | Three-Body | melodic, rhythmic, complex | 4 |
| Rossler Drift | Rossler | atmospheric, melodic, meditative | 2 |
| FM Chaos | Lorenz | experimental, electronic, energetic | 4 |
| Pendulum Meditation | Double Pendulum | meditative, atmospheric, drone | 2 |
| Thomas Labyrinth | Thomas | atmospheric, experimental, eerie | 3 |
| Neural Burst | Hindmarsh-Rose | rhythmic, percussive, experimental | 4 |
| Chemical Wave | Oregonator | atmospheric, evolving, hypnotic | 3 |
| Sprott Minimal | Sprott E | experimental, electronic, minimalist | 2 |
| Substorm Pulse | WINDMI | rhythmic, atmospheric, electronic | 3 |
| Market Collapse | Finance | experimental, eerie, complex | 4 |
| Hyperdimensional | Hyperchaos | experimental, complex, electronic | 5 |
| Magyar Trance | Dadras | meditative, melodic, atmospheric | 3 |

---

## Collaborative session guide

math-sonify includes two complementary collaboration features: a low-level protocol (`collaboration.rs`) for sharing attractor state between performers, and a full-featured **Collaborative Session** server (`collab.rs`) for real-time multi-user parameter editing.

### Collaborative Session server (`collab.rs`)

The session server is a raw-TCP WebSocket-style server that accepts JSON connections from any number of participants. Each participant can claim ownership of specific ODE parameters, edit them in real time, and all changes propagate immediately to every other connected client.

**Key types:**

| Type | Role |
|------|------|
| `CollabServer` | TCP listener; spawns a thread per client |
| `SessionEvent` | Events emitted to the simulation thread (`ParamChanged`, `ClientJoined`, `ClientLeft`) |
| `SharedSynthState` | ODE parameters + sonification mode + scale, wrapped in `Arc<RwLock<>>` for lock-free reads |
| `ParticipantCursor` | Each participant has a unique colour highlight on the parameter they are currently editing |
| `SessionLog` | Full ordered history of every parameter change for post-session replay |

**Wire protocol** (newline-delimited JSON):

```json
// Client -> Server
{ "claim":   ["rho", "sigma"] }
{ "set":     { "rho": 28.5 } }
{ "release": ["rho"] }

// Server -> Client
{ "welcome":     { "client_id": 3 } }
{ "update":      { "rho": 28.5, "owner": 3 } }
{ "error":       "parameter 'rho' is owned by client 1" }
{ "peer_joined": { "client_id": 4, "total": 2 } }
{ "peer_left":   { "client_id": 4, "total": 1 } }
```

**Conflict resolution:** last-write-wins per parameter. A participant must first `claim` a parameter; attempts to `set` an unclaimed or foreign-owned parameter are rejected with an `error` message.

**Starting the server:**

```rust
use math_sonify::collab::{CollabServer, SessionEvent};
use crossbeam_channel::unbounded;

let (tx, rx) = unbounded::<SessionEvent>();
let server = CollabServer::new("127.0.0.1:9001", tx).unwrap();
server.run_background();

// In the simulation thread:
for event in rx.try_iter() {
    match event {
        SessionEvent::ParamChanged { name, value, .. } => { /* apply to ODE */ }
        _ => {}
    }
}
```

Connect any WebSocket client (browser, `websocat`, Python `websockets`) to `ws://127.0.0.1:9001` to join the session.

### Legacy performance protocol (`collaboration.rs`)

math-sonify also includes a JSON-based collaborative performance protocol that lets multiple performers share attractor state in real time. Any transport layer (WebSocket, UDP, OSC) can carry the messages; the module itself only handles serialisation and session logic.

### Concepts

- **Session** -- a named room (e.g. `"concert-2026-03-22"`) that holds up to 8 performers.
- **Performer** -- identified by a unique string ID, carries live `(x, y, z)` attractor coordinates, BPM, volume, and an RGB colour.
- **Messages** -- typed JSON objects: `JoinSession`, `LeaveSession`, `StateUpdate`, `ParameterSync`, `ChatMessage`, `KickOff`.

### Example flow

```rust
use math_sonify::collaboration::{CollaborationClient, PerformerState};

// Create local performer
let performer = PerformerState::new("alice-01", "Alice");
let mut client = CollaborationClient::new(performer);

// Join
let join_msg = client.join_message("my-session");
let json = CollaborationClient::serialize_message(&join_msg);
// ... send json over your WebSocket / UDP socket ...

// Each sim tick: push current attractor state
let update = client.push_xyz(lorenz_x, lorenz_y, lorenz_z);
let json = CollaborationClient::serialize_message(&update);
// ... send json ...

// Receive a message
let incoming_json = r#"{"type":"KickOff","session_id":"my-session","bpm":128.0}"#;
let msg = CollaborationClient::deserialize_message(incoming_json).unwrap();
```

### Server-side session tracking

```rust
use math_sonify::collaboration::CollaborationSession;

let mut session = CollaborationSession::new("my-session");

// On receive JoinSession
session.join(performer_state)?;

// On receive StateUpdate
session.update_state(performer_state);

// Broadcast mean-attractor coordinates to new joiners
let sync_msg = session.broadcast_message();
```

### Message reference

```json
{ "type": "JoinSession",   "session_id": "room1", "performer": { ... } }
{ "type": "LeaveSession",  "performer_id": "alice-01" }
{ "type": "StateUpdate",   "performer": { "x": 1.2, "y": -3.4, "z": 0.8, ... } }
{ "type": "ParameterSync", "params": { "rho": 28.0, "sigma": 10.0 } }
{ "type": "ChatMessage",   "performer_id": "alice-01", "text": "raising sigma now" }
{ "type": "KickOff",       "session_id": "room1", "bpm": 128.0 }
```

---

## Presets

math-sonify ships with ~40 named presets organised into four moods:

- **Atmospheric** -- Midnight Approach, Breathing Galaxy, Aurora Borealis, Deep Hypnosis, Cathedral Organ, Substorm, and more.
- **Rhythmic** -- Frozen Machinery, The Phase Transition, Clockwork Insect, Industrial Heartbeat, Velocity Band, and more.
- **Experimental** -- Neon Labyrinth, Dissociation, Jerk Circuit, Invisible Hand, Hyperchaos Engine, and more.
- **Melodic** -- Glass Harp, Electric Kelp, The Butterfly's Aria, Solar Wind, and more.

The **AUTO** arrangement generator picks 6 presets from a mood pool, scatters system parameters into varied dynamical regimes, randomises synthesis settings, and builds an 8-scene timeline with morphs as the main musical event.

---

## Audio output

math-sonify outputs 32-bit IEEE float stereo PCM at the system default sample rate (44100 or 48000 Hz).

| Export method | Details |
|---------------|---------|
| Clip save (`S`) | Last 60 seconds -> 32-bit float WAV in `clips/` |
| Loop export | Current loop region -> WAV |
| MIDI export | Trajectory -> SMF `.mid` importable into any DAW |
| Headless render | `--headless --duration 60 --output clip.wav` -- no display required |

---

## Installation

### Download

Grab a prebuilt app from the [latest release](https://github.com/Mattbusel/math-sonify/releases/latest). Pick the file that matches your computer:

| System | File |
|--------|------|
| Windows (64-bit) | `math-sonify-vX.Y.Z-x86_64-pc-windows-msvc.zip` |
| Mac with Apple Silicon (M1 and later) | `math-sonify-vX.Y.Z-aarch64-apple-darwin.tar.gz` |
| Mac with Intel chip | `math-sonify-vX.Y.Z-x86_64-apple-darwin.tar.gz` |
| Linux (64-bit) | `math-sonify-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz` |

Unpack it and run `math-sonify` (`math-sonify.exe` on Windows). Keep `config.toml` next to it if you want to change the defaults. `math-sonify --help` lists the headless WAV render options. `SHA256SUMS.txt` on the release page lets you check the download.

The binaries are not code-signed, so your system will be cautious the first time:

- **Windows:** SmartScreen may say "Windows protected your PC" or "unknown publisher". Click **More info**, then **Run anyway**.
- **macOS:** Gatekeeper may refuse to open it. Right-click (or Control-click) the file, choose **Open**, then **Open** again. From a terminal, `xattr -d com.apple.quarantine ./math-sonify` does the same.
- **Linux:** needs ALSA (`libasound2`), which almost every desktop already has.

### With Cargo

If you have [Rust](https://rustup.rs/) installed:

```bash
cargo install math-sonify
```

On Linux, install the audio and windowing headers first, for example on Debian/Ubuntu: `sudo apt install pkg-config libasound2-dev libx11-dev libxcursor-dev libxrandr-dev libxi-dev libxkbcommon-dev libgl1-mesa-dev libudev-dev`.

### From source

Requires [Rust](https://rustup.rs/) 1.75+ and a working audio output device.

```bash
git clone https://github.com/Mattbusel/math-sonify
cd math-sonify
cargo run --release
```

### Build the VST3 / CLAP plugin

The plugin lives in the `plugin/` workspace crate (it is not published to crates.io because nih-plug is only available from git):

```bash
cargo build --release -p math-sonify-plugin
```

Copy the output to your DAW plugin folder:

| Platform | File | Destination |
|----------|------|-------------|
| Windows  | `math_sonify_plugin.dll` | `C:\Program Files\Common Files\VST3\` |
| Linux    | `libmath_sonify_plugin.so` | `~/.vst3/` |
| macOS    | `libmath_sonify_plugin.dylib` | `~/Library/Audio/Plug-Ins/VST3/` |

After copying, trigger a plugin rescan in your DAW (**Options > Plug-in Manager** in Ableton; **Plug-in Database > Rescan** in FL Studio).

---

## VST/CLAP plugin setup

1. Run `cargo build --release -p math-sonify-plugin`.
2. Locate the output file in `target/release/`.
3. Copy to the system VST3 folder for your platform (table above).
4. Open your DAW and trigger a plugin rescan.
5. Search for "math-sonify" in the plugin browser.
6. The plugin exposes all system parameters as automatable VST3 parameters.
7. MIDI output from the plugin can be routed to any instrument track.

---

## GUI

Five top-level tabs:

- **SYNTH** -- system selector, parameter sliders, sonification mode, scale, effects chain, randomize, preset browser.
- **MIXER** -- per-layer volume/pan/ADSR, master effects (EQ, delay, chorus, reverb), VU meters, WAV export, MIDI record/export.
- **ARRANGE** -- scene timeline, morph time controls, AUTO arrangement generator with mood selection.
- **MATH VIEW** -- live phase portrait (XY/XZ/YZ/3D), bifurcation diagram, custom ODE text input, state readout.
- **WAVEFORM** -- oscilloscope and spectrum analyzer.

Performance mode (`F`) switches to fullscreen phase portrait only.

---

## Keyboard shortcut reference

| Key | Action |
|-----|--------|
| `F` | Toggle fullscreen performance mode |
| `Space` | Pause / resume simulation |
| `R` | Reset attractor to default initial condition |
| `S` | Save clip (last 60 seconds as WAV) |
| `Ctrl+S` | Save current configuration to `config.toml` |
| `1` -- `7` | Switch sonification mode |
| `<` / `>` | Previous / next dynamical system |
| `Up` / `Down` | Increase / decrease simulation speed by 10% |
| `E` | Toggle Evolve (autonomous parameter wandering) |
| `A` | Toggle AUTO arrangement playback |
| `P` | Play / stop scene arranger |
| `M` | Toggle MIDI record |
| `Escape` | Exit fullscreen |

---

## Configuration

The application reads `config.toml` from the current working directory at startup. The file is watched with `notify`; edits take effect without restarting.

```toml
[system]
name  = "lorenz"   # see full system list above
dt    = 0.001      # ODE integration time step (clamped 0.0001..0.1)
speed = 1.0        # simulation speed multiplier (0..100)

[lorenz]
sigma = 10.0
rho   = 28.0
beta  = 2.6667

[rossler]
a = 0.2
b = 0.2
c = 5.7

[hyperchaos]
a = 35.0
b = 3.0
c = 28.0
d = -7.0     # must be negative

[windmi]
a = 0.9
b = 2.5

[finance]
a = 3.0
b = 0.1
c = 1.0

[kuramoto]
n_oscillators = 8
coupling      = 1.5

[duffing]
delta = 0.3
alpha = -1.0
beta  = 1.0
gamma = 0.5
omega = 1.2

[van_der_pol]
mu = 2.0

[audio]
sample_rate      = 44100
buffer_size      = 512
reverb_wet       = 0.4
delay_ms         = 300.0
delay_feedback   = 0.3
master_volume    = 0.7
bit_depth        = 16.0    # 1..32 (32 = bypass bitcrusher)
rate_crush       = 0.0     # 0..1 (0 = bypass)
chorus_mix       = 0.0
chorus_rate      = 0.5     # Hz
chorus_depth     = 3.0     # ms
waveshaper_drive = 1.0
waveshaper_mix   = 0.0

[sonification]
mode                = "direct"
# Modes: direct | orbital | granular | spectral | fm | am | vocal | waveguide | resonator
scale               = "pentatonic"
# Scales: pentatonic | natural_minor | harmonic_minor | dorian | phrygian | lydian
#         | mixolydian | locrian | whole_tone | blues | hirajoshi | hungarian_minor
#         | octatonic | chromatic | just_intonation | microtonal | edo19 | edo31 | edo24
#         | harmonic_series
base_frequency      = 220.0
octave_range        = 3.0
transpose_semitones = 0.0
chord_mode          = "none"
# Chord modes: none | major | minor | power | sus2 | octave | dom7 | open_fifth | cluster
portamento_ms       = 80.0
voice_levels        = [1.0, 0.8, 0.6, 0.4]
voice_shapes        = ["sine", "sine", "sine", "sine"]
# Shapes: sine | saw | square | triangle | noise

[viz]
trail_length = 800
projection   = "xy"   # xy | xz | yz | 3d
glow         = true
theme        = "neon" # neon | amber | ice | mono
```

---

## Audio-driven ODE morphing guide

In addition to the classic forward pipeline (ODE state → audio), math-sonify can reverse the flow: use **incoming microphone audio** to continuously modify ODE parameters in real time.

### How it works

```
Microphone / line-in (cpal default input)
    |
    v  per-frame (configurable hop size, default 512 samples)
AudioInputAnalyzer
    |  computes:
    |    RMS        → Lorenz σ  (louder = more chaos, σ ∈ [5, 30])
    |    Centroid   → Lorenz ρ  (brighter spectrum = higher ρ, ρ ∈ [15, 60])
    |    Flux       → system switch trigger (transients trigger attractor changes)
    |    8-band energy → Lorenz β (mid-band energy → β ∈ [1.5, 4.0])
    v
AudioFeatures { rms, centroid, flux, bands: [f32; 8] }
    |
    v
AudioOdeBridge  (delta suppression: only emits patch when change exceeds threshold)
    |
    v
OdePatch → simulation thread (applies sigma/rho/beta overrides)
```

### Dual mode

`DualMode` lets both pipelines run simultaneously:

| Mode | Description |
|------|-------------|
| `ForwardOnly` | Classic: ODE state drives audio synthesis (default) |
| `ReverseOnly` | Microphone input drives ODE parameters only |
| `Both` | Both paths active simultaneously: environment modulates the attractor which modulates the sound which feeds back into the environment |

### Usage

```rust
use math_sonify::audio_driven::{AudioOdeBridge, BridgeConfig, DualMode, DualModeKind};
use crossbeam_channel::unbounded;

// Build the reverse pipeline
let dual = DualMode::new(BridgeConfig::default());
let (mut analyzer, patch_rx, _stop) = dual.build_reverse_pipeline();

// Feed audio samples from cpal input callback:
// analyzer.feed(sample);  // called per sample in the cpal callback

// In the simulation thread:
for patch in patch_rx.try_iter() {
    if let Some(sigma) = patch.sigma { ode_params.sigma = sigma; }
    if let Some(rho)   = patch.rho   { ode_params.rho   = rho;   }
    if patch.trigger_system_switch   { /* switch to next attractor */ }
}
```

### Configuration (`BridgeConfig`)

| Field | Default | Description |
|-------|---------|-------------|
| `sigma_min` / `sigma_max` | 5.0 / 30.0 | Lorenz σ range |
| `rho_min` / `rho_max` | 15.0 / 60.0 | Lorenz ρ range |
| `beta_min` / `beta_max` | 1.5 / 4.0 | Lorenz β range |
| `flux_switch_threshold` | 0.6 | Flux above this triggers a system switch |
| `fft_size` | 1024 | FFT frame size (power of two) |
| `hop_size` | 512 | Samples between analysis frames |

---

## Mathematical background

### Dynamical systems and attractors

A **dynamical system** is a set of differential equations `dx/dt = f(x)` or a map `x_{n+1} = f(x_n)`. The long-term behaviour of trajectories in phase space determines the system's character:

- **Fixed point**: all trajectories converge to a single point (stable equilibrium).
- **Limit cycle**: trajectories converge to a closed loop (periodic oscillation).
- **Quasi-periodic**: trajectories wind around a torus; the ratio of frequencies is irrational.
- **Chaotic attractor (strange attractor)**: trajectories are bounded but never repeat; nearby trajectories diverge exponentially (sensitive dependence on initial conditions).

### Lyapunov exponents

The **maximal Lyapunov exponent** λ₁ quantifies the average rate of exponential divergence of nearby trajectories:

```
||δx(t)|| ≈ e^{λ₁ t} ||δx(0)||
```

- λ₁ < 0: stable fixed point or limit cycle.
- λ₁ = 0: quasi-periodic or at a bifurcation boundary.
- λ₁ > 0: chaotic. The Lorenz system at standard parameters has λ₁ ≈ 0.906.

math-sonify estimates λ₁ using a standard rescaling algorithm: a shadow trajectory is integrated alongside the main one, the separation is measured every N steps, its logarithm is accumulated, and the separation is rescaled. This runs at `LYAP_INTERVAL_TICKS` (every 2 seconds of sim time).

### Hindmarsh-Rose Neuron

`src/hindmarsh_rose.rs` provides a clean typed API for the Hindmarsh-Rose
bursting neuron model (`HindmarshRoseConfig`, `HindmarshRoseState`,
`HindmarshRoseNeuron`). The neuron implements the classic three-variable system:

```text
dx/dt = y - a·x³ + b·x² - z + I_ext
dy/dt = c - d·x² - y
dz/dt = r · (s·(x - x_rest) - z)
```

Classic chaotic-bursting parameters: `a=1, b=3, c=1, d=5, r=0.001, s=4,
x_rest=-1.6, i_ext=1.5`.

#### Headless CLI

```bash
# Render hindmarsh-rose to WAV
math-sonify --headless --attractor hindmarsh-rose --duration 5 --output hr.wav

# Run spectral analysis on the trajectory
math-sonify --headless --attractor hindmarsh-rose --spectrum
```

### Spectral Analysis

`src/spectrum_analyzer.rs` provides `SpectralAnalyzer` which computes the DFT
of an arbitrary sample sequence and returns dominant frequencies.

- Cooley-Tukey radix-2 FFT for power-of-2 lengths; O(N²) DFT fallback.
- Hann windowing to reduce spectral leakage.
- `DftResult { frequencies, magnitudes, dominant_freq, spectral_centroid }`.
- `SpectralAnalyzer::dominant_frequencies(result, top_k)`: sorted by magnitude.

### FFT spectral analysis

The FFT module (`spectrum.rs`) computes a 1024-point Hann-windowed FFT of the synthesised audio output and displays:

- Magnitude spectrum (dB scale, linear frequency axis).
- Spectral centroid (brightness indicator).
- Fundamental frequency estimate via parabolic interpolation on the magnitude peak.

The audio-driven morphing module uses the same FFT on the *input* signal to extract audio features.

### Chaos onset in the Lorenz system

The Lorenz system `(σ=10, β=8/3, ρ)` undergoes the following transitions as ρ increases:

| ρ range | Behaviour |
|---------|-----------|
| ρ < 1 | All trajectories converge to origin |
| 1 < ρ < 13.93 | Two stable fixed points (C+ and C−) |
| 13.93 < ρ < 24.06 | Unstable limit cycles; trajectories still attracted to C± |
| ρ > 24.74 | Strange attractor (chaos onset), the classic butterfly |

---

## Building and testing

```bash
# Run all unit and integration tests (1,800+ tests, no display required)
cargo test --lib --tests

# Release binary
cargo build --release --bin math-sonify

# Release plugin
cargo build --release -p math-sonify-plugin

# Documentation
cargo doc --no-deps --open
```

The test suite covers: ODE solver accuracy (attractor bounds, energy conservation, synchronization thresholds), scale quantization, polyphony, config parsing and clamping, scene arranger timeline consistency, oscillator amplitude bounds, ADSR envelope behavior, all-presets load/validate, lerp_config correctness for every system, bifurcation parameter sweeps, MIDI frame recording and SMF export, preset gallery filtering and discovery, and collaboration session/client message round-trips.

---

## Troubleshooting

**No audio / device not found**
- math-sonify uses `cpal::default_host().default_output_device()`. Ensure a device is selected in OS audio settings.
- Windows exclusive mode: close any application holding the device exclusively.
- Linux ALSA: `sudo apt install libasound2-dev`, add user to `audio` group.
- Sample rate mismatch: set `sample_rate = 48000` in `config.toml`.

**High CPU usage**
- Increase `buffer_size` to 1024 or 2048.
- Disable Evolve mode when not in use.
- For Three-Body and Lorenz-96, reduce `system.speed`.

**Distorted audio**
- Lower `audio.master_volume`.
- Set `waveshaper_drive = 1.0` and `waveshaper_mix = 0.0`.

**Phase portrait blank**
- Wait 2-3 seconds for the trail to build after startup or after pressing `R`.

**Config not loading**
- math-sonify looks for `config.toml` in the **current working directory**.

**VST3/CLAP not appearing**
- Copy to the correct system folder and trigger a plugin rescan in your DAW.
- The plugin is built from the `plugin/` crate with `cargo build --release -p math-sonify-plugin`.

**MIDI export produces empty file**
- Start recording before the session (click **Record MIDI** in the MIXER tab), then export.
- Headless: pass `--export-midi output.mid` on the command line.

---

## Bifurcation Sweeper (`src/bifurcation.rs`)

The bifurcation sweeper runs a dynamical system across a continuous range of a single parameter, records the steady-state attractor at each step, and exports the results in two formats.

| Output | Description |
|--------|-------------|
| SVG diagram | Attractor z-coordinate vs parameter value: classic bifurcation plot rendered as a dark-background SVG. |
| Sweep WAV | Each parameter step rendered as a short audio clip, concatenated into a single mono WAV file that audibly sweeps through the parameter range. |

### Usage

Trigger from the UI with the **Bifurcation Sweep** button in the **Bifurc** tab (tab 7), or call from Rust:

```rust
use math_sonify::bifurcation::{BifurcationConfig, BifurcationSweeper};

let config = BifurcationConfig {
    parameter_name: "rho".into(),
    range_start: 0.5,
    range_end: 30.0,
    steps: 200,
    duration_per_step_ms: 300,
};
let result = BifurcationSweeper::sweep(&config, &base_cfg, Path::new("recordings"))?;
println!("WAV written to: {}", result.audio_path.display());
```

Files are written to the `recordings/` directory (created automatically).

---

## Preset Interpolation and Morphing (`src/preset_interpolation.rs`)

Linear interpolation between any two named presets. All numeric fields are blended continuously; string fields (system name, mode, scale, chord mode) switch at t = 0.5.

### Key types

| Type | Purpose |
|------|---------|
| `PresetInterpolator` | Single shot: interpolate between two configs at any `t` in [0, 1]. |
| `PresetMorphSchedule` | A sequence of `(preset_name, duration_ms)` pairs forming a morph timeline. |
| `MorphTimeline` | Stateful player for a `PresetMorphSchedule`; call `.tick()` each frame. |
| `MorphState` | Current position (0–1) between source and target with completion check. |

### Usage

```rust
use math_sonify::preset_interpolation::{interpolate, PresetMorphSchedule, MorphTimeline};

// Single interpolation at t = 0.5
let mid = interpolate(&load_preset("Lorenz Ambience"), &load_preset("FM Chaos"), 0.5);

// Full morph timeline
let sched = PresetMorphSchedule::from_pairs(&[
    ("Lorenz Ambience", 8_000),
    ("FM Chaos",        6_000),
    ("Thomas Labyrinth", 10_000),
]);
let mut timeline = MorphTimeline::new(sched);
loop {
    let cfg = timeline.tick(); // call each UI frame
    engine.apply_config(cfg);
    if timeline.is_finished() { break; }
}
```

The **Morph** control in the **ARRANGE** tab exposes source/target preset selectors and a duration slider.

---

## Collaborative OSC Sync (`src/osc_sync.rs`, `osc` feature)

Enables real-time parameter synchronization between multiple running instances over UDP multicast.

Enable with `--features osc` (adds the `rosc` optional dependency).

### Supported OSC paths

| Path | Arguments | Action |
|------|-----------|--------|
| `/mathsonify/param/{name}` | `f32` value | Set a named parameter on all peers |
| `/mathsonify/preset/{name}` | _(none)_ | Switch preset across all instances |
| `/mathsonify/sync/beat` | `f32` timestamp | Beat sync for tempo alignment |

### Components

| Type | Role |
|------|------|
| `OscSyncServer` | Listens on UDP port 9001; joins multicast group `239.0.0.1`. |
| `OscSyncClient` | Broadcasts messages to the same multicast group. |
| `CollaborativeSession` | Tracks connected peers by IP; applies last-writer-wins conflict resolution with monotonic timestamps. Peer count shown in status bar. |

```rust
use math_sonify::osc_sync::{OscSyncServer, OscSyncClient, CollaborativeSession};

let server  = OscSyncServer::new()?;
let client  = OscSyncClient::new()?;
let session = CollaborativeSession::new();

// Broadcast a parameter change:
client.send_param("reverb_wet", 0.6)?;

// Receive and apply incoming changes:
if let Some((addr, msg)) = server.try_recv() {
    session.apply(addr, msg);
}
println!("{} peer(s) connected", session.peer_count());
```

---

## Audio Recording Mode (`src/recorder.rs`)

Press **R** to start/stop recording. Files are saved to `recordings/YYYYMMDD_HHMMSS.wav` (epoch timestamp). A pulsing red **REC** indicator appears in the status bar while active.

### Configuration

| Option | Values | Default |
|--------|--------|---------|
| Bit depth | 16-bit int, 32-bit float | 32-bit float |
| Sample rate | 44.1 kHz, 48 kHz | matches audio engine |

### Types

| Type | Purpose |
|------|---------|
| `AudioRecorder` | Appends live stereo interleaved f32 samples to a WAV file. |
| `SegmentRecorder` | Fixed-duration clip (default 60 s), auto-named by system + preset. |

```rust
use math_sonify::recorder::{AudioRecorder, RecordingDepth, RecordingSampleRate, SegmentRecorder};

// Open a rolling recorder
let mut rec = AudioRecorder::start(
    Path::new("recordings"),
    RecordingDepth::Bits32,
    RecordingSampleRate::Hz44100,
)?;
rec.push_samples(&stereo_f32_samples)?;
let path = rec.stop()?;

// Auto-named segment (60 s clip)
let mut seg = SegmentRecorder::start(
    Path::new("recordings"),
    "lorenz",
    "Lorenz Ambience",
    60,
    RecordingDepth::Bits32,
    RecordingSampleRate::Hz44100,
)?;
let done = seg.push_samples(&samples)?;  // returns true when clip is full
if done { seg.finish()?; }
```

---

## Contributing

1. Fork and create a feature branch.
2. Run `cargo fmt --all` and `cargo clippy --all-targets --all-features -- -D warnings`.
3. Add tests for new public API (unit tests in the module, integration tests in `tests/integration.rs`).
4. Open a pull request. CI (fmt, clippy, test, doc, release build) must pass.

Code style: no `unsafe` without comment, no `.unwrap()` in `src/` outside tests, audio thread must be real-time safe (no heap allocation, no blocking I/O).

---

## Rössler Attractor (`src/rossler.rs`)

A standalone module providing idiomatic config/state types for the Rössler spiral strange attractor.

### Types

| Type | Description |
|------|-------------|
| `RosslerConfig { a, b, c }` | Classic params: `a=0.2, b=0.2, c=5.7` (default) |
| `RosslerState { x, y, z }` | Current phase-space position |
| `RosslerAttractor` | RK4 integrator with `step(dt)`, `state()`, `derivatives()` |

### Equations of motion

```
dx/dt = -y - z
dy/dt =  x + a·y
dz/dt =  b + z·(x - c)
```

With `a=0.2, b=0.2, c=5.7` the attractor is bounded (|x|, |y| < 30) and exhibits near-periodic chaos.

### Example

```rust
use math_sonify::rossler::{RosslerAttractor, RosslerConfig};

let mut attractor = RosslerAttractor::new(RosslerConfig::default());
for _ in 0..1000 {
    attractor.step(0.01);
}
let s = attractor.state();
println!("x={:.3} y={:.3} z={:.3}", s.x, s.y, s.z);
```

---

## Van der Pol Oscillator (`src/vanderpol.rs`)

A standalone module for the Van der Pol self-sustaining limit-cycle oscillator.

### Types

| Type | Description |
|------|-------------|
| `VanDerPolConfig { mu }` | Nonlinearity parameter; `mu=1.0` is classic |
| `VanDerPolState { x, y }` | Displacement and velocity |
| `VanDerPolOscillator` | RK4 integrator with `step(dt)`, `state()`, `derivatives()` |

### Equations of motion

```
dx/dt = y
dy/dt = μ·(1 − x²)·y − x
```

For μ > 0 the system converges to a stable limit cycle. Larger μ gives
increasingly relaxation-oscillator-like behaviour with sharp transitions.

### Example

```rust
use math_sonify::vanderpol::{VanDerPolConfig, VanDerPolOscillator};

let mut osc = VanDerPolOscillator::new(VanDerPolConfig { mu: 2.0 });
for _ in 0..2000 {
    osc.step(0.01);
}
let s = osc.state();
println!("x={:.3} y={:.3}", s.x, s.y);
```

---

## FM Synthesis (`src/synthesis/physical.rs`)

DX7-style frequency modulation synthesis is now available as a `PhysicalSynth` mode.

### New types

| Type | Description |
|------|-------------|
| `FmConfig { carrier_ratio, modulator_ratio, modulation_index }` | Classic DX7-style parameters |
| `AdsrEnvelope { attack_samples, decay_samples, sustain_level, release_samples }` | Sample-accurate ADSR |
| `FmSynth` | FM synthesizer implementing `PhysicalSynth` |
| `PhysicalMode::Fm` | New factory variant |

### State mapping

| State dimension | FM parameter |
|----------------|--------------|
| `state[0]` | Carrier frequency (log-mapped over `freq_min..freq_max`) |
| `state[1]` | Modulation index (0→4) |
| `state[2]` | ADSR re-trigger threshold |

### Usage

```rust
use math_sonify::synthesis::{build_physical_synth, FmConfig, FmSynth, PhysicalMode};

let mut synth = build_physical_synth(PhysicalMode::Fm, 80.0, 1200.0, 44100.0);
let state = [1.0f64, 0.5, 0.0];
let sample = synth.next_sample(&state, 44100.0);
```

---

## Multi-Attractor Blend (`src/blend.rs`)

Smoothly interpolates between two attractor states and sequences multiple attractors with S-curve crossfades.

```rust
use math_sonify::blend::{AttractorBlend, AttractorState, BlendConfig, MultiAttractorSequencer, SequenceEntry};

// Linear blend at 50%
let a = AttractorState::new(1.0, 2.0, 3.0);
let b = AttractorState::new(10.0, 20.0, 30.0);
let cfg = BlendConfig::new(0.5);
let mid = AttractorBlend::interpolate(a, b, &cfg);

// S-curve crossfade between two trajectories
let a_traj: Vec<AttractorState> = vec![AttractorState::new(0.0, 0.0, 0.0); 100];
let b_traj: Vec<AttractorState> = vec![AttractorState::new(5.0, 5.0, 5.0); 100];
let blended = AttractorBlend::smooth_transition(&a_traj, &b_traj, 50);

// Alpha schedule (smoothstep 0→1)
let schedule = AttractorBlend::morph(64);

// Sequence two attractors with crossfade
let entries = vec![
    SequenceEntry { attractor_name: "lorenz".to_string(), duration_samples: 200, crossfade_samples: 40 },
    SequenceEntry { attractor_name: "rossler".to_string(), duration_samples: 200, crossfade_samples: 40 },
];
let trajectory = MultiAttractorSequencer::render(&entries, 0.01);
```

**CLI:** `--blend lorenz:rossler`: renders a blended trajectory and prints state count.

**Key types:**
- `AttractorState { x, y, z }`: phase-space point
- `BlendConfig { alpha }`: blend weight (0 = A, 1 = B)
- `AttractorBlend::smooth_transition`: smoothstep S-curve crossfade
- `AttractorBlend::morph`: alpha schedule from 0 to 1
- `MultiAttractorSequencer::render`: full multi-segment sequencer

---

## Scale/Mode Mapper (`src/scale_mapper.rs`)

Maps attractor state values to musical pitches in a chosen scale/mode.

```rust
use math_sonify::scale_mapper::{MusicalScale, ScaleMapper, ScaleMode, midi_to_freq};
use math_sonify::blend::AttractorState;

// Create a D major scale
let scale = MusicalScale::new(62, ScaleMode::Major);
println!("{:?}", scale.pitch_class_set()); // [0, 2, 4, 5, 7, 9, 11]

// Quantize a value to the nearest note
let midi = scale.quantize(0.3, 2); // across 2 octaves

// Get a triad
let chord = scale.chord(-0.5); // [root, third, fifth]

// Full attractor → MIDI pipeline
let mapper = ScaleMapper::new(scale, 2);
let state = AttractorState::new(2.5, -0.8, 14.0);
let pitch = mapper.map_state(&state);
println!("MIDI {} ({:.1} Hz), degree {}, chord {:?}",
    pitch.midi_note, pitch.freq_hz, pitch.scale_degree, pitch.chord);

// Hz conversion
let freq = midi_to_freq(69); // 440.0
```

**CLI:** `--scale major:60`: maps a sample Lorenz trajectory to the C major scale and prints MIDI notes.

**Supported modes:** `Major`, `Minor`, `Pentatonic`, `Dorian`, `Phrygian`, `Lydian`, `WholeTone`, `Chromatic`.

**Key types:**
- `MusicalScale { root_midi, mode }`: scale definition
- `MusicalScale::pitch_class_set()`: semitone intervals above root
- `MusicalScale::quantize(value, octaves)`: maps [-1,1] → nearest MIDI note
- `MusicalScale::chord(value)`: returns triad `[root, third, fifth]`
- `ScaleMapper::map_state(state)` → `MappedPitch { midi_note, freq_hz, scale_degree, chord }`

---

## License

MIT. See [LICENSE](LICENSE).

---

Built with [Rust](https://www.rust-lang.org), [cpal](https://github.com/RustAudio/cpal), [egui](https://github.com/emilk/egui), [nih-plug](https://github.com/robbert-vdh/nih-plug), [crossbeam](https://github.com/crossbeam-rs/crossbeam), [parking_lot](https://github.com/Amanieu/parking_lot), [hound](https://github.com/ruuda/hound), [rayon](https://github.com/rayon-rs/rayon), [tracing](https://github.com/tokio-rs/tracing), [serde](https://github.com/serde-rs/serde), [serde_json](https://github.com/serde-rs/json), [midly](https://github.com/nicholasgasior/midly).