-
-
Notifications
You must be signed in to change notification settings - Fork 122
Technical Manual
This document introduces you to Strudel in a technical sense. If you just want to use Strudel, have a look at the Tutorial.
There are different packages for different purposes. They..
- split up the code into smaller chunks
- can be selectively used to implement some sort of time based system
Please refer to the individual README files in the packages folder
The REPL is the place where all packages come together to form a live coding system. It can also be seen as a reference implementation for users of the library.
More info in the REPL README
The End User Code is written in JavaScript with added syntax sugar. The eval package evaluates the user code
after a transpilation step, which resolves the syntax sugar. If you don't want the syntax sugar, you can omit the eval package and call the native javascript eval
instead.
JavaScript Transpilation = converting valid JavaScript to valid JavaScript:
"c3 [e3 g3]".fast(2)
becomes
mini('c3 [e3 g3]')
.withMiniLocation([1, 0, 0], [1, 11, 11]) // source location
.fast(2);
- double quoted strings and backtick strings are turned into
mini
calls (single quoted strings are left as is) - The source location is added by chaining
withMiniLocation
, which enables the real time highlighting later - (psuedo) variable names that look like notes (like
c4
,bb2
orfs3
) are turned into strings - support for top level await
- operator overloading could be implemented in the future
This is how it works:
- The user code is parsed with a shift parser, generating an AST
- The AST is transformed to resolve the syntax sugar
- The AST is used to generate code again (shift-codegen)
Shift will most likely be replaced with acorn in the future, see https://github.com/tidalcycles/strudel/issues/174
Another important part of the user code is the mini notation, which allows to express rhythms in a short manner.
- the mini notation is implemented as a PEG grammar, living in the mini package
- it is based on krill by Mdashdotdashn
- the peg grammar is used to generate a parser with peggyjs
- the generated parser takes a mini notation string and outputs an AST
- the AST can then be used to construct a pattern using the regular Strudel API
Here's an example AST:
{
"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 } }
}
]
},
}
]
}
which translates to seq(c3, seq(e3, g3))
When the user code has been evaluated, we hopefully get a Pattern instance, which we can use to query events from.
These events can then be used to trigger side effects in the real world. On that note, Events are mostly called Hap(s) in the codebase, because JS already has a built in Event
class.
Querying = Asking a Pattern for Events within a certain time span
seq('c3', ['e3', 'g3']) // <--- Pattern
.queryArc(0, 2) // query events within 0 and 2 cycles
.map((hap) => hap.showWhole()); // make readable
yields
[
'0/1 -> 1/2: c3', // cycle 0
'1/2 -> 3/4: e3',
'3/4 -> 1/1: g3',
'1/1 -> 3/2: c3', // cycle 1
'3/2 -> 7/4: e3',
'7/4 -> 2/1: g3',
];
The scheduler will query events repeatedly, creating a possibly endless loop of time slices. Here is a simplified example of how it works
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
The third and last step is to use the scheduled events to make sound. Patterns are wrapped with param functions to compose different properties of the sound.
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()`,
]}
/>
Here is an example Hap value with different properties:
{ note: 'a4', s: 'sawtooth', gain: 0.5, cutoff: 267 }
- Patterns represent just values in time!
- Suitable for any time based output (music, visuals, movement, .. ?)
At the time of writing this doc, the following outputs are supported:
- Web Audio API
.out()
see /webaudio - MIDI
.midi()
see /midi - OSC
.osc()
see /osc - Serial
.serial()
see /serial - Tone.js
.tone()
(deprecated?) /tone - WebDirt
.webdirt()
(deprecated?) /webdirt - Speech
.speak()
(experimental) part of /core
These could change, so make sure to check the packages folder.