notebook/strudel/Strudel Making Sound.md
2025-07-09 16:40:18 +09:00

45 KiB
Raw Permalink Blame History

Strudel Making Sound

Samples

Samples are the most common way to make sound with tidal and strudel. A sample is a (commonly short) piece of audio that is used as a basis for sound generation, undergoing various transformations. Music that is based on samples can be thought of as a collage of sound. Read more about Sampling

Strudel allows loading samples in the form of audio files of various formats (wav, mp3, ogg) from any publicly available URL.

Default Samples

By default, strudel comes with a built-in “sample map”, providing a solid base to play with.

s("bd sd [~ bd] sd,hh*16, misc")

Here, we are using the s function to play back different default samples (bd, sd, hh and misc) to get a drum beat.

For drum sounds, strudel uses the comprehensive tidal-drum-machines library, with the following naming convention:

Drum Abbreviation
Bass drum, Kick drum bd
Snare drum sd
Rimshot rim
Clap cp
Closed hi-hat hh
Open hi-hat oh
Crash cr
Ride rd
High tom ht
Medium tom mt
Low tom lt

More percussive sounds:

Source Abbreviation
Shakers (and maracas, cabasas, etc) sh
Cowbell cb
Tambourine tb
Other percussions perc
Miscellaneous samples misc
Effects fx

Furthermore, strudel also loads instrument samples from VCSL by default.

To see which sample names are available, open the sounds tab in the REPL.

Note that only the sample maps (mapping names to URLs) are loaded initially, while the audio samples themselves are not loaded until they are actually played. This behaviour of loading things only when they are needed is also called lazy loading. While it saves resources, it can also lead to sounds not being audible the first time they are triggered, because the sound is still loading. This might be fixed in the future

Sound Banks

If we open the sounds tab and then drum-machines, we can see that the drum samples are all prefixed with drum machine names: RolandTR808_bd, RolandTR808_sd, RolandTR808_hh etc..

We could use them like this:

s("RolandTR808_bd RolandTR808_sd,RolandTR808_hh*16")

… but thats obviously a bit much to write. Using the bank function, we can shorten this to:

s("bd sd,hh*16").bank("RolandTR808")

You could even pattern the bank to switch between different drum machines:

s("bd sd,hh*16").bank("<RolandTR808 RolandTR909>")

Behind the scenes, bank will just prepend the drum machine name to the sample name with _ to get the full name. This of course only works because the name after _ (bd, sd etc..) is standardized. Also note that some banks won’t have samples for all sounds!

Selecting Sounds

If we open the sounds tab again, followed by tab drum machines, there is also a number behind each name, indicating how many individual samples are available. For example RolandTR909_hh(4) means there are 4 samples of a TR909 hihat available. By default, s will play the first sample, but we can select the other ones using n, starting from 0:

s("hh*8").bank("RolandTR909").n("0 1 2 3")

Numbers that are too high will just wrap around to the beginning

s("hh*8").bank("RolandTR909").n("0 1 2 3 4 5 6 7")

Here, 0-3 will play the same sounds as 4-7, because RolandTR909_hh only has 4 sounds.

Selecting sounds also works inside the mini notation, using “:” like this:

s("bd*4,hh:0 hh:1 hh:2 hh:3 hh:4 hh:5 hh:6 hh:7")
.bank("RolandTR909")

Loading Custom Samples

You can load a non-standard sample map using the samples function.

Loading samples from file URLs

In this example we assign names bassdrum, hihat and snaredrum to specific audio files on a server:

samples({
bassdrum: 'bd/BT0AADA.wav',
hihat: 'hh27/000_hh27closedhh.wav',
snaredrum: ['sd/rytm-01-classic.wav', 'sd/rytm-00-hard.wav'],
}, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/');

s("bassdrum snaredrum:0 bassdrum snaredrum:1, hihat*16")

You can freely choose any combination of letters for each sample name. It is even possible to override the default sounds. The names you pick will be made available in the s function. Make sure that the URL and each sample path form a correct URL!

In the above example, bassdrum will load:

https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/bd/BT0AADA.wav
|----------------------base path --------------------------------|--sample path-|

Note that we can either load a single file, like for bassdrum and hihat, or a list of files like for snaredrum! As soon as you run the code, your chosen sample names will be listed in sounds -> user.

Loading Samples from a strudel.json file

The above way to load samples might be tedious to write out / copy paste each time you write a new pattern. To avoid that, you can simply pass a URL to a strudel.json file somewhere on the internet:

samples('https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/strudel.json')
s("bd sd bd sd,hh*16")

The file is expected to define a sample map using JSON, in the same format as described above. Additionally, the base path can be defined with the _base key. The last section could be written as:

{
  "_base": "https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/",
  "bassdrum": "bd/BT0AADA.wav",
  "snaredrum": "sd/rytm-01-classic.wav",
  "hihat": "hh27/000_hh27closedhh.wav"
}

Github Shortcut

Because loading samples from github is common, there is a shortcut:

samples('github:tidalcycles/dirt-samples')
s("bd sd bd sd,hh*16")

The format is samples('github:<user>/<repo>/<branch>'). If you omit branch (like above), the main branch will be used. It assumes a strudel.json file to be present at the root of the repository:

https://raw.githubusercontent.com/<user>/<repo>/<branch>/strudel.json

From Disk via “Import Sounds Folder”

If you don’t want to upload your samples to the internet, you can also load them from your local disk. Go to the sounds tab in the REPL and open the import-sounds tab below the search bar. Press the “import sounds folder” button and select a folder that contains audio files. The folder you select can also contain subfolders with audio files. Example:

└─ samples
   ├─ swoop
   │  ├─ swoopshort.wav
   │  ├─ swooplong.wav
   │  └─ swooptight.wav
   └─ smash
      ├─ smashhigh.wav
      ├─ smashlow.wav
      └─ smashmiddle.wav

In the above example the folder samples contains 2 subfolders swoop and smash, which contain audio files. If you select that samples folder, the user tab (next to the import-sounds tab) will then contain 2 new sounds: swoop(3) smash(3) The individual samples can the be played normally like s("swoop:0 swoop:1 smash:2"). The samples within each sound use zero-based indexing in alphabetical order.

From Disk via @strudel/sampler

Instead of loading your samples into your browser with the “import sounds folder” button, you can also serve the samples from a local file server. The easiest way to do this is using @strudel/sampler:

cd samples
npx @strudel/sampler

Then you can load it via:

samples('http://localhost:5432/');

n("<0 1 2>").s("swoop smash")

The handy thing about @strudel/sampler is that it auto-generates the strudel.json file based on your folder structure. You can see what it generated by going to http://localhost:5432 with your browser.

Note: You need NodeJS installed on your system for this to work.

Specifying Pitch

To make sure your samples are in tune when playing them with note, you can specify a base pitch like this:

samples({
'gtr': 'gtr/0001_cleanC.wav',
'moog': { 'g3': 'moog/005_Mighty%20Moog%20G3.wav' },
}, 'github:tidalcycles/dirt-samples');
note("g3 [bb3 c4] <g4 f4 eb4 f3>@2").s("gtr,moog").clip(1)
.gain(.5)

We can also declare different samples for different regions of the keyboard:

setcpm(60)
samples({
'moog': {
  'g2': 'moog/004_Mighty%20Moog%20G2.wav',
  'g3': 'moog/005_Mighty%20Moog%20G3.wav',
  'g4': 'moog/006_Mighty%20Moog%20G4.wav',
}}, 'github:tidalcycles/dirt-samples')

note("g2!2 <bb2 c3>!2, <c4@3 [<eb4 bb3> g4 f4]>")
.s('moog').clip(1)
.gain(.5)

The sampler will always pick the closest matching sample for the current note!

Note that this notation for pitched sounds also works inside a strudel.json file.

Shabda

If you don’t want to select samples by hand, there is also the wonderful tool called shabda. With it, you can enter any sample name(s) to query from freesound.org. Example:

samples('shabda:bass:4,hihat:4,rimshot:2')

$: n("0 1 2 3 0 1 2 3").s('bass')
$: n("0 1*2 2 3*2").s('hihat').clip(1)
$: n("~ 0 ~ 1 ~ 0 0 1").s('rimshot')

You can also generate artificial voice samples with any text, in multiple languages. Note that the language code and the gender parameters are optional and default to en-GB and f

samples('shabda/speech:the_drum,forever')
samples('shabda/speech/fr-FR/m:magnifique')

$: s("the_drum*2").chop(16).speed(rand.range(0.85,1.1))
$: s("forever magnifique").slow(4).late(0.125)

Sampler Effects

Sampler effects are functions that can be used to change the behaviour of sample playback.

begin

a pattern of numbers from 0 to 1. Skips the beginning of each sample, e.g. 0.25 to cut off the first quarter from each sample.

  • amount (number|Pattern): between 0 and 1, where 1 is the length of the sample
samples({ rave: 'rave/AREUREADY.wav' }, 'github:tidalcycles/dirt-samples')
s("rave").begin("<0 .25 .5 .75>").fast(2)

end

The same as .begin, but cuts off the end off each sample.

  • length (number|Pattern): 1 = whole sample, .5 = half sample, .25 = quarter sample etc..
s("bd*2,oh*4").end("<.1 .2 .5 1>").fast(2)

loop

Loops the sample. Note that the tempo of the loop is not synced with the cycle tempo. To change the loop region, use loopBegin / loopEnd.

  • on (number|Pattern): If 1, the sample is looped
s("casio").loop(1)

loopBegin

Synonyms: loopb

Begin to loop at a specific point in the sample (inbetween begin and end). Note that the loop point must be inbetween begin and end, and before loopEnd! Note: Samples starting with wt_ will automatically loop! (wt = wavetable)

  • time (number|Pattern): between 0 and 1, where 1 is the length of the sample
s("space").loop(1)
.loopBegin("<0 .125 .25>")._scope()

loopEnd

Synonyms: loope

End the looping section at a specific point in the sample (inbetween begin and end). Note that the loop point must be inbetween begin and end, and after loopBegin!

  • time (number|Pattern): between 0 and 1, where 1 is the length of the sample
s("space").loop(1)
.loopEnd("<1 .75 .5 .25>")._scope()

cut

In the style of classic drum-machines, cut will stop a playing sample as soon as another samples with in same cutgroup is to be played. An example would be an open hi-hat followed by a closed one, essentially muting the open.

  • group (number|Pattern): cut group number
s("[oh hh]*4").cut(1)

clip

Synonyms: legato

Multiplies the duration with the given number. Also cuts samples off at the end if they exceed the duration.

  • factor (number|Pattern): = 0
note("c a f e").s("piano").clip("<.5 1 2>")

loopAt

Makes the sample fit the given number of cycles by changing the speed.

samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
s("rhodes").loopAt(2)

fit

Makes the sample fit its event duration. Good for rhythmical loops like drum breaks. Similar to loopAt.

samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
s("rhodes/2").fit()

chop

Cuts each sample into the given number of parts, allowing you to explore a technique known as 'granular synthesis'. It turns a pattern of samples into a pattern of parts of samples.

samples({ rhodes: 'https://cdn.freesound.org/previews/132/132051_316502-lq.mp3' })
s("rhodes")
 .chop(4)
 .rev() // reverse order of chops
 .loopAt(2) // fit sample into 2 cycles

striate

Cuts each sample into the given number of parts, triggering progressive portions of each sample at each loop.

s("numbers:0 numbers:1 numbers:2").striate(6).slow(3)

slice

Chops samples into the given number of slices, triggering those slices with a given pattern of slice numbers. Instead of a number, it also accepts a list of numbers from 0 to 1 to slice at specific points.

samples('github:tidalcycles/dirt-samples')
s("breaks165").slice(8, "0 1 <2 2*2> 3 [4 0] 5 6 7".every(3, rev)).slow(0.75)
samples('github:tidalcycles/dirt-samples')
s("breaks125").fit().slice([0,.25,.5,.75], "0 1 1 <2 3>")

splice

Works the same as slice, but changes the playback speed of each slice to match the duration of its step.

samples('github:tidalcycles/dirt-samples')
s("breaks165")
.splice(8,  "0 1 [2 3 0]@2 3 0@2 7")

speed

Changes the speed of sample playback, i.e. a cheap way of changing pitch.

  • speed (number|Pattern): inf to inf, negative numbers play the sample backwards.
s("bd*6").speed("1 2 4 1 -2 -4")
speed("1 1.5*2 [2 1.1]").s("piano").clip(1)

Synths

In addition to the sampling engine, strudel comes with a synthesizer to create sounds on the fly.

Basic Waveforms

The basic waveforms are sine, sawtooth, square and triangle, which can be selected via sound (or s):

note("c2 <eb2 <g2 g1>>".fast(2))
.sound("<sawtooth square triangle sine>")
._scope()

If you don’t set a sound but a note the default value for sound is triangle!

Noise

You can also use noise as a source by setting the waveform to: white, pink or brown. These are different flavours of noise, here written from hard to soft.

sound("<white pink brown>")._scope()

Here’s a more musical example of how to use noise for hihats:

sound("bd*2,<white pink brown>*8")
.decay(.04).sustain(0)._scope()

Some amount of pink noise can also be added to any oscillator by using the noise paremeter:

note("c3").noise("<0.1 0.25 0.5>")._scope()

You can also use the crackle type to play some subtle noise crackles. You can control noise amount by using the density parameter:

s("crackle*4").density("<0.01 0.04 0.2 0.5>".slow(2))._scope()

Additive Synthesis

To tame the harsh sound of the basic waveforms, we can set the n control to limit the overtones of the waveform:

note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth")
.n("<32 16 8 4>")
._scope()

When the n control is used on a basic waveform, it defines the number of harmonic partials the sound is getting. You can also set n directly in mini notation with sound:

note("c2 <eb2 <g2 g1>>".fast(2))
.sound("sawtooth:<32 16 8 4>")
._scope()

Note for tidal users: n in tidal is synonymous to note for synths only. In strudel, this is not the case, where n will always change timbre, be it though different samples or different waveforms.

Vibrato

vib

Synonyms: vibrato, v

Applies a vibrato to the frequency of the oscillator.

  • frequency (number|Pattern): of the vibrato in hertz
note("a e")
.vib("<.5 1 2 4 8 16>")
._scope()
// change the modulation depth with ":"
note("a e")
.vib("<.5 1 2 4 8 16>:12")
._scope()

vibmod

Synonyms: vmod

Sets the vibrato depth in semitones. Only has an effect if vibrato | vib | v is is also set

  • depth (number|Pattern): of vibrato (in semitones)
note("a e").vib(4)
.vibmod("<.25 .5 1 2 12>")
._scope()
// change the vibrato frequency with ":"
note("a e")
.vibmod("<.25 .5 1 2 12>:8")
._scope()

FM Synthesis

FM Synthesis is a technique that changes the frequency of a basic waveform rapidly to alter the timbre.

You can use fm with any of the above waveforms, although the below examples all use the default triangle wave.

fm

Synonyms: fmi

Sets the Frequency Modulation of the synth. Controls the modulation index, which defines the brightness of the sound.

  • brightness (number|Pattern): modulation index
note("c e g b g e")
.fm("<0 1 2 8 32>")
._scope()

fmh

Sets the Frequency Modulation Harmonicity Ratio. Controls the timbre of the sound. Whole numbers and simple ratios sound more natural, while decimal numbers and complex ratios sound metallic.

  • harmonicity (number|Pattern):
note("c e g b g e")
.fm(4)
.fmh("<1 2 1.5 1.61>")
._scope()

fmattack

Attack time for the FM envelope: time it takes to reach maximum modulation

  • time (number|Pattern): attack time
note("c e g b g e")
.fm(4)
.fmattack("<0 .05 .1 .2>")
._scope()

fmdecay

Decay time for the FM envelope: seconds until the sustain level is reached after the attack phase.

  • time (number|Pattern): decay time
note("c e g b g e")
.fm(4)
.fmdecay("<.01 .05 .1 .2>")
.fmsustain(.4)
._scope()

fmsustain

Sustain level for the FM envelope: how much modulation is applied after the decay phase

  • level (number|Pattern): sustain level
note("c e g b g e")
.fm(4)
.fmdecay(.1)
.fmsustain("<1 .75 .5 0>")
._scope()

fmenv

Ramp type of fm envelope. Exp might be a bit broken..

  • type (number|Pattern): lin | exp
note("c e g b g e")
.fm(4)
.fmdecay(.2)
.fmsustain(0)
.fmenv("<exp lin>")
._scope()

Wavetable Synthesis

Strudel can also use the sampler to load custom waveforms as a replacement of the default waveforms used by WebAudio for the base synth. A default set of more than 1000 wavetables is accessible by default (coming from the AKWF set). You can also import/use your own. A wavetable is a one-cycle waveform, which is then repeated to create a sound at the desired frequency. It is a classic but very effective synthesis technique.

Any sample preceded by the wt_ prefix will be loaded as a wavetable. This means that the loop argument will be set to 1 by default. You can scan over the wavetable by using loopBegin and loopEnd as well.

samples('bubo:waveforms');
note("<[g3,b3,e4]!2 [a3,c3,e4] [b3,d3,f#4]>")
.n("<1 2 3 4 5 6 7 8 9 10>/2").room(0.5).size(0.9)
.s('wt_flute').velocity(0.25).often(n => n.ply(2))
.release(0.125).decay("<0.1 0.25 0.3 0.4>").sustain(0)
.cutoff(2000).cutoff("<1000 2000 4000>").fast(4)
._scope()

ZZFX

The Zuper Zmall Zound Zynth” ZZFX is also integrated in strudel. Developed by Frank Force, it is a synth and FX engine originally intended to be used for size coding games.

It has 20 parameters in total, here is a snippet that uses all:

note("c2 eb2 f2 g2") // also supports freq
.s("{z_sawtooth z_tan z_noise z_sine z_square}%4")
.zrand(0) // randomization
// zzfx envelope
.attack(0.001)
.decay(0.1)
.sustain(.8)
.release(.1)
// special zzfx params
.curve(1) // waveshape 1-3
.slide(0) // +/- pitch slide
.deltaSlide(0) // +/- pitch slide (?)
.noise(0) // make it dirty
.zmod(0) // fm speed
.zcrush(0) // bit crush 0 - 1
.zdelay(0) // simple delay
.pitchJump(0) // +/- pitch change after pitchJumpTime
.pitchJumpTime(0) // >0 time after pitchJump is applied
.lfo(0) // >0 resets slide + pitchJump + sets tremolo speed
.tremolo(0) // 0-1 lfo volume modulation amount
//.duration(.2) // overwrite strudel event duration
//.gain(1) // change volume
._scope() // vizualise waveform (not zzfx related)

Audio Effects

Whether you’re using a synth or a sample, you can apply any of the following built-in audio effects. As you might suspect, the effects can be chained together, and they accept a pattern string as their argument.

Filters

Filters are an essential building block of subtractive synthesis. Strudel comes with 3 types of filters:

  • low-pass filter: low frequencies may pass, high frequencies are cut off
  • high-pass filter: high frequencies may pass, low frequencies are cut off
  • band-pass filters: only a frequency band may pass, low and high frequencies around are cut off

Each filter has 2 parameters:

  • cutoff: the frequency at which the filter starts to work. e.g. a low-pass filter with a cutoff of 1000Hz allows frequencies below 1000Hz to pass.
  • q-value: Controls the resonance of the filter. Higher values sound more aggressive. Also see Q-Factor

lpf

Synonyms: cutoff, ctf, lp

Applies the cutoff frequency of the low-pass filter.

When using mininotation, you can also optionally add the 'lpq' parameter, separated by ':'.

  • frequency (number|Pattern): audible between 0 and 20000
s("bd sd [~ bd] sd,hh*6").lpf("<4000 2000 1000 500 200 100>")
s("bd*16").lpf("1000:0 1000:10 1000:20 1000:30")

lpq

Synonyms: resonance

Controls the low-pass q-value.

  • q (number|Pattern): resonance factor between 0 and 50
s("bd sd [~ bd] sd,hh*8").lpf(2000).lpq("<0 10 20 30>")

hpf

Synonyms: hp, hcutoff

Applies the cutoff frequency of the high-pass filter.

When using mininotation, you can also optionally add the 'hpq' parameter, separated by ':'.

  • frequency (number|Pattern): audible between 0 and 20000
s("bd sd [~ bd] sd,hh*8").hpf("<4000 2000 1000 500 200 100>")
s("bd sd [~ bd] sd,hh*8").hpf("<2000 2000:25>")

hpq

Synonyms: hresonance

Controls the high-pass q-value.

  • q (number|Pattern): resonance factor between 0 and 50
s("bd sd [~ bd] sd,hh*8").hpf(2000).hpq("<0 10 20 30>")

bpf

Synonyms: bandf, bp

Sets the center frequency of the band-pass filter. When using mininotation, you can also optionally supply the 'bpq' parameter separated by ':'.

  • frequency (number|Pattern): center frequency
s("bd sd [~ bd] sd,hh*6").bpf("<1000 2000 4000 8000>")

bpq

Synonyms: bandq

Sets the band-pass q-factor (resonance).

  • q (number|Pattern): q factor
s("bd sd [~ bd] sd").bpf(500).bpq("<0 1 2 3>")

ftype

Sets the filter type. The ladder filter is more aggressive. More types might be added in the future.

  • type (number|Pattern): 12db (0), ladder (1), or 24db (2)
note("{f g g c d a a#}%8").s("sawtooth").lpenv(4).lpf(500).ftype("<0 1 2>").lpq(1)
note("c f g g a c d4").fast(2)
.sound('sawtooth')
.lpf(200).fanchor(0)
.lpenv(3).lpq(1)
.ftype("<ladder 12db 24db>")

vowel

Formant filter to make things sound like vowels.

  • vowel (string|Pattern): You can use a e i o u ae aa oe ue y uh un en an on, corresponding to [a] [e] [i] [o] [u] [æ] [ɑ] [ø] [y] [ɯ] [ʌ] [œ̃] [ɛ̃] [ɑ̃] [ɔ̃]. Aliases: aa = Ã¥ = ɑ, oe = ø = ö, y = ı, ae = æ.
note("[c2 <eb2 <g2 g1>>]*2").s('sawtooth')
.vowel("<a e i <o u>>")
s("bd sd mt ht bd [~ cp] ht lt").vowel("[a|e|i|o|u]")

Amplitude Envelope

The amplitude envelope controls the dynamic contour of a sound. Strudel uses ADSR envelopes, which are probably the most common way to describe an envelope:

ADSR

image link

attack

Synonyms: att

Amplitude envelope attack time: Specifies how long it takes for the sound to reach its peak value, relative to the onset.

  • attack (number|Pattern): time in seconds.
note("c3 e3 f3 g3").attack("<0 .1 .5>")

decay

Amplitude envelope decay time: the time it takes after the attack time to reach the sustain level. Note that the decay is only audible if the sustain value is lower than 1.

  • time (number|Pattern): decay time in seconds
note("c3 e3 f3 g3").decay("<.1 .2 .3 .4>").sustain(0)

sustain

Synonyms: sus

Amplitude envelope sustain level: The level which is reached after attack / decay, being sustained until the offset.

  • gain (number|Pattern): sustain level between 0 and 1
note("c3 e3 f3 g3").decay(.2).sustain("<0 .1 .4 .6 1>")

release

Synonyms: rel

Amplitude envelope release time: The time it takes after the offset to go from sustain level to zero.

  • time (number|Pattern): release time in seconds
note("c3 e3 g3 c4").release("<0 .1 .4 .6 1>/2")

adsr

ADSR envelope: Combination of Attack, Decay, Sustain, and Release.

  • time (number|Pattern): attack time in seconds
  • time (number|Pattern): decay time in seconds
  • gain (number|Pattern): sustain level (0 to 1)
  • time (number|Pattern): release time in seconds
note("[c3 bb2 f3 eb3]*2").sound("sawtooth").lpf(600).adsr(".1:.1:.5:.2")

Filter Envelope

Each filter can receive an additional filter envelope controlling the cutoff value dynamically. It uses an ADSR envelope similar to the one used for amplitude. There is an additional parameter to control the depth of the filter modulation: lpenv|hpenv|bpenv. This allows you to play subtle or huge filter modulations just the same by only increasing or decreasing the depth.

note("[c eb g <f bb>](3,8,<0 1>)".sub(12))
.s("<sawtooth>/64")
.lpf(sine.range(300,2000).slow(16))
.lpa(0.005)
.lpd(perlin.range(.02,.2))
.lps(perlin.range(0,.5).slow(3))
.lpq(sine.range(2,10).slow(32))
.release(.5)
.lpenv(perlin.range(1,8).slow(2))
.ftype('24db')
.room(1)
.juxBy(.5,rev)
.sometimes(add(note(12)))
.stack(s("bd*2").bank('RolandTR909'))
.gain(.5).fast(2)

There is one filter envelope for each filter type and thus one set of envelope filter parameters preceded either by lp, hp or bp:

  • lpattack, lpdecay, lpsustain, lprelease, lpenv: filter envelope for the lowpass filter.
    • alternatively: lpa, lpd, lps, lpr and lpe.
  • hpattack, hpdecay, hpsustain, hprelease, hpenv: filter envelope for the highpass filter.
    • alternatively: hpa, hpd, hps, hpr and hpe.
  • bpattack, bpdecay, bpsustain, bprelease, bpenv: filter envelope for the bandpass filter.
    • alternatively: bpa, bpd, bps, bpr and bpe.

lpattack

Synonyms: lpa

Sets the attack duration for the lowpass filter envelope.

  • attack (number|Pattern): time of the filter envelope
note("c2 e2 f2 g2")
.sound('sawtooth')
.lpf(300)
.lpa("<.5 .25 .1 .01>/4")
.lpenv(4)

lpdecay

Synonyms: lpd

Sets the decay duration for the lowpass filter envelope.

  • decay (number|Pattern): time of the filter envelope
note("c2 e2 f2 g2")
.sound('sawtooth')
.lpf(300)
.lpd("<.5 .25 .1 0>/4")
.lpenv(4)

lpsustain

Synonyms: lps

Sets the sustain amplitude for the lowpass filter envelope.

  • sustain (number|Pattern): amplitude of the lowpass filter envelope
note("c2 e2 f2 g2")
.sound('sawtooth')
.lpf(300)
.lpd(.5)
.lps("<0 .25 .5 1>/4")
.lpenv(4)

lprelease

Synonyms: lpr

Sets the release time for the lowpass filter envelope.

  • release (number|Pattern): time of the filter envelope
note("c2 e2 f2 g2")
.sound('sawtooth')
.clip(.5)
.lpf(300)
.lpenv(4)
.lpr("<.5 .25 .1 0>/4")
.release(.5)

lpenv

Synonyms: lpe

Sets the lowpass filter envelope modulation depth.

  • modulation (number|Pattern): depth of the lowpass filter envelope between 0 and n
note("c2 e2 f2 g2")
.sound('sawtooth')
.lpf(300)
.lpa(.5)
.lpenv("<4 2 1 0 -1 -2 -4>/4")

Pitch Envelope

You can also control the pitch with envelopes! Pitch envelopes can breathe life into static sounds:

n("<-4,0 5 2 1>*<2!3 4>")
.scale("<C F>/8:pentatonic")
.s("gm_electric_guitar_jazz")
.penv("<.5 0 7 -2>*2").vib("4:.1")
.phaser(2).delay(.25).room(.3)
.size(4).fast(1.5)

You also create some lovely chiptune-style sounds:

n(run("<4 8>/16")).jux(rev)
.chord("<C^7 <Db^7 Fm7>>")
.dict('ireal')
.voicing().add(note("<0 1>/8"))
.dec(.1).room(.2)
.segment("<4 [2 8]>")
.penv("<0 <2 -2>>").patt(.02).fast(2)

Let’s break down all pitch envelope controls:

pattack

Synonyms: patt

Attack time of pitch envelope.

  • time (number|Pattern): time in seconds
note("c eb g bb").pattack("0 .1 .25 .5").slow(2)

pdecay

Synonyms: pdec

Decay time of pitch envelope.

  • time (number|Pattern): time in seconds
note("<c eb g bb>").pdecay("<0 .1 .25 .5>")

prelease

Synonyms: prel

Release time of pitch envelope

  • time (number|Pattern): time in seconds
note("<c eb g bb> ~")
.release(.5) // to hear the pitch release
.prelease("<0 .1 .25 .5>")

penv

Amount of pitch envelope. Negative values will flip the envelope. If you don't set other pitch envelope controls, pattack:.2 will be the default.

  • semitones (number|Pattern): change in semitones
note("c")
.penv("<12 7 1 .5 0 -1 -7 -12>")

pcurve

Curve of envelope. Defaults to linear. exponential is good for kicks

  • type (number|Pattern): 0 = linear, 1 = exponential
note("g1*4")
.s("sine").pdec(.5)
.penv(32)
.pcurve("<0 1>")

panchor

Sets the range anchor of the envelope:

  • anchor 0: range = [note, note + penv]

  • anchor 1: range = [note - penv, note] If you don't set an anchor, the value will default to the psustain value.

  • anchor (number|Pattern): anchor offset

note("c c4").penv(12).panchor("<0 .5 1 .5>")

Dynamics

gain

Controls the gain by an exponential amount.

  • amount (number|Pattern): gain.
s("hh*8").gain(".4!2 1 .4!2 1 .4 1").fast(2)

velocity

Sets the velocity from 0 to 1. Is multiplied together with gain.

s("hh*8")
.gain(".4!2 1 .4!2 1 .4 1")
.velocity(".4 1")

compressor

Dynamics Compressor. The params are compressor("threshold:ratio:knee:attack:release") More info here

s("bd sd [~ bd] sd,hh*8")
.compressor("-20:20:10:.002:.02")

postgain

Gain applied after all effects have been processed.

s("bd sd [~ bd] sd,hh*8")
.compressor("-20:20:10:.002:.02").postgain(1.5)

xfade

Cross-fades between left and right from 0 to 1:

  • 0 = (full left, no right)
  • .5 = (both equal)
  • 1 = (no left, full right)
xfade(s("bd*2"), "<0 .25 .5 .75 1>", s("hh*8"))

Panning

jux

The jux function creates strange stereo effects, by applying a function to a pattern, but only in the right-hand channel.

s("bd lt [~ ht] mt cp ~ bd hh").jux(rev)
s("bd lt [~ ht] mt cp ~ bd hh").jux(press)
s("bd lt [~ ht] mt cp ~ bd hh").jux(iter(4))

juxBy

Synonyms: juxby

Jux with adjustable stereo width. 0 = mono, 1 = full stereo.

s("bd lt [~ ht] mt cp ~ bd hh").juxBy("<0 .5 1>/2", rev)

pan

Sets position in stereo.

  • pan (number|Pattern): between 0 and 1, from left to right (assuming stereo), once round a circle (assuming multichannel)
s("[bd hh]*2").pan("<.5 1 .5 0>")
s("bd rim sd rim bd ~ cp rim").pan(sine.slow(2))

Waveshaping

coarse

fake-resampling for lowering the sample rate. Caution: This effect seems to only work in chromium based browsers

  • factor (number|Pattern): 1 for original 2 for half, 3 for a third and so on.
s("bd sd [~ bd] sd,hh*8").coarse("<1 4 8 16 32>")

crush

bit crusher effect.

  • depth (number|Pattern): between 1 (for drastic reduction in bit-depth) to 16 (for barely no reduction).
s("<bd sd>,hh*3").fast(2).crush("<16 8 7 6 5 4 3 2>")

distort

Synonyms: dist

Wave shaping distortion. CAUTION: it can get loud. Second option in optional array syntax (ex: ".9:.5") applies a postgain to the output. Most useful values are usually between 0 and 10 (depending on source gain). If you are feeling adventurous, you can turn it up to 11 and beyond ;)

  • distortion (number|Pattern):
s("bd sd [~ bd] sd,hh*8").distort("<0 2 3 10:.5>")
note("d1!8").s("sine").penv(36).pdecay(.12).decay(.23).distort("8:.4")

Global Effects

Local vs Global Effects

While the above listed “local” effects will always create a separate effects chain for each event, global effects use the same chain for all events of the same orbit:

orbit

An orbit is a global parameter context for patterns. Patterns with the same orbit will share the same global effects.

  • number (number|Pattern):
stack(
  s("hh*6").delay(.5).delaytime(.25).orbit(1),
  s("~ sd ~ sd").delay(.5).delaytime(.125).orbit(2)
)

Delay

delay

Sets the level of the delay signal.

When using mininotation, you can also optionally add the 'delaytime' and 'delayfeedback' parameter, separated by ':'.

  • level (number|Pattern): between 0 and 1
s("bd bd").delay("<0 .25 .5 1>")
s("bd bd").delay("0.65:0.25:0.9 0.65:0.125:0.7")

delaytime

Synonyms: delayt, dt

Sets the time of the delay effect.

  • seconds (number|Pattern): between 0 and Infinity
s("bd bd").delay(.25).delaytime("<.125 .25 .5 1>")

delayfeedback

Synonyms: delayfb, dfb

Sets the level of the signal that is fed back into the delay. Caution: Values >= 1 will result in a signal that gets louder and louder! Don't do it

  • feedback (number|Pattern): between 0 and 1
s("bd").delay(.25).delayfeedback("<.25 .5 .75 1>")

Reverb

room

Sets the level of reverb.

When using mininotation, you can also optionally add the 'size' parameter, separated by ':'.

  • level (number|Pattern): between 0 and 1
s("bd sd [~ bd] sd").room("<0 .2 .4 .6 .8 1>")
s("bd sd [~ bd] sd").room("<0.9:1 0.9:4>")

roomsize

Synonyms: rsize, sz, size

Sets the room size of the reverb, see room. When this property is changed, the reverb will be recaculated, so only change this sparsely..

  • size (number|Pattern): between 0 and 10
s("bd sd [~ bd] sd").room(.8).rsize(1)
s("bd sd [~ bd] sd").room(.8).rsize(4)

roomfade

Synonyms: rfade

Reverb fade time (in seconds). When this property is changed, the reverb will be recaculated, so only change this sparsely..

  • seconds (number): for the reverb to fade
s("bd sd [~ bd] sd").room(0.5).rlp(10000).rfade(0.5)
s("bd sd [~ bd] sd").room(0.5).rlp(5000).rfade(4)

roomlp

Synonyms: rlp

Reverb lowpass starting frequency (in hertz). When this property is changed, the reverb will be recaculated, so only change this sparsely..

  • frequency (number): between 0 and 20000hz
s("bd sd [~ bd] sd").room(0.5).rlp(10000)
s("bd sd [~ bd] sd").room(0.5).rlp(5000)

roomdim

Synonyms: rdim

Reverb lowpass frequency at -60dB (in hertz). When this property is changed, the reverb will be recaculated, so only change this sparsely..

  • frequency (number): between 0 and 20000hz
s("bd sd [~ bd] sd").room(0.5).rlp(10000).rdim(8000)
s("bd sd [~ bd] sd").room(0.5).rlp(5000).rdim(400)

iresponse

Synonyms: ir

Sets the sample to use as an impulse response for the reverb.

  • sample (string|Pattern): to use as an impulse response
s("bd sd [~ bd] sd").room(.8).ir("<shaker_large:0 shaker_large:2>")

Phaser

phaser

Synonyms: ph

Phaser audio effect that approximates popular guitar pedals.

  • speed (number|Pattern): speed of modulation
n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
.phaser("<1 2 4 8>")

phaserdepth

Synonyms: phd

The amount the signal is affected by the phaser effect. Defaults to 0.75

  • depth (number|Pattern): number between 0 and 1
n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
.phaser(2).phaserdepth("<0 .5 .75 1>")

phasercenter

Synonyms: phc

The center frequency of the phaser in HZ. Defaults to 1000

  • centerfrequency (number|Pattern): in HZ
n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
.phaser(2).phasercenter("<800 2000 4000>")

phasersweep

Synonyms: phs

The frequency sweep range of the lfo for the phaser effect. Defaults to 2000

  • phasersweep (number|Pattern): most useful values are between 0 and 4000
n(run(8)).scale("D:pentatonic").s("sawtooth").release(0.5)
.phaser(2).phasersweep("<800 2000 4000>")

MIDI, OSC and MQTT

Normally, Strudel is used to pattern sound, using its own ‘web audio’-based synthesiser called SuperDough.

It is also possible to pattern other things with Strudel, such as software and hardware synthesisers with MIDI, other software using Open Sound Control/OSC (including the SuperDirt synthesiser commonly used with Strudel’s sibling TidalCycles), or the MQTT ‘internet of things’ protocol.

MIDI

Strudel supports MIDI without any additional software (thanks to webmidi), just by adding methods to your pattern:

midiin(inputName?)

MIDI input: Opens a MIDI input port to receive MIDI control change messages.

  • input (string|number): MIDI device name or index defaulting to 0
let cc = await midin('IAC Driver Bus 1')
note("c a f e").lpf(cc(0).range(0, 1000)).lpq(cc(1).range(0, 10)).sound("sawtooth")

midi(outputName?,options?)

Either connect a midi device or use the IAC Driver (Mac) or Midi Through Port (Linux) for internal midi messages. If no outputName is given, it uses the first midi output it finds.


$: chord("<C^7 A7 Dm7 G7>").voicing().midi('IAC Driver')

In the console, you will see a log of the available MIDI devices as soon as you run the code, e.g.

 `Midi connected! Using "Midi Through Port-0".`

The .midi() function accepts an options object with the following properties:

$: note("d e c a f").midi('IAC Driver', { isController: true, midimap: 'default'})

Available Options

Option Type Default Description
isController boolean false When true, disables sending note messages. Useful for MIDI controllers
latencyMs number 34 Latency in milliseconds to align MIDI with audio engine
noteOffsetMs number 10 Offset in milliseconds for note-off messages to prevent glitching
midichannel number 1 Default MIDI channel (1-16)
velocity number 0.9 Default note velocity (0-1)
gain number 1 Default gain multiplier for velocity (0-1)
midimap string ’default’ Name of MIDI mapping to use for control changes
midiport string/number - MIDI device name or index

midiport(outputName)

Selects the MIDI output device to use, pattern can be used to switch between devices.

$: midiport('IAC Driver');
$: note('c a f e').midiport('<0 1 2 3>').midi();

MIDI port: Sets the MIDI port for the event.

  • port (number|Pattern): MIDI port
note("c a f e").midiport("<0 1 2 3>").midi()

midichan(number)

Selects the MIDI channel to use. If not used, .midi will use channel 1 by default.

midicmd(command)

midicmd sends MIDI system real-time messages to control timing and transport on MIDI devices.

It supports the following commands:

  • clock/midiClock - Sends MIDI timing clock messages
  • start - Sends MIDI start message
  • stop - Sends MIDI stop message
  • continue - Sends MIDI continue message

// You can control the clock with a pattern and ensure it starts in sync when the repl begins. // Note: It might act unexpectedly if MIDI isn’t set up initially.

$:stack(
midicmd("clock*48,<start stop>/2").midi('IAC Driver')
)

control, ccn && ccv

  • control sends MIDI control change messages to your MIDI device.
  • ccn sets the cc number. Depends on your synths midi mapping
  • ccv sets the cc value. normalized from 0 to 1.
note("c a f e").control([74, sine.slow(4)]).midi()
note("c a f e").ccn(74).ccv(sine.slow(4)).midi()

In the above snippet, ccn is set to 74, which is the filter cutoff for many synths. ccv is controlled by a saw pattern. Having everything in one pattern, the ccv pattern will be aligned to the note pattern, because the structure comes from the left by default. But you can also control cc messages separately like this:

$: note("c a f e").midi()
$: ccv(sine.segment(16).slow(4)).ccn(74).midi()

Instead of setting ccn and ccv directly, you can also create mappings with midimaps:

midimaps

Adds midimaps to the registry. Inside each midimap, control names (e.g. lpf) are mapped to cc numbers.

midimaps({ mymap: { lpf: 74 } })
$: note("c a f e")
.lpf(sine.slow(4))
.midimap('mymap')
.midi()
midimaps({ mymap: {
  lpf: { ccn: 74, min: 0, max: 20000, exp: 0.5 }
}})
$: note("c a f e")
.lpf(sine.slow(2).range(400,2000))
.midimap('mymap')
.midi()

defaultmidimap

configures the default midimap, which is used when no "midimap" port is set

defaultmidimap({ lpf: 74 })
$: note("c a f e").midi();
$: lpf(sine.slow(4).segment(16)).midi();

progNum (Program Change)

progNum sends MIDI program change messages to switch between different presets/patches on your MIDI device. Program change values should be numbers between 0 and 127.

// Switch between programs 0 and 1 every cycle
progNum("<0 1>").midi()

// Play notes while changing programs
note("c3 e3 g3").progNum("<0 1 2>").midi()

Program change messages are useful for switching between different instrument sounds or presets during a performance. The exact sound that each program number maps to depends on your MIDI device’s configuration.

sysex, sysexid && sysexdata (System Exclusive Message)

sysex sends MIDI System Exclusive (SysEx) messages to your MIDI device. ysEx messages are device-specific commands that allow deeper control over synthesizer parameters. The value should be an array of numbers between 0-255 representing the SysEx data bytes.

// Send a simple SysEx message
let id = 0x43; //Yamaha
//let id = "0x00:0x20:0x32"; //Behringer ID can be an array of numbers
let data = "0x79:0x09:0x11:0x0A:0x00:0x00"; // Set NSX-39 voice to say "Aa"
$: note("c a f e").sysex(id, data).midi();
$: note("c a f e").sysexid(id).sysexdata(data).midi();

The exact format of SysEx messages depends on your MIDI device’s specification. Consult your device’s MIDI implementation guide for details on supported SysEx messages.

midibend && miditouch

midibend sets MIDI pitch bend (-1 - 1) miditouch sets MIDI key after touch (0-1)

note("c a f e").midibend(sine.slow(4).range(-0.4,0.4)).midi()
note("c a f e").miditouch(sine.slow(4).range(0,1)).midi()

OSC/SuperDirt/StrudelDirt

In TidalCycles, sound is usually generated using SuperDirt, which runs inside SuperCollider. Strudel also supports using SuperDirt, although it requires installing some additional software.

There is also StrudelDirt which is SuperDirt with some optimisations for working with Strudel. (A longer term aim is to merge these optimisations back into mainline SuperDirt)

Prequisites

To get SuperDirt to work with Strudel, you need to

  1. install SuperCollider + sc3 plugins, see Tidal Docs (Install Tidal) for more info.
  2. install SuperDirt, or the StrudelDirt fork which is optimised for use with Strudel
  3. install node.js
  4. download Strudel Repo (or git clone, if you have git installed)
  5. run pnpm i in the strudel directory
  6. run pnpm run osc to start the osc server, which forwards OSC messages from Strudel REPL to SuperCollider

Now you’re all set!

Usage

  1. Start SuperCollider, either using SuperCollider IDE or by running sclang in a terminal
  2. Open the Strudel REPL

…or test it here:

If you now hear sound, congratulations! If not, you can get help on the #strudel channel in the TidalCycles discord.

Note: if you have the ‘Audio Engine Target’ in settings set to ‘OSC’, you do not need to add .osc() to the end of your pattern.

Pattern.osc

Sends each hap as an OSC message, which can be picked up by SuperCollider or any other OSC-enabled software. For more info, read MIDI & OSC in the docs

SuperDirt Params

Please refer to Tidal Docs for more info.

But can we use Strudel offline?

MQTT

MQTT is a lightweight network protocol, designed for ‘internet of things’ devices. For use with strudel, you will need access to an MQTT server known as a ‘broker’ configured to accept secure ‘websocket’ connections. You could run one yourself (e.g. by running mosquitto), although getting an SSL certificate that your web browser will trust might be a bit tricky for those without systems administration experience. Alternatively, you can use a public broker.

Strudel does not yet support receiving messages over MQTT, only sending them.

Usage

The following example shows how to send a pattern to an MQTT broker:

Other software can then receive the messages. For example using the mosquitto commandline client tools:


> mosquitto_sub -h mqtt.eclipseprojects.io -p 1883 -t "/strudel-pattern"
> hello
> world
> hello
> world
> ...

Control patterns will be encoded as JSON, for example:

Will send messages like the following:


{"s":"sax","speed":2}
{"s":"sax","speed":2}
{"s":"sax","speed":3}
{"s":"sax","speed":2}
...