mirror of
https://codeberg.org/uzu/strudel
synced 2026-07-24 05:57:17 -04:00
tweaks
This commit is contained in:
+35
-40
@@ -8,7 +8,7 @@ author:
|
||||
affiliation: Then Try This
|
||||
email: alex@slab.org
|
||||
abstract: |
|
||||
This paper introduces Strudel, which brings the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
This paper introduces Strudel, which faithfully ports the TidalCycles approach to live coding algorithmic patterns to native JavaScript and the web. We begin by giving a little background of the first year of development, before sharing some detail about its implementation and examples of use. We go on to outline the wide range of synthesis and other outputs available in Strudel, including WebAudio, MIDI, OSC (for SuperDirt), WebSerial and CSound, and introduce Strudel's REPL live editor, including its built-in visualisations. We then compare Strudel with Tidal, the trade-offs involved between JavaScript and Haskell, and the unique capabilities offered by Strudel for aligning patterns, before concluding with some thoughts about the future.
|
||||
bibliography: citations.json
|
||||
fontsize: 11pt
|
||||
geometry: margin=2cm
|
||||
@@ -23,37 +23,36 @@ date: '2022-12-14'
|
||||
|
||||
In the following paper, we introduce *Strudel*, an alternative
|
||||
implementation of the TidalCycles (or 'Tidal' for short) live coding
|
||||
system, using the JavaScript programming language. Strudel is an
|
||||
system. Whereas Tidal is implemented in the pure functional Haskell programming language, Strudel brings it to the multi-paradigm, web-centric JavaScript language. Strudel is an
|
||||
attempt to make live coding more accessible, by creating a system that
|
||||
runs entirely in the browser, while opening Tidal's approach to
|
||||
car run entirely in the browser, while opening Tidal's approach to
|
||||
algorithmic patterns [@mcleanAlgorithmicPattern2020a] up to modern
|
||||
audio/visual web technologies. The Strudel REPL is a live code editor
|
||||
dedicated to manipulating patterns while they play, with builtin
|
||||
visual feedback. While Strudel is written in JavaScript, the API is
|
||||
optimized for simplicity and readability by applying code
|
||||
visual feedback. While Strudel is written in standard JavaScript, it does support some optimisations for simplicity, readability and editability by applying code
|
||||
transformations on the syntax tree level, allowing language operations
|
||||
that would otherwise be impossible. The application supports multiple
|
||||
ways to output sound, including Tone.js, Web Audio Nodes, OSC (Open
|
||||
Sound Control) messages, Web Serial, Web MIDI and Csound. The project
|
||||
is split into multiple packages, allowing granular reuse in other
|
||||
applications. Apart from TidalCycles, Strudel draws inspiration from
|
||||
many prior existing projects like TidalVortex
|
||||
several other existing projects like TidalVortex
|
||||
[@mcleanTidalVortexZero2022], Gibber [@robertsGibberLiveCoding2012],
|
||||
Estuary [@ogbornEstuaryBrowserbasedCollaborative2017], Hydra
|
||||
[@jackHydra2022], Ocarina [@solomonPurescriptocarina2022] and
|
||||
Feedforward [@mcleanFeedforward2020]. This paper expands the Strudel
|
||||
Demo paper for the Web Audio Conference 2022 [@StrudelWAC2022].
|
||||
Feedforward [@mcleanFeedforward2020]. This paper builds upon a
|
||||
demo paper presented at Web Audio Conference 2022 [@StrudelWAC2022].
|
||||
|
||||
The first tentative commit to the Strudel project was on 22nd January
|
||||
2022 by Alex McLean, with the core representation implemented over the
|
||||
following few days. Although this was his first attempt at a
|
||||
JavaScript-based application, by 27th January, Alex had managed to
|
||||
upload the initial version to the 'npm' javascript package database,
|
||||
upload an initial version to the 'npm' javascript package database,
|
||||
sharing with the wider community for comment. By 4th February, Felix
|
||||
Roos had discovered Strudel and contributed a 'REPL' user interface to
|
||||
it, and then contributed a scheduler the next day, so that Strudel
|
||||
could already make sound. At this point, Alex and Felix shared
|
||||
ownership to the repository, and the project has since proved to be a
|
||||
it, and a scheduler the following day, so that Strudel
|
||||
could already begin to make sound. At this point, Alex and Felix shared
|
||||
ownership of the repository, and the project has since proved to be a
|
||||
productive confluence of Felix's own work into music representation
|
||||
and visualisation, with Alex's experience with making Tidal. Felix has
|
||||
since become the primary contributor to Strudel, with Alex continuing
|
||||
@@ -61,10 +60,10 @@ to jump between developing both Strudel and Tidal. Aspects of
|
||||
Strudel's development have therefore fed back into TidalCycles, and
|
||||
both systems have maintained a shared conceptual underpinning. We plan
|
||||
to continue working towards feature parity between these systems,
|
||||
although within the syntactical trade-offs and library ecosystems of
|
||||
although within the different syntactical trade-offs and library ecosystems of
|
||||
JavaScript and Haskell, some divergence is inevitable and healthy.
|
||||
|
||||
Over the first year of its life, Strudel is now a fully-fledged live
|
||||
Now celebrating the first year of its life, Strudel is now a fully-fledged live
|
||||
coding environment, porting Tidal's core represention of patterns,
|
||||
pattern transformations, and mini-notation for polymetric sequences,
|
||||
combined with a wealth of features for synthesising and visualising
|
||||
@@ -72,13 +71,13 @@ those patterns.
|
||||
|
||||
# From Tidal to Strudel and back
|
||||
|
||||
As mentioned above, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
As mentioned earlier, the original Tidal is implemented as a domain specific language (DSL) embedded in the Haskell pure functional programming language, and takes advantage of Haskell's terse syntax and advanced, 'strong' type system. JavaScript on the other hand, is a multi-paradigm programming language, with a dynamic type system. Because Tidal leans heavily on many of Haskell's more unique features, it was not always clear that it could meaningfully be ported to a multi-paradigm scripting language. However, this possibility was already demonstrated with an earlier port to Python [TidalVortex; @mcleanTidalVortexZero2022], and we have now successfully implemented Tidal's pure functional representation of patterns in Strudel, including partial application, currying, and the functor, applicative and monadic structures that underlie Tidal's expressive pattern transformations. The result is a terse and highly composable system, where everything is either a pattern, or a function for combining and manipulating patterns, offering a rich creative ground for exploration.
|
||||
|
||||
This development process has been far from a one-way port, however. The process of porting Tidal's concepts has also opened up new possibilities, some just from revisiting every design decision, and some from the particular affordances and constraints offered by JavaScript. This has lead to new features (and indeed bugfixes) that have found their way back to Tidal where appropriate, and ongoing work that we will return to in the conclusion of this paper.
|
||||
|
||||
# Representing Patterns
|
||||
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern can output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
Patterns are the essence of Tidal. Its patterns are abstract entities that represent flows of time as functions, adapting a technique called pure functional reactive programming. Taking a time span as its input, a Pattern will output a set of events that happen within that time span. It depends on the structure of the Pattern how the events are located in time.
|
||||
From now on, this process of generating events from a time span will be called **querying**.
|
||||
Example:
|
||||
|
||||
@@ -101,16 +100,16 @@ The resulting events are:
|
||||
|
||||
Each event has a value, a begin time and an end time, where time is represented as a fraction. In the above case, the events are placed in sequential order, where c3 takes the first half, and e3 and g3 together take the second half. This temporal placement is the result of the `sequence` function, which divides its arguments equally over one cycle. If an argument is an array, the same rule applies to that part of the cycle. In the example, e3 and g3 are divided equally over the second half of the whole cycle.
|
||||
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new one is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
Note that the query function is not just a way to access a pattern, but true to the principles of functional programming, is the pattern itself. This means that in theory there is no way to change a pattern, it is opaque as a pure function. In practice though, Strudel and Tidal are all about transforming patterns, so how is this done? The answer is, by replacing the pattern with a new one, that calls the old one. This new function is only able to manipulate the query before passing it to the old pattern, and manipulate the results from it before returning them to caller. But, this is enough to support all the temporal and structural manipulations provided by Strudel (and Tidal's) extensive library of functions.
|
||||
|
||||
The above examples do not represent how Strudel is used in practice. In the live coding editor, the user only has to type in the pattern itself, the querying will be handled by the scheduler. The scheduler will repeatedly query the pattern for events, which are then scheduled as sound synthesis or other event triggers.
|
||||
Also, the above event data structure has been simplified for readability.
|
||||
|
||||
{ width=60% }
|
||||
|
||||
# Making Patterns
|
||||
m# Making Patterns
|
||||
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library to create, combine and transform patterns.
|
||||
In practice, the end-user live coder will not deal with constructing patterns directly, but will rather build patterns using Strudel's extensive combinator library, to create, combine and transform patterns.
|
||||
|
||||
The live coder will rarely use the `sequence` function as seen above, as sequencing is implicit in many functions. For example in the following, the `note` function constructs a pattern of notes, sequencing its arguments in the same manner as the previous example.
|
||||
|
||||
@@ -118,13 +117,13 @@ The live coder will rarely use the `sequence` function as seen above, as sequenc
|
||||
note(c3, [e3, g3])
|
||||
```
|
||||
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This last example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
Perhaps more often, they will use the mini-notation for even terser notation of rhythmic sequences: ^[This example is also valid Tidal code, albeit the parenthesis is not required in its Haskell syntax in this case. Tidal does not support passing sequences as lists directly to the `note` function, however.].
|
||||
|
||||
```js
|
||||
note("c3 [e3 g3]")
|
||||
```
|
||||
|
||||
Such sequences are often treated only as a starting point for manipulation, where they then undergo pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
Such sequences are often treated only as a starting point for manipulation, where functions are then applied which represent pattern transformations such as repetition, symmetry, interference/combination or randomisation, potentially at multiple timescales. Because Strudel patterns are represented as pure functions of time rather than as data structures, very long and complex generative results can be represented and manipulated without having to store the resulting sequences in memory.
|
||||
|
||||
# Pattern Example
|
||||
|
||||
@@ -140,7 +139,7 @@ The following example showcases how patterns can be utilized to create musical c
|
||||
.delay(.8).delaytime(.125)
|
||||
```
|
||||
|
||||
The pattern starts with a rhythm of numbers in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The pattern starts with a rhythmic pattern of numbers expressed in mini-notation, which are later interpreted inside the scale of D minor.
|
||||
The first line could also be expressed without mini-notation:
|
||||
|
||||
```js
|
||||
@@ -183,7 +182,7 @@ Strudel's new default output uses the Web Audio API to create a new audio graph
|
||||
|
||||
WebDirt [@ogbornDktr0WebDirt2022] was created as part of the Estuary Live Coding System [@ogbornEstuaryBrowserbasedCollaborative2017], and proved to be a solid choice for handling samples in Strudel as well. We are however focused on working more directly with the Web Audio API to be able to integrate new features more tightly.
|
||||
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@SuperDirt2022], which is the well-developed Supercollider-based synthesis framework that Tidal live coders generally use as standard.
|
||||
Using the OSC protocol via Strudel's provided Node.js-based OSC proxy server, it is possible to send network messages to trigger events. This is mainly used to render sound using SuperDirt [@SuperDirt2022], which is the well-developed Supercollider-based synthesis framework used by Tidal live coders as standard.
|
||||
|
||||
Recently, the experimental integration of Csound proved to bring a new dimension of sound design capabilities to Strudel. Thanks to the WebAssembly distribution of this classic system [@CsoundWebAssembly], Csound 'orchestra' synthesisers can be embedded in and then patterned with Strudel code.
|
||||
|
||||
@@ -231,17 +230,13 @@ mini("c3 [e3 g3]*2").withMiniLocation([1,0,0],[1,14,14])
|
||||
|
||||
Here, the string is wrapped in `mini`, which will create a pattern from a mini-notation string. Additionally, the `withMiniLocation` method passes the original source code location of the string to the pattern, which enables highlighting active events.
|
||||
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation.
|
||||
|
||||
After the transpilation, the code is ready to be evaluated into a `Pattern`.
|
||||
|
||||
Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
Other convenient features like pseudo variables, operator overloading and top level await are possible with transpilation. After the transpilation, the code is ready to be evaluated into a `Pattern`. Behind the scenes, the user code string is parsed with `acorn`, turning it into an Abstract Syntax Tree (AST). The AST allows changing the structure of the code before generating the transpiled version using `escodegen`.
|
||||
|
||||
### Mini-notation
|
||||
|
||||
While the transpilation allows JavaScript to express Patterns in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
While the transpilation allows us to express patterns in Javascript in a less verbose way, it is still preferable to use the mini-notation as a more compact way to express rhythm. Strudel aims to provide the same mini-notation features and syntax as used in Tidal.
|
||||
|
||||
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the given structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
The mini-notation parser is implemented using `peggy`, which allows generating performant parsers for Domain Specific Languages (DSLs) using a concise grammar notation. The generated parser turns the mini-notation string into an AST which is used to call the respective Strudel functions with the required structure. For example, `"c3 [e3 g3]*2"` will result in the following calls:
|
||||
|
||||
```js
|
||||
seq(
|
||||
@@ -255,12 +250,12 @@ seq(
|
||||
|
||||
### Highlighting Locations
|
||||
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location can be used to highlight active events in real time.
|
||||
As seen in the examples above, both the JS and the mini-notation parser add source code locations using `withMiniLocation` and `withLocation` methods. While the JS parser adds locations relative to the user code as a whole, the mini-notation adds locations relative to the position of the mini-notation string. The absolute location of elements within mini-notation can be calculated by simply adding both locations together. This absolute location is then used to highlight active events in real time.
|
||||
|
||||
## Scheduling Events
|
||||
|
||||
After an instance of `Pattern` is obtained from the user code,
|
||||
it is used by the scheduler to get queried for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
the scheduler queries it for events. Once started, the scheduler runs at a fixed interval to query the active pattern for events within the current interval's time span. A simplified implementation looks like this:
|
||||
|
||||
```js
|
||||
let pattern = seq('c3', ['e3', 'g3']); // pattern from user
|
||||
@@ -277,7 +272,7 @@ setInterval(() => {
|
||||
}, interval * 1000); // query each "interval" seconds
|
||||
```
|
||||
|
||||
Note that the above code is simplified for illustrative purposes. The actual implementation has to work around imprecise callbacks of `setInterval`. More about the implementation details can be read in [this blog post](https://loophole-letters.vercel.app/web-audio-scheduling).
|
||||
Note that the above code is simplified for illustrative purposes, for example the actual implementation has to work around imprecise callbacks of `setInterval`. ^[Further implementation details of the Strudel scheduler can be read in this blog post: [loophole-letters.vercel.app/web-audio-scheduling](https://loophole-letters.vercel.app/web-audio-scheduling).]
|
||||
|
||||
The fact that `Pattern.queryArc` is a pure function that maps a time span to a set of events allows us to choose any interval we like without changing the resulting output. It also means that when the pattern is changed from outside, the next scheduling callback will work with the new pattern, keeping its clock running.
|
||||
|
||||
@@ -334,7 +329,7 @@ The above example will create an `OscillatorNode` for each event, where the freq
|
||||
|
||||
One core aspect of Strudel, inherited from Tidal, is the flexible way that patterns can be combined, irrespective of their structure. Its declarative approach means a live coder does not have to think about the details of *how* this is done, only *what* is to be done.
|
||||
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, with the second step broken down into two substeps, giving four events in total. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
As a simple example, consider two number patterns `"0 [1 2] 3"`, and `"10 20"`. The first has three contiguous steps of equal lengths, the second of which is broken down into two substeps, giving four events in total. The second pattern simply has two steps, taking up half a cycle each. There are a very large number of ways in which the structure of these two patterns could be combined, but the default method in both Strudel and Tidal is to line up the cycles of the two patterns, and then take events from the first pattern and match them with those in the second pattern. Therefore, the following two lines are equivalent:
|
||||
|
||||
```js
|
||||
"0 [1 2] 3".add("10 20")
|
||||
@@ -389,20 +384,20 @@ versus Strudel's simplicity.
|
||||
To demonstrate this, consider the following Tidal pattern:
|
||||
|
||||
```haskell
|
||||
iter 4 $ every 3 (||+ n "10 20") $ (n "0 1 3") # s "triangle" # crush 4
|
||||
iter 4 $ every 3 (||+ n "10 20") $ n "20 21 23" # s "triangle" # crush 4
|
||||
```
|
||||
|
||||
This can be directly translated to the Strudel equivalent:
|
||||
|
||||
```js
|
||||
iter(4, every(3, add.squeeze("10 20"), n("0 1 3").s("triangle").crush(4)))
|
||||
iter(4, every(3, x => x.add.squeeze(n("10 20")), n("20 21 23").s("triangle").crush(4)))
|
||||
```
|
||||
|
||||
Although for a more canonical Strudel expression, we would reorder it
|
||||
as:
|
||||
|
||||
```js
|
||||
n("0 1 3").every(3, add.squeeze("10 20")).iter(4).s("triangle").crush(4)
|
||||
n("20 21 23").every(3, x => x.add.squeeze(n("10 20"))).iter(4).s("triangle").crush(4)
|
||||
```
|
||||
|
||||
The Strudel example uses the `.` method call operator for all
|
||||
@@ -426,17 +421,17 @@ features.
|
||||
|
||||
With Strudel, we have little choice but to embrace the affordances and
|
||||
constraints offered by JavaScript, and while designing a
|
||||
domain-specific language based on method calls is a
|
||||
domain-specific language based on chaining method calls is a
|
||||
challenge, through creative adoption of functional programming
|
||||
techniques like partial application, we are so far very happy with the
|
||||
results. Tidal's functional reactive approach to pattern-making has in
|
||||
general translated well to JavaScript, and opportunities and
|
||||
constraints have overall traded off to create a very approachable and
|
||||
constraints have traded off to create a very approachable and
|
||||
useable live coding environment.
|
||||
|
||||
## The trade-off of flexible typing
|
||||
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that it is easy to implement pattern methods where computationally expensive conversion from floating point to rational numbers are performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
We have identified one problem with porting Tidal to JavaScript where we have missed Haskell's strict typing and type inference. In both Tidal and Strudel, time is rational, where any point in time is represented as the ratio of two integers. This allows representation of musical ratios such that are impossible to represent accurately using the more common floating point numbers. However while libraries are available that support rational numbers in JavaScript, the lack of strict typing means that computationally expensive conversion from floating point to rational numbers may be performed late, and therefore often enough to overload the CPUs, due to the large number of iterative calculations required to estimate a ratio for a given floating point number. To mitigate this problem, we might consider moving to TypeScript in the future.
|
||||
|
||||
# Future Outlook
|
||||
|
||||
|
||||
Reference in New Issue
Block a user