-
-
Save trentgill/bf930d4cb3f7f9e4a4c981432bf02356 to your computer and use it in GitHub Desktop.
| -- base sequins | |
| s1 = s{0,4,7} | |
| s1(),s1(),s1() --> 0, 4, 7 | |
| -- apply a pattern 'transformer' function to maybe add an octave | |
| s1:fn(function(n) return (n>5) and n or n+12 end) | |
| s1(),s1(),s1() --> 12, 16, 7 | |
| -- we can cancel the transformer with an empty fn | |
| s1:fn() | |
| s1(),s1(),s1() --> 0, 4, 7 | |
| --[[ motivation | |
| why do it? why not just apply the function at the consumer? | |
| i think the reason is for better encapsulation of concerns. | |
| when applying a transformer, you are thinking in terms of the data. | |
| it feels ideal to wrap that data-manipulation up with the data | |
| storage. it's very similar to mapping over an table in a non-destructive | |
| way. | |
| also means sequins can use values that the consumer won't understand | |
| natively. insead you can deal with values in an intermediate format | |
| (eg note-numbers like above) then wrap the output-conversion into | |
| the sequins itself. without this feature, we end up building trivial | |
| anonymous functions throughout our code. and we all know anon fns in | |
| lua are not exactly terse. | |
| ]] | |
| -- when using these transformations, imagine we want to capture the | |
| -- transform at a given moment (we found a nice algo). there are a few | |
| -- approaches to capturing it: | |
| -- 1) capture the transformer function | |
| mytransform = s1.fn -- can pass this around (shoutout ryan!) | |
| -- 2) copy the sequins (including the transformer) | |
| s2 = s1:copy() | |
| s1:fn() -- clear the transformer in the original | |
| -- s2 is now a duplicate sequins with the old transformer function | |
| -- 3) bake the transformer into a new sequins | |
| s2 = s1:bake() -- copies & executes the function per value (recursive) | |
| s1:fn() -- clear the transformer in the original | |
| -- query why we ever need to bake? | |
| -- my assumption is to incrementally approach pattern transformation | |
| -- you could just write a big function, but maybe you want to generate | |
| -- multiple different patterns iteratively... | |
| --[[ operator shortcut | |
| one of the main use cases in mind is the classic conversion from n-style | |
| note numbers to volts. this use case is so common that there's a shortcut | |
| available for using operator syntax over the sequins table directly. | |
| ]] | |
| -- division shortcut | |
| s1 = s{0,3,6,9}/12 | |
| -- equivalent to: | |
| s1 = s{0,3,6,9}:fn(function(n) return n/12 end) | |
| --[[ operator limitations | |
| due to lua's grammar, this syntax is only available when assigning | |
| (operators are expressions), so it's really just a shortcut at | |
| initialization. | |
| only a single operator can be applied, and any subsequent operations will | |
| override the previous one (perhaps with an error message?) | |
| ]] | |
| -- ERROR case | |
| s1 = s{0,3,6,9}/12 + 1 -- +1 will override /12 | |
| --> 1,4,7,10 -- note that the /12 is abandoned | |
| --[[ shortcut alternative | |
| because there's really only 1 use-case i'm thinking of for the operator | |
| shortcut, perhaps we should just have a pre-def'd function for notes-to-volts | |
| ironically, this is the classic (much derided) `n2v` function. | |
| ]] | |
| -- using n2v | |
| s1 = s{0,3,6,9}:fn(n2v) | |
| --[[ don't abandon ship yet! | |
| one of the cool things about operator syntax is the ability to do math | |
| between multiple sequins. especially when their lengths are unequal. | |
| take for example an octave-jumping modifier: | |
| ]] | |
| s1 = s{0,4,5,7,8} + s{0,0,12,0} | |
| -- equivalent to | |
| s1 = s{0,4,5,7,8}:fn( | |
| function(n,...) -- additional args to :fn are passed in as var args | |
| return n + {...}[1]() -- annoying unwrap of varargs | |
| end, s{0,0,12,0}) -- the sequins will be stored in the sequins | |
| -- above it's clear that the 2nd sequins will be captured inside the 1st's | |
| -- transformer function, and perhaps the explicit behaviour is a good thing. | |
| -- but there is something about the former that makes me want to generate | |
| -- a bunch of sequences with long lengths, then print out the good bits. | |
| -- that said, the 'equivalent' syntax is remarkably close to the clock | |
| -- libraries additional argument passing (so it's ugly but consistent). | |
| -- what if the :bake method took an argument for number of steps to capture | |
| s2 = s1:bake(32) -- duplicates s1 (temporarily) | |
| -- create a new empty sequins (return value) | |
| -- fill it up by calling the temp copy 32 times. | |
| -- so a pattern generator could look like | |
| patt = (s{0,7,2,9,4} + s{0,12}):bake(32) | |
| -- this alternate usage of :bake could potentially use a new name | |
| -- maybe 'flatten' makes more sense, or perhaps we could flip them? | |
| -- while the patterns aren't *that* complex, the truncation to a known | |
| -- number of elements leads to more rhythmically grounded patterns as well | |
| -- as a tertiary phasing quality. eg: the previous example repeats every | |
| -- 10 elements, but producing 32 vals leaves an additional 2 steps on the | |
| -- end that rapidly repeat afterward. | |
| -- often polyrhythmic patterns are fascinating, and hearing the patterns | |
| -- shift over each other is very rewarding, but after a prolonged time | |
| -- there can be a loss of a rhythmic centre which is negative (in some | |
| -- circumstances). having the ability to truncate to a fixed length leads | |
| -- to more controllable patterns. | |
| --[[ sequins in transformers | |
| speaking of the above usage of a sequins operation, it should be noted | |
| that in the regular function-style transformer config, you can fill the | |
| transformation with arbitrarily many sequins. it is just a regular old | |
| first-class function, which can close over state. | |
| but honestly once you start to get into complex multi-variate transformer | |
| functions, you probably want to be writing them as a separate chunk of | |
| lua, and just passing them in as functions to :fn. anything short of that | |
| will eventually just lead to much confusion. | |
| ]] |
@dndrks
thanks for the thoughts!
indeed easy cancellation of a transformer was focused on bc of your thoughts in that direction!
i'm excited about the :bake(n) method too! seems like a great way to rapidly generate musical ideas that aren't random, but quickly get more complicated that i can hold in my head.
there is a difference with copy and bake, specifically that a lone copy will duplicate the whole sequins (including the transformer function). you could do this to have 2 identical sequins that don't share an index. bake on the other hand destructively changes the data in the table (optionally returning a copy, as discussed above). so they are different, but i'm not really sure how they will get used, so it's hard to commit without thinking further ahead...
s1 = s{0,3,6,9}/12 + 1 could be implemented 2 different ways:
-- sequentially modify
s1 = s{0,3,6,9}/12
s1 = s1:bake()+1 -- awkward because you have to re-assign like this
-- you can of course do it inline, but i think it's ugly
s1 = (s{0,3,6,9}/12):bake()+1
-- i'd really suggest using an anonymous function for anything beyond a single step
s1 = s{0,3,6,9}:fn(function(n) return n/12 + 1 end)lastly, i'm considering changing :fn to :map as the behaviour is exactly mapping the function over the data (lazily). i appreciate that this reference is perhaps not known to new scripters, but i think encourage people to learn about functional programming techniques is a good thing. plus everyone loves the word map :)
s1 = s{0,3,6,9}:map(function(n) return n/12 + 1 end)also thinking about some fun stuff you can do with built-in functions:
s{1,2,3}:map(math.random)
--> math.random(1)
--> math.random(2)
--> math.random(3)
--> math.random(1)this line will generate a random number from 1 up to the sequins value. so it's a probability sequencer.
because any additional args to map are passed in as subsequent args, you can create an inverse range (random from sequins value up to max)
s{1,2,3}:map(math.random, 5)
--> math.random(1, 5)
--> math.random(2, 5)
--> math.random(3, 5)
--> math.random(1, 5)i'm sure there are many more standard functions (or crow/norns specific functions) that could very naturally consume sequins values. it would be a worthwhile (and fun) exercise to comb the function lists to identify other interesting fns to use out of the box.
daaaang, this is comprehensive + very very exciting @trentgill !!
some stuff:
fnis super-rad. i love being able to know that my base pattern is taken care of and that i'm free to imagine (and revoke) transformations on-the-fly.bake! the idea that i could get a printout of the weird algorithmic thing that's happening is very exciting -- i'm curious how it'd deal with non-terminating transformations? or maybe baking could accept a number of iterations to capture? AHAHAHA i just made my way down to line 115. ok, rad, yes, perfect use case!:copy()and:bake()(with no arguments) are the same thing, then i don't see a need to specify betweenbakeandflatten, yeah?s1 = s{0,3,6,9}/12 + 1? usingbaketo flatten the/12and then performing+1on that?