Skip to content

Technical Manual

Felix Roos edited this page Sep 13, 2022 · 2 revisions

This document introduces you to Strudel in a technical sense. If you just want to use Strudel, have a look at the Tutorial.

Strudel Packages

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

REPL

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

High Level Overview

1. End User Code

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.

🍭 Syntax Sugar

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 or fs3) 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

Mini Notation

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))

2. Querying & Scheduling

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

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',
];

🗓️ Scheduling

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

3. Sound Output

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, .. ?)

Supported Outputs

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.