Skip to content

Instantly share code, notes, and snippets.

@trentgill
Created November 14, 2021 05:01
Show Gist options
  • Save trentgill/bf930d4cb3f7f9e4a4c981432bf02356 to your computer and use it in GitHub Desktop.
Save trentgill/bf930d4cb3f7f9e4a4c981432bf02356 to your computer and use it in GitHub Desktop.
sequins_arithmetic
-- 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.
]]
@trentgill
Copy link
Author

trentgill commented Nov 15, 2021

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment