Coding Music in Strudel

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/60 converte 140 BPM in beat per secondo
  • /4 perché 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
  • !16 e !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 tempo
  • cut(1) evita sovrapposizioni inutili del charleston
  • struct("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:

  1. Composizione (note, accordi, pattern)
  2. Sound design (filtri, inviluppi, room, delay, curve)
  3. 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))
    )
  ]
)

 

Leave a Comment