Teach music agents retriggered 808 drums

This commit is contained in:
Meta Vens 2026-06-01 12:07:51 +01:00
commit fd6aa863a7
9 changed files with 125 additions and 66 deletions

View file

@ -85,8 +85,8 @@ Subtractive drums:
```aqua
sequence name=kick pattern=x...x...x...x... step=.125 high=1 low=0
voice name=kick_body wave=sine freq=54 gain=@/seq/kick env=ad attack=.002 decay=.14
voice name=kick_click wave=noise gain=@/seq/kick hpf=2500 lpf=7000 env=ad attack=.001 decay=.035
voice name=kick_body wave=sine freq=54 gain=@/seq/kick trigger=.125 env=ad attack=.002 decay=.14 pitch_ramp=-3.8
voice name=kick_click wave=noise gain=@/seq/kick trigger=.125 hpf=.72 lpf=.24 env=ad attack=.001 decay=.035
```
Drums need body plus skin. Use sine/triangle bodies for kick/snare weight,
@ -94,6 +94,20 @@ filtered noise only for click, skin, dust, or air, and gates from target tempo.
Noise alone is a placeholder unless it has band limits, a gate, and a musical
job.
Important: sequenced gain is not drum articulation by itself. A normal one-shot
voice has one song-age envelope; `gain=@/seq/kick` can reopen a dead envelope
without restarting the kick pitch fall or transient. Patterned percussion must
use `trigger=<step_seconds>` on every drum voice that needs per-hit envelope,
pitch-ramp, FM-decay, filter-ramp, or phaser motion. This gives the voice a
local retrigger clock while the song form keeps running.
808-style percussion should start from the stock `patches/808/*.aqua` shapes,
then make the role explicit in the song patch: kick body plus click, snare body
plus filtered noise, clap as staggered filtered noise taps, hats as short
high-passed noise/metal layers, and optional ghost/skip lanes. Juice them with
`drive`, `fold`, narrow filters, and sample-hold or square modulators for grit;
do not submit one broadband pink-noise lane and call it a kit.
`sub_kick_grid` is the reusable drum shorthand. It owns the subtractive drum
family: kick body, kick skin, snare body, snare skin, and hat tick, plus
tempo-derived lane gates. Keep the owner names but rewrite `kick`, `snare`,

View file

@ -15,37 +15,35 @@ song_form name=electro unit=bar
sections=intro:0-4:.35,build:4-8:.85,drop:8-16:1,break:16-20:.55,build2:20-24:.85,final_drop:24-32:1
energy=0:.22,4:.55,7.5:.92,8:1,16:.42,20:.62,23.5:.95,24:1,32:.18
sub_kick_grid name=main
kick=x...x...x...x...
snare=....x.......x...
hat=x.x.x.xx.x.x.xxx.
step=.117187
kick_freq=50
kick_decay=.11
kick_lpf=.52
kick_pitch_ramp=-4.2
kick_skin_freq=4200
kick_skin_decay=.018
kick_skin_hpf=.78
kick_skin_lpf=.22
snare_freq=205
snare_decay=.08
snare_skin_freq=6200
snare_skin_decay=.045
snare_skin_hpf=.80
snare_skin_lpf=.20
hat_gain=.08
hat_freq=11200
hat_decay=.018
hat_hpf=.92
hat_lpf=.13
# 808-derived drum authority: library shapes, then retriggered, driven, folded, and animated.
sequence name=kick_808 pattern=x...x...x...x... step=.117187 high=1.18 low=0
sequence name=kick_ghost pattern=..x.....x.....x. step=.117187 high=.30 low=0
sequence name=snare_808 pattern=....x.......x... step=.117187 high=.72 low=0
sequence name=clap_808 pattern=....x.......x... step=.117187 high=.42 low=0
sequence name=hat_808 pattern=x.x.x.xx.x.x.xxx. step=.117187 high=.22 low=.022
sequence name=hat_open pattern=..x.....x...x... step=.117187 high=.16 low=0
sequence name=perc_skip pattern=...x......x...x. step=.117187 high=.18 low=0
dust_hat pattern=x..x.x..x..x.x.. step=.117187 gain=.012 sustain=64
mod name=drum_crush
wave=sample_hold
hz=16
gain=.018
drive=.28
fold=.18
noise=.045
hpf=.025
mod name=hat_sizzle
wave=square
hz=8
noise=.055
hpf=.045
lpf=-.025
bass_response name=sub
root=55
progression=0,5,3,4
pattern=x..x.x...x..xx..
pattern=x.......x...x...
step=.117187
scene=0:.04,8:.10,16:.18,24:.14,32:.03,40:.14,56:.20,64:.08
wave=triangle
@ -54,16 +52,18 @@ bass_response name=sub
fmi=.24
chords name=drop_prog root=110 scale=minor progression=0,5,3,4 voicing=0,2,4 paths=/chords/drop/root,/chords/drop/third,/chords/drop/fifth step=bar
scale name=hook path=/seq/hook/freq root=440 scale=minor degrees=0,2,3,5,7,5,3,2,0,-2,0,3 step=.234375
scale name=hook path=/seq/hook/freq root=440 scale=minor degrees=0,2,4,2,0,-2,0,4 step=.46875
sequence name=hook_gate pattern=x.x.xx..x..x.xx. step=.117187 high=.32 low=0
sequence name=bass_a pattern=x...x..x....x.x. step=.117187 high=.36 low=0
sequence name=bass_b pattern=..x...x.xx...x.. step=.117187 high=.28 low=0
sequence name=bass_c pattern=...x.xx...x..x.. step=.117187 high=.22 low=0
sequence name=stab_gate pattern=x...x...xx..x... step=.117187 high=.34 low=0
sequence name=fill_gate pattern=............xxxx step=.117187 high=.24 low=0
sequence name=hook_phrase pattern=x...x.......x...x.....x....... step=.117187 high=.26 low=0
sequence name=hook_answer pattern=........x.......x.......x...x... step=.117187 high=.16 low=0
sequence name=hook_breath pattern=x...............x............... step=.117187 high=.055 low=0
sequence name=bass_a pattern=x.......x....... step=.117187 high=.32 low=0
sequence name=bass_b pattern=....x.......x... step=.117187 high=.22 low=0
sequence name=bass_c pattern=..........x...x. step=.117187 high=.18 low=0
sequence name=stab_gate pattern=......x......... step=.117187 high=.22 low=0
sequence name=fill_gate pattern=..............x. step=.117187 high=.18 low=0
mix name=lead points=0:.02,8:.05,14:.16,16:.24,24:.16,32:.04,40:.15,48:.26,56:.32,64:.08
mix name=lead points=0:.01,8:.025,14:.06,16:.08,24:.06,32:.015,40:.055,48:.10,56:.12,64:.025
mix name=pad points=0:.025,8:.06,15:.12,16:.025,28:.03,32:.10,40:.035,48:.07,64:.025
mix name=air points=0:.006,8:.014,15:.04,16:.004,32:.025,40:.01,47:.05,48:.006,64:.004
section_rise name=riser start=8 peak=15.5 end=16 low=0 high=.25
@ -76,13 +76,23 @@ spectrum layer=halo root=110 freq=220 spread=.009 pad_mode=bandwidth pad_bandwid
texture name=air_wash role=air gain=@/mix/air/gain sustain=64
texture name=codec_spray role=codec pattern=..x...x...x..x.. step=.117187 gain=.006 sustain=64
# Explicit transient reinforcements. The sugar owns the kit; these make it read.
voice name=kick_sub_punch wave=sine freq=46 gain=@/seq/main/kick env=ad attack=.001 decay=.075 sustain=64 pitch_ramp=-5.8 lpf=.28 drive=.05
voice name=snare_crack wave=noise freq=7200 gain=@/seq/main/snare env=ad attack=.001 decay=.032 sustain=64 hpf=.88 lpf=.16 bpf=.72 bpf_q=3.5 drive=.09
voice name=hat_needle wave=noise freq=12500 gain=@/seq/main/hat env=ad attack=.001 decay=.012 sustain=64 hpf=.94 lpf=.10 bpf=.80 bpf_q=4.5
# 808 library voices, stretched and dirtied for the drop.
voice name=eight08_kick_body wave=sine freq=58 gain=@/seq/kick_808 trigger=.117187 attack=.001 sustain=.045 decay=.42 sustain_level=.606 pitch_ramp=-3.8 min_freq=32 lpf=.85 drive=.42 fold=.12
voice name=eight08_kick_ghost wave=sine freq=52 gain=@/seq/kick_ghost trigger=.117187 attack=.001 sustain=.03 decay=.30 sustain_level=.42 pitch_ramp=-2.4 min_freq=31 lpf=.70 drive=.30 fold=.08
voice name=eight08_kick_click wave=noise freq=2600 gain=@/seq/kick_808 trigger=.117187 attack=.001 sustain=.006 decay=.024 noise=.92 hpf=.72 lpf=.24 bpf=.50 bpf_q=3.2 drive=.42 fold=.22
voice name=eight08_snare_body wave=sine freq=180 gain=@/seq/snare_808 trigger=.117187 attack=.001 sustain=.02 decay=.12 pitch_ramp=-1.2 lpf=.52 drive=.28 fold=.08
voice name=eight08_snare_noise wave=noise freq=140 gain=@/seq/snare_808 trigger=.117187 attack=.001 sustain=.035 decay=.20 noise=.85 hpf=.45 lpf=.55 bpf=.38 bpf_q=2.4 drive=.55 fold=.24
voice name=eight08_clap_one wave=noise freq=1800 gain=@/seq/clap_808 trigger=.117187 attack=.001 sustain=.018 decay=.11 noise=.95 hpf=.55 lpf=.42 phaser=.004 drive=.38 fold=.16
voice name=eight08_clap_two wave=noise freq=2200 gain=@/seq/clap_808 trigger=.117187 attack=.006 sustain=.022 decay=.13 noise=.95 hpf=.58 lpf=.38 phaser=.008 drive=.42 fold=.18
voice name=eight08_clap_three wave=noise freq=2600 gain=@/seq/clap_808 trigger=.117187 attack=.012 sustain=.028 decay=.15 noise=.95 hpf=.62 lpf=.34 phaser=.012 drive=.46 fold=.20
voice name=eight08_hat_noise wave=noise freq=9000 gain=@/seq/hat_808 trigger=.117187 attack=.001 sustain=.006 decay=.055 noise=1 hpf=.90 lpf=.24 drive=.30 fold=.20
voice name=eight08_hat_metal wave=square freq=6800 gain=@/seq/hat_808 trigger=.117187 attack=.001 sustain=.005 decay=.04 hpf=.90 lpf=.24 duty=.42 drive=.24 fold=.30
voice name=eight08_open_hat wave=noise freq=9800 gain=@/seq/hat_open trigger=.117187 attack=.001 sustain=.018 decay=.18 noise=1 hpf=.88 lpf=.28 drive=.34 fold=.22
voice name=eight08_perc_skip wave=square freq=540 gain=@/seq/perc_skip trigger=.117187 attack=.001 sustain=.025 decay=.14 duty=.42 hpf=.18 lpf=.72 drive=.34 fold=.26
voice name=hook_front wave=saw freq=@/seq/hook/freq gain=@/mix/lead/gain sustain=64 decay=.22 lpf=.58 bpf=.42 bpf_q=2.1 fm=2.8 fmi=.22 drive=.12
voice name=hook_edge wave=square freq=@/seq/hook/freq gain=@/seq/hook_gate sustain=64 decay=.09 hpf=.18 lpf=.68 duty=.38 fm=4.2 fmi=.18 fold=.05
voice name=hook_front wave=triangle freq=@/seq/hook/freq gain=@/seq/hook_phrase attack=.004 sustain=.11 decay=.24 lpf=.64 bpf=.44 bpf_q=1.8 fm=.9 fmi=.07 vibrato=.003 vibrato_hz=5.2 formants=620:90:.34,1160:150:.22,2300:320:.10 formant_mix=.16 drive=.04
voice name=hook_answer wave=sine freq=@/seq/hook/freq gain=@/seq/hook_answer attack=.006 sustain=.08 decay=.18 hpf=.04 lpf=.58 bpf=.38 bpf_q=1.4 fm=1.4 fmi=.05 vibrato=.002 vibrato_hz=6.1 formants=520:100:.22,1320:180:.28 formant_mix=.12
voice name=hook_breath wave=noise freq=7600 gain=@/seq/hook_breath attack=.006 sustain=.06 decay=.16 hpf=.72 lpf=.18 bpf=.64 bpf_q=2.4
# Complextro cut-ups: separate timbres take turns instead of one polite bass patch.
voice name=bass_chop_a wave=saw freq=@/chords/drop/root gain=@/seq/bass_a sustain=64 decay=.10 lpf=.24 fm=3.2 fmi=.65 drive=.22 fold=.08

View file

@ -41,7 +41,8 @@ public static class FaustEmitter
source.AppendLine("softclip(x) = ma.tanh(x * 1.35);");
source.AppendLine("fold(x) = 2.0 * abs(2.0 * (x / 4.0 - floor(x / 4.0)) - 1.0) - 1.0;");
source.AppendLine("release_start(a,d,g) = max(g, a + d);");
source.AppendLine("oneshot_adsr(a,d,s,r,g) = select2(age < a, select2(age < a + d, select2(age < release_start(a,d,g), select2(age < release_start(a,d,g) + r, 0.0, s * (1.0 - (age - release_start(a,d,g)) / max(0.0001, r))), s), 1.0 - (1.0 - s) * ((age - a) / max(0.0001, d))), age / max(0.0001, a));");
source.AppendLine("oneshot_adsr_at(t,a,d,s,r,g) = select2(t < a, select2(t < a + d, select2(t < release_start(a,d,g), select2(t < release_start(a,d,g) + r, 0.0, s * (1.0 - (t - release_start(a,d,g)) / max(0.0001, r))), s), 1.0 - (1.0 - s) * ((t - a) / max(0.0001, d))), t / max(0.0001, a));");
source.AppendLine("oneshot_adsr(a,d,s,r,g) = oneshot_adsr_at(age,a,d,s,r,g);");
source.AppendLine("seg(t,t0,d,a,b) = a + (b - a) * clip01((t - t0) / max(0.0001, d));");
source.AppendLine("bez(t,a,b,c,d) = (1.0 - t) * (1.0 - t) * (1.0 - t) * a + 3.0 * (1.0 - t) * (1.0 - t) * t * b + 3.0 * (1.0 - t) * t * t * c + t * t * t * d;");
source.AppendLine("seg_bez(t,t0,d,a,b,c,e) = bez(clip01((t - t0) / max(0.0001, d)), a, b, c, e);");
@ -50,8 +51,10 @@ public static class FaustEmitter
source.AppendLine("seg_exp(t,t0,d,a,b) = exp(log(max(0.00001, a)) + (log(max(0.00001, b)) - log(max(0.00001, a))) * clip01((t - t0) / max(0.0001, d)));");
source.AppendLine("seg_curve(c,t,t0,d,a,b) = select2(c < 0.5, seg_exp(t,t0,d,a,b), seg(t,t0,d,a,b));");
source.AppendLine("rl_release_start(r1,r2,r3,g) = max(g, r1 + r2 + r3);");
source.AppendLine("rl4_env_from(s0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = select2(age < r1, select2(age < r1 + r2, select2(age < r1 + r2 + r3, select2(age < rl_release_start(r1,r2,r3,g), select2(age < rl_release_start(r1,r2,r3,g) + r4, l4, seg_curve(c4, age, rl_release_start(r1,r2,r3,g), r4, l3, l4)), l3), seg_curve(c3, age, r1 + r2, r3, l2, l3)), seg_curve(c2, age, r1, r2, l1, l2)), seg_curve(c1, age, 0, r1, s0, l1));");
source.AppendLine("rl4_env(r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = rl4_env_from(0.0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g);");
source.AppendLine("rl4_env_from_at(t,s0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = select2(t < r1, select2(t < r1 + r2, select2(t < r1 + r2 + r3, select2(t < rl_release_start(r1,r2,r3,g), select2(t < rl_release_start(r1,r2,r3,g) + r4, l4, seg_curve(c4, t, rl_release_start(r1,r2,r3,g), r4, l3, l4)), l3), seg_curve(c3, t, r1 + r2, r3, l2, l3)), seg_curve(c2, t, r1, r2, l1, l2)), seg_curve(c1, t, 0, r1, s0, l1));");
source.AppendLine("rl4_env_at(t,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = rl4_env_from_at(t,0.0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g);");
source.AppendLine("rl4_env_from(s0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = rl4_env_from_at(age,s0,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g);");
source.AppendLine("rl4_env(r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g) = rl4_env_at(age,r1,l1,c1,r2,l2,c2,r3,l3,c3,r4,l4,c4,g);");
source.AppendLine("lfo_sin(hz, phase) = sin(2.0 * ma.PI * (age * hz + phase));");
source.AppendLine("lfo_tri(hz, phase) = 1.0 - 4.0 * abs((age * hz + phase - floor(age * hz + phase)) - 0.5);");
source.AppendLine("lfo_sq(hz, phase) = select2((age * hz + phase - floor(age * hz + phase)) < 0.5, -1.0, 1.0);");
@ -319,6 +322,10 @@ public static class FaustEmitter
var formant = ModExpressionForTarget(voice.Modulators, ModTarget.FormantMix);
var lpfMod = ModExpressionForTarget(voice.Modulators, ModTarget.LowPass);
var hpfMod = ModExpressionForTarget(voice.Modulators, ModTarget.HighPass);
var voiceAge = $"{name}_age";
source.AppendLine(voice.Note.TriggerIntervalSeconds > 0
? $"{voiceAge} = wrap01(age / {F(voice.Note.TriggerIntervalSeconds)}) * {F(voice.Note.TriggerIntervalSeconds)};"
: $"{voiceAge} = age;");
var minFreq = parameters.Expression(OwnerField(ownerPath, "pitch/min_freq"), voice.Pitch.MinFrequencyHz);
var noteFreq = NoteFrequencyExpression(source, patch.Playback, voice.Note, name, OwnerField(ownerPath, "note/frequency"), parameters);
@ -328,19 +335,19 @@ public static class FaustEmitter
var vibratoDepth = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato"), voice.Pitch.VibratoDepth);
var vibratoHz = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato_hz"), voice.Pitch.VibratoHz);
var vibratoDelay = parameters.Expression(OwnerField(ownerPath, "pitch/vibrato_delay"), voice.Pitch.VibratoDelaySeconds);
var baseFreq = $"max({minFreq}, {noteFreq} * pow(2.0, {pitchRamp} * age + 0.5 * {pitchDelta} * age * age))";
var baseFreq = $"max({minFreq}, {noteFreq} * pow(2.0, {pitchRamp} * {voiceAge} + 0.5 * {pitchDelta} * {voiceAge} * {voiceAge}))";
var hasVibrato = voice.Pitch.VibratoDepth != 0 && voice.Pitch.VibratoHz > 0 ||
parameters.IsBound(OwnerField(ownerPath, "pitch/vibrato")) ||
parameters.IsBound(OwnerField(ownerPath, "pitch/vibrato_hz"));
var vibrato = hasVibrato
? $" * (1.0 + select2(age < {vibratoDelay}, 0.0, sin(2.0 * ma.PI * (age - {vibratoDelay}) * {vibratoHz}) * {vibratoDepth}))"
? $" * (1.0 + select2({voiceAge} < {vibratoDelay}, 0.0, sin(2.0 * ma.PI * ({voiceAge} - {vibratoDelay}) * {vibratoHz}) * {vibratoDepth}))"
: "";
var arpeggio = voice.Arpeggio is null
? "1.0"
: $"select2(age < {parameters.Expression(OwnerField(ownerPath, "arpeggio/delay"), voice.Arpeggio.DelaySeconds)}, {parameters.Expression(OwnerField(ownerPath, "arpeggio/multiplier"), voice.Arpeggio.Multiplier)}, 1.0)";
: $"select2({voiceAge} < {parameters.Expression(OwnerField(ownerPath, "arpeggio/delay"), voice.Arpeggio.DelaySeconds)}, {parameters.Expression(OwnerField(ownerPath, "arpeggio/multiplier"), voice.Arpeggio.Multiplier)}, 1.0)";
var frequency = $"(({baseFreq}){vibrato}) * {arpeggio} * pow(2.0, patch_mod_pitch + {pitch})";
var dutyExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "osc/duty"), voice.Oscillator.Duty)} + {parameters.Expression(OwnerField(ownerPath, "duty/ramp"), voice.Duty.RampPerSecond)} * age + patch_mod_duty + {duty})";
var fmIndex = $"max(0.0, {parameters.Expression(OwnerField(ownerPath, "fm/index"), voice.Fm.Index)} + patch_mod_fm_index + {fmIndexMod}) * {FmDecay(parameters.Expression(OwnerField(ownerPath, "fm/decay"), voice.Fm.IndexDecaySeconds), voice.Fm.IndexDecaySeconds, parameters.IsBound(OwnerField(ownerPath, "fm/decay")))}";
var dutyExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "osc/duty"), voice.Oscillator.Duty)} + {parameters.Expression(OwnerField(ownerPath, "duty/ramp"), voice.Duty.RampPerSecond)} * {voiceAge} + patch_mod_duty + {duty})";
var fmIndex = $"max(0.0, {parameters.Expression(OwnerField(ownerPath, "fm/index"), voice.Fm.Index)} + patch_mod_fm_index + {fmIndexMod}) * {FmDecay(parameters.Expression(OwnerField(ownerPath, "fm/decay"), voice.Fm.IndexDecaySeconds), voice.Fm.IndexDecaySeconds, parameters.IsBound(OwnerField(ownerPath, "fm/decay")), voiceAge)}";
var oscillator = oscillatorOverride ??
(voice.VocalNetwork is { } vocalNetwork
? VocalNetworkExpression(source, patch, vocalNetwork, ownerPath, name, frequency, parameters, warnings, options)
@ -352,11 +359,12 @@ public static class FaustEmitter
? AcousticNetworkExpression(source, patch, acousticNetwork, ownerPath, name, frequency, parameters, warnings, options)
: OscillatorExpression(patch, voice, ownerPath, frequency, dutyExpression, fmIndex, parameters));
var envelope = voice.RateLevelEnvelope is not null
? RateLevelEnvelopeExpression(voice.RateLevelEnvelope, noteGate)
? RateLevelEnvelopeExpression(voice.RateLevelEnvelope, noteGate, voiceAge)
: EnvelopeExpression(
voice.Envelope,
noteGate,
UsesHostPlayback(patch.Playback) || voice.Note.Source == NoteSource.Host,
voiceAge,
field => parameters.Expression(OwnerField(ownerPath, field), field switch
{
"env/attack" => voice.Envelope.AttackSeconds,
@ -378,13 +386,13 @@ public static class FaustEmitter
var foldExpression = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/fold"), voice.Color.Fold)} + patch_mod_fold + {fold})";
var formantMix = $"clip01({parameters.Expression(OwnerField(ownerPath, "color/formant_mix"), voice.Color.FormantMix)} + patch_mod_formant_mix + {formant})";
var lpfEnvelope = voice.Filter.LowPassEnvelope is { } filterEnvelope
? $" + {RateLevelEnvelopeExpression(filterEnvelope, noteGate)}"
? $" + {RateLevelEnvelopeExpression(filterEnvelope, noteGate, voiceAge)}"
: "";
var hpfEnvelope = voice.Filter.HighPassEnvelope is { } highPassEnvelope
? $" + {RateLevelEnvelopeExpression(highPassEnvelope, noteGate)}"
? $" + {RateLevelEnvelopeExpression(highPassEnvelope, noteGate, voiceAge)}"
: "";
var lpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/lpf"), voice.Filter.LowPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/lpf_ramp"), voice.Filter.LowPassRamp)} * age * 1.8){lpfEnvelope} + patch_mod_lpf + {lpfMod})";
var hpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/hpf"), voice.Filter.HighPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/hpf_ramp"), voice.Filter.HighPassRamp)} * age * 2.0){hpfEnvelope} + patch_mod_hpf + {hpfMod})";
var lpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/lpf"), voice.Filter.LowPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/lpf_ramp"), voice.Filter.LowPassRamp)} * {voiceAge} * 1.8){lpfEnvelope} + patch_mod_lpf + {lpfMod})";
var hpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/hpf"), voice.Filter.HighPass)} * (1.0 + {parameters.Expression(OwnerField(ownerPath, "filter/hpf_ramp"), voice.Filter.HighPassRamp)} * {voiceAge} * 2.0){hpfEnvelope} + patch_mod_hpf + {hpfMod})";
var bpf = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/bpf"), voice.Filter.BandPass)})";
var notch = $"clip01({parameters.Expression(OwnerField(ownerPath, "filter/notch"), voice.Filter.Notch)})";
var highPassOrder = Math.Clamp(voice.Filter.HighPassOrder, 1, 12);
@ -423,7 +431,7 @@ public static class FaustEmitter
parameters.IsBound(OwnerField(ownerPath, "phaser/offset")) ||
parameters.IsBound(OwnerField(ownerPath, "phaser/ramp")))
{
var delay = $"min(2047.0, max(0.0, abs({parameters.Expression(OwnerField(ownerPath, "phaser/offset"), voice.Phaser.OffsetSeconds)} + {parameters.Expression(OwnerField(ownerPath, "phaser/ramp"), voice.Phaser.RampSecondsPerSecond)} * age) * ma.SR))";
var delay = $"min(2047.0, max(0.0, abs({parameters.Expression(OwnerField(ownerPath, "phaser/offset"), voice.Phaser.OffsetSeconds)} + {parameters.Expression(OwnerField(ownerPath, "phaser/ramp"), voice.Phaser.RampSecondsPerSecond)} * {voiceAge}) * ma.SR))";
source.AppendLine($"{name}_phased = {name}_filtered + ({name}_filtered : de.fdelay(2048, {delay}));");
}
else
@ -508,7 +516,7 @@ public static class FaustEmitter
return control;
}
private static string EnvelopeExpression(Envelope envelope, string gate, bool hostGate, Func<string, string> value)
private static string EnvelopeExpression(Envelope envelope, string gate, bool hostGate, string age, Func<string, string> value)
{
var attack = value("env/attack");
var decay = value("env/decay");
@ -516,13 +524,13 @@ public static class FaustEmitter
var release = value("env/release");
return hostGate
? $"en.adsr({attack}, {decay}, {sustain}, {release}, {gate})"
: $"oneshot_adsr({attack}, {decay}, {sustain}, {release}, {gate})";
: $"oneshot_adsr_at({age}, {attack}, {decay}, {sustain}, {release}, {gate})";
}
private static string RateLevelEnvelopeExpression(RateLevelEnvelope envelope, string gate) =>
private static string RateLevelEnvelopeExpression(RateLevelEnvelope envelope, string gate, string age = "age") =>
envelope.StartLevel == 0
? $"rl4_env({F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})"
: $"rl4_env_from({F(envelope.StartLevel)}, {F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})";
? $"rl4_env_at({age}, {F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})"
: $"rl4_env_from_at({age}, {F(envelope.StartLevel)}, {F(envelope.Rate1Seconds)}, {F(envelope.Level1)}, {Curve(envelope.Curve1)}, {F(envelope.Rate2Seconds)}, {F(envelope.Level2)}, {Curve(envelope.Curve2)}, {F(envelope.Rate3Seconds)}, {F(envelope.Level3)}, {Curve(envelope.Curve3)}, {F(envelope.Rate4Seconds)}, {F(envelope.Level4)}, {Curve(envelope.Curve4)}, {gate})";
private static string Curve(RateLevelCurve curve) =>
curve == RateLevelCurve.Exponential ? "1" : "0";
@ -1439,6 +1447,7 @@ public static class FaustEmitter
op.Envelope,
UsesHostPlayback(playback) || op.Note.Source == NoteSource.Host ? graphNoteGate : F(op.Note.GateSeconds),
UsesHostPlayback(playback) || op.Note.Source == NoteSource.Host,
"age",
field => field switch
{
"env/attack" => parameters.Expression($"{operatorPath}/env/attack", op.Envelope.AttackSeconds),
@ -1580,8 +1589,8 @@ public static class FaustEmitter
private static string FmDecay(float seconds) => seconds > 0 ? $"exp(-age / {F(Math.Max(seconds, 0.0001f))})" : "1.0";
private static string FmDecay(string seconds, float defaultSeconds, bool isBound) =>
defaultSeconds > 0 || isBound ? $"exp(-age / max({seconds}, 0.0001))" : "1.0";
private static string FmDecay(string seconds, float defaultSeconds, bool isBound, string age = "age") =>
defaultSeconds > 0 || isBound ? $"exp(-{age} / max({seconds}, 0.0001))" : "1.0";
private static string ParameterIdentifier(int index) => $"patch_param_{index}";

View file

@ -77,7 +77,8 @@ public enum PlaybackMode
public sealed record Note(
float FrequencyHz = 440,
float GateSeconds = 0.1f,
NoteSource Source = NoteSource.OneShot);
NoteSource Source = NoteSource.OneShot,
float TriggerIntervalSeconds = 0);
public sealed record Playback(
PlaybackMode Mode = PlaybackMode.OneShot,

View file

@ -1801,6 +1801,9 @@ public static class PatchScript
sustainLevel = 1 / gainScale;
}
var noteSource = ParseNoteSource(GetAny(fields, ["note_source", "source"], "oneshot"), line);
var triggerInterval = TryGetAny(fields, ["trigger", "retrigger", "trigger_every", "repeat_every", "hit_step"], out var triggerText)
? ParseFloat(triggerText, line)
: 0;
if (TryGetAny(fields, ["midi"], out var midi) && ParseBool(midi, line))
{
noteSource = NoteSource.Host;
@ -1842,7 +1845,7 @@ public static class PatchScript
frequency,
GetBoundFloat(fields, line, 0.5f, OwnerField(ownerPath, "osc/duty"), "duty", "du"),
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "osc/phase"), "phase", "pa")),
Note = new Note(frequency, gateSeconds, noteSource),
Note = new Note(frequency, gateSeconds, noteSource, triggerInterval),
Envelope = envelopeSpec?.Envelope ?? new Envelope(
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "env/attack"), "attack", "a"),
GetBoundFloat(fields, line, 0, OwnerField(ownerPath, "env/decay"), "env_decay", "ed"),

View file

@ -275,3 +275,4 @@
{"timestamp":"2026-06-01T04:40:00+01:00","topic":"music-agent-orchestration-generalization","claim":"The durable lesson from song-agent and orchestration work is now summarized as a report: agents learn only when role ownership, audible continuity, render failures, studio reasoning, curation, storage, and retrieval are all explicit evidence paths. The mistakes to stop repeating are syntax attendance as musicianship, texture as arrangement, metrics as ears, weak winners as doctrine, invisible targets, non-retrievable memory, hidden parser/render faults, monolithic trial stores, and proposed sugar masquerading as live syntax.","evidence":"docs/music-agent-orchestration-lessons.md; state/map.yaml"}
{"timestamp":"2026-06-01T05:20:00+01:00","topic":"song-form-sugar-and-electro-demo","claim":"AquaSynth now has `song_form` arrangement sugar that owns named section gates and an optional whole-track energy contour, lowering into visible `/form/<name>/<section>` and `/form/<name>/energy` parameters plus control curves. The new `patches/advanced/electro-drop-demo.aqua` uses it with `sub_kick_grid`, `bass_response`, `additive_wash`, `dust_hat`, `section_rise`, scale/chord/mix lanes, and complextro-style alternating bass timbres to make a structured electro demo with build, drop, break, second build, and final drop.","evidence":"src/AquaSynth.Core/PatchScript.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/music-production-knowledge/aqua-dsp-usage-manual.md; patches/advanced/electro-drop-demo.aqua; dotnet test tests\\AquaSynth.Dsl.Tests\\AquaSynth.Dsl.Tests.csproj --no-restore --filter \"ParserSupportsSongFormSugar|ParserSupportsProductionAbstractionSugar|PatchLibraryScriptsParseAndExportFaust\": passed"}
{"timestamp":"2026-06-01T04:43:51+01:00","topic":"log-mel-steered-production-sugar","claim":"Music agents should steer patch revisions by directly comparing target and candidate log-mel spectrogram/features between attempts, then editing the lane owner that explains the visible mismatch. RMS, cosine, and motion metrics are warning lights, not optimization targets. Aqua DSL now includes reusable production sugar for subtractive drum grids, gated dust hats, bass response lanes, additive wash beds, and section-rise mix automation, and the Aqua DSP manual teaches those abstractions as inspectable owners rather than hidden instruments.","evidence":"src/AquaSynth.Core/PatchScript.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/music-production-knowledge/aqua-dsp-usage-manual.md"}
{"timestamp":"2026-06-01T11:35:00+01:00","topic":"sequenced-drum-retrigger-ownership","claim":"Music agents must not treat sequenced gain as percussion articulation. One-shot voices age from song start, so gain gates can reopen dead envelopes and make 808-style drums sound like generic noise or weak human-music mush. Patterned drum/transient voices that need per-hit envelope, pitch fall, FM decay, filter ramp, or phaser motion must use `trigger=<step_seconds>` and should start from the stock 808 kick/snare/hat/clap shapes split into explicit body, click, skin, clap-tap, hat, open-hat, ghost, and skip layers with drive/fold/sample-hold texture.","evidence":"src/AquaSynth.Core/Model.cs; src/AquaSynth.Core/PatchScript.cs; src/AquaSynth.Core/FaustExport.cs; tests/AquaSynth.Dsl.Tests/PatchScriptTests.cs; docs/music-production-knowledge/aqua-dsp-usage-manual.md; tools/run-song-snippet-trial-swarm.ps1; patches/advanced/electro-drop-demo.aqua"}

View file

@ -151,6 +151,7 @@ live_invariants:
- Song-snippet parity now treats target analysis as CultMesh database evidence. `song-prepare` writes each 30-second clip's log-mel spectrogram CSV, per-band first/second derivative stats, RMS envelope, envelope autocorrelation, and analysis report. `song-score` upserts those as typed `SongChallengeEvidenceDocument` records in the same `.cc` store as `IpaTrialResult`, adds target metrics and `cultmesh://aquasynth/song-challenge-evidence/...` artifacts to every result, and `search/index` derive Qdrant chunks from the database records. `pattern` and `scale` DSL sugar are live authoring conveniences that lower into normal parameters and hold-loop curves; prompt agents may propose richer Strudel-like syntax, but candidate patches must use implemented syntax.
- Song-scene scoring now records instrument-role ownership so the music-generator curriculum does not learn that simple oscillator blips are good instruments by default. `song-score` emits `instrument_voice_syrinx`, `instrument_drum_subtractive`, `instrument_pad_additive`, `musical_instrument_score`, and `chip_distress_risk`; verdict/distillation ranking rewards owned roles and demotes chip-distress-risk candidates unless preserved as contrast. Swarm prompts now require voice-like leads to prefer syrinx/acoustic sources with pressure/opening/radiation motion, percussion to prefer subtractive pitched bodies plus filtered noise skins, and pads/beds to prefer additive/PAD `layer`/`harmonics`/`spectrum` banks.
- Song-scene harness supports full-song curriculum and in-agent revision. `song-prepare --duration-seconds 0` freezes the whole decoded song from sample zero, and `run-song-snippet-trial-swarm.ps1 -FullSongs` samples recursively from the Reinier parent folder including `.m4a/.mp4`. Target and candidate analysis now include whitened spectral autocorrelation over band-whitened log-mel flux. Candidate-side Faust-render artifacts include log-mel spectrogram/stats, RMS envelope/autocorr, whitened spectral autocorr, and an analysis markdown report. Swarm prompts require five local score/revise attempts per target before publishing the final `.aqua`.
- Patterned AquaSynth percussion now needs explicit voice-local retrigger ownership. Sequenced gain (`gain=@/seq/kick`) only changes level; without `trigger=<step_seconds>`, a one-shot drum envelope, pitch fall, FM decay, filter ramp, or phaser motion keeps aging from song start and later hits become dead-gate mush. Music agents should start 808 percussion from `patches/808/kick.aqua`, `snare.aqua`, `hat.aqua`, and `clap.aqua`, split body/click/skin/clap/hat layers explicitly, add `trigger`, and use drive/fold/sample-hold texture instead of broadband noise pretending to be a kit.
- TurbulenceJet now includes a release-timed burst-noise component in addition to the deterministic pressure pulse, but a stronger trial barely moved papa; the source is not the main remaining articulation bottleneck. Graph radiation now mixes 30% raw boundary flow with 70% high-passed flow after local admittance weighting, down from the over-bright 15/85 blend. Latest radiation/source artifact under `artifacts/parity/pink-trombone-utterance-logmel/20260527T174236049`: mama/papa/thrombosis cosine 0.6578/0.7692/0.5585, rmsRatio 3.0006/2.6153/2.3488, articulation 0.1437/0.1173/0.1191. Static `closure-release` under `artifacts/parity/pink-trombone-logmel/20260527T174139661` has rmsRatio 0.9900 and speechBandRatio 0.0842. This is still not speaking; next pressure is true tract resonance/passive scattering and pressure storage, not more burst gain.
- Same-path two-port area discontinuities now emit `graph_area_energy_in_*` and `graph_area_energy_out_*` probes alongside the three-port branch `graph_connection_energy_*` probes. Use these to inspect passivity and formant-body failures at the scatter sites rather than judging only final output loudness.
- `FaustExportOptions.DebugProbeUi` wraps graph probe signals in Faust `vbargraph` UI controls under `/debug/...` without changing audio output arity. Native streaming Faust patches now separate writable controls from read-only probe zones and can read/snapshot probe values after `ProcessBlock`; use this for acoustic graph passivity/radiation inspection, leave it off for normal parity renders.

View file

@ -218,6 +218,23 @@ public sealed class PatchScriptTests
Assert.Equal(1f, energy.Points[2].Value, 3);
}
[Fact]
public void VoiceTriggerIntervalRetriggersLocalEnvelopeAge()
{
const string script = """
sequence name=kick pattern=x... step=.125 high=.9 low=0
voice wave=sine freq=58 gain=@/seq/kick trigger=.125 attack=.001 sustain=.04 decay=.22 pitch_ramp=-3.8
""";
var patch = PatchScript.Parse(script);
Assert.Equal(.125f, patch.Voices[0].Note.TriggerIntervalSeconds, 5);
var export = FaustEmitter.Emit(patch).Source;
Assert.Contains("voice_0_age = wrap01(age / 0.125) * 0.125;", export);
Assert.Contains("oneshot_adsr_at(voice_0_age", export);
Assert.Contains("-3.8 * voice_0_age", export);
}
[Fact]
public void ParserSupportsVoiceVowelFrames()
{

View file

@ -674,6 +674,7 @@ Self-iteration loop:
- The analysis worker records song-continuity metrics: candidate_motion_coverage, motion_coverage_ratio, candidate_first_second_energy_share, first_second_energy_excess, candidate_tail_energy_share, and mode_collapse_risk. A patch that plays a short phrase in the first second and then coasts on low-motion texture/noise is failed song evidence even if its RMS or log-mel score looks tolerable.
- If an attempt has mode_collapse_risk >= .45, the next attempt must add later-section motifs or eventful motion across the target's full duration. Do not merely raise the noise bed or pad gain; that is hiding, not composing.
- If an attempt has high template_loop_risk or noise_percussion_risk, rebuild the role ownership before touching level: drums need pitched body plus filtered skin and target-specific gates; texture needs band limits and musical motion.
- Percussion articulation warning: sequenced gain alone does not retrigger a one-shot drum envelope. For patterned drums, every body/click/skin/hat/clap voice that should strike per step must include `trigger=<step_seconds>` matching the lane step, otherwise pitch ramps and envelopes age from song start and later hits become dead-gate mush.
- Keep intermediate candidates inside the private iteration root; publish only one final .aqua per target to the official target patch output directory.
Reusable abstraction mining:
@ -701,6 +702,7 @@ Instrument ownership:
- If the patch has a singing, creature, or alien lead role, use the syrinx/acoustic owner: source_port kind=syrinx, pressure/opening curves, acoustic_network, and acoustic_voice, then shape it with filters/modulation.
- Acoustic/syrinx graph syntax is complete-graph syntax. Do not write isolated source_port or acoustic_network fragments. If you use source_port, you must also define the referenced acoustic_path/path, terminals/connections/radiation_port, wave_clock when needed, acoustic_network, and acoustic_voice. If you cannot build that complete scaffold from a known-valid local patch such as patches/advanced/syrinx-voice.aqua, use a formant-filtered ordinary lead for this attempt and write the missing syrinx scaffold as an Aqua gap instead of submitting a non-rendering badge.
- If the patch has drums or rhythmic transients, use subtractive ownership: pitched sine/triangle body, filtered noise skin, short envelope, and pattern gate.
- For 808 percussion, start from `patches/808/kick.aqua`, `snare.aqua`, `hat.aqua`, and `clap.aqua` as reference shapes. In the song patch, split them into explicit retriggered layers: kick body/click/ghost, snare body/noise, staggered clap taps, closed/open hats, and optional perc skips. Add `drive`, `fold`, narrow hpf/lpf/bpf values, and sample-hold or square LFO modulators for crushed texture. If the render still sounds like generic pink noise, the drum owner failed; do not solve it with global gain.
- If the patch has a pad, bed, drone, or harmonic wash, use additive/PAD ownership: `layer engine=add` or `engine=pad`, `harmonics`, `spectrum`, and slow modulation.
- Ordinary simple voices are still legal as helpers, but a candidate built mostly from square/sine/saw blips with no syrinx, subtractive drum, additive/PAD, or shaped texture role will be treated as chip-distress-risk evidence.
@ -715,6 +717,7 @@ Rhythm and tempo:
- new sugar now exists: `pattern name=kick_seq path=/seq/kick pattern=x..x step=<beat_seconds/4> high=1 low=0`
- use beat-derived times from `beat_seconds`; eighth notes are `beat_seconds/2`, sixteenths are `beat_seconds/4`.
- bind parameters into voices or primitives, for example gain=@/seq/kick, tremolo_hz=<tempo_bpm/60>, noise=@/seq/hiss, freq=@/seq/pitch.
- Use `trigger=<step_seconds>` on sequenced drum/transient voices when their envelope, pitch fall, FM decay, or filter motion must restart per hit. Example: `voice name=kick_body wave=sine freq=58 gain=@/seq/kick trigger=<beat_seconds/4> attack=.001 sustain=.045 decay=.42 sustain_level=.606 pitch_ramp=-3.8 drive=.42 fold=.12`.
- Do not invent unimplemented syntax like `s("bd sn")` inside the `.aqua` candidate. You may propose it in `instrument-conventions.md`.
Composition sugar available today: