feat(sc): humanize + synth rotation + auto layer

User : "tu refait aussi les tracks avec toutes les possibilité
de synth disponible pour plus de variation et d'humanité dans
ces créations". Niveau choisi : subtil.

3 axes implémentés en helpers globaux qui s'appliquent à TOUS
les 345 tracks existants sans les réécrire :

1) HUMANIZE (engine.scd, opt-in via ~humanizeOn défaut true)
   - Timing offset Pwhite ±3.5ms par note (1.4% sur dur 0.25)
   - Velocity Pwhite ±10% par note
   - Micro-detune ±3 cents sur acid/melody/harmony midinote
   - Appliqué aux 7 Pdef (kickSeq/hatSeq/snareSeq/clapSeq/percSeq/
     acidSeq/melodySeq/harmonySeq) via Pfunc factories ~hTime
     ~hVel ~hDet déclarées dans ~reload
   - Désactivable : ~humanizeOn = false; ~reload.value
   - Tunables : ~humanizeTiming, ~humanizeVelocity, ~humanizeDetune

2) SYNTH ROTATION (palette/synth_rotation.scd, opt-in défaut OFF)
   - 8 rotators (acid/bass/lead/pad/pluck/kick/hat/snare/melody)
   - Pool de 19 versions par catégorie (acid_v1..v19 etc.)
   - Cycle toutes les ~rotationEvery=8 notes (configurable)
   - Vérifie SynthDescLib avant de switcher pour éviter crash
   - Activation : ~setRotationOn.(true)

3) AUTO LAYER (palette/auto_layer.scd, opt-in défaut OFF)
   - 3 layers parallèles à melody/harmony/acid
   - Mirror Pdef qui rejouent le même score avec instrument
     différent (warmPad/choir/sub_boom par défaut)
   - Octave offset configurable, amp réduit (0.20-0.30)
   - Pan opposé, attack/release plus mou, cutoff moins ouvert
   - Activation : ~setAutoLayer.(true)

Hook chargement dans live/_load.scd après scales/melodies/
harmonics. Les modules sont silencieux jusqu'à activation.
This commit is contained in:
L'électron rare
2026-05-07 21:05:24 +02:00
parent 747d7b1e2a
commit 985d3708d1
4 changed files with 221 additions and 22 deletions
+41 -22
View File
@@ -186,51 +186,69 @@ s.waitForBoot {
~acidPan = ~acidPan ? 0;
~melodyPan = ~melodyPan ? 0;
~harmonyPan = ~harmonyPan ? 0;
// Humanize subtil : variations naturelles de timing + velocity +
// micro-detune pour eviter le rendu robotique. Toggle via ~humanizeOn.
// Niveaux subtils (cf. user choice) : timing 1.5%, vel 10%, detune 3c.
~humanizeOn = ~humanizeOn ? true;
~humanizeTiming = ~humanizeTiming ? 0.0035; // +/- 3.5ms sur dur 0.25s = 1.4%
~humanizeVelocity = ~humanizeVelocity ? 0.10; // +/- 10%
~humanizeDetune = ~humanizeDetune ? 0.03; // +/- 3 cents (0.03 semitone)
// Pfunc renvoie un offset de timing pour chaque note, ou 0 si OFF.
~hTime = { Pfunc { if (~humanizeOn) { rrand(~humanizeTiming.neg, ~humanizeTiming) } { 0 } } };
~hVel = { Pfunc { if (~humanizeOn) { 1 + rrand(~humanizeVelocity.neg, ~humanizeVelocity) } { 1 } } };
~hDet = { Pfunc { if (~humanizeOn) { rrand(~humanizeDetune.neg, ~humanizeDetune) } { 0 } } };
Pdef(\kickSeq, Pbind(
\instrument, \kick, \dur, 0.25,
\amp, Pseq(~kickSteps.collect { |s| s * ~kickAmp }, inf),
\timingOffset, ~hTime.value,
\amp, Pseq(~kickSteps.collect { |s| s * ~kickAmp }, inf) * ~hVel.value,
\freq, Pfunc { ~kickFreq }, \pitchAmt, Pfunc { ~kickPitchAmt },
\decay, Pfunc { ~kickDecay }, \drive, Pfunc { ~kickDrive }, \click, Pfunc { ~kickClick },
\scBus, ~scBus.index, \scShape, Pfunc { ~kickScShape },
));
Pdef(\hatSeq, Pbind(
\instrument, \hat, \dur, 0.25,
\amp, Pseq(~hatSteps.collect { |s| s * ~hatAmp }, inf),
\timingOffset, ~hTime.value,
\amp, Pseq(~hatSteps.collect { |s| s * ~hatAmp }, inf) * ~hVel.value,
));
Pdef(\snareSeq, Pbind(
\instrument, \snare, \dur, 0.25,
\amp, Pseq(~snareSteps.collect { |s| s * ~snareAmp }, inf),
\timingOffset, ~hTime.value,
\amp, Pseq(~snareSteps.collect { |s| s * ~snareAmp }, inf) * ~hVel.value,
\freq, Pfunc { ~snareFreq }, \noiseAmt, Pfunc { ~snareNoiseAmt }, \decay, Pfunc { ~snareDecay },
));
Pdef(\clapSeq, Pbind(
\instrument, \clap, \dur, 0.25,
\amp, Pseq(~clapSteps.collect { |s| s * ~clapAmp }, inf),
\timingOffset, ~hTime.value,
\amp, Pseq(~clapSteps.collect { |s| s * ~clapAmp }, inf) * ~hVel.value,
));
Pdef(\percSeq, Pbind(
\instrument, ~percInst, \dur, 0.25,
\amp, Pseq(~percSteps.collect { |s| s * ~percAmp }, inf),
\timingOffset, ~hTime.value,
\amp, Pseq(~percSteps.collect { |s| s * ~percAmp }, inf) * ~hVel.value,
\freq, Pfunc { if (~percInst == \tom) { ~tomFreq } { ~cowbellFreq } },
));
Pdef(\acidSeq, PmonoArtic(\acid,
\dur, 0.25,
\legato, 0.85,
\midinote, Pseq(~acidNotes, inf),
\accent, Pseq(~acidAccents, inf),
\slideTime, Pseq(~acidSlides.collect { |s| if(s == 1) {0.12} {0.005} }, inf),
\cutoff, Pfunc { ~acidCutoff },
\cutoffEnv, Pfunc { ~acidCutoffEnv },
\rq, Pfunc { ~acidRq },
\drive, Pfunc { ~acidDrive },
\amp, Pfunc { ~acidAmp },
\pan, Pfunc { ~acidPan ? 0 },
\scBus, ~scBus.index, // lit le sidechain
\scAmt, Pfunc { ~acidScAmt },
\dur, 0.25,
\timingOffset,~hTime.value,
\legato, 0.85,
\midinote, Pseq(~acidNotes, inf) + ~hDet.value,
\accent, Pseq(~acidAccents, inf),
\slideTime, Pseq(~acidSlides.collect { |s| if(s == 1) {0.12} {0.005} }, inf),
\cutoff, Pfunc { ~acidCutoff },
\cutoffEnv, Pfunc { ~acidCutoffEnv },
\rq, Pfunc { ~acidRq },
\drive, Pfunc { ~acidDrive },
\amp, Pfunc { ~acidAmp } * ~hVel.value,
\pan, Pfunc { ~acidPan ? 0 },
\scBus, ~scBus.index, // lit le sidechain
\scAmt, Pfunc { ~acidScAmt },
));
Pdef(\melodySeq, Pbind(
\instrument, ~melodyInst, \dur, 0.25,
\timingOffset, ~hTime.value,
\legato, 0.9,
\type, Pseq(~melodyNotes.collect { |n| if(n > 0) { \note } { \rest } }, inf),
\midinote, Pseq(~melodyNotes.collect { |n| if(n > 0) { n } { 60 } }, inf),
\midinote, Pseq(~melodyNotes.collect { |n| if(n > 0) { n } { 60 } }, inf) + ~hDet.value,
\cutoff, Pfunc { ~melodyCutoff },
\rq, Pfunc { ~melodyRq },
\attack, Pfunc { ~melodyAttack },
@@ -242,7 +260,7 @@ s.waitForBoot {
\wobbleDepth, Pfunc { ~melodyWobbleDepth },
\wobbleShape, Pfunc { ~melodyWobbleShape },
\subAmt, Pfunc { ~melodySubAmt },
\amp, Pfunc { ~melodyAmp * ~melodyBoost },
\amp, Pfunc { ~melodyAmp * ~melodyBoost } * ~hVel.value,
\pan, Pfunc { ~melodyPan ? 0 },
\scBus, ~scBus.index,
\scAmt, Pfunc { ~leadScAmt },
@@ -251,9 +269,10 @@ s.waitForBoot {
// (32th, 16th, 8th) pour creer du contrepoint / phasing
Pdef(\harmonySeq, Pbind(
\instrument, ~harmonyInst, \dur, Pfunc { ~harmonyDur },
\timingOffset, ~hTime.value,
\legato, 0.9,
\type, Pseq(~harmonyNotes.collect { |n| if(n > 0) { \note } { \rest } }, inf),
\midinote, Pseq(~harmonyNotes.collect { |n| if(n > 0) { n } { 60 } }, inf),
\midinote, Pseq(~harmonyNotes.collect { |n| if(n > 0) { n } { 60 } }, inf) + ~hDet.value,
\cutoff, Pfunc { ~harmonyCutoff },
\rq, Pfunc { ~harmonyRq },
\attack, Pfunc { ~harmonyAttack },
@@ -263,7 +282,7 @@ s.waitForBoot {
\drive, Pfunc { ~harmonyDrive },
\detune, Pfunc { ~harmonyDetune },
\sawAmt, Pfunc { ~harmonySawAmt },
\amp, Pfunc { ~harmonyAmp * ~melodyBoost },
\amp, Pfunc { ~harmonyAmp * ~melodyBoost } * ~hVel.value,
\pan, Pfunc { ~harmonyPan ? 0 },
\scBus, ~scBus.index,
\scAmt, Pfunc { ~leadScAmt },
+3
View File
@@ -37,6 +37,9 @@ Routine({
(~base ++ "palette/scales.scd").load;
(~base ++ "palette/melodies/index.scd").load;
(~base ++ "palette/harmonics/index.scd").load;
// Helpers humanity : humanize est dans engine, rotation + layer ici.
(~base ++ "palette/synth_rotation.scd").load;
(~base ++ "palette/auto_layer.scd").load;
// --- 2. setup.scd : definit ~check / ~setupAll / ~teardown -----
"[live/_load] setup.scd ...".postln;
+85
View File
@@ -0,0 +1,85 @@
// =====================================================================
// auto_layer.scd -- Layering automatique de timbres
//
// Pour chaque note jouee par melody/harmony/acid, un layer secondaire
// est joue en parallele a une amplitude reduite. Donne du corps et
// de l'epaisseur sans toucher aux scores existants.
//
// USAGE :
// ~setAutoLayer.(true) -> active toutes les couches
// ~layerMelodyInst = \warmPad -> override le timbre du layer melody
// ~layerMelodyAmp = 0.3 -> volume relatif (0..1)
// ~layerMelodyOctave = -1 -> offset octave (-2..+2)
//
// Le layer reuse le meme score que le canal principal (\midinote etc.)
// mais avec un instrument different. Pdef(\xxxLayer) tournent en
// parallele de Pdef(\xxxSeq).
//
// =====================================================================
(
~autoLayerOn = ~autoLayerOn ? false;
~layerMelodyInst = ~layerMelodyInst ? \warmPad;
~layerMelodyAmp = ~layerMelodyAmp ? 0.30;
~layerMelodyOctave = ~layerMelodyOctave ? -1;
~layerHarmonyInst = ~layerHarmonyInst ? \choir;
~layerHarmonyAmp = ~layerHarmonyAmp ? 0.25;
~layerAcidInst = ~layerAcidInst ? \sub_boom;
~layerAcidAmp = ~layerAcidAmp ? 0.20;
~layerAcidOctave = ~layerAcidOctave ? -1;
~setAutoLayer = { |b|
~autoLayerOn = b;
if (b) {
// Cree les Pdef de layer en mirroir des Pdef principaux
Pdef(\melodyLayer, Pbind(
\instrument, Pfunc { ~layerMelodyInst },
\dur, 0.25,
\timingOffset, ~hTime.value,
\type, Pseq(~melodyNotes.collect { |n| if(n > 0) {\note} {\rest} }, inf),
\midinote, Pseq(~melodyNotes.collect { |n|
if(n > 0) { n + (12 * ~layerMelodyOctave) } { 60 }
}, inf) + ~hDet.value,
\attack, Pfunc { ~melodyAttack * 1.5 }, // un peu plus mou
\release, Pfunc { ~melodyRelease * 1.3 },
\cutoff, Pfunc { ~melodyCutoff * 0.7 }, // plus assourdi
\amp, Pfunc { ~melodyAmp * ~layerMelodyAmp } * ~hVel.value,
\pan, Pfunc { (~melodyPan ? 0).neg * 0.5 }, // pan oppose attenue
\scBus, ~scBus.index,
\scAmt, Pfunc { ~leadScAmt * 1.2 },
));
Pdef(\harmonyLayer, Pbind(
\instrument, Pfunc { ~layerHarmonyInst },
\dur, Pfunc { ~harmonyDur ? 0.25 },
\timingOffset, ~hTime.value,
\type, Pseq(~harmonyNotes.collect { |n| if(n > 0) {\note} {\rest} }, inf),
\midinote, Pseq(~harmonyNotes.collect { |n| if(n > 0) {n} {60} }, inf) + ~hDet.value,
\attack, Pfunc { ~harmonyAttack * 1.5 },
\release, Pfunc { ~harmonyRelease * 1.3 },
\amp, Pfunc { ~harmonyAmp * ~layerHarmonyAmp } * ~hVel.value,
\scBus, ~scBus.index,
\scAmt, Pfunc { ~leadScAmt * 1.2 },
));
Pdef(\acidLayer, Pbind(
\instrument, Pfunc { ~layerAcidInst },
\dur, 0.25,
\timingOffset, ~hTime.value,
\midinote, Pseq(~acidNotes.collect { |n| n + (12 * ~layerAcidOctave) }, inf),
\amp, Pfunc { ~acidAmp * ~layerAcidAmp } * ~hVel.value,
\scBus, ~scBus.index,
\scAmt, Pfunc { ~acidScAmt * 1.5 },
));
Pdef(\melodyLayer).play;
Pdef(\harmonyLayer).play;
Pdef(\acidLayer).play;
"[auto_layer] ON -- 3 layers en parallele".postln;
} {
Pdef(\melodyLayer).stop;
Pdef(\harmonyLayer).stop;
Pdef(\acidLayer).stop;
"[auto_layer] OFF".postln;
};
};
"[auto_layer] loaded - ~setAutoLayer.(true) to enable".postln;
)
+92
View File
@@ -0,0 +1,92 @@
// =====================================================================
// synth_rotation.scd -- Rotation auto entre les versions de SynthDef
//
// La palette dispose de ~20 versions de chaque SynthDef (acid_v1..v19,
// bass808_v1..v19, lead_v1..v19, etc.) qui sont sous-utilisees parce
// que les tracks figent l'instrument. Ce module fournit un Pfunc qui
// cycle/randomise sur des banques par categorie, chaque N notes.
//
// USAGE :
// ~rotateAcid.value -> Pfunc qui renvoie un nom \acid_vXX
// ~rotateLead.value -> idem pour lead
// ~rotateBass.value -> idem pour bass808
// ~setRotationOn.(true) -> active toutes les rotations
// ~setRotationOn.(false)-> revient aux SynthDefs canoniques
//
// Integration : remplacer dans engine.scd
// \instrument, \acid -> \instrument, ~rotateAcid.value
// \instrument, ~melodyInst -> \instrument, ~rotateMelody.value
//
// =====================================================================
(
~rotationOn = ~rotationOn ? false; // OFF par defaut, opt-in
~rotationEvery = ~rotationEvery ? 8; // change tous les N events
~setRotationOn = { |b|
~rotationOn = b;
("[rotation] " ++ if(b) {"ON"} {"OFF"} ++
" (every " ++ ~rotationEvery ++ " events)").postln;
};
// Banques de versions pour chaque famille. On peut elargir/restreindre
// pour piloter le caractere global (ex: que des versions saturees).
~rotPoolAcid = (1..19).collect { |i| ("acid_v" ++ i).asSymbol };
~rotPoolBass = (1..19).collect { |i| ("bass808_v" ++ i).asSymbol };
~rotPoolLead = (1..19).collect { |i| ("lead_v" ++ i).asSymbol };
~rotPoolPad = (1..19).collect { |i| ("pad_v" ++ i).asSymbol };
~rotPoolPluck = (1..19).collect { |i| ("pluck_v" ++ i).asSymbol };
~rotPoolKick = (1..19).collect { |i| ("kick_v" ++ i).asSymbol };
~rotPoolHat = (1..19).collect { |i| ("hat_v" ++ i).asSymbol };
~rotPoolSnare = (1..19).collect { |i| ("snare_v" ++ i).asSymbol };
// Compteur global d'events pour switch synchro.
~rotIdx = ~rotIdx ? 0;
// Construit un Pfunc qui renvoie un instrument :
// - default fallback si rotation OFF
// - sinon : pioche dans le pool toutes les ~rotationEvery notes
~makeRotator = { |pool, fallback|
Pfunc {
if (~rotationOn and: { pool.size > 0 }) {
// Index avance par groupe de ~rotationEvery
var groupIdx = (~rotIdx div: ~rotationEvery) % pool.size;
~rotIdx = ~rotIdx + 1;
// Verifie que le SynthDef existe avant de le renvoyer
if (SynthDescLib.global.at(pool[groupIdx]).notNil) {
pool[groupIdx]
} { fallback }
} { fallback }
}
};
~rotateAcid = { ~makeRotator.(~rotPoolAcid, \acid) };
~rotateBass = { ~makeRotator.(~rotPoolBass, \bass808) };
~rotateLead = { ~makeRotator.(~rotPoolLead, \lead) };
~rotatePad = { ~makeRotator.(~rotPoolPad, \pad) };
~rotatePluck = { ~makeRotator.(~rotPoolPluck, \pluck) };
~rotateKick = { ~makeRotator.(~rotPoolKick, \kick) };
~rotateHat = { ~makeRotator.(~rotPoolHat, \hat) };
~rotateSnare = { ~makeRotator.(~rotPoolSnare, \snare) };
// Rotator generique pour la melody (utilise ~melodyInst comme fallback)
~rotateMelody = {
Pfunc {
if (~rotationOn) {
// Choisit dans le pool correspondant a ~melodyInst
var pool = (
lead: ~rotPoolLead,
pad: ~rotPoolPad,
pluck: ~rotPoolPluck,
).at(~melodyInst);
if (pool.notNil and: { pool.size > 0 }) {
var groupIdx = (~rotIdx div: ~rotationEvery) % pool.size;
~rotIdx = ~rotIdx + 1;
pool[groupIdx]
} { ~melodyInst }
} { ~melodyInst }
}
};
"[synth_rotation] loaded - ~setRotationOn.(true) to enable".postln;
)