REPL

Besides a UI for playback control and meta information, the main part of the REPL interface is the code editor powered by CodeMirror. In it, the user can edit and evaluate pattern code live, using one of the available synthesis outputs to create music and/or sound art. The control flow of the REPL follows 3 basic steps:

  1. The user writes and updates code. Each update transpiles and evaluates it to create a Pattern instance
  2. For each scheduling tick, all generated Events are triggered by calling their onTrigger method, which is set by the output.

User Code

To create a Pattern from the user code, two steps are needed:

  1. Transpile the JS input code to make it functional
  2. Evaluate the transpiled code

Transpilation & Evaluation

In the JavaScript world, using transpilation is a common practise to be able to use language features that are not supported by the base language. Tools like babel will transpile code that contains unsupported language features into a version of the code without those features.

note("c3 [e3 g3]*2")

is transpiled to:

note(m('c3 [e3 g3]*2', 5))

Here, the string is wrapped in m, which will create a pattern from a mini-notation string. As the second parameter, it gets passed source code location of the string, which enables highlighting active events later.

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

seq(
  reify('c3').withLoc(6, 9),
  seq(reify('e3').withLoc(10, 12), reify('g3',).withLoc(13, 15))
)

Highlighting Locations

As seen in the examples above, both the mini-notation parser adds the source code locations using withLoc. This location is calculated inside the m function, as the sum of 2 locations:

  1. the location where the mini notation string begins, as obtained from the JS parser
  2. the location of the substring inside the mini notation, as obtained from the mini notation parser

The sum of both is passed to withLoc to tell each element its location, which can be later used for highlighting when it’s active.

Mini Notation

Another important part of the user code is the mini notation, which allows to express rhythms in a short manner.

  • 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

Here’s an example AST for c3 [e3 g3]

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

Vim Keybindings

See the separate page on Vim shortcuts for a quick reference: /technical-manual/vim

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:

let pattern = seq('c3', ['e3', 'g3']); // pattern from user
let interval = 0.5; // query interval in seconds
let time = 0; // beginning of current time span
let minLatency = 0.1; // min time before a hap should trigger
setInterval(() => {
  const haps = pattern.queryArc(time, time + interval);
  time += interval; // increment time
  haps.forEach((hap) => {
    const deadline = hap.whole.begin - time + minLatency;
    onTrigger(hap, deadline, duration);
  });
}, 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.

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.

Output

The last step is to trigger each event in the chosen output.

Control Parameters

To be able to manipulate multiple aspects of sound in parallel, so called control parameters are used to shape the value of each event. Example:

note('c3 e3')
  .cutoff(1000)
  .s('sawtooth')
  .queryArc(0, 1)
  .map((hap) => hap.value);
/* [
  { note: 'c3', cutoff: 1000, s: 'sawtooth' }
  { note: 'e3', cutoff: 1000, s: 'sawtooth' }
] */

Here, the control parameter functions note, cutoff and s are used, where each controls a different property in the value object. Each control parameter function accepts a primitive value, a list of values to be sequenced into a Pattern, or a Pattern. In the example, note gets a Pattern from a mini-notation expression (double quoted), while cutoff and s are given a Number and a (single quoted) String respectively.

const { x, y } = createParams('x', 'y');
x(sine.range(0, 200)).y(cosine.range(0, 200));

This example creates the custom control parameters x and y which are then used to form a pattern that descibes the coordinates of a circle.

Outputs

Now that we know how the value of an event is manipulated using control parameters, we can look at how outputs can use that value to generate anything. The scheduler above was calling the onTrigger function which is used to implement the output. A very simple version of the web audio output could look like this:

function onTrigger(hap, deadline, duration) {
  const { note } = hap.value;
  const time = getAudioContext().currentTime + deadline;
  const o = getAudioContext().createOscillator();
  o.frequency.value = getFreq(note);
  o.start(time);
  o.stop(time + event.duration);
  o.connect(getAudioContext().destination);
}

I want to help, how do I contribute to the Docs?