project name: | repl-looper |
project url: | https://github.com/awwaiid/repl-looper |
author: | awwaiid |
description: | mash of REPL and looper; a live performance code sequencer with web UI |
discussion url: | https://llllllll.co/t/51485 |
tags: | sequencer looper grid |
Anagogically integrated UI for Norns / Matron / Grid
Experimental performance and creative tool, mashing together several things that I like. REPL (Read-Eval-Print-Loop, a code execution console) for interactive code creating. Grid to have a tactile UI (maybe we’ll throw in a midi pedal too). Dance between sound-generating code, recording commands, looping, slicing, mixing, generating tools and patterns on the fly, and maybe make some self-modifying loops.
Check out the dev branch and dev branch development journal for ongoing development work. The main
branch is the current “stable” (haha) version.
all
helperloop:pan(-0.5)
to pan the whole looploop:noLoop()
to stop loopingloop:once()
to play onceloop:align(other_loop)
to slide events into alignmentloop:slice(s1)
now takes sample-variable instead of sample-variable-nameloop:split(other_loop)
to work more reliably and take a base-commandloop:merge(other_loop)
to keep relative timings (using the new loop:align
)ALL
to all
Install directly from maiden or by running this in the maiden/matron console:
;install https://github.com/awwaiid/repl-looper
Then start repl-looper
on the norns.
There are two ways to interact with repl-looper
– Directly on the norns with a USB keyboard, or with a web browser. Both ways work with a monome-grid (optional).
You can access the web interface at:
http://bit.ly/norns-repl-looper or http://norns.local/api/v1/dust/code/repl-looper/ui/dist/repl-looper.html
Both interfaces offer the same REPL functionality, but the web-UI adds some further visualization.
The workflow is an alternative to maiden’s REPL with a few different features and style. The basic idea is the same – you run Lua commands and see the results. Verify that everything is running with some simple math, type in:
2+2
Which should output 4
(give or take). While you can run any Lua commands you want, repl-looper
comes with a built-in engine and a bunch of tools. The built-in engine is a mash-up of Timber, Goldeneye, and Molly the Poly, along with some lua wrappers. Try these commands:
-- Timber piano shortcut
p'c'
p'd'
p(68)
molly:note(60)
molly:randomize()
molly:note(62)
molly:stop()
Press <tab>
for some completions. You can press <up>
or <down>
arrows to select previous commands or tab-completion choices. Press <enter>
without any command to run the previous command again.
Next thing to play with is loops/sequences. There are 8 pre-defined 16-step loops, one for each row, put into variables a
to h
. Start by recording into loop a
:
a:rec()
Loop a
is now playing and recording! You can see the current step and steps with recorded events on the grid. Now run:
p'c'
Wait a bit, and then run:
p'd'
The loop is playing and will wrap back around at 16 steps and keep recording, so if you add more events they stack up on top of each other. Now try:
a:stop() -- this stops recording but keeps playing
a:stop() -- stop again to also stop playing
At this point press some of the grid buttons to trigger all the events recorded at that step immediately (this ignores their sub-step timing). Here are a few more things to try:
a:show() -- show the loop contents
a:play() -- start loop again
a:clear() -- erase loop contents
Now that you have the script running on norns and the local UI up in a browser and everything working together, read through the Usage Reference below and Techniques to get more some ideas!
<up>
and <down>
arrows to select commands and output from history<enter>
to execute the most recent item from history (re-run command)<tab>
to see what functions/methods are available (tab-complete)p
instead of p()
s808.<tab>
and have shortcuts like CP
There are a lot of pre-defined variables, functions, and objects to make live-coding with repl-looper
convenient. These have been built organically based on usage, suggestions are welcome! In particular tab-complete-friendly prefixes for method names could be improved.
a
..h
– loops, one per grid-row
loops
is a table of all eight, so you can do all(loops):amp(0, 10)
to fade everything at oncep
– a Timber-based piano sample playerpiano
– the underlying piano sample (via Timber)
piano:reverse()
molly
– an instance of Molly, which wraps Molly The Poly
molly2
… molly8
– more pre-defined Mollysmollies
is a table of all eight, so you can do all(mollies):stop()
h:gen("molly:note(`60+m`)")
will put one molly-note on each step for a playable synth!s808.*
with these shortcuts
BD
, BD
, CH
, CY
, LC
, MC
, RS
, BS
, CL
, HC
, LT
, MT
, SD
, CB
, CP
, HT
, MA
, OH
CP
or CP()
or s808.CP:play()
s808.BS:amp(2)
h:gen(keys(s808))
will put one sample on each step for a playable drum kit!There are 8 pre-defined loops, a
, b
, c
, d
, e
, f
, g
, and h
. Each loops is assigned to a row on the grid. So a
is on the first row and h
is on the bottom row.
Here we use loop a
as the example, but you could run these commands on any loop.
myloop = Loop.new()
– Create a new Loop (I usually use a
..h
pre-defined loops instead)a.current_step
– current step (integer)a:setLength(steps)
– Set the number of steps (quarter-notes)a:show()
– Convert into a lua-style string and show ita:play()
– Start playing and loopinga:once()
– Play once but do not loopa:stop()
– Stop playing or recordinga:noLoop()
– Stop looping when we get to the enda:nextStep()
– Increment the current step to the next stepa:prevStep()
– Decrement the current step to the previous stepa:setStep(step)
– Set the current stepa:rec()
– Start recording. Commands run in the REPL get recorded at the current time. You can also trigger commands by pressing grid buttons.a:clear()
– Remove all eventsa:put(step, command)
– Assign a command at a specific step
a:gen(code_string, condition, offset)
– Generate events in a loop programmatically, with macro expansion. Very powerful!
a:gen("CH")
– puts the “CH” function on every stepa:gen("CH", 1/2)
– puts the “CH” on every half stepa:gen("CH", 2, 3)
– puts the “CH” on every other step starting with step 3a:gen("CH", { 1, 3, 4.5 })
– puts the “CH” on the given steps (even fractional)a:gen({"BD","SD","CP"})
– puts each one on each stepn
for the one-based step/column and m
for the zero-based step/columa:gen("p(`50+m`)")
– Generates p(50)
, p(51)
, p(52)
, and so on filling up all 16 stepsa:gen("piano:filterFreq(`100 * n`)")
– Generates low-pass for 100 hz, 200 hz, 300 hz, etcgen
for grid button-release events. This way you can start playing a note on button-down and stop the note on button-releasea:gen("molly:note(`60+m`)", "molly:offNote(`60+m`)")
– only for grid triggered events the “off” event will be executed on button releasea:quantize()
– Events are recorded at sub-steps by default; this immediately rounds them to the nearest stepa:clone(other_loop)
– Copy this loop onto another (empty the other out)a:merge(other_loop)
– Merge other_loop into this one. New loop is the longest of the two
other_loop
is cleareda:split(other_loop)
– Split the current loop, putting some events into other_loop
and keeping some
a:split(other_loop, base_cmd)
– You can pass in a base-command and it will try to keep things like that in the current loopa:slice(sample, step_offset, step_count, reverse)
– Take a Goldeneye/Timber sample and slice it
reverse
is true then start from the end of the samplea:align(other_loop)
– Slides events into alignment with another loop based on the current step of each; does not modify the loop otherwise
a
and loop b
are playing but their current-steps (playheads) are different, this will shift loop a
events and current-step so that they are lined upall(loops):align(a)
a:amp(level)
– Set the amp (0..1) for the whole loop; the included engine loops everything through the triggered loop!
a:amp(level, lag_time)
– Fade to the given volume over the given number of secondsa:amp(0, 10)
will fade out the whole loop over 10 secondsa:pan(amount)
– Pan the whole loop. Panning goes from -1 (left) to 1 (right), and 0 is centert1 = Timber.new("path/to/file.wav")
– Timber wrapper (works with .ogg too!)s1 = Sample.new("path/to/file.wav")
– Goldeneye wrapper (works with .ogg too!)m1 = Molly.new()
– Molly the Poly wrapper (you can have several!)
molly:randomize()
– Randomize the synth params!molly
and molly2..molly8
are pre-created for conveniencet1:<tab>
) to explore! There are wrapper methods that generally do what the engine does
molly
sample by typing molly:<tab>
Loop:slice(...)
method depend on these wrappers. Otherwise this is arbitrary lua code getting sequencedall
object wrapperall(mollies):stop()
– All the pre-created mollies are stored in mollies
, this stops them all!all(loops):amp(0, 10)
– All the pre-created loops are stored in loops
, this fades them all out!all{a,b,c}:play()
– Play all three loops, equivalent of a:play();b:play();c:play()
all{a,b,c}:play(1):amp(1, 10)
– Chained command that both plays and fades-in the loopsWhen developing you can run the Web-UI directly on your laptop, but you’ll need to use docker or install dependencies (nodejs/npm). You get live-reload of changes and such. Slightly-evil the dist
dir is then checked in to git for serving from the norns/maiden webserver. For the UI (a VueJS app):
cd ui
# Direct
npm install
npm run dev
# OR Docker, if you like
docker-compose up
On the norns side we need to run the lua server-side. During dev I do it this way:
# Auto-push to norns
./util/watch-sync.sh
a2
..h2
that span coluns 9..16 on grid