monome / crow

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

Sequins.lua library #387

Closed trentgill closed 3 years ago

trentgill commented 3 years ago

Fixes #359

Introduces a new core library to crow for imbuing tables of data with 'behaviours'. Particularly focused on note sequences, but flexible enough to be useful for many things (ideas: building markov chains, event sequencing, generative arrangement).

--- reference
-- data can be anything a lua table can store
words = {'hi', 'there'}
notes = {2, 9, -4, 0}
fns = {lfo, pulse, ramp, ar}
tabs = { {2,3}, {'even', 'different', 'types'}, {4,5} }

seq = sequins{words} -- *copies* table into a sequins and stores in seq
seq() --> returns the values from the sequins ('hi', 'there', 'hi', ...)

tab2 = {1, 2, sequins{3, 4} } -- creates a table where one element is a sequins
seq2 = sequins{tab2} -- note how tab2 contains nested sequins which will be resolved at runtime
seq2_ = sequins{1, 2, sequins{3, 4} } -- these nested sequins can be written inline
--> calling seq2() returns 1, 2, 3, 1, 2, 4, 1 ... -- note how the nested sequins vals are interleaved

seq = sequins{...}:step(-1) -- change increment step
seq = sequins{...}:select(2) -- set the table index which will be used for the next call
seq:select(3)() --> after selecting, can call the result to get the value immediately

--- method chains
-- these fns modify the state of the sequins destructively
sequins{fns}
  :step(-2) -- change increment value per call. indexes are wrapped to the table length
  :select(5) -- sets the index to be selected on the next call. value is wrapped into range
--- the following method chains are mainly for use inside nested sequins
-- they manipulate behaviour of the parent sequins
  :every(2) -- only returns a value every 2nd call. if no value returned, parent sequins will try to get the next value
  :times(3) -- forces the parent sequins to ask it for the next 3 elements
  :count(10) -- will only provide values the first 10 times it's requested, then returns nil.
  :all() -- like times(#sequins) where the nested sequins will return all of it's values once
  :once() -- like count(1)

Some examples to demonstrate use-cases:

s = sequins

--- sequencing lydian arpeggios with a wholetone run flourish
lydian = {0,2,4,6,7,9,11}
wholet = {0,2,4,6,8,10}
melody = s{ s(lydian):all():step(3)
          , s(wholet):all():every(3)
          }
metro[1].event = melody
metro[1]:start()

--- using a sequins inside a method chain modifier so behaviour changes over time
arp = s(lydian):step( s{2,2,-2} ) -- lydian scale in thirds

--- arranger can be built such that a single function can be called to get 'next note'
-- while changing sequence is done asynchronously
mel1 = s{0,7,2}
mel2 = s{3,6,9,13}:step(-1) -- runs right to left
mel3 = s{3,-3}:select(2) -- starts from the 2nd element
arr = s{mel1, mel2, mel3}:step(0) -- step(0) means we'll always stay on the given melody
metro[2].event = function() output[1].volts = arr()/12 end -- install arrangement into a metro
metro[2]:start() -- every metro tick will get a new note
-- now we can modify the sequence asychronously
arr:select(2) -- switch to the second sequence
trentgill commented 3 years ago

Specifically I'd love feedback on naming & syntax. Use of every, count and times are all somewhat ambiguous but i'm also trying to keep them short so it's more readable.

The basic usage seems very clear & satisfying, but I'm not entirely sure I've approached the more complex ideas the best way. Having nested sequins is really powerful, I just want to make sure i'm providing the clearest interface into that complexity.

tehn commented 3 years ago

super exciting, i'll sit with it and let it sink in so i can give proper feedback on syntax.

fyi it looks like some public things got into this commit, not sure if you were trying to separate them

trentgill commented 3 years ago

thanks for the note re: public. i'd like to keep them separate for now, so i'll go do some git fu...

tehn commented 3 years ago

i really like the syntax-- that the simple things are straightforward and nesting allows for some weird meta-exploration.

a full tutorial/study on this would help people see the full potential. looking forward to playing!

rbrt-fm commented 3 years ago

i've been playing around with this for a bit this evening and it's super fun, but I've been getting a runtime error in druid when trying to use the select method repl:1:1 attempt to call a nil value (method 'select')

trentgill commented 3 years ago

@rbrt-fm i just pushed a new commit fixing the select method. lmk if it fixes your problem and i'd love to hear your thoughts on using the library!