Coding Music in Strudel: quando il codice DIVENTA un brano (con breakdown della mia composizione)
Se pensi che “programmare” significhi solo app, siti e server… oggi ti faccio cambiare idea: con Strudel puoi scrivere musica come se stessi scrivendo software. Non è una metafora: è proprio codice che genera ritmo, armonia, arrangiamento, automazioni, transizioni, energia.
Strudel è un ambiente di live coding musicale nel browser, ispirato al pattern language di TidalCycles e “tradotto” in JavaScript. (strudel.cc)
La cosa bella? Lo apri, scrivi, premi play… e stai già suonando.
Qui sotto ti spiego una mia composizione che trovi su Youtube (quella che hai visto nel codice): come imposto il tempo, come costruisco bassline, arp, drums, e soprattutto come organizzo un arrangiamento lungo con arrange() e stack().
1) Tempo: setcps(140/60/4) (e perché è una bomba)
In Strudel il tempo spesso si ragiona in CPS = cycles per second (cicli al secondo), invece del classico BPM. (strudel.cc)
Io imposto così:
setcps(140/60/4)
Traduzione “umana”:
140/60converte 140 BPM in beat per secondo/4perché 1 ciclo spesso lo immagini come 1 battuta da 4 beat (4/4)
Quindi: sto dicendo a Strudel “vai a 140 BPM, ma ragiona in cicli musicali”. Risultato: tutto ciò che scrivo (pattern, slow(), fast(), struct()) si incastra con un clock solidissimo.
Se ti interessa: Strudel spiega anche la relazione tra setcps e setcpm (cycles per minute). (strudel.cc)
2) Samples pack: samples('github:algorave-dave/samples')
samples('github:algorave-dave/samples')
Qui carico un pacchetto di samples da GitHub (comodissimo: basta un riferimento pubblico). Strudel supporta il caricamento di sample da URL pubblici e “sound bank” condivisi. (strudel.cc)
Questo significa una cosa enorme: la tua DAW diventa un repo. E tu non “cerchi” suoni: li versioni.
3) L’idea: lasciare “manopole” nel codice (variabili dinamiche)
Io preparo due array:
const gain = [
"2",
"{0.75 2.5}*4",
]
const Structures = [
"~",
"x*4",
]
Queste sono “manopole” da performance: invece di riscrivere tutto al volo, mi lascio macro pronte da infilare in postgain() o in struct() quando voglio cambiare energia.
E questa è mentalità da live coding serio: il codice non è statico, è uno strumento che suoni.
4) Bassline: supersaw + filtro + spazio (energia controllata)
Il basso è una sequenza di note ripetute con densità alta:
const bassline = note("[eb1 eb2]!16 [f2 f1]!16 [g2 g1]!16 [f2 f1]!8 [bb3 bb2]!8")
.sound("supersaw")
.slow(8)
.postgain(2)
.lpf(slider(5000, 300, 5000))
.room(0.9)
.lpf(300)
.room(0.4)
Cose importanti qui:
note("...")definisce pattern melodico/armonico!16e!8= ripetizioni serrate (densità ritmica)sound("supersaw")= timbro pieno (tipico energia rave/club)slow(8)= spalmo la frase su più cicli (respira, non è un mitra continuo)lpf(...)= low pass filter: taglio le alte per controllare l’aggressivitàslider(...)= parametro “manuale” da muovere live (una vera manopola)room(...)= spazio / ambience per dare profondità
Poi ho anche una versione più alta (basslinehigh) che uso nei drop per far “urlare” la parte senza cambiare la grammatica del brano.
5) Arpeggiatore: progressione, scelta dinamica e inviluppi
Qui creo un set di pattern:
const arpeggiator = [
"{d4 bb3 eb3 d3 bb2 eb2}%16",
"{c4 bb3 f3 c3 bb2 f2}%16",
"{d4 bb3 g3 d3 bb2 g2}%16",
"{c4 bb3 f3 c3 bb2 f2}%16",
]
Poi seleziono con pick() usando un pattern di indici:
const main_arp = note(pick(arpeggiator, "<0 1 2 3>".slow(2)))
.sound("supersaw")
.lpenv(slider(56.06375, 1.25, 600))
.lpf(300)
.sustain(0.5).release(0.01).attack(0)
.room(0.9)
Qui la “cosa grossa” è che anche la scelta degli accordi è un pattern. Non sto dicendo “suona questo e basta”: sto dicendo “scegli tra queste frasi secondo una logica temporale”.
E con lpenv(...) (envelope sul filtro) do movimento: il suono “apre e chiude” come se stessi automatizzando in una DAW… ma lo sto facendo in codice.
6) Drums: kick tech + 808 hats/snare + struct per il groove
const kick = s("tech:5").postgain(6).curve(2).pdec(1)
const hats = s("hh*16").bank("RolandTR808").postgain(0.8).room(0).speed(0.8).cut(1)
const snare = s("~ sn").bank("RolandTR808").postgain(1.2).room(0.3)
const clap = s("~ ~ cp ~").postgain(1.5).room(1.3)
s("...")è la factory classica per pattern di sample (Strudel lavora tantissimo così). (strudel.cc)hh*16= hi-hat a 16 colpi per ciclo (spinta costante)~= pausa (spazio = groove)bank("RolandTR808")= estetica immediata: suona “giusto” senza perdere tempocut(1)evita sovrapposizioni inutili del charlestonstruct("x ~ ~ ~ ...")sul kick mi dà il pattern di presenza (dove entra e dove respira)
7) Arrangiamento lungo: arrange() + stack() = struttura vera, non loop infinito
Questa è la parte che trasforma “pattern belli” in brano.
arrange() in Strudel serve per mettere sezioni una dopo l’altra su più cicli. (strudel.cc)
E stack() sovrappone più layer (come tracce in una DAW). (Scerifforosso)
Ecco l’idea della mia struttura:
- INTRO (8): solo arp, filtro chiuso → tensione
- BUILD-UP (8): entra kick, filtro si apre → sale energia
- PRE-DROP (2): strings “in/out” → effetto risucchio
- DROP 1 (16): full power
- BREAKDOWN (8): respiro + atmosfera
- BUILD-UP 2 (8): ancora più spinta + automazioni
- PRE-DROP 2 (2): transizione cinematica
- DROP 2 (24): pattern più complesso + extra percussioni
- OUTRO (8): fade controllato
E la magia è che non sto “tirando clip”: sto descrivendo una timeline con codice.
8) Automazioni: sine.range(...) (il movimento che fa sembrare tutto vivo)
Uso spesso:
.lpf(sine.range(200, 800).slow(8))
.postgain(sine.range(0.5, 1.5).slow(8))
Questo vuol dire: filtro e volume non sono fissi, ma oscillano con una sinusoide nel tempo. Risultato: il brano respira, pulsa, cambia pelle.
Questa è una delle cose più sottovalutate: tanti fanno live coding “a loop”, ma quando inizi a trattare parametri come segnali (LFO), entri nel livello “pro”.
9) Transizioni intelligenti: early() e late() (micro-arrangiamento)
Nelle pre-drop uso:
.early("0:1")= suona solo nella prima metà.late("1:2")= suona solo nella seconda metà
È una tecnica semplice ma potentissima per creare chiamata/risposta, ingresso/uscita, tensione/risoluzione, senza dover creare mille pattern separati.
10) Il punto: perché questa roba è “grossa”?
Perché qui stai facendo tre cose insieme:
- Composizione (note, accordi, pattern)
- Sound design (filtri, inviluppi, room, delay, curve)
- Arrangiamento (struttura lunga, transizioni, intensità)
…e le stai facendo con lo stesso linguaggio: pattern.
È coding, sì. Ma è anche performance. È come suonare un synth dove i tasti sono funzioni.
Codice completo della composizione (Strudel)
setcps(140/60/4)
samples('github:algorave-dave/samples')
// ----- VARIABILI DINAMICHE -----
const gain = [
"2",
"{0.75 2.5}*4",
]
const Structures = [
"~",
"x*4",
]
// ----- BASSLINE -----
const bassline = note("[eb1 eb2]!16 [f2 f1]!16 [g2 g1]!16 [f2 f1]!8 [bb3 bb2]!8")
.sound("supersaw")
.slow(8)
.postgain(2)
.lpf(slider(5000, 300, 5000))
.room(0.9)
.lpf(300)
.room(0.4)
._punchcard({ height: 200, width: 1670 })
// ----- BASSLINE -----
const basslinehigh = note("[eb1 eb2]!16 [f2 f1]!16 [g2 g1]!16 [f2 f1]!8 [bb5 bb4]!8")
.sound("supersaw")
.slow(8)
.postgain(2)
.lpf(slider(4097.6, 300, 5000))
.room(0.9)
.lpf(300)
.room(0.4)
._punchcard({ height: 200, width: 1670 })
// ----- ARPEGGIATOR -----
const arpeggiator = [
"{d4 bb3 eb3 d3 bb2 eb2}%16",
"{c4 bb3 f3 c3 bb2 f2}%16",
"{d4 bb3 g3 d3 bb2 g2}%16",
"{c4 bb3 f3 c3 bb2 f2}%16",
]
const main_arp = note(pick(arpeggiator, "<0 1 2 3>".slow(2)))
._punchcard({ height: 200, width: 1670 })
.sound("supersaw")
.lpenv(slider(56.06375, 1.25, 600))
.lpf(300)
.sustain(0.5).release(0.01).attack(0)
.room(0.9)
.lpenv(1.25)
// ----- DRUMS -----
const kick = s("tech:5").postgain(6).curve(2).pdec(1)
const hats = s("hh*16").bank("RolandTR808").postgain(0.8).room(0).speed(0.8).cut(1)
const snare = s("~ sn").bank("RolandTR808").postgain(1.2).room(0.3)
const clap = s("~ ~ cp ~").postgain(1.5).room(1.3)
// ----- ARRANGIAMENTO -----
arrange(
// INTRO - solo arp + filtro chiuso (8 bars)
[8,
main_arp
.lpf(sine.range(200, 800).slow(8))
.postgain(sine.range(0.5, 1.5).slow(8))
],
// BUILD-UP - aggiungo kick + filtro si apre (8 bars)
[8,
stack(
kick.struct("x ~ ~ ~ x ~ ~ ~"),
main_arp
.lpf(sine.range(800, 2000).slow(4))
.postgain(1.5)
)
],
// PRE-DROP 2 - string sequence in/out (2 bars)
[2,
stack(
note("eb1")
.sound("gm_synth_strings_1")
.n(7)
.room(0.9)
.delay(0.5)
.postgain(sine.range(0, 4).slow(4)) // crescendo lento
.early("0:1"), // suona solo prima metà
)
],
// DROP 1 - full power! (16 bars)
[16,
stack(
kick.struct("x*4").postgain(6),
hats,
snare.fast(2),
clap,
basslinehigh.lpf(300).postgain("2"),
main_arp.lpf(2000).postgain("{0.75 2.5}*4")
)
],
// BREAKDOWN - solo arp + atmosfera (8 bars)
[8,
stack(
hats.postgain(0.4),
main_arp
.lpf(sine.range(400, 1200).slow(8))
.delay(0.6)
.room(0.9)
.postgain(sine.range(0.8, 1.5).slow(8))
)
],
// BUILD-UP 2 - ritorno energia (8 bars)
[8,
stack(
kick.struct("x*2 ~ x*4 ~").postgain(sine.range(4, 8).slow(4)),
hats.postgain(sine.range(0.5, 1.2).slow(2)),
basslinehigh.lpf(sine.range(200, 800).slow(4)).postgain(1.5),
main_arp.lpf(sine.range(1000, 3000).slow(2)).postgain(2)
)
],
// PRE-DROP 2 - string sequence in/out (2 bars)
[2,
stack(
note("eb2")
.sound("gm_synth_strings_1")
.n(7)
.room(0.9)
.delay(0.5)
.postgain(sine.range(0, 4).slow(4)) // crescendo lento
.early("0:1"), // suona solo prima metà
note("bb1")
.sound("gm_synth_strings_1")
.n(7)
.room(0.9)
.delay(0.5)
.postgain(sine.range(4, 0).slow(4)) // decrescendo
.late("1:2"), // suona solo seconda metà
bassline.lpf(400).postgain(2)
)
],
// DROP 2 - più intenso con pattern ritmico complesso (24 bars)
[24,
stack(
kick.struct("x*4").postgain(6),
hats,
snare.fast(2),
clap,
bassline
.lpf(300)
.postgain("{0.75 2.5!9 0.75 2.5!5 0.75 2.5 0.75 2.5!7 0.75 2.5!3 <2.5 0.75> 2.5}%16"),
main_arp
.lpf(sine.range(1500, 3000).slow(4))
.postgain("{0.75 2.5!9 0.75 2.5!5 0.75 2.5 0.75 2.5!7 0.75 2.5!3 <2.5 0.75> 2.5}%16"),
s("xps:[2,5,5,7,3,12,2,9]").postgain(0.8).room(0.5).fast(2) // extra perc
)
],
// OUTRO - fade out graduale (8 bars)
[8,
stack(
kick.struct("x ~ x ~").postgain(sine.range(6, 0).slow(8)),
hats.postgain(sine.range(0.8, 0).slow(8)),
main_arp
.lpf(sine.range(2000, 200).slow(8))
.room(sine.range(0.6, 0.95).slow(8))
.postgain(sine.range(1.5, 0).slow(8))
)
]
)