A closer look at tidal.pegjs

rhythmical
code

January 15, 2022

tidal.pegjs is a library that is able to parse tidal style mini notation. It would be a really valuable addition to rhythmical. It could be used like this:

{
  "parallel": [
    {
      "instrument": "drums",
      "tidal": "[bd sn,hh*7]"
    },
    {
      "instrument": "piano",
      "tidal": "[C2 [D2 G2]]"
    }
  ]
}

The combination of the mini notation with the object notation would be quite powerful, as the object notation can do some things the mini notation can't. The implementation could work like a unified plugin, that resolves the tidal strings in the rhythmical AST.

Table of Contents

Querying Events with tidal.pegjs

Just like with tidal, events can be queried inside a specific time arc:

export const p = Pattern('[C2 [D2 G2]]');
const events = p.query(0, 1);
const readable = events.map(({ value, arc: { start, end } }) => ({
  value,
  start: start.n + '/' + start.d,
  end: end.n + '/' + end.d,
}));

yields:

[
  {
    "value": "C2",
    "start": "0/1",
    "end": "1/2"
  },
  {
    "value": "D2",
    "start": "1/2",
    "end": "3/4"
  },
  {
    "value": "G2",
    "start": "3/4",
    "end": "1/1"
  }
]

or, using p.print():

0 - 1/2: [ C2 ]
1/2 - 3/4: [ D2 ]
3/4 - 1: [ G2 ]

This transformation is exactly what rhythmical does, through some minor differences in the format.

Converting to playable events

To convert the above event format to events that are understood by my Player component, I can use the following code:

// get value of fraction
const f = ({ n, d }) => n / d;
// convert tidal.pegjs event to rhythmical flat event
export const e =
  (duration, instrument) =>
  ({ value, arc: { start, end } }) => ({
    value,
    time: f(start) * duration,
    duration: (f(end) - f(start)) * duration,
    instrument,
  });

// query pattern for events
export const q = curry((start, end, duration, pattern: string, instrument: string) => {
  const p = Pattern(pattern);
  return p.query(start, end).map(e(duration, instrument));
});
<Player instruments={{ drums }} center={0} events={q(0, 2, 2, 'drums', '[bd sn,hh*7]')} />

or with different instruments:

<Player
  instruments={{ drums, piano }}
  center={0}
  fold={true}
  events={[
    ...q(0, 2, 3, 'drums', '[[[bd ~ bd] sn] [bd bd sn],hh*12]'),
    ...q(0, 2, 3, 'piano', '[[C2 G1]*3 [<[Bb1 G1 Bb1] [Bb1 F1 Bb1]>]]'),
    ...q(0, 2, 3, 'piano', '[[E3,G3,B3] [[F3,G3,Bb3] ~ <[F3,Ab3,Bb3] [F3,A3,Bb3]>]]'),
  ]}
/>

The syntax would be much nicer if this was a rhythmical object like this:

{
  "parallel": [
    { "instrument": "drums", "tidal": "[[[bd ~ bd] sn] [bd bd sn],hh*12]" },
    { "instrument": "piano", "tidal": "[[C2 G1]*3 [<[Bb1 G1 Bb1] [Bb1 F1 Bb1]>]]" },
    { "instrument": "piano", "tidal": "[[E3,G3,B3] [[F3,G3,Bb3] ~ <[F3,Ab3,Bb3] [F3,A3,Bb3]>]]" }
  ]
}

I want to implement this when rhythmical ASTs are ready.

Getting the AST

To generate events, the grammar generates the AST which is used as an intermediate representation. We can get obtain the AST like this:

import Pattern from 'tidal.pegjs/dist/pattern.js';

export const p = Pattern('[A [B C]]');
console.log(p.__data);

yields

{
  "type": "group",
  "values": [
    {
      "type": "string",
      "value": "A"
    },
    {
      "type": "group",
      "values": [
        {
          "type": "string",
          "value": "B"
        },
        {
          "type": "string",
          "value": "C"
        }
      ]
    }
  ]
}

The above AST is similar to unified ASTs with the only difference being the values prop for children. We can transform any tree to unified structure using the following function

export const unifyAST = curry((props, data) => {
  const {
    getChildren = (node) => node.children,
    getType = (node) => node.type,
    map = ({ type, data, children }) => ({ type, data, ...(children ? { children } : {}) }),
  } = props;
  const type = getType(data);
  const children = getChildren(data);
  if (!children) {
    return map({ type, data });
  }
  return map({
    type,
    data,
    children: children.map(unifyAST(props)),
  });
});

In the case of the tree returned by tidal.pegjs, we can then do:

export const unifyPatternAST = (patternData: Pattern) => {
  return unifyAST(
    {
      getChildren: (node) => node.values,
      map: ({ type, data: { value }, children }) => (children ? { type, children } : { type, value }),
    },
    patternData
  );
};
export const unifyPattern = (pattern: string) => unifyPatternAST(Pattern(pattern).__data);

const ast = unifyPattern('[A [B C]]');

console.log('ast', ast);

Which creates the following diff:

1
{
1
{
2
  "type": "group",
2
  "type": "group",
3
-
  "values": [
3
+
  "children": [
4
    {
4
    {
5
      "type": "string",
5
      "type": "string",
6
      "value": "A"
6
      "value": "A"
7
    },
7
    },
8
    {
8
    {
9
      "type": "group",
9
      "type": "group",
10
-
      "values": [
10
+
      "children": [
11
        {
11
        {
12
          "type": "string",
12
          "type": "string",
13
          "value": "B"
13
          "value": "B"
14
        },
14
        },
15
        {
15
        {
16
          "type": "string",
16
          "type": "string",
17
          "value": "C"
17
          "value": "C"
18
        }
18
        }
19
      ]
19
      ]
20
    }
20
    }
21
  ]
21
  ]
22
}
22
}

This format can also be used for visualization with d3-hierarchy:

More complex features

When using other features of the library:

unifyPattern('[0 [1 2]*4 <5 6 7> 8]');

... some properties are swallowed form the original ast. To make sure no information is lost, we can define which properties should move based on type:

import { pick } from 'ramda';

// props per type that should be picked to be spread to the unified object
const customProps = {
  number: ['value'],
  speed: ['rate'],
};

export const unifyPatternAST = (patternData: Pattern) => {
  console.log('unifyPatternAST', patternData);
  const u = unifyAST(
    {
      getChildren: (node) => node.values || (node.value?.type ? [node.value] : null), // value can be an object = single child
      map: ({ type, data, children }) => ({
        type,
        ...pick(customProps[type] || [], data),
        ...(children ? { children } : {}),
      }),
    },
    patternData
  );
  console.log('unifyPattern', u);
  return u;
};

Now all children are properly populated:

not a browser

Outlook

In a future post, I want to take a closer look at the way tidal.pegjs is querying the events. So far, rhythmical could only generate a finite set of events that were looped. Having a query in combination with onestep (< . . >) is very powerful. I am still not sure how to integrate tidal.pegjs, but it must be one of two ways:

  1. query events with tidal.pegjs and insert them into a rhythmical AST
  2. insert tidal.pegjs AST into rhythmical and find a way to query the events with rhythmical

I think the second approach would be the best. Until then, cheers.

Felix Roos 2023