import { Slides, Slide } from './Slides.jsx'; import { MaxiRepl } from './MaxiRepl'; import ImgTile from './ImgTile';
# Strudel ## Algorithmic Patterns for the Web ![whirlpool](./strudel_icon.png) Alex McLean, Felix Roos & Tidal Cycles Community
## Strudel is..
- zero-install live coding editor for the web - Tidal Cycles port to JavaScript - a terse language for algorithmic patterns - for dynamic music sequencing - algorithmic composition - live improvisation - a set of reusable packages - probably more exciting than your regular DAW
![whirlpool](./strudel_icon.png)
## About Me
- 💻 Studied Computer Science and Media, working as a software developer by day - 🎺 Trumpet Player, studied Jazz and Popular Music - 🎤 Dilettante Music Producer of all kinds of genres - 💿 ocassionally publishes music with Lui Mafuta & Puste - 🔌 Modular Synth & Electronics Lover, Dilettante DJ .. - 💌 Coding Blog: [loophole-letters.vercel.app](https://loophole-letters.vercel.app/) - 👀 Loves visualizations
{/* */} {/* */} {/* */}
## A Taste of Strudel " .color('salmon') .off(1/4, x=>x.add(2).color('green')) .off(1/2, x=>x.add(6).color('steelblue')) .scale('D minor') .legato(.5) .echo(2, 1/8, .5) .pianoroll() .note().piano().out()`, // `stack( s("bd,[~ ],hh(3,4)") // drums .speed(perlin.range(.7,.9)) // random sample speed variation ,"" // bassline .off(1/8,x=>x.add(12).degradeBy(.5)) // random octave jumps .add(perlin.range(0,.5)) // random pitch variation .superimpose(add(.05)) // add second, slightly detuned voice .n() // wrap in "n" .decay(.15).sustain(0) // make each note of equal length .s('sawtooth') // waveform .gain(.4) // turn down .cutoff(sine.slow(7).range(300,5000)) // automate cutoff ,">".voicings() // chords .superimpose(x=>x.add(.04)) // add second, slightly detuned voice .add(perlin.range(0,.5)) // random pitch variation .n() // wrap in "n" .s('sawtooth') // waveform .gain(.16) // turn down .cutoff(500) // fixed cutoff .attack(1) // slowly fade in ,"a4 c5 ".struct("x(5,8)") .superimpose(x=>x.add(.04)) // add second, slightly detuned voice .add(perlin.range(0,.5)) // random pitch variation .n() // wrap in "n" .decay(.1).sustain(0) // make notes short .s('triangle') // waveform .degradeBy(perlin.range(0,.5)) // randomly controlled random removal :) .echoWith(4,.125,(x,n)=>x.gain(.15*1/(n+1))) // echo notes ) .out() .slow(3/2)`, /* `// froos - strudel schneckno samples({ clubkick: 'clubkick/2.wav', sd: ['808sd/SD0010.WAV','808sd/SD0050.WAV'], hh: 'hh/000_hh3closedhh.wav', clak: 'clak/000_clak1.wav', jvbass: ['jvbass/000_01.wav','jvbass/001_02.wav','jvbass/003_04.wav','jvbass/004_05.wav','jvbass/005_06.wav'] }, 'https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master/'); stack( s(",[~ ],hh(3,4,2)") .n("<0 1 2>").speed(.8), n("<0 1*2 4(3,8) 2>").speed(.6) .off(1/8, x => x.speed(1.2).degradeBy(.5)) .s("jvbass") ).reset(">") .cutoff(perlin.slow(7).range(100,20000)) .webdirt().slow(3/2)`,*/ /* `stack( // drum pattern by yaxu "24*2 [~ 25]".chunk(4, x => x.sub("12").fast(2)) .tone(membrane().toDestination()), "~ x ~ x".every(2, x => x.fast(2)).iter(4) .tone(noise().toDestination()), "c4*8".add(saw.mul(12).slow(1.5)).degrade(0.05) .tone(metal().set(adsr(0,.06,0,0)).chain(vol(0.5),out())) )`,*/ /* `stack( note("<[0 2 4 2] [0 3 5 3]>".scale('D3 dorian')) .s('jazzbass'), note("0,2,4".add("<0 1 2 1>").scale('D3 dorian')) .s('stage73') ).clip(1).out()`, */ ]} /> ## Live Coding "In live coding performance, performers create time-based works by programming them while these same works are being executed."
Roberts & Wakefield, 2016

Screenshot: "Olivia Jack -{' '} Hydra , Live Coding Visuals in the Browser".{' '}

## Tidal Cycles
- A popular software for live coding music, developed since 2009 - a powerful pattern language, inspired by classical music - functional reactive programming (FRP)

Screenshot:{' '} "Yaxu & hellocatfood - LIVE code for Noise Quest"

## Strudel: How it started
"Any application that can be written in JavaScript, will eventually be written in JavaScript."
Jeff Atwood, 2009{' '}
![tidal-vortex-strudel](./tidal-vortex-strudel.png)
![strudel port quote](./strudel-port.png) - Started by Alex McLean in January 2022 - based on a port of a rewrite of Tidal - I implemented the REPL + additional strudel packages - Help from Tidal Community Members - Inspired by: Tidal Vortex, Gibber, Estuary, Hydra, WAGS and Feedforward.
## High Level Overview
➡️
## User Code
- JavaScript + syntax sugar (transpilation with shift-ast) - "Mini Notation" for terse rhythm notation - `seq(c3, [e3, g3])` === `"c3 [e3 g3]"` - All Parameters can be a Pattern
") .scale(cat('F#2 major','A2 major').slow(8)) .note().piano().out().pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`, /* `stack( // melody \`< [e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [e5 f5] [g5 a5] [~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ g6] [g6 ~] [e5 ~] [[d5@2 c5] [~@2 e5]] ~ [~ [c5@2 d5]] [e5 e5] [d5 c5] [a5 g5] [c6 [e5@2 d5]] [~ c5] [c5 d5] [e5 [c5@2 c5]] [~ c5] [f5 e5] [c5 d5] [~ [g6@2 ~] ~@2] [g5 ~] [~ a5] [b5 c6] [b5@2 ~@2 g5] ~ [f5 ~] [[g5@2 f5] ~] [[e5 ~] [f5 ~]] [[f#5 ~] [g5 ~]] [~ a5] [b5 c6] [b5@2 ~@2 g5] ~ [eb6 d6] [~ c6] ~!2 >\` .legato(.95), // sub melody \`< [~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2 [~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2 [~ g4]!2 [~ ab4]!2 [~ a4]!2 [~ bb4]!2 [~ a4]!2 [~ g4]!2 [d4 e4] [f4 gb4] ~!2 [~ c5]!4 [~ a4]!2 [[c4 ~] [d4 ~]] [[eb4 ~] [e4 ~]] [~ c5]!4 [~ eb5]!2 [g4*2 [f4 ~]] [[e4 ~] [d4 ~]] >\`, // bass \`< c3!7 a3 f3!2 e3!2 ~!4 c3!7 a3 f3!2 e3!2 ~!4 f3!2 e3!2 d3!2 ~!2 f3!2 e3!2 ab3!2 ~!2 >\` .legato(.5) ).fast(2).pianoroll({fold:1,cycles:8,overscan:8}) .note().piano().out()`, `stack( "c2 ", "<[e3,[g3 a3 bb3 a3]]!11 [e3,g3]>".slow(2) .struct("<[[x@2 x]*4]!11 [[~@2 x] [[x@2 ~]*9]@3]>".slow(2)) ) .velocity("[.7@2 .9]*2".add(rand.range(-.1,.1))) .transpose("<1 6 1@2 6@2 1@2 8 6 1 8>/2") .pianoroll({ fold:1, cycles:16 }) .note().piano().out()`, */ ]} />
## 🍭 Syntax Sugar JavaScript Transpilation: converting valid JavaScript to valid JavaScript
https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs#L40
🍭 Before (User Code) 🤖 After (Ready to Eval)
"c3 [e3 g3]".fast(2)
```js mini('c3 [e3 g3]') .withMiniLocation([1, 0, 0], [1, 11, 11]) // source location .fast(2); ```
[Blog Post](https://loophole-letters.vercel.app/shift-ast), [AST Explorer](https://astexplorer.net/)
## 👁 Parsing Mini Notation
1. write [peg](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill.pegjs), generate [parser](https://github.com/tidalcycles/strudel/blob/main/packages/mini/krill-parser.js) with [parser generator (peggyjs)](https://peggyjs.org/) 2. pass string to generated parser: `mini('c3 [e3 g3]')` 3. obtain Abstract Syntax Tree (AST) 4. [transform](https://github.com/tidalcycles/strudel/blob/main/packages/mini/mini.mjs#L76) AST into Pattern: `seq('c3', ['e3', 'g3'])`
[OhmJS Editor](https://ohmjs.org/editor/)
## High Level Overview
➡️
## 🕵🏽 Querying Events Asking a Pattern for Events within a certain time span
```js seq('c3', ['e3', 'g3']) // <--- Pattern .queryArc(0, 2) // query events within 0 and 2 seconds .map((event) => event.showWhole()); // make readable ``` ```js [ '0/1 -> 1/2: c3', '1/2 -> 3/4: e3', '3/4 -> 1/1: g3', '1/1 -> 3/2: c3', // second time '3/2 -> 7/4: e3', '7/4 -> 2/1: g3', ]; ```
The scheduler will query the active pattern at regular intervals!
## 🗓️ Scheduling Query events repeatedly ```js let step = 0.5; // query interval in seconds let tick = 0; // how many intervals have passed let pattern = seq('c3', ['e3', 'g3']); // pattern from user setInterval(() => { const events = pattern.queryArc(tick * step, ++tick * step); events.forEach((event) => { console.log(event.showWhole()); const o = getAudioContext().createOscillator(); o.frequency.value = getFreq(event.value); o.start(event.whole.begin); o.stop(event.whole.begin + event.duration); o.connect(getAudioContext().destination); }); }, step * 1000); // query each "step" seconds ``` ## High Level Overview
➡️
## Sound Output Patterns are wrapped with param functions to compose different properties of the sound. bb1]]") // sets frequency .s("") // sound source .gain(.5) // turn down volume .cutoff(sine.range(200,1000).slow(4)) // modulated cutoff .slow(2) .out().logValues()`, ]} /> ```js { note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 } ``` ![waa nodes](./waa-nodes.png) Supports Web Audio API, Web MIDI, OSC, Speech, Web Serial, ... ? ## 🔭 Future Outlook
- Alternative Sound Backends - Collaborative Mode - Context Aware Documentation / Completion / Visualization - Port more Tidal Features
## 🌀 Try it out now
- Strudel REPL: [strudel.tidalcycles.org](https://strudel.tidalcycles.org/?EeNsQ8hdNZwN) (-> [offline version](http://localhost:3000/#Y29uc3QgZGVsYXkgPSBuZXcgRmVlZGJhY2tEZWxheSgxLzMsIC41KS5jaGFpbih2b2woLjIpLCBvdXQoKSkKbGV0IGthbGltYmEgPSBhd2FpdCBzYW1wbGVyKHsKICBDNTogJy4vc2FtcGxlcy9rYWxpbWJhL2thbGltYmEtYzUubXAzJwp9KQprYWxpbWJhID0ga2FsaW1iYS5jaGFpbih2b2woMC42KS5jb25uZWN0KGRlbGF5KSxvdXQoKSk7CmNvbnN0IHNjYWxlcyA9IGNhdCgnQyBtYWpvcicsICdDIG1peG9seWRpYW4nLCAnRiBseWRpYW4nLCBbJ0YgbWlub3InLCAnRGIgbWFqb3InXSkKCnN0YWNrKAogICJbMCAyIDQgNiA5IDIgMCAtMl0qMyIKICAuYWRkKCI8MCAyPi80IikKICAuc2NhbGUoc2NhbGVzKQogIC5zdHJ1Y3QoIngqOCIpCiAgLnZlbG9jaXR5KCI8LjggLjMgLjY%2BKjgiKQogIC5zbG93KDIpCiAgLnRvbmUoa2FsaW1iYSksCiAgIjxjMiBjMiBmMiBbW0YyIEMyXSBkYjJdPiIKICAuc2NhbGUoc2NhbGVzKQogIC5zY2FsZVRyYW5zcG9zZSgiWzAgPDIgND5dKjIiKQogIC5zdHJ1Y3QoIngqNCIpCiAgLnZlbG9jaXR5KCI8LjggLjU%2BKjQiKQogIC52ZWxvY2l0eSgwLjgpCiAgLnNsb3coMikKICAudG9uZShrYWxpbWJhKQopCiAgLmxlZ2F0bygiPC40IC44IDEgMS4yIDEuNCAxLjYgMS44IDI%2BLzgiKQogIC5mYXN0KDEpCi5waWFub3JvbGwoe2F1dG9yYW5nZTowfSk%3D)) - Interactive Tutorial: [strudel.tidalcycles.org/tutorial](https://strudel.tidalcycles.org/) - Stay Tuned: [@tidalcycles](https://twitter.com/tidalcycles) - Github: [https://github.com/tidalcycles/strudel](https://github.com/tidalcycles/strudel)
### Happy Strudeling!
## More? ~, ~ sd, hh*3") //.cutoff(perlin.range(100,900)) //.sometimes(x=>x.speed("2")) , note( "Cm7 ".voicings() ).rhodes().slow(4) , note( "[c2 ](3,9)".slow(2) //.superimpose(add(0.02)) ) .s("").gain(.5) .cutoff(800), note( "0 2 4 1".iter(4) .off(1/12, add("6")) .degradeBy(.5) .scale('C5 minor') .legato(.05) .echo(2, 1/6, .5) ) .piano().hcutoff(2000) ).out() //.cutoff(800) //.reset("") `, ]} /> ## Pattern Factories seq, cat, stack " .pianoroll() .note().piano().out()`, `stack(c3,e3,g3) // "c3,e3,g3" .pianoroll() .note().piano().out()`, `stack(c3, seq(e3, cat(g3, c4))) // "c3,[e3,]" .pianoroll() .note().piano().out()`, ]} /> ## Time Control fast / slow, early / late, rev, palindrome ".early(.16) ).late(.16) .pianoroll() .note().piano().out()`, `"c3 e3 g3 bb3".rev() .pianoroll() .note().piano().out()`, ]} /> ## Arithmetic & Patternified Params add, sub, mul, div ").slow(2) .pianoroll({fold:1,cycles:8}) .note().piano().out()`, ]} /> ## Scales generating notes from numbers ") .scale('F#2 major') .pianoroll({autorange:1}) .note().piano().out()`, `"-7, 0 2 4 7".add("<0 1 2 1>") .scale(cat('F#2 major','A2 major').slow(8)) .pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'}) .note().piano().out()`, ]} /> ## Articulation velocity, legato ") .scale(cat('F#2 major','A2 major').slow(8)) .legato(.25) .velocity(.5) .pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'}) .note().piano().out()`, `"-7, 0 2 4 7".add("<0 1 2 1>") .scale(cat('F#2 major','A2 major').slow(8)) .legato("<0.25 2>/8") .velocity("<1 .8>/8") .pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'}) .note().piano().out()`, ]} /> ## Continuous Signals / Randomness sine / saw / tri / square / rand / perlin, range, segment x.fast(3/2).color('#ff00ff50')) .fast(1) .s('triangle').out() .segment(64) .pianoroll({fold:0,vertical:0,cycles:2}) // strudel disable-highlighting`, ]} /> ## Humanization "articulate randomness" ") .scale(cat('F#2 major','A2 major').slow(8)) .legato(perlin.range(.5,2)) .velocity("<0.4 .3 .7>*3") .late(perlin.range(0,.05)) .pianoroll({autorange:1,cycles:16,fold:1,inactive:'orange'})`, ]} /> ## Superposition superimpose, off, echo, echoWith x.add(5).color('green')) .scale('C major').slow(2) .pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`, `"0 2 4 7" //.off(1/6, x=>x.add(9).color('green')) //.off(1/3, x=>x.add(7).color('brown')) .scale('C major').slow(2) .pianoroll({autorange:1,cycles:8,fold:0,inactive:'indigo'})`, `"0 2 4 7".add("<0 1>/8") //.echo(6,.125,.8) .scale('C major').slow(2) .color("indigo steelblue orange") .pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`, ]} /> ## Phasing Oscillator Detune in slow motion x.fast(1.01).color('#ff00ff50')) .slow(2) .pianoroll({fold:1,vertical:0,cycles:8})`, `// Steve Reich - Clapping Music s("cp").struct("x!3 ~ x!2 ~ x ~ x!2 ~").speed(.8) .jux(x=>x.speed(1.2) .late("<0 1 2 3 4 5 6 7 8 9 10 11>/8".div(12)) ).slow(2).webdirt()`, ]} /> ## Chord Voicings chord dictionaries + smooth voice leading (blog post) E2") .slow(4) .pianoroll({cycles:8,fold:1,inactive:'orange'})`, `// froos - giant steps, stride version stack( // melody seq( "[F#5 D5] [B4 G4] Bb4 [B4 A4]", "[D5 Bb4] [G4 Eb4] F#4 [G4 F4]", "Bb4 [B4 A4] D5 [D#5 C#5]", "F#5 [G5 F5] Bb5 [F#5 [F#5 ~@3]]", ), // chords seq( "[B^7 D7] [G^7 Bb7] Eb^7 [Am7 D7]", "[G^7 Bb7] [Eb^7 F#7] B^7 [Fm7 Bb7]", "Eb^7 [Am7 D7] G^7 [C#m7 F#7]", "B^7 [Fm7 Bb7] Eb^7 [C#m7 F#7]" ) .struct("~ [x ~]".fast(4*8)) .voicings(['E3', 'G4']), // bass seq( "[B2 D2] [G2 D2] [Eb2 Bb2] [A2 D2]", "[G2 Bb2] [Eb2 F#2] [B2 F#2] [F2 Bb2]", "[Eb2 Bb2] [A2 D2] [G2 D2] [C#2 F#2]", "[B2 F#2] [F2 Bb2] [Eb2 Bb2] [C#2 F#2]" ) .struct("x ~".fast(4*8)) ).slow(25) .pianoroll({cycles:8,fold:1,inactive:'orange'})`]}/> ## Speech ".speak()']} /> ## Strudel Packages `npm i @strudel.cycles/*`, where \* is one of:
- core: tidal pattern engine - mini: mini notation parser + core binding - eval: user code evaluator. syntax sugar + highlighting - tone: bindings for Tone.js instruments and effects - osc: bindings to communicate via OSC - midi: webmidi bindings - serial: webserial bindings - tonal: tonal functions - xen: microtonal / xenharmonic functions - ... there are [more](https://github.com/tidalcycles/strudel/tree/main/packages)