Skip to content

Instantly share code, notes, and snippets.

@trentgill
Created November 14, 2021 05:01
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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.
]]
@dndrks
Copy link

dndrks commented Nov 15, 2021

daaaang, this is comprehensive + very very exciting @trentgill !!

some stuff:

  • being able to cancel out a transformer with an empty 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.
  • i dig 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!
    • generally, if :copy() and :bake() (with no arguments) are the same thing, then i don't see a need to specify between bake and flatten, yeah?
  • so what would be the supported way to do s1 = s{0,3,6,9}/12 + 1 ? using bake to flatten the /12 and then performing +1 on that?

@trentgill
Copy link
Author

@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)

@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