monome / crow

Crow speaks and listens and remembers bits of text. A scriptable USB-CV-II machine
GNU General Public License v3.0
166 stars 34 forks source link

Sequins transformers #449

Closed trentgill closed 1 year ago

trentgill commented 3 years ago

sequins transformer maps

this new feature for sequins allows a value transformation function to be attached to a sequins. whenever a sequins object is called, the value it produces is passed through the transformer function before being returned.

this enables rapid pattern permutation with the full power of lua, while the interface to produce that value is left untouched. it is another tool for describing a sequence of values that will be produced in the future.

usage is with the method :map which takes its name from the functional programming technique of 'mapping' a function over a collection. here we map our transformer over the output values of the sequins. one key difference is that our map transformation is a non-destructive effect that happens whenever a new value is taken from the sequins.

because a lua function can have non-determinism (eg. math.random()), this can create infinitely long sequences, or at least stochastic sequences.

transformers can also use sequins, which you pass in as additional arguments to :map. this gets into 'pattern synthesis' (thanks @dndrks) territory, enabling complex interactions to unfold, especially when using sequins of differing lengths.

while any lua action is allowed, we suggest using transformers exclusively to manipulate the data in a pure fashion. if you want each value to be sent to an IO (read: real-world) function, call the sequins in a timeline or event handler.

s = sequins

-- create a major scale where each note is randomly set to root octave, or up once octave
sfn = s{0,4,7,11}:map(function(n) return n + (math.random() > 0.5 and 12 or 0) end)

-- use a transformer to limit the range
sfn1 = s{0,4,7}:map(function(n) return n>5 and n-12 or n end)
sfn1() ... --> 0,4,-5,0 repeats

-- cyclically add octaves
sfn2 = s{0,3,6,9}:map(function(n, sq)
        return n + sq()
    end, s{-12,0,12})
sfn2() ... --> -12,3,18,-3,0,15,-6,9,-12 repeats

-- you can of course pre-define your functions for clarity
dropC = function(n) return n == 0 and -12 or n end -- drop 0 (middle-C) 1 octave
sfn3 = s{0,2,4,s{7,9}}:map(dropC)
sfn3() ... --> -12,2,4,7-12,2,4,9,-12 repeats

-- __ more examples here __

additionally there are shortcuts to do a single arithmetic operation, using lua's arithmetic operators. this can make transposition easy (+2), or conversion from notes-numbers to volts (/12).

you can of course apply another sequins as demonstrated above, but with a much tighter syntax. when applying other sequins, remember that the sequins on the left of the operator is the 'primary' sequins that captures the sequins (or literal) on the right hand side. this is true for literals too - the sequins must be on the left (so no, you can't negative a sequins directly, use a little function).

-- transpose up 2 semitones
sop1 = s{0,2,4,7,9} + 2
sop1() ... --> 2,4,6,9,11,2 repeats

-- convert notes to volts (great for `ii` functions that expect volts)
sop2 = s{0,3,0,2}/12
sop2() ... --> 0, (3/12), 0, (2/12), 0 repeats

-- now we can just use the sequins directly
ii.jf.play_note(sop2(), 2)

-- which is great with timeline, avoiding trivial anonymous functions
melody = s{0,7,s{2,4},9}/12
timeline.loop{ s{3,3,2}, {ii.jf.play_note, melody, 2.5} }

-- or we can use this on 2 sequins to generate complexity from simple elements
-- synthesize a pattern with 2 sequins
sop3 = s{0,7,s{2,4},9} + s{0,0,12,0,12}
sop3() ... --> this would be a nice long pattern in a major pentatonic style

baking sequins

when you start working with transformation maps, you will likely want to work iteratively, building up your transformations gradually. when you land on the right transformation, rather than copying the whole sequins & creating increasingly complex map functions, you can bake the current transformation into a new sequins.

the bake function returns a completely flat sequins (ie, no nested sequins). the default behaviour of :bake() with no arguments will create a new sequins of the same length as the original sequins (any nested sequins are treated as only 1 value).

if you want to capture variations built into your original sequins, you can capture an arbitrary number of values from your sequins with :bake(n) where n is the number of values in the newly baked sequins. this technique is very useful for flattening nested tables, or flattening tables with sequins-enabled transformations.

additionally, the :bake(n) method can be super useful for taking sequins of odd-lengths & forcing them into a repeating 8/16/32 (etc) step pattern. under the hood, :bake(n) is just calling your sequins n-times and making a sequins out of the result, so it's not linked to any of the more complex sequins behaviour.

TODO pull examples from https://gist.github.com/trentgill/bf930d4cb3f7f9e4a4c981432bf02356 and comments below.

trentgill commented 3 years ago

Just leaving a note here that while :settable() method is now working successfully across all datatypes, it needs to be refactored as it's a mess of nested loops, and i'm quite sure there is a far simpler recursive solution. as it stands there is a lot of 'how to copy' code mixed in with 'what are we copying' code.

i think it should just be a set of shallow if cases that ask whether the input is a sequins/flow-mod/transformer/literal/table. the only one that should do any marshalling is the table case as it's the most general & we don't know what to do with it.

trentgill commented 1 year ago

Merging now to get these into the main branch. All of the above is working well & could use some testing. It would be great to push the bake() method and see if it strains the system. In general i think this is a really powerful & useful set of utilities to create musical patterns on crow.