docs: add v2 reorganization design spec

Approved design for full restructure of sound_algo to atomic
granularity (1 file = 1 unit). Covers:
- synth/ (38 SynthDefs in drums/bass/lead/pad/world/master)
- fx/ (~30 FX in bus/insert/trick)
- melodies/ (133 melodies organized by musical genre, ~mAcidN naming)
- patterns/ (120+ patterns by instrument and genre)
- tracks/ (23 subdirs, 1 file per executable section)
- audio/ (formerly live/, minus SynthDefs)
- new live API (~mAcid, ~pAcid, ~fx) replaces ~kk/~mm/~ff/~cc/~p

7-phase migration plan with E2E tests green at each step.
This commit is contained in:
electron-rare
2026-05-04 23:02:17 +02:00
parent 62e10b35f2
commit 9d4483ba10
@@ -0,0 +1,504 @@
# Refonte complète de l'organisation du code et des fichiers
**Date** : 2026-05-04
**Statut** : Design approuvé, en attente de plan d'implémentation
**Cible** : passage à une organisation atomique granulaire (1 fichier = 1 unité)
---
## Contexte
Le projet `sound_algo` a actuellement une structure mixte :
- 6 fichiers contenant 38 SynthDef éparpillés (engine.scd + synthdefs/{asia,extra,authentic}.scd + live/{live,fx_bus}.scd)
- 4 fichiers groupés contenant 133 mélodies (`palette/melodies/{short,long,xlong,epic}.scd`)
- 6 fichiers contenant 120+ patterns rythmiques (`palette/rhythm/`)
- 23 fichiers track monolithiques où chaque section est imbriquée dans une `Routine`
- API live `~kk/~mm/~ff/~cc/~p` classifiée par technique (longueur de mélodie, type de FX) plutôt que par genre musical
**Limites observées** :
- Difficile d'ajouter une mélodie / pattern / SynthDef sans toucher un gros fichier multi-blocs
- Impossible d'exécuter une section de track isolément (tout est dans la Routine parente)
- Classification par technique sépare les mélodies d'un même genre dans 4 fichiers différents
- L'API live force à connaître la longueur (`~mShort/~mLong`) plutôt que l'intention musicale
## Objectif
Refondre l'organisation pour atteindre :
1. **Granularité atomique** : 1 fichier = 1 SynthDef, 1 FX, 1 mélodie, 1 pattern, 1 section de track
2. **Classification par genre musical** (pas par technique) pour mélodies et patterns
3. **Auto-discovery** : ajouter un nouveau fichier suffit (pas de mise à jour d'index manuel)
4. **Sections de track exécutables** Cmd+Enter individuellement, sans dépendre du Routine parent
5. **Nouvelle API live** par genre, l'ancienne disparaît complètement (pas de double maintenance)
## Contraintes
- Les 10 tests E2E doivent rester verts à chaque phase de migration
- Convention SC : variables `~xxx` minuscules ; un seul bloc top-level dans les fichiers `.load`-compatibles ; balance parens P:0 B:0
- `web_bridge.scd` doit continuer à fonctionner (le bridge OSC alimenté par la web surface)
- Les 23 tracks A-W doivent rester jouables (boutons jump, chains, web surface)
- Repo GitHub privé déjà push, branche `main` protégée par convention
---
## Architecture cible
### Structure top-level
```
sound_algo/
├── 00_load.scd # loader / index — entry point
├── 01_live.scd # tableau de bord live, Cmd+Enter par bloc
├── engine.scd # Pdef + ~reload + helpers (plus de SynthDef)
├── setup.scd # ~check / ~setupAll / ~teardown
├── web_bridge.scd # OSC bridge SC ↔ web
├── synth/ # 1 fichier = 1 SynthDef, sous-dossiers par rôle
├── fx/ # 1 fichier = 1 FX, sous-dossiers par routage
├── melodies/ # 1 fichier = 1 mélodie, sous-dossiers par genre
├── patterns/ # 1 fichier = 1 pattern, sous-dossiers par genre
├── tracks/ # 1 sous-dossier par track, 1 fichier par section
├── palette/ # banques compositionnelles (scales, chords, ...)
├── audio/ # ce qui était live/ sans les SynthDef
├── control/ # jump, midi, playlist
├── tests/ # E2E
├── examples/ # tutoriels
├── web/ # Node bridge + UI
├── _archive/ # legacy (ignoré)
└── docs/
└── superpowers/specs/
```
### `synth/` — 38 SynthDef en sous-dossiers par rôle
```
synth/
├── _index.scd
├── drums/ kick.scd, kick_gabber.scd, hat.scd, snare.scd, snare_gated.scd,
│ clap.scd, perc.scd, rim.scd, cowbell.scd, tom.scd
├── bass/ acid.scd, reese.scd, reese_dnb.scd, reese_hard.scd, sub_boom.scd,
│ fm_bass.scd, hooverbass.scd, didgeridoo.scd
├── lead/ saw3.scd, lead.scd, square_lead.scd, fm_lead.scd, organ_lead.scd,
│ hard_lead.scd, supersaw.scd, stab.scd, pluck.scd, fm_bell.scd, rhodes.scd
├── pad/ pad.scd, warm_pad.scd, vocal_pad.scd, drone.scd
├── world/ koto.scd, erhu.scd, pipa.scd, gong.scd, handpan.scd, guzheng.scd, growl.scd
└── master/ master_lim.scd, master_comp.scd, master_sat.scd, vinyl.scd
```
**Convention par fichier** :
```supercollider
// synth/drums/kick.scd
(
SynthDef(\kick, {
arg freq = 55, pitchAmt = 3.0, ...;
...
}).add;
)
```
Un seul bloc top-level, **uniquement** un `SynthDef.add`. Pas de variable `~xxx`, pas de helpers.
### `fx/` — ~30 FX en sous-dossiers par routage
```
fx/
├── _index.scd
├── bus/ # FX wet send, lus depuis ~fxBuses
│ reverb.scd, delay.scd, chorus.scd, phaser.scd, flanger.scd,
│ bitcrush.scd, tape.scd, shimmer.scd, dist.scd, send.scd
├── insert/ # FX en chaîne avant masterLim
│ vcf_moog.scd, vcf_ms20.scd, vcf_ladder.scd, vcf_formant.scd,
│ vcf_comb.scd, vcf_hpf.scd, vcf_bpf.scd, vcf_notch.scd,
│ ringmod.scd, freq_shift.scd, gran.scd, comb.scd, formant.scd,
│ auto_filter.scd, tremolo.scd, autopan.scd, stutter.scd,
│ resonator.scd, spring_rev.scd, plate.scd
└── trick/ # FX oneshot
riser.scd, sweep.scd, impact.scd, crash.scd
```
### Auto-glob index
Chaque dossier (sauf `tracks/`) a un `_index.scd` qui scanne récursivement et charge :
```supercollider
// synth/_index.scd
(
PathName(thisProcess.nowExecutingPath.dirname).deepFiles
.select { |f| f.extension == "scd"
and: { f.fileName != "_index.scd" } }
.sort { |a, b| a.fullPath < b.fullPath }
.do { |f| f.fullPath.load };
)
```
`tracks/_index.scd` ne charge PAS les tracks au boot — elles sont lazy, on charge à la demande via `~jumpTo` ou Cmd+Enter manuel sur `_track.scd`.
### `melodies/` — 133 fichiers en sous-dossiers par genre
Mapping inverse depuis `~melodyBank` (palette/melodies/bank.scd actuel) → genre dominant pour chaque mélodie. Une mélodie présente dans plusieurs genres reste dans son genre principal (pas de duplication).
```
melodies/
├── _index.scd
├── acid/ m1.scd ... m12.scd # ~mAcid1..~mAcid12
├── trance/ m1.scd ... m{N}.scd # ~mTrance1..
├── detroit/ ... # ~mDetroit*
├── dnb/ ... # ~mDnb*
├── dub/ ... # ~mDub*
├── dubstep/ ... # ~mDubstep*
├── house/ ... # ~mHouse*
├── hardstyle/ ... # ~mHardstyle*
├── phonk/ ... # ~mPhonk*
├── synthwave/ ... # ~mSynthwave*
├── oriental/ ... # ~mOriental*
├── world/ ... # ~mWorld*
├── ambient/ ... # ~mAmbient*
└── industrial/ ... # ~mIndustrial*
```
**Convention par fichier mélodie** :
```supercollider
// melodies/acid/m1.scd
(
~mAcid1 = [36, 0, 36, 0, 39, 36, 41, 36,
36, 0, 36, 39, 41, 0, 43, 39];
)
```
La longueur (32/64/96/128 pas) est détectée via `.size` à l'usage — pas de classification technique séparée.
### `patterns/` — 120+ fichiers en sous-dossiers
```
patterns/
├── _index.scd
├── kick/ techno.scd, gabber.scd, dub.scd, ... # ~pKickTechno, ...
├── hat/ offbeat.scd, eighth.scd, sixteen.scd, ...
├── snare/ two4.scd, ghost.scd, ...
├── clap/ ...
├── perc/ ...
└── genre/ # patterns multi-instrument complets
├── _index.scd
├── acid/ house.scd, hard_techno.scd, ...
├── trance/ trance138.scd, psy_trance.scd, ...
├── dnb/ liquid.scd, neurofunk.scd, jungle.scd, ...
├── dubstep/ ...
├── house/ deep.scd, tech.scd, ...
├── hardstyle/ ...
├── phonk/ ...
├── industrial/ ...
├── breakcore/ ...
├── footwork/ ...
├── idm/ ...
└── glitch_hop/ ...
```
**Pattern simple** (1 instrument) :
```supercollider
// patterns/kick/techno.scd
( ~pKickTechno = [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0]; )
```
**Pattern multi-instrument** (`genre/`) :
```supercollider
// patterns/genre/acid/house.scd
(
~pAcidHouse = (
kick: [1,0,0,0, 1,0,0,0, 1,0,0,0, 1,0,0,0],
hat: [0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0],
snare: [0,0,0,0, 1,0,0,0, 0,0,0,0, 1,0,0,0],
clap: [0!16],
perc: [0!16],
bpm: 125,
bassline: [36, 0, 36, 0, 39, 36, 41, 36, 36, 0, 36, 39, 41, 0, 43, 39],
style: \acid_house
);
)
```
### `tracks/` — sous-dossier par track + sections autonomes
```
tracks/
├── _index.scd # ne charge PAS les tracks au boot
├── A_acid_journey/
│ ├── _track.scd # orchestrateur Routine
│ ├── 01_minimal.scd # Cmd+Enter pour appliquer la section
│ ├── 02_acid_rave.scd
│ ├── 03_detroit.scd
│ ├── 04_hard_push.scd
│ ├── 05_supersaw_dub.scd
│ ├── 06_guzheng_acid.scd
│ ├── 07_outro.scd
│ └── README.md # timeline, BPM, style — optionnel
├── B_deep_to_hard/
│ ├── _track.scd
│ └── ...
└── ... (W tracks, soit 23 dossiers)
```
**Section autonome** :
```supercollider
// tracks/A_acid_journey/02_acid_rave.scd
// Section [A 2:00] : acid rave -- kick T1 + melody LEAD + variations
(
"[A 2:00] acid rave -- kick T1 + melody LEAD + variations".postln;
~bpm = 132; TempoClock.default.tempo = ~bpm/60;
~kickFreq = 55; ~kickPitchAmt = 3.0; ~kickDecay = 0.32;
~kickDrive = 1.3; ~kickClick = 0.2;
~hatSteps = [0,0,1,0, 0,0,1,0, 0,0,1,0, 0,0,1,0]; ~hatAmp = 0.18;
~melodyInst = \lead;
~melodyAttack = 0.012; ~melodyRelease = 0.25; ~melodyCutoff = 2400;
~melodyAmp = 0.16;
~acidNotes = [36,36,48,36,39,36,43,39, 36,41,36,39,43,36,36,41];
~acidSlides = [0,0,1,0,0,0,0,1, 0,0,0,1,0,0,0,0];
~melodyNotes = ~mAcid1; // nouvelle API par genre
~reload.value;
[\kickSeq, \hatSeq, \snareSeq, \clapSeq, \percSeq,
\acidSeq, \melodySeq, \harmonySeq].do { |k| Pdef(k).play };
)
```
**Section idempotente** : un Cmd+Enter applique tous les paramètres + reload, ne lance PAS de Routine, ne fait PAS de `.wait`.
**Orchestrateur `_track.scd`** :
```supercollider
// tracks/A_acid_journey/_track.scd
(
~track !? { ~track.stop };
~track = Routine({
var dir = thisProcess.nowExecutingPath.dirname;
(dir +/+ "01_minimal.scd").load;
120.wait;
(dir +/+ "02_acid_rave.scd").load;
120.wait;
(dir +/+ "03_detroit.scd").load;
90.wait;
// ...
"[A] === fin ACID JOURNEY ===".postln;
~masterFadeOut.(8);
}).play;
)
```
### `audio/` — ce qui était `live/` sans les SynthDef
```
audio/
├── _index.scd
├── fx_bus.scd # ~fxBuses + ~send + ~setFx + ~fxPreset + ~addFx +
│ # ~lfo / ~lfoStop / ~tap (LFO sur params FX)
├── insert.scd # ~vcfOn / ~vcfOff / ~vcfSweep / ~vcfPreset
├── tricks.scd # ~drop, ~breakdown, ~buildup, ~stutter, ~glitch,
│ # ~freeze, ~tapeStop, ~tapeReverse, ~jumpCut,
│ # ~bitCrushNow, ~ringMod, ~freqShift, ~granFreeze,
│ # ~scratch, ~repeat, ~swap, ~reverbTail, ~handsUp,
│ # ~chokePerc, ~chokeLead
│ # + ~trickRoutines, ~trickStop, ~trickStopAll
├── lfo.scd # ~lfoTo, ~lfoSync, ~lfoStopVar, ~lfoStopAllVars,
│ # ~lfos dict, ~setVar, ~getVar, ~envSweep, ~envs
├── mixer.scd # ~setVol, ~mute, ~solo, ~mixPreset, ~saveMix,
│ # ~masterVol, ~masterFade, ~masterFadeOut, ~xfade
├── scenes.scd # ~saveScene, ~loadScene, ~listScenes, ~deleteScene
├── loops.scd # ~loopOn, ~loopOff, ~loopChain, ~loopFade
├── chains.scd # ~ch dict + ~chainRoutine + ~chStop + helpers
├── wrappers.scd # ~mAcid, ~mTrance, ~pAcid, ~fx, ~synth (généré
│ # dynamiquement depuis melodies/ patterns/ synth/ fx/)
└── panel.scd # ex live_fx_panel — IDE-only
```
**`engine.scd`** ne contient plus de SynthDef. Il garde uniquement `~reload`, les Pdef (avec Pfunc déjà appliqué post-fix RAM), `~scBus`, `~melodyBoost`.
### `palette/` — banques compositionnelles
```
palette/
├── _index.scd
├── scales.scd # 16 ~scaleXxx + ~genMelody + ~mutate
├── harmonics/
│ ├── _index.scd
│ ├── helpers.scd # ~genThirdHarmony, ~genCounterMelody, ...
│ └── presets.scd # ~enableHarmony.(\third) -- 10 presets
├── chords_progressions.scd # ~progDetroit, ~progTrance, ...
├── timbres.scd # 26 ~timbreXxx -- L1-L26
├── styles.scd # 5 ~styleXxx complets
├── fx_genre.scd # combos FX par genre
├── fills.scd # ex palette/rhythm/fills.scd
├── sequences.scd # ex palette/rhythm/sequences.scd
└── tweaks_template.scd
```
### `control/` (chemins jump.scd ajustés)
```
control/
├── _index.scd
├── midi.scd # MIDIClient.init + mapping
├── jump.scd # ~jumpTo charge tracks/<X>/NN_section.scd
└── playlist.scd
```
`~jumpTo.(\A, \acid_rave)` ne contient plus une closure inline. Il fait :
```supercollider
~jumpTo = { |trackLetter, slug|
var dir = ~base ++ "tracks/" ++ ~trackDir.(trackLetter);
var matches = PathName(dir).files
.select { |f| f.fileName.contains(slug.asString) };
if (matches.notEmpty) {
~track !? { ~track.stop; ~track = nil };
matches.first.fullPath.load; // applique la section
} { "[JUMP] " ++ slug ++ " inexistante".postln };
};
```
## Nouvelle API live
Les wrappers actuels `~kk/~mm/~ff/~cc/~p` et tous les sous-wrappers (`~mShort, ~mGen, ~kPat, ~fxDrop, ~chIntro, ~pKit`...) sont **supprimés**. Nouvelle API par genre, plus orthogonale.
### Mélodies — `~mGenre.(N | nil | rnd)`
```supercollider
~mAcid.(1) # applique ~mAcid1 -> ~melodyNotes + reload
~mAcid.() # random dans le dossier acid/
~mTrance.(3) # applique ~mTrance3
~mDnb.() # random dnb
~mList.(\acid) # liste les mélodies du genre acid
~mList.() # liste tous les genres et leurs counts
~mNext.() # mélodie suivante (dans le genre courant)
~mPrev.() # précédente
~mInst.(\warmPad) # change l'instrument melody
~mHarm.(\third) # 10 presets harmonie (\off \third \fifth ...)
```
### Patterns — `~pGenre.(\name)`
```supercollider
~pAcid.(\house) # applique ~pAcidHouse (kick + hat + snare + bpm)
~pAcid.() # random dans patterns/genre/acid/
~pTrance.(\trance138)
~pDubstep.(\classic)
~pKick.(\techno) # pattern kick seul
~pHat.(\offbeat)
~pSnare.(\two4)
~pList.(\acid)
```
### Synth — `~synth.(\group, \name)`
```supercollider
~synth.(\drums, \kick) # info SynthDef
~melodyInst = \warmPad # change l'instrument actif
~kickInst = \kickGabber # change le kick (re-route Pdef)
~synthList.(\drums) # liste tous les drums
```
### FX — `~fx.(\type, \name, ...args)`
```supercollider
~fx.(\bus, \rev, 0.4) # ~send vers reverb wet 0.4
~fx.(\insert, \vcfMoog, 1500, 0.6) # active VCF Moog en chain
~fx.(\trick, \riser, 4) # oneshot riser dur=4s
# Tricks scéniques (inchangés)
~drop.()
~breakdown.(8)
~buildup.(8)
~glitch.(2)
~stutter.(8, 1)
~freeze.()
~tapeStop.(2)
~tapeReverse.(4)
~jumpCut.(0.5)
```
### Chains — `~ch.(\name, ...args)`
```supercollider
~ch.(\intro)
~ch.(\drop)
~ch.(\breakdown)
~ch.(\buildup, 8)
~ch.(\intro2drop, 4, 4)
~ch.(\dropToBreak, 8)
~ch.(\outroFade, 12)
~chStop.()
```
### Mixer / Scenes / Jump (inchangés)
```supercollider
~setVol.(\kick, 0.8)
~mute.(\acid) ; ~solo.(\melody)
~mixPreset.(\drumsHeavy)
~masterVol.(0.7) ; ~masterFadeOut.(8)
~saveMix.(\verseA)
~saveScene.(\drop1) ; ~loadScene.(\drop1)
~jumpTo.(\A, \acid_rave)
```
### Génération dynamique des wrappers
`audio/wrappers.scd` introspect les dossiers `melodies/`, `patterns/`, `synth/`, `fx/` au boot pour générer les helpers `~mAcid`, `~pTrance`, etc. Ajouter un nouveau genre = créer un dossier + un fichier dedans, l'API est mise à jour automatiquement au prochain `~setupAll`.
## Plan de migration (7 phases)
Chaque phase = 1 commit propre, testé, pushé. Si une phase fail, rollback de ce commit sans casser les précédents.
| # | Phase | Risque | Tests E2E |
|---|---|---|---|
| 1 | **Extract SynthDef** : créer `synth/` et `fx/`, déplacer les 38+30 SynthDef. `engine.scd` perd ses SynthDef mais garde Pdef. Auto-glob `_index.scd`. | Bas | Tous doivent passer |
| 2 | **Renommer `live/` → `audio/`** + déplacer `palette/rhythm/{fills,sequences}.scd``palette/`. Ajuster `setup.scd`, `00_load.scd`, `_load.scd`, `web_bridge.scd`. | Moyen | Tous doivent passer |
| 3 | **Éclater mélodies** : `melodies/<genre>/m{N}.scd`. Mapping inverse depuis `~melodyBank`. Variables `~m{Genre}{N}`. Supprimer `palette/melodies/`. | Élevé | `e2e_05_melodies` réécrit |
| 4 | **Éclater patterns** : `patterns/<genre>/<name>.scd`. Variables `~p{Genre}{Name}`. Supprimer `palette/rhythm/{kicks,percussions,drum_kits,patterns_genre}.scd`. | Élevé | tests adaptés |
| 5 | **Éclater tracks** : 23 dossiers `tracks/<X>_<slug>/{_track.scd, NN_section.scd}`. Section files autonomes. `_track.scd` orchestrateur. | Élevé | `e2e_09_tracks` adapté + `e2e_10_timestamps` adapté |
| 6 | **Nouvelle API live** : `audio/wrappers.scd` génère `~mAcid/~pAcid/~fx/~synth` dynamiquement. Réécrit `01_live.scd` + `web_bridge.scd` + `web/public/control/app.js` (chips dynamiques via `/api/genres`). Supprime ancienne API. | Très élevé | `e2e_08_live` réécrit |
| 7 | **Nettoyage** : suppression `_archive/` legacy, mise à jour `CLAUDE.md`, `README.md`, skills, `tests/CLAUDE.md`. Commit final tag `v2.0`. | Bas | Tous doivent passer |
## Acceptance criteria
- `bash tests/run_all.sh` retourne 10/10 PASS
- `awk balance` P:0 B:0 sur tous les `.scd`
- Boot complet via `(~base ++ "00_load.scd").load``~check.value` reporte tout `[OK]`
- `01_live.scd` Cmd+Enter sur chaque bloc fonctionne (transport, mixer, mélodies, patterns, FX, tricks, chains, scenes, jump)
- Web surface : sliders/chips fonctionnent (genres dynamiques chargés depuis `/api/genres`)
- Track A `_track.scd` Cmd+Enter joue l'intégralité, ET chaque section individuelle Cmd+Enter applique ses params
- `~jumpTo.(\A, \acid_rave)` charge `tracks/A_acid_journey/02_acid_rave.scd` au runtime
- Aucune fuite RAM scsynth/sclang après 30 min de session
- Documentation : root CLAUDE.md + nested CLAUDE.md dans chaque nouveau dossier
## Risques identifiés et mitigation
| Risque | Mitigation |
|---|---|
| Mélodie présente dans plusieurs genres → mauvais classement | Mapping inverse depuis `~melodyBank` actuel + arbitre par genre dominant ; possibilité de la dupliquer manuellement après si dissonance |
| Web surface (`web/public/control/app.js`) hardcoded sur l'ancienne API | Phase 6 inclut endpoint `GET /api/genres` qui scanne `melodies/` et `patterns/` ; les chips JS sont rendues dynamiquement |
| Tests E2E qui hardcodent les anciens chemins (`palette/melodies/short.scd`) | Adapter chaque test E2E dans la phase qui change ses chemins. `e2e_05`, `e2e_09`, `e2e_10` impactés |
| `~jumpTo` actuel utilise des closures inline dans `control/jump.scd` | Phase 5 réécrit `~jumpTo` pour `.load` dynamique d'un fichier section |
| Boot lent à cause de 200+ `.load` séquentiels | Mesurer après phase 1. Si > 5s, batcher ou paralléliser via `Task` |
| Dépendances non triviales entre `synth/` et `palette/` (ex: scales lues par certains SynthDef) | Aucune actuellement (les SynthDef sont autonomes), mais à valider phase 1 |
## Notes complémentaires
- Le pattern `Pfunc { ~xxx }` introduit en commit `62e10b3` (fix RAM LFO+VCF) reste appliqué — la migration ne le touche pas
- Le pattern `~trickRoutines` introduit en commit `c4f17fd` reste appliqué dans `audio/tricks.scd`
- `~crossfadeFxRoutine` et `~chainRoutine` (commit `5f96d57`) restent appliqués dans `audio/fx_bus.scd` et `audio/chains.scd`
- L'auto-launch web (commit `afaa072`) reste dans `web_bridge.scd`
---
**Statut** : design approuvé section par section. Prochaine étape : génération du plan d'implémentation détaillé via la skill `superpowers:writing-plans`.