mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-27 15:29:18 -04:00
982 lines
25 KiB
Plaintext
982 lines
25 KiB
Plaintext
import { Slides, Slide } from './Slides.jsx';
|
|
import { MaxiRepl } from './MaxiRepl';
|
|
import ImgTile from './ImgTile';
|
|
|
|
<Slides>
|
|
<Slide>
|
|
<div className="w-full h-full flex justify-center items-center">
|
|
<div>
|
|
|
|
# Strudel
|
|
|
|
## Algorithmic Patterns for the Web
|
|
|
|

|
|
|
|
Alex McLean, Felix Roos & Tidal Cycles Community
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
<img src="./ada.jpg" className="h-[600px] rounded-md" />
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
<img src="./ada.jpg" className="h-[400px] rounded-md" />
|
|
<div className="flex space-x-2 items-center">
|
|
<em className="hidden">
|
|
"The Analytical Engine weaves algebraic patterns, just as the Jacquard loom weaves flowers and leaves." - Ada
|
|
Lovelace
|
|
</em>
|
|
<em>
|
|
“[The Analytical Engine] might act upon other things besides <strong>number</strong>, were objects found whose
|
|
mutual fundamental relations could be expressed by those of the abstract science of <strong>operations</strong>{' '}
|
|
[...]
|
|
<span className="hidden">
|
|
, and which should be also susceptible of adaptations to the action of the operating notation and mechanism of the
|
|
engine.{' '}
|
|
</span>
|
|
<br />
|
|
Supposing, for instance, that the fundamental relations of pitched sounds in the science of harmony and of musical composition
|
|
were susceptible of such expression and adaptations, the [Analytical] Engine might compose elaborate and scientific pieces
|
|
of music of any degree of complexity or extent."
|
|
<br />
|
|
-- Ada Lovelace, (1815 - 1852)
|
|
</em>
|
|
</div>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Strudel is..
|
|
|
|
<div className="flex">
|
|
|
|
<div className="text-left">
|
|
|
|
- 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
|
|
|
|
</div>
|
|
|
|

|
|
|
|
</div>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
<img src="./codeandmusic.png" className="h-[700px] rounded-md -my-24" />
|
|
|
|
<div className="text-left">
|
|
|
|
- How to represent and control music with code?
|
|
- How to leverage unique features of computing?
|
|
- How to break loose from old conventions?
|
|
- How to improvise electronic music?
|
|
- Answer: Live Coding?
|
|
|
|
</div>
|
|
|
|
</Slide><Slide>
|
|
|
|
## About Me
|
|
|
|
<div className="text-left">
|
|
|
|
- 💻 working as a frontend developer by day, studied Computer Science
|
|
- 🎺 Trumpet Player, studied Jazz and Popular Music
|
|
- 💿 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
|
|
|
|
<div className="grid grid-cols-5 gap-4 mt-12">
|
|
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Flambdoma.png&w=2048&q=75" />
|
|
{/* <ImgTile
|
|
height={300}
|
|
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fharmonic-spiral.png&w=2048&q=75"
|
|
/> */}
|
|
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fpiano-roll.png&w=2048&q=75" />
|
|
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcof.png&w=2048&q=75" />
|
|
<ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2F251graph.png&w=2048&q=75" />
|
|
<ImgTile
|
|
height={200}
|
|
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fdiy-modular-synth%2Fmidi2cv-styleshot.png&w=2048&q=75"
|
|
/>
|
|
{/* <ImgTile height={200} src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fcolorpiano.png&w=2048&q=75" /> */}
|
|
{/*
|
|
<ImgTile
|
|
height={300}
|
|
src="https://loophole-letters.vercel.app/_next/image?url=%2Fimg%2Fvoicing-permutation.png&w=2048&q=75"
|
|
/> */}
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Slide><Slide>
|
|
|
|
|
|
## Some of my other Projects loosely related to Strudel
|
|
|
|
|
|
|
|
## A Taste of Strudel
|
|
|
|
<MaxiRepl
|
|
canvasHeight={600}
|
|
code={[
|
|
`"<0 2 [4 6](3,4,1) 3*2>"
|
|
.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,[~ <sd!3 sd(3,4,2)>],hh(3,4)") // drums
|
|
.speed(perlin.range(.7,.9)) // random sample speed variation
|
|
,"<a1 b1*2 a1(3,8) e2>" // 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
|
|
,"<Am7!3 <Em7 E7b13 Em7 Ebm7b5>>".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 <e6 a6>".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("<clubkick*2>,[~ <sd!3 sd(3,4,2)>],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("<x@7 <x(5,8,3) x(3,8) x(3,4,1)>>")
|
|
.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()`, */
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Live Coding
|
|
|
|
<i className="text-3xl">
|
|
"In live coding performance, performers create time-based works by programming them while these same works are being
|
|
executed." <br />
|
|
Roberts & Wakefield, 2016
|
|
</i>
|
|
<img src="./hydra-screenshot.png" className="h-[600px] rounded-md" />
|
|
<p>
|
|
Screenshot: "Olivia Jack -{' '}
|
|
<a href="https://hydra.ojack.xyz/" target="_blank">
|
|
Hydra
|
|
</a>
|
|
, Live Coding Visuals in the Browser".{' '}
|
|
</p>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Tidal Cycles
|
|
|
|
<div className="text-left">
|
|
|
|
- A popular software for live coding music, developed since 2009
|
|
- a powerful pattern language, inspired by classical music
|
|
- functional reactive programming (FRP)
|
|
|
|
</div>
|
|
|
|
<img src="./tidal-screenshot2.png" className="h-[500px] rounded-md" />
|
|
<p>
|
|
Screenshot:{' '}
|
|
<a href="https://youtu.be/tY8BmTnlUA0?t=594" target="_blank">
|
|
"Yaxu & hellocatfood - LIVE code for Noise Quest"
|
|
</a>
|
|
</p>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## What is Strudel?
|
|
|
|
<div className="text-left">
|
|
|
|
- a port of Tidal Cycles to JavaScript
|
|
- a browser based editor for zero-install live coding: <a href="https://strudel.tidalcycles.org/" target="_blank" className="text-indigo-400">strudel.tidalcycles.org</a>
|
|
- many ways to make sound: Web Audio, OSC, MIDI, Speech ...
|
|
|
|
<div className="hidden">
|
|
|
|
- "Strudel" = internationally known as delicious pastry, but also "swirl" / "whirlpool" in german
|
|
- (a tidal can cause a whirlpool!)
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<img src="./strudel-screenshot.png" className="h-[500px] rounded-md mb-0" />
|
|
<p>
|
|
Screenshot:{' '}
|
|
<a href="https://youtu.be/IcMSocdKwvw?t=330" target="_blank">
|
|
"Algorave 10th Birthday March 2022 - froos"
|
|
</a>
|
|
</p>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Strudel: How it started
|
|
|
|
<div className="text-left mb-0">
|
|
|
|
<blockquote>
|
|
"Any application that can be written in JavaScript, will eventually be written in JavaScript."
|
|
<br />
|
|
Jeff Atwood, 2009{' '}
|
|
</blockquote>
|
|
|
|

|
|
|
|
</div>
|
|
|
|
<div className="text-left">
|
|
|
|

|
|
|
|
- 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.
|
|
|
|
</div>
|
|
|
|
</Slide><Slide>
|
|
|
|
## High Level Overview
|
|
|
|
<div className="absolute top-[250px] text-8xl">➡️</div>
|
|
|
|
<img src="./strudelflow.png" className="h-[800px] mt-24" />
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## REPL = Read Eval Play Loop
|
|
|
|
<div className="text-left">
|
|
|
|
1. Read: User code is parsed, syntax sugar is dissolved
|
|
2. Eval: Transformed code is evaluated, creating a **Pattern**
|
|
3. Play: The **Pattern** is queried for events, which trigger sounds and effects
|
|
4. Loop: The new output is fed back into the ears of the human, informing the next move
|
|
|
|
</div>
|
|
|
|
<img src="./repl.png" className="-my-16" />
|
|
|
|
</Slide><Slide>
|
|
|
|
## User Code
|
|
|
|
<div className="text-left">
|
|
|
|
- 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
|
|
|
|
</div>
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`note("c3 [e3 g3]")
|
|
.piano().out().pianoroll()`,
|
|
`// mini notation also works with samples
|
|
s("bd,[~ sd],hh*4").out()`,
|
|
`\`[[e5 [b4 c5] d5 [c5 b4]]
|
|
[a4 [a4 c5] e5 [d5 c5]]
|
|
[b4 [~ c5] d5 e5]
|
|
[c5 a4 a4 ~]
|
|
[[~ d5] [~ f5] a5 [g5 f5]]
|
|
[e5 [~ c5] e5 [d5 c5]]
|
|
[b4 [b4 c5] d5 e5]
|
|
[c5 a4 a4 ~]],
|
|
[[e2 e3]*4]
|
|
[[a2 a3]*4]
|
|
[[g#2 g#3]*2 [e2 e3]*2]
|
|
[a2 a3 a2 a3 a2 a3 b1 c2]
|
|
[[d2 d3]*4]
|
|
[[c2 c3]*4]
|
|
[[b1 b2]*2 [e2 e3]*2]
|
|
[[a1 a2]*4]\`.slow(16)
|
|
.note().out()
|
|
.pianoroll({ fold:1, cycles:16 })`,
|
|
`"-7, 0 2 4 7".add("<0 1 2 1>")
|
|
.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 <g2 g1>",
|
|
"<[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()`, */
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## 🍭 Syntax Sugar
|
|
|
|
JavaScript Transpilation: converting valid JavaScript to valid JavaScript
|
|
|
|
<div className="hidden">https://github.com/tidalcycles/strudel/blob/main/packages/eval/shapeshifter.mjs#L40</div>
|
|
|
|
<img src="./shiftflow.png" className="-my-16" />
|
|
|
|
<div className="text-left">
|
|
<div className="grid grid-cols-2 gap-4 mt-4">
|
|
|
|
<span>🍭 Before (User Code)</span>
|
|
<span>🤖 After (Ready to Eval)</span>
|
|
|
|
<pre>"c3 [e3 g3]".fast(2)</pre>
|
|
|
|
```js
|
|
mini('c3 [e3 g3]')
|
|
.withMiniLocation([1, 0, 0], [1, 11, 11]) // source location
|
|
.fast(2);
|
|
```
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="hidden">
|
|
|
|
[Blog Post](https://loophole-letters.vercel.app/shift-ast), [AST Explorer](https://astexplorer.net/)
|
|
|
|
</div>
|
|
|
|
</Slide><Slide>
|
|
|
|
## 👁 Parsing Mini Notation
|
|
|
|
<div className="text-left">
|
|
|
|
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'])`
|
|
|
|
</div>
|
|
|
|
<img src="./pegflow.png" className="-my-16" />
|
|
|
|
[OhmJS Editor](https://ohmjs.org/editor/)
|
|
|
|
</Slide><Slide>
|
|
|
|
## High Level Overview
|
|
|
|
<div className="absolute top-[545px] text-8xl">➡️</div>
|
|
|
|
<img src="./strudelflow.png" className="h-[800px] mt-24" />
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## AST of `c3 [e3 g3]`
|
|
|
|
<div className="text-left">
|
|
|
|
```js
|
|
{
|
|
"type_": "pattern",
|
|
"arguments_": { "alignment": "h" },
|
|
"source_": [
|
|
{
|
|
"type_": "element", "source_": "c3",
|
|
"location_": { "start": { "offset": 1, "line": 1, "column": 2 }, "end": { "offset": 4, "line": 1, "column": 5 } }
|
|
},
|
|
{
|
|
"type_": "element",
|
|
"location_": { "start": { "offset": 4, "line": 1, "column": 5 }, "end": { "offset": 11, "line": 1, "column": 12 } }
|
|
"source_": {
|
|
"type_": "pattern", "arguments_": { "alignment": "h" },
|
|
"source_": [
|
|
{
|
|
"type_": "element", "source_": "e3",
|
|
"location_": { "start": { "offset": 5, "line": 1, "column": 6 }, "end": { "offset": 8, "line": 1, "column": 9 } }
|
|
},
|
|
{
|
|
"type_": "element", "source_": "g3",
|
|
"location_": { "start": { "offset": 8, "line": 1, "column": 9 }, "end": { "offset": 10, "line": 1, "column": 11 } }
|
|
}
|
|
]
|
|
},
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
</div>
|
|
|
|
= `seq(c3, seq(e3, g3))`
|
|
|
|
</Slide><Slide>
|
|
|
|
## 🕵🏽 Querying Events
|
|
|
|
Asking a Pattern for Events within a certain time span
|
|
|
|
<div className="text-left">
|
|
|
|
```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',
|
|
];
|
|
```
|
|
|
|
</div>
|
|
|
|
The scheduler will query the active pattern at regular intervals!
|
|
|
|
</Slide><Slide>
|
|
|
|
## 🗓️ 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
|
|
```
|
|
|
|
</Slide><Slide>
|
|
|
|
## High Level Overview
|
|
|
|
<div className="absolute top-[855px] text-8xl">➡️</div>
|
|
|
|
<img src="./strudelflow.png" className="h-[800px] mt-24" />
|
|
|
|
</Slide><Slide>
|
|
|
|
## Sound Output
|
|
|
|
Patterns are wrapped with param functions to compose different properties of the sound.
|
|
|
|
<MaxiRepl
|
|
canvasHeight={0}
|
|
code={[
|
|
`note("[c2(3,8) [<eb2 g1> bb1]]") // sets frequency
|
|
.s("<sawtooth square>") // 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 }
|
|
```
|
|
|
|

|
|
|
|
Supports Web Audio API, Web MIDI, OSC, Speech, Web Serial, ... ?
|
|
|
|
</Slide><Slide>
|
|
|
|
## 🔭 Future Outlook
|
|
|
|
<div className="text-left">
|
|
|
|
- Alternative Sound Backends
|
|
- Collaborative Mode
|
|
- Context Aware Documentation / Completion / Visualization
|
|
- Port more Tidal Features
|
|
|
|
</div>
|
|
|
|
## 🌀 Try it out now
|
|
|
|
<div className="text-left">
|
|
|
|
- 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)
|
|
|
|
</div>
|
|
|
|
### Happy Strudeling!
|
|
|
|
</Slide><Slide>
|
|
|
|
## More?
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`stack(
|
|
s("<bd [bd ~ bd]> ~, ~ sd, hh*3")
|
|
//.cutoff(perlin.range(100,900))
|
|
//.sometimes(x=>x.speed("2"))
|
|
,
|
|
note(
|
|
"Cm7 <Dm7 C#m7>".voicings()
|
|
).rhodes().slow(4)
|
|
,
|
|
note(
|
|
"[c2 <eb2 g1>](3,9)".slow(2)
|
|
//.superimpose(add(0.02))
|
|
)
|
|
.s("<sawtooth square>").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("<x@3 x*2>")
|
|
`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## Strudel Workshop
|
|
|
|

|
|
|
|
</Slide><Slide>
|
|
|
|
## Pattern Factories
|
|
|
|
seq, cat, stack
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`seq(c3,e3,g3) // "c3 e3 g3"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`cat(c3,e3,g3) // "<c3 e3 g3>"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`stack(c3,e3,g3) // "c3,e3,g3"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`stack(c3, seq(e3, cat(g3, c4))) // "c3,[e3,<g3 c4>]"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Time Control
|
|
|
|
fast / slow, early / late, rev, palindrome
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"c3 e3 g3".fast(2) // "[c3 e3 g3]*2"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`"c3 e3 g3".slow(2) // "[c3 e3 g3]/2"
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`stack(
|
|
"c3 e3".color('steelblue'),
|
|
"<g3 a3 bb3 a3>".early(.16)
|
|
).late(.16)
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
`"c3 e3 g3 bb3".rev()
|
|
.pianoroll()
|
|
.note().piano().out()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Arithmetic & Patternified Params
|
|
|
|
add, sub, mul, div
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"30,42 45 49 54".add(0).slow(2)
|
|
.pianoroll({autorange:0,minMidi:28,maxMidi:56})
|
|
.note().piano().out()`,
|
|
`"30,42 45 49 54".add("<0 1 2 1>").slow(2)
|
|
.pianoroll({fold:1,cycles:8})
|
|
.note().piano().out()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Scales
|
|
|
|
generating notes from numbers
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"-7,0 2 4 7".add("<0 1 2 1>")
|
|
.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()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Articulation
|
|
|
|
velocity, legato
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"-7, 0 2 4 7".add("<0 1 2 1>")
|
|
.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()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Continuous Signals / Randomness
|
|
|
|
sine / saw / tri / square / rand / perlin, range, segment
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`sine
|
|
.range(42,54).slow(4).segment(8)
|
|
.pianoroll({ minMidi:42, maxMidi: 54, autorange:0,
|
|
cycles:8,fold:0,inactive:'orange'})
|
|
.s('triangle').out()`,
|
|
`s("hh*8").speed(sine.range(.5,2).slow(4))
|
|
.out()`,
|
|
`sine.range(42,50).color('#00ff0050')
|
|
.superimpose(x=>x.fast(3/2).color('#ff00ff50'))
|
|
.fast(1)
|
|
.s('triangle').out()
|
|
.segment(64)
|
|
.pianoroll({fold:0,vertical:0,cycles:2})
|
|
// strudel disable-highlighting`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Humanization
|
|
|
|
"articulate randomness"
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"-7, 0 2 4 7".add("<0 1 2 1>")
|
|
.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'})`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## Shuffling
|
|
|
|
iter
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"c3 e3 g3 c4".iter(4)
|
|
.pianoroll({autorange:1,cycles:8,fold:1,inactive:'indigo'})`,
|
|
`stack("-7", "0 2 4 7".iter(4)).add("<0 1 2 1>")
|
|
.scale(cat('F#2 major','A2 major').slow(8))
|
|
.legato(perlin.range(.5,2))
|
|
.velocity(rand.range(.6,.9))
|
|
.pianoroll({autorange:1,cycles:16,fold:1,inactive:'indigo'})`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Superposition
|
|
|
|
superimpose, off, echo, echoWith
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"0 [2 1] 4 7"
|
|
.superimpose(x=>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'})`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Phasing
|
|
|
|
Oscillator Detune in slow motion
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`// Steve Reich - Piano Phase (1967)
|
|
"E4 F#4 B4 C#5 D5 F#4 E4 C#5 B4 F#4 D5 C#5".color('#00ff0050')
|
|
.superimpose(x=>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()`,
|
|
]}
|
|
/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Chord Voicings
|
|
|
|
chord dictionaries + smooth voice leading (<a href="https://loophole-letters.vercel.app/rhythmical-chords" target="_blank">blog post</a>)
|
|
|
|
<MaxiRepl
|
|
code={[
|
|
`"Am7 [Bm7b5 E7b9]"
|
|
.struct("x(3,8)".fast(2))
|
|
.voicings(['E3', 'G4'])
|
|
.stack("A2 E2 <B2 B1> 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'})`]}/>
|
|
|
|
</Slide><Slide>
|
|
|
|
## Speech
|
|
|
|
<MaxiRepl code={['"<hello world>".speak()']} />
|
|
|
|
</Slide><Slide>
|
|
|
|
## Strudel Packages
|
|
|
|
`npm i @strudel.cycles/*`, where \* is one of:
|
|
|
|
<div className="text-left">
|
|
|
|
- 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)
|
|
|
|
</div>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## Workflow
|
|
|
|
<div className="text-left">
|
|
|
|
- Everything is in one [monorepo](https://github.com/tidalcycles/strudel)
|
|
- Development via npm workspaces
|
|
- Packages are published with lerna
|
|
- REPL is Deployed to github pages
|
|
- Features are added with Pull Requests
|
|
- Automated tests run for every PR
|
|
- Snapshot tests
|
|
|
|
</div>
|
|
|
|
</Slide><Slide hidden>
|
|
|
|
## Tech Stack
|
|
|
|
<div className="text-left">
|
|
|
|
- REPL: Codemirror 6 + react + tailwind
|
|
- Everything is built with vite
|
|
- In source documentation with jsdoc
|
|
- Tutorial written with MDX
|
|
- jsdoc is inserted into MDX with nunjucks
|
|
|
|
</div>
|
|
|
|
</Slide>
|
|
</Slides>
|