-
-
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:
fn
is 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 betweenbake
andflatten
, yeah?s1 = s{0,3,6,9}/12 + 1
? usingbake
to flatten the/12
and then performing+1
on that?