Mini dsl for sequences of durations and values

Pdv - (P(dur-val))

Ryhthmically sequences numeric values which can then be used with any event key, e.g. degree or midinote. Rhythms are configured as groups of values which can be nested, stretched randomized and alternated.

Code is available here with some basic documentation.

8 Likes

This is really cool, thanks for sharing! Your parser code is simple and elegant (unlike mine, which evolved over time and could probably use some revisiting ;-). For my own interest (and maybe yours?), I thought Iā€™d compare your Pdv to what I support in Bacalaoā€™s pattern notation. Itā€™s actually pretty similarā€¦

The difference is I assign the duration 1 to the whole pattern sequence, treating it like ā€œone barā€ instead of one beat, so youā€™d need to stretch it 4 times to get the same play speed yours would give by default. (Playing these patterns in Bacalao using b.p() or b.once() handles this for you, so it actually fits into a bar duration.)

Also, I donā€™t have any ā€œmarkerā€ key at the start of a pattern (to use with Pgate), which is a neat idea! Iā€™d be curious to see how you use it when live coding or composingā€¦I guess to make some longer timescale changes (latching on one new change per loop through the pattern).

Here is how I would write some of your examples using Bacalao:

// basic sequence
~p = [degree: "0 1 2 3"].pb.asStream;
~p.next(())

// sequence with rest
~p = [deg: "0 1 ~ 2"].pb.asStream; // note that you can abbreviate many keys (deg)
~p.next(())

// sequence with sub division
~p = [deg: "0 1 [2 3]"].pb.asStream;
~p.next(())

// sequence with nested sub divisions
~p = [deg: "0 1 [2 [3 4]]"].pb.asStream;
~p.next(())

// sequence with irregular sub division
~p = [deg: "0 1 [2 3 4]"].pb.asStream; // another way to write it
~p = "0 1 [2 3 4]".bparse(\degree).asStream; // another way to write it
~p.next(())

// sequence with sub division stretched (Bacalao uses '@' for stretching instead of '^')
~p = [deg: "0 1 [2 3 4]@2"].pb.asStream;
~p.next(())

// sequence with repeated value (we interpret this differently)
// For Bacalao, "0!4 1" would be the same as "0 0 0 0 1"...
~p = [deg: "0!4 1"].pb.asStream;
~p.next(())
// ...whereas "0*4 1" is "[0 0 0 0] 1", which is how you are using '!'
// (I'm using '*' for that, as TidalCycles does)
~p = [deg: "0*4 1"].pb.asStream;
~p.next(())
 
// sequence with shuffled values each cycle
//   ~p = Pbind(\degree, Pdv.parse("[0 1 2 3]$")) 
// (I don't have an equivalent in Bacalao pattern notation, but you can do this)
~p = [deg: "0 1 2 3"].pb.scramble.asStream;
~p.next(())

// sequence with randomly selected value
// (I don't have an equivalent to your '#' in pattern notation, but one way would be to
// choose from a environment variable holding an array)
~myNotes = [0, 1, 2, 3]; ~p = [deg: "myNotes:r 7"].pb.asStream;
// You can also just use regular SC code inside the pattern -- as
// long as there are no spaces in it!
~p = [deg: "Pwhite(0,3) 7"].pb.asStream;
~p.next(())

// sequence with alternating values - similar to ppatlace
~p = [deg: "0 <1 2>"].pb.asStream;
~p.next(())
  
// sequence with alternating values with grouping
//   ~p = Pbind(\degree, Pdv.parse("0 <1 [2 3]>")).asStream;
// (Hmm...my pattern parser isn't able to handle grouping
// within alternation right now ;-)

// use with midinote
~p = [midinote: "60 <62 63 64> <67 69>"].pb.asStream; // can also use 'mn' as short key
~p.next(())

If you use my Array.pb method, you create a Pbind: when there are multiple patterns (with durations), the rightmost one will override the durations for every other key. However, you can use my PtimeChain operator (Array.tc shortcut) to make the leftmost argument set the durations, similar to chaining in TidalCycles:

~p = [deg: "0 <1 2 3> 4 <7 8>", oct: "4@3 5", pan: "[-1 1]!2"].tc.asStream;
~p.next(())

Whereas <> is the normal chaining operator in SC, you can also use my time-chaining operator << (here Iā€™m also using the interpreter pre-processor so it looks more like TidalCycles):

Bacalao.start // start pre-processor that interprets deg"0 1" notation as [deg: "0 1"].pb

~p = (deg"0 <1 2 3> 4 <7 8>" << oct"4@3 5" << pan"[-1 1]!2").asStream;
~p.next(())

Note that my ā€œDSLā€ also supports ā€œcharacter patternsā€, where each character is an event (like a step sequencer), and spaces are rests. It allows things like this:

~p = "01 2__3 ".cparse(\degree)  // '_' extends/holds the previous note
// or, if the pre-processor is being used (note this uses single quotes)
~p = deg'01 2__3 '

Which are both equivalent to:

Pbind('degree', Pseq([ 0, 1, Rest(1), 2, 3, Rest(1) ]), 'dur', Pseq([ 0.125, 0.125, 0.125, 0.375, 0.125, 0.125 ]))

You can use any letters/numbers, and there is a way to tell it what Dictionary environment variable to use to lookup values from those characters. Also, for some things like frequency or amplitude, there is a ā€œreasonableā€ default mapping from characters ā€œa-zA-Z0-9ā€ to parameter values (for example, \amp ā€˜9ā€™ is loud, ā€˜0ā€™ is quiet), that can be overridden.

Anyhow, I just thought Iā€™d share a (not-so) different approach to writing patterns with durations.

2 Likes

This grew out of a previous function I had written that just used arrays as arrays i.e. no string parsing, e.g. [0, 1, [2, 3]].pdv - which was kind of inspired by the SequencableCollection convertRhythm method. Initially I was going to stick with arrays and extensions. So, including an alternating sequence would be like [0, 1, [2, 3, 4].alt].pdv but that became kind of annoying. Once I switched to string parsing, specifying alternating values really just came down to a choice between <> or (). The other operators $ ^ and # were more or less arbitrary. The ! operator was obvious for a repeat operator and I did look to see what tidalcycles used for rests so that is where the ~ came from. But it is quite interesting to end up with something so close to what you came up with even though I think we arrived there from pretty different starting points.

Bacalao looks super nice, btw

1 Like

I added support for a couple more operators

  • weighted choice among values
"[4 5 6]#(321)"

equivalent of

Pwrand([4,5 6], [3 2 1].normalizeSum, inf)
  • % chance a value is chosen or replaced by a rest - can be applied to value or group
"0%3 1%4 2%8"

is basically the equivalent of

Pseq([ 
    { if (0.3.coin) {0} {\} }, 
    { if (0.4.coin) {1} {\} }, 
    { if (0.9.coin) {2} {\} } 
], inf ).collect(_.value).asStream.nextN(8)

"[0 1 2]%5"

would be roughly equivalent to

(
var chance = 0.5;
Pseq( 
    [0, 1, 2], inf 
).collect({|val| if (chance.coin) {val} {\rest} }).asStream.nextN(8)
)

(there might be a terser way to write the equivalent with patterns but it hurts my brain to think through it)

Nice, those are useful additions to notation, without being too verbose.

Somewhat-related things in Bacalao (though not part of the pattern parser): Pattern.degrade method (eliminate some Events based on probability, basically the same as your % notation here) and Pattern.sometimesBy (based on a probability, replace an Event by one from a different stream, or modify an Event by a function). This was inspired by the rarely, sometimes, often and similar functions in TidalCycles.