Norns: combining scripts

I had the same problem adding the script via maiden. Try adding the folder directly via Cyberduck.

1 Like

ok that worked thanks :slight_smile:

2 Likes

Do combo scripts show in Maiden’s updater view? How are those suggestions “curated”?

I love everything about this stuff I just can’t piece it together for lack of brainzpower My current norns use case is more buddha machine-like perhaps. just pressing buttons for winding down. I’m so happy for it, and also just passively slow-learning stuff by browsing these threads is great.

Glad you got it working! The decimator engine was added to otis in an update, you may need to update your copy of Otis :slight_smile:

1 Like

Fantastic scripts thankyou!

1 Like

The updater view presents a list of scripts that have been added to a file in the https://github.com/monome/norns-community repo. There are a number of folks from lines who can add to that, including merge pull requests from other contributors. So if someone created a new script, or combo script, they should also create a pull request against the norns-community repo to expose their script in the updater.

Just curious if it’s possible to use say a looper script and a more effects based script at the same time?

I think I read that anything can be recorded to tape so I imagine if I can’t use multiple scripts I could just use an efx script on a recording from a looper script.

Just ordered a Norns shield and really excited. Loopers/samplers r a big part of my workflow and I think the Norns is gonna open some doors for me :hole:

the fugu/reels link you posted is down, any chance you could link it again? it’s not totally clear to me what should replace ‘— your script’ here. would be awesome to add reels to animator, fugu, etc

reels
function init()
   reels.init()
end

function key(n,z)
  if reels.active then
    reels:key(n,z)
  else
   -- your script
  end
end

function enc(n,d)
   if reels.active then
      reels:enc(n,d)
   else
   -- your script
   end
end

function redraw()
  reels:redraw()
end

also, thanks @Justmat for oats and earthbound! bummed i’m just finding these now, these are amazing

2 Likes

unfortunately i dont have it anymore.

to include reels into other scripts you need to modify script enc() and key() functions with if-else conditions from my example.

--your script - line where original function of script should be moved.

reels.init() and reels:redraw() should be just appended into corresponding functions

just getting around to this - works great. thank you! have been thinking about picking up a norns shield for this exact purpose. expands on the stock ‘tape’ functionality nicely. added reels to animator and showers, will get around to more and update here. quick audio snippet running showers, a field recording on reel 1 and me running my hands over a music easel keyboard into a short buffer on reel 2

edit - added a merged fugu, reels script to github

4 Likes

ooo that’s fun. I’ll have to try this out!

A nice mod might be to sync reels with animator’s clock and add a quantized record option. Would be a bit more work though. Could be fun for combining reels with any clock based script, actually

1 Like

Do you guys think that pedalboard and MLR could be combined? My idea is to color the input signal with the pedalboard script, and then use that signal as the incoming signal to MLR, I think it could be pretty sweet but not sure if it’s possible?

2 Likes

Technically, sure. As far as sound processing goes, Pedalboard doesn’t use Softcut at all, and MLR uses softcut exclusively. I think combining, or switching between, the two UIs would be the only real challenge.

3 Likes

I’m having some trouble getting mlr to work with a UI-less sequencer script I’ve thrown together based on the sequins demo script. The idea is to sequence eurorack via crow while also using mlr. The sequins is determined by a series of letters input into the script itself, so there’s no need for a UI at the moment. It works well on its own. But I’m not sure what’s going on with mlr. Does anyone have any ideas? Here’s the (working) crow sequencer script (which I’m calling lied) on its own:

lied script
--- Lied.


local MusicUtil = require "musicutil"
local TabUtil = require "tabutil"

-- TODO operate by reference so the scale can change without recreating the sequin
dim = {3,6,9,15,18,21,-6}
bass = {-6,-12,-15}
lyd = {12,14,16,18,19,9,11,24}
text = "C l o u d"

function init()
 anumbs = string.gsub(text, "a", 1)
 bnumbs = string.gsub(anumbs, "b", 2)
 cnumbs = string.gsub(bnumbs, "c", 3)
 dnumbs = string.gsub(cnumbs, "d", 4)
 enumbs = string.gsub(dnumbs, "e", 5)
 fnumbs = string.gsub(enumbs, "f", 6)
 gnumbs = string.gsub(fnumbs, "g", 7)
 hnumbs = string.gsub(gnumbs, "h", 8)
 inumbs = string.gsub(hnumbs, "i", 9)
 jnumbs = string.gsub(inumbs, "j", 10)
 knumbs = string.gsub(jnumbs, "k", 11)
 lnumbs = string.gsub(knumbs, "l", 12)
 mnumbs = string.gsub(lnumbs, "m", 13)
 nnumbs = string.gsub(mnumbs, "n", 14)
 onumbs = string.gsub(nnumbs, "o", 15)
 pnumbs = string.gsub(onumbs, "p", 16)
 qnumbs = string.gsub(pnumbs, "q", 17)
 rnumbs = string.gsub(qnumbs, "r", 18)
 snumbs = string.gsub(rnumbs, "s", 19)
 tnumbs = string.gsub(snumbs, "t", 20)
 unumbs = string.gsub(tnumbs, "u", 21)
 vnumbs = string.gsub(unumbs, "v", 22)
 wnumbs = string.gsub(vnumbs, "w", 23)
 xnumbs = string.gsub(wnumbs, "x", 24)
 ynumbs = string.gsub(xnumbs, "y", 25)
 znumbs = string.gsub(ynumbs, "z", 26)
 Anumbs = string.gsub(znumbs, "A", 27)
 Bnumbs = string.gsub(Anumbs, "B", 28)
 Cnumbs = string.gsub(Bnumbs, "C", 29)
 Dnumbs = string.gsub(Cnumbs, "D", 30)
 Enumbs = string.gsub(Dnumbs, "E", 31)
 Fnumbs = string.gsub(Enumbs, "F", 32)
 Gnumbs = string.gsub(Fnumbs, "G", 33)
 Hnumbs = string.gsub(Gnumbs, "H", 34)
 Inumbs = string.gsub(Hnumbs, "I", 35)
 Jnumbs = string.gsub(Inumbs, "J", 36)
 Knumbs = string.gsub(Jnumbs, "K", 37)
 Lnumbs = string.gsub(Knumbs, "L", 38)
 Mnumbs = string.gsub(Lnumbs, "M", 39)
 Nnumbs = string.gsub(Mnumbs, "N", 40)
 Onumbs = string.gsub(Nnumbs, "O", 41)
 Pnumbs = string.gsub(Onumbs, "P", 42)
 Qnumbs = string.gsub(Pnumbs, "Q", 43)
 Rnumbs = string.gsub(Qnumbs, "R", 44)
 Snumbs = string.gsub(Rnumbs, "S", 45)
 Tnumbs = string.gsub(Snumbs, "T", 46)
 Unumbs = string.gsub(Tnumbs, "U", 47)
 Vnumbs = string.gsub(Unumbs, "V", 48)
 Wnumbs = string.gsub(Vnumbs, "W", 49)
 Xnumbs = string.gsub(Wnumbs, "X", 50)
 Ynumbs = string.gsub(Xnumbs, "Y", 51)
 Znumbs = string.gsub(Ynumbs, "Z", 52)
 print(Znumbs)
 
 crow.input[1].mode('clock')  
 crow.output[2].action = "{to(5,0),to(0,0.025)}"
 crow.output[4].action = "{to(5,0),to(0,0.025)}"
 
function Split(s, delimiter)
 result = {};
 for match in (s..delimiter):gmatch("(.-)"..delimiter) do
   table.insert(result, match);
 end
 return result;
end

lied = Split(Znumbs, "%s")
print(lied)

 -- create our sequin which interleaves the playback of two tables of notes (from above)
 local mysequin =
   sequins.new( { sequins.new( lied, 'prev' )
          , sequins.count( 5, sequins.new( lied, 'next' ) )
          }
        , 'next' )
        
 -- iniates a timebase which will pull a note from the sequin on each step
 myarp = run_arp( play_note
                , mysequin
                , 11/5
                )
 myotherarp = run_arp( play_other_note
                     , mysequin
                     , 5/1)
mythirdarp = run_arp( play_with
                     , mysequin
                     , 4/1)
end
 
function play_note(v)
 --v = v + 48 -- shift up to good range
 local freq = v/12
 crow.output[1].volts = freq
 crow.output[1].scale( {1,3,5,6,8,10,11,13}, 12, 1.0 )
 crow.output[2].execute()
end

function play_other_note(v)
 --v = v + 48 -- shift up to good range
 local freq = v/12
 crow.output[3].volts = freq
 crow.output[3].scale( {1,3,5,6,8,10,11,13}, 12, 1.0 )
 crow.output[4].execute()
end

function play_with(v)
   local freq = v/12
   crow.ii.wdel.clock_ratio( 1, v )
end

function run_arp(fn,seq,sync)
 return clock.run(
   function()
     while true do
       clock.sync(sync)
       fn( seq() )
     end
   end)
end


--- TODO this is a new file called sequins.lua
sequins = {}

-- arg1: value table eg{0,2,4,7,9}
-- arg2: behaviour enum:{'next','prev',rand','drunk'} -- n as next#
-- retv: function that generates the next note
function sequins.new( vals, behaviour )
 local ix = 1

 local function generate()
   local newix = 0
   if behaviour == 'next' then
     newix = ix + 1
   elseif behaviour == 'prev' then
     newix = ix - 1
   elseif behaviour == 'rand' then
     newix = math.random(#vals)
   elseif behaviour == 'drunk' then
     newix = ix + math.random(-1,1)
   end
   -- clamp to vals table length
   while newix < 1 do newix = newix + #vals end
   while newix > #vals do newix = newix - #vals end

   local val = vals[newix]

   -- currently we explictly step the index forward in some cases but not others
   -- ideally this would be cleaned up & have a single return point
   if type(val) == 'function' then
     local v, action = val() -- allows nested tables
     if action == 'skip' then
       ix = newix -- nb: doesn't matter if it's before/after recursion
       return generate( vals, behaviour )
     elseif action == 'again' then
       -- ix stays the same!
     else -- was a nested table, but just returned a value (no action)
       ix = newix
     end
     return v
   else -- assume a number
     ix = newix
     return val
   end
 end

 return generate -- named function so we can use recursion inside
end

-- returns a value on everyth call. otherwise instructs the caller to skip it.
function sequins.every( everyth, sequin )
 local ev = everyth
 return
   function()
     ev = e%everyth +1
     if ev == 1 then
       return sequin()
     else
       return 0, 'skip'
     end
   end
end

-- returns count values in a row, stopping the calling sequin from moving forward until count is exhausted
function sequins.count( count, sequin )
 local cou = count
 return
   function()
     cou = cou%count +1
     if cou == 1 then
       print'match'
       return sequin()
     else
       return sequin(), 'again'
     end
   end
end

And here’s the (broken) combo script with mlr:

mlr lied combo script
-- mlr
-- v2.2.4 @tehn
-- llllllll.co/t/21145
--
-- /////////
-- ////
-- ////////////
-- //////////
-- ///////
-- /
-- ////
-- //
-- /////////
-- ///
-- /
--
-- ////
-- /
--
-- /
--
--
local MusicUtil = require "musicutil"
local TabUtil = require "tabutil"
text = "C l o u d"

function init()
  anumbs = string.gsub(text, "a", 1)
  bnumbs = string.gsub(anumbs, "b", 2)
  cnumbs = string.gsub(bnumbs, "c", 3)
  dnumbs = string.gsub(cnumbs, "d", 4)
  enumbs = string.gsub(dnumbs, "e", 5)
  fnumbs = string.gsub(enumbs, "f", 6)
  gnumbs = string.gsub(fnumbs, "g", 7)
  hnumbs = string.gsub(gnumbs, "h", 8)
  inumbs = string.gsub(hnumbs, "i", 9)
  jnumbs = string.gsub(inumbs, "j", 10)
  knumbs = string.gsub(jnumbs, "k", 11)
  lnumbs = string.gsub(knumbs, "l", 12)
  mnumbs = string.gsub(lnumbs, "m", 13)
  nnumbs = string.gsub(mnumbs, "n", 14)
  onumbs = string.gsub(nnumbs, "o", 15)
  pnumbs = string.gsub(onumbs, "p", 16)
  qnumbs = string.gsub(pnumbs, "q", 17)
  rnumbs = string.gsub(qnumbs, "r", 18)
  snumbs = string.gsub(rnumbs, "s", 19)
  tnumbs = string.gsub(snumbs, "t", 20)
  unumbs = string.gsub(tnumbs, "u", 21)
  vnumbs = string.gsub(unumbs, "v", 22)
  wnumbs = string.gsub(vnumbs, "w", 23)
  xnumbs = string.gsub(wnumbs, "x", 24)
  ynumbs = string.gsub(xnumbs, "y", 25)
  znumbs = string.gsub(ynumbs, "z", 26)
  Anumbs = string.gsub(znumbs, "A", 27)
  Bnumbs = string.gsub(Anumbs, "B", 28)
  Cnumbs = string.gsub(Bnumbs, "C", 29)
  Dnumbs = string.gsub(Cnumbs, "D", 30)
  Enumbs = string.gsub(Dnumbs, "E", 31)
  Fnumbs = string.gsub(Enumbs, "F", 32)
  Gnumbs = string.gsub(Fnumbs, "G", 33)
  Hnumbs = string.gsub(Gnumbs, "H", 34)
  Inumbs = string.gsub(Hnumbs, "I", 35)
  Jnumbs = string.gsub(Inumbs, "J", 36)
  Knumbs = string.gsub(Jnumbs, "K", 37)
  Lnumbs = string.gsub(Knumbs, "L", 38)
  Mnumbs = string.gsub(Lnumbs, "M", 39)
  Nnumbs = string.gsub(Mnumbs, "N", 40)
  Onumbs = string.gsub(Nnumbs, "O", 41)
  Pnumbs = string.gsub(Onumbs, "P", 42)
  Qnumbs = string.gsub(Pnumbs, "Q", 43)
  Rnumbs = string.gsub(Qnumbs, "R", 44)
  Snumbs = string.gsub(Rnumbs, "S", 45)
  Tnumbs = string.gsub(Snumbs, "T", 46)
  Unumbs = string.gsub(Tnumbs, "U", 47)
  Vnumbs = string.gsub(Unumbs, "V", 48)
  Wnumbs = string.gsub(Vnumbs, "W", 49)
  Xnumbs = string.gsub(Wnumbs, "X", 50)
  Ynumbs = string.gsub(Xnumbs, "Y", 51)
  Znumbs = string.gsub(Ynumbs, "Z", 52)
  print(Znumbs)
  
  crow.input[1].mode('clock')  
  crow.output[2].action = "{to(5,0),to(0,0.025)}"
  crow.output[4].action = "{to(5,0),to(0,0.025)}"
  
  function Split(s, delimiter)
  result = {};
  for match in (s..delimiter):gmatch("(.-)"..delimiter) do
    table.insert(result, match);
  end
  return result;
end

lied = Split(Znumbs, "%s")
print(lied)

  -- create our sequin which interleaves the playback of two tables of notes (from above)
  local mysequin =
    sequins.new( { sequins.new( lied, 'prev' )
           , sequins.count( 5, sequins.new( lied, 'next' ) )
           }
         , 'next' )
         
  -- iniates a timebase which will pull a note from the sequin on each step
  myarp = run_arp( play_note
                 , mysequin
                 , 11/5
                 )
  myotherarp = run_arp( play_other_note
                      , mysequin
                      , 5/1)
 mythirdarp = run_arp( play_with
                      , mysequin
                      , 4/1)
end
  
function play_note(v)
  --v = v + 48 -- shift up to good range
  local freq = v/12
  crow.output[1].volts = freq
  crow.output[1].scale( {1,3,5,6,8,10,11,13}, 12, 1.0 )
  crow.output[2].execute()
end

function play_other_note(v)
  --v = v + 48 -- shift up to good range
  local freq = v/12
  crow.output[3].volts = freq
  crow.output[3].scale( {1,3,5,6,8,10,11,13}, 12, 1.0 )
  crow.output[4].execute()
end

function play_with(v)
    local freq = v/12
    crow.ii.wdel.clock_ratio( 1, v )
end

function run_arp(fn,seq,sync)
  return clock.run(
    function()
      while true do
        clock.sync(sync)
        fn( seq() )
      end
    end)
end


--- TODO this is a new file called sequins.lua
sequins = {}

-- arg1: value table eg{0,2,4,7,9}
-- arg2: behaviour enum:{'next','prev',rand','drunk'} -- n as next#
-- retv: function that generates the next note
function sequins.new( vals, behaviour )
  local ix = 1

  local function generate()
    local newix = 0
    if behaviour == 'next' then
      newix = ix + 1
    elseif behaviour == 'prev' then
      newix = ix - 1
    elseif behaviour == 'rand' then
      newix = math.random(#vals)
    elseif behaviour == 'drunk' then
      newix = ix + math.random(-1,1)
    end
    -- clamp to vals table length
    while newix < 1 do newix = newix + #vals end
    while newix > #vals do newix = newix - #vals end

    local val = vals[newix]

    -- currently we explictly step the index forward in some cases but not others
    -- ideally this would be cleaned up & have a single return point
    if type(val) == 'function' then
      local v, action = val() -- allows nested tables
      if action == 'skip' then
        ix = newix -- nb: doesn't matter if it's before/after recursion
        return generate( vals, behaviour )
      elseif action == 'again' then
        -- ix stays the same!
      else -- was a nested table, but just returned a value (no action)
        ix = newix
      end
      return v
    else -- assume a number
      ix = newix
      return val
    end
  end

  return generate -- named function so we can use recursion inside
end

-- returns a value on everyth call. otherwise instructs the caller to skip it.
function sequins.every( everyth, sequin )
  local ev = everyth
  return
    function()
      ev = e%everyth +1
      if ev == 1 then
        return sequin()
      else
        return 0, 'skip'
      end
    end
end

-- returns count values in a row, stopping the calling sequin from moving forward until count is exhausted
function sequins.count( count, sequin )
  local cou = count
  return
    function()
      cou = cou%count +1
      if cou == 1 then
        print'match'
        return sequin()
      else
        return sequin(), 'again'
      end
    end
end

end

local g = grid.connect()

local fileselect = require 'fileselect'
local textentry = require 'textentry'
local pattern_time = require 'pattern_time'

local TRACKS = 6
local FADE = 0.01

-- softcut has ~350s per buffer
local CLIP_LEN_SEC = 45
local MAX_CLIPS = 7

local vREC = 1
local vCUT = 2
local vCLIP = 3
local vTIME = 15

-- events
local eCUT = 1
local eSTOP = 2
local eSTART = 3
local eLOOP = 4
local eSPEED = 5
local eREV = 6
local ePATTERN = 7

local quantize = 0

local quantizer

local function update_tempo()
  local t = params:get("clock_tempo")
  local d = params:get("quant_div")
  local interval = (60/t) / d
  print("q > "..interval)
  quantizer.time = interval
  for i=1,TRACKS do
    if track[i].tempo_map == 1 then
      update_rate(i)
    end
  end
end

local prev_tempo = params:get("clock_tempo")
function clock_update_tempo ()
  while true do
    clock.sync(1/24)
    local curr_tempo = params:get("clock_tempo")
    if prev_tempo ~= curr_tempo then
      prev_tempo = curr_tempo
      update_tempo()
    end
  end
end

function event_record(e)
  for i=1,4 do
    pattern[i]:watch(e)
  end
  recall_watch(e)
end


function event(e)
  if quantize == 1 then
    event_q(e)
  else
    if e.t ~= ePATTERN then event_record(e) end
    event_exec(e)
  end
end

local quantize_events = {}

function event_q(e)
  table.insert(quantize_events,e)
end

function event_q_clock()
  if #quantize_events > 0 then
    for k,e in pairs(quantize_events) do
      if e.t ~= ePATTERN then event_record(e) end
      event_exec(e)
    end
    quantize_events = {}
  end
end


function event_exec(e)
  if e.t==eCUT then
    if track[e.i].loop == 1 then
      track[e.i].loop = 0
      softcut.loop_start(e.i,clip[track[e.i].clip].s)
      softcut.loop_end(e.i,clip[track[e.i].clip].e)
    end
    local cut = (e.pos/16)*clip[track[e.i].clip].l + clip[track[e.i].clip].s
    softcut.position(e.i,cut)
    --softcut.reset(e.i)
    if track[e.i].play == 0 then
      track[e.i].play = 1
      ch_toggle(e.i,1)
    end
  elseif e.t==eSTOP then
    track[e.i].play = 0
    track[e.i].pos_grid = -1
    ch_toggle(e.i,0)
    dirtygrid=true
  elseif e.t==eSTART then
    track[e.i].play = 1
    ch_toggle(e.i,1)
    dirtygrid=true
  elseif e.t==eLOOP then
    track[e.i].loop = 1
    track[e.i].loop_start = e.loop_start
    track[e.i].loop_end = e.loop_end
    --print("LOOP "..track[e.i].loop_start.." "..track[e.i].loop_end)
    local lstart = clip[track[e.i].clip].s + (track[e.i].loop_start-1)/16*clip[track[e.i].clip].l
    local lend = clip[track[e.i].clip].s + (track[e.i].loop_end)/16*clip[track[e.i].clip].l
    --print(">>>> "..lstart.." "..lend)
    softcut.loop_start(e.i,lstart)
    softcut.loop_end(e.i,lend)
    if view == vCUT then dirtygrid=true end
  elseif e.t==eSPEED then
    track[e.i].speed = e.speed
    update_rate(e.i)
    --n = math.pow(2,track[e.i].speed + params:get("speed_mod"..e.i))
    --if track[e.i].rev == 1 then n = -n end
    --engine.rate(e.i,n)
    if view == vREC then dirtygrid=true end
  elseif e.t==eREV then
    track[e.i].rev = e.rev
    update_rate(e.i)
    --n = math.pow(2,track[e.i].speed + params:get("speed_mod"..e.i))
    --if track[e.i].rev == 1 then n = -n end
    --engine.rate(e.i,n)
    if view == vREC then dirtygrid=true end
  elseif e.t==ePATTERN then
    if e.action=="stop" then pattern[e.i]:stop()
    elseif e.action=="start" then pattern[e.i]:start()
    elseif e.action=="rec_stop" then pattern[e.i]:rec_stop()
    elseif e.action=="rec_start" then pattern[e.i]:rec_start()
    elseif e.action=="clear" then pattern[e.i]:clear()
    end
  end
end

------ patterns
pattern = {}
for i=1,4 do
  pattern[i] = pattern_time.new()
  pattern[i].process = event_exec
end

------ recalls
recall = {}
for i=1,4 do
  recall[i] = {}
  recall[i].recording = false
  recall[i].has_data = false
  recall[i].active = false
  recall[i].event = {}
end

function recall_watch(e)
  for i=1,4 do
    if recall[i].recording == true then
      --print("recall: event rec")
      table.insert(recall[i].event, e)
      recall[i].has_data = true
    end
  end
end

function recall_exec(i)
  for _,e in pairs(recall[i].event) do
    event_exec(e)
  end
end

view = vREC
view_prev = view

v = {}
v.key = {}
v.enc = {}
v.redraw = {}
v.gridkey = {}
v.gridredraw = {}

viewinfo = {}
viewinfo[vREC] = 1
viewinfo[vCUT] = 0
viewinfo[vTIME] = 0

focus = 1
alt = 0

track = {}
for i=1,TRACKS do
  track[i] = {}
  track[i].head = (i-1)%4+1
  track[i].play = 0
  track[i].rec = 0
  track[i].rec_level = 1
  track[i].pre_level = 0
  track[i].loop = 0
  track[i].loop_start = 0
  track[i].loop_end = 16
  track[i].clip = i
  track[i].pos = 0
  track[i].pos_grid = -1
  track[i].speed = 0
  track[i].rev = 0
  track[i].tempo_map = 0
end


set_clip_length = function(i, len)
  clip[i].l = len
  clip[i].e = clip[i].s + len
  local bpm = 60 / len
  while bpm < 60 do
    bpm = bpm * 2
    print("bpm > "..bpm)
  end
  clip[i].bpm = bpm
end

clip_reset = function(i, length)
  set_clip_length(i, length)
  clip[i].name = "-"
end

clip = {}
for i=1,16 do
  clip[i] = {}
  clip[i].s = 2 + (i-1)*CLIP_LEN_SEC
  clip[i].name = "-"
  set_clip_length(i,4)
end



calc_quant = function(i)
  local q = (clip[track[i].clip].l/16)
  print("q > "..q)
  return q
end

calc_quant_off = function(i, q)
  local off = q
  while off < clip[track[i].clip].s do
    off = off + q
  end
  off = off - clip[track[i].clip].s
  print("off > "..off)
  return off
end

set_clip = function(i, x)
  --track[i].play = 0
  --ch_toggle(i,0)
  track[i].clip = x
  softcut.loop_start(i,clip[track[i].clip].s)
  softcut.loop_end(i,clip[track[i].clip].e)
  local q = calc_quant(i)
  local off = calc_quant_off(i, q)
  softcut.phase_quant(i,q)
  softcut.phase_offset(i,off)
  --track[i].loop = 0
end

set_rec = function(n)
  if track[n].rec == 1 then
    softcut.pre_level(n,track[n].pre_level)
    softcut.rec_level(n,track[n].rec_level)
  else
    softcut.pre_level(n,1)
    softcut.rec_level(n,0)
  end
end

held = {}
heldmax = {}
done = {}
first = {}
second = {}
for i = 1,8 do
  held[i] = 0
  heldmax[i] = 0
  done[i] = 0
  first[i] = 0
  second[i] = 0
end


key = function(n,z) _key(n,z) end
enc = function(n,d)
  if n==1 then params:delta("output_level",d)
  else _enc(n,d) end
end
redraw = function() _redraw() end
g.key = function(x,y,z) _gridkey(x,y,z) end

set_view = function(x)
  --print("set view: "..x)
  if x == -1 then x = view_prev end
  view_prev = view
  view = x
  _key = v.key[x]
  _enc = v.enc[x]
  _redraw = v.redraw[x]
  _gridkey = v.gridkey[x]
  _gridredraw = v.gridredraw[x]
  redraw()
  dirtygrid=true
end

gridredraw = function()
  if not g then return end
  if dirtygrid == true then
    _gridredraw()
    dirtygrid = false
  end
end


function ch_toggle(i,x)
  softcut.play(i,x)
  softcut.rec(i,x)
end


UP1 = controlspec.new(0, 1, 'lin', 0, 1, "")
UP0 = controlspec.new(0, 1, 'lin', 0, 0, "")
cs_PAN = controlspec.new(-1, 1, 'lin', 0, 0, "")
BI1 = controlspec.new(-1, 1, 'lin', 0, 0, "")

-------------------- init
init = function()
  params:set_action("clock_tempo", function() update_tempo() end)
  params:add_number("quant_div", "quant div", 1, 32, 4)
  params:set_action("quant_div",function() update_tempo() end)

  p = {}

	audio.level_cut(1)
	audio.level_adc_cut(1)

  for i=1,TRACKS do
    params:add_separator()

    softcut.enable(i,1)

  	softcut.level_input_cut(1, i, 1.0)
  	softcut.level_input_cut(2, i, 1.0)

    softcut.play(i,0)
    softcut.rec(i,0)

    softcut.level(i,1)
    softcut.pan(i,0)
    softcut.buffer(i,1)

    softcut.pre_level(i,1)
    softcut.rec_level(i,0)

    softcut.fade_time(i,FADE)
    softcut.level_slew_time(i,0.1)
    softcut.rate_slew_time(i,0)

    softcut.loop_start(i,clip[track[i].clip].s)
    softcut.loop_end(i,clip[track[i].clip].e)
    softcut.loop(i,1)
    softcut.position(i, clip[track[i].clip].s)

    params:add_control(i.."vol", i.."vol", UP1)
    params:set_action(i.."vol", function(x) softcut.level(i,x) end)
    params:add_control(i.."pan", i.."pan", cs_PAN)
    params:set_action(i.."pan", function(x) softcut.pan(i,x) end)
    params:add_control(i.."rec", i.."rec", UP1)
    params:set_action(i.."rec",
      function(x)
        track[i].rec_level = x
        set_rec(i)
      end)
    params:add_control(i.."pre", i.."pre", controlspec.UNIPOLAR)
    params:set_action(i.."pre",
      function(x)
        track[i].pre_level = x
        set_rec(i)
      end)
    params:add_control(i.."speed_mod", i.."speed_mod", controlspec.BIPOLAR)
    params:set_action(i.."speed_mod", function() update_rate(i) end)

    params:add_control(i.."rate_slew", i.."rate_slew", UP0)
    params:set_action(i.."rate_slew", function(x) softcut.rate_slew_time(i,x) end)

    params:add_control(i.."level_slew", i.."level_slew", controlspec.new(0.0,10.0,"lin",0.1,0.1,""))
    params:set_action(i.."level_slew", function(x) softcut.level_slew_time(i,x) end)
    params:add_file(i.."file", i.."file", "")
    params:set_action(i.."file",
      --function(n) print("FILESELECT > "..i.." "..n) end)
      function(n) fileselect_callback(n,i) end)

    update_rate(i)
    set_clip(i,i)
    --softcut.phase_quant(i,calc_quant(i))
  end

  quantizer = metro.init()
  quantizer.time = 0.125
  quantizer.count = -1
  quantizer.event = event_q_clock
  quantizer:start()
  --pattern_init()
  set_view(vREC)

  update_tempo()

  gridredrawtimer = metro.init(function() gridredraw() end, 0.02, -1)
  gridredrawtimer:start()
  dirtygrid = true

  grid.add = draw_grid_connected

  screenredrawtimer = metro.init(function() redraw() end, 0.1, -1)
  screenredrawtimer:start()

  params:bang()

  softcut.event_phase(phase)
  softcut.poll_start_phase()

  clock.run(clock_update_tempo)
end

-- poll callback
phase = function(n, x)
  --if n == 1 then print(x) end
  local pp = ((x - clip[track[n].clip].s) / clip[track[n].clip].l)-- * 16 --TODO 16=div
  --x = math.floor(track[n].pos*16)
  --if n==1 then print("> "..x.." "..pp) end
  x = math.floor(pp * 16)
  if x ~= track[n].pos_grid then
    track[n].pos_grid = x
    if view == vCUT then dirtygrid=true end
  end
end



update_rate = function(i)
  local n = math.pow(2,track[i].speed + params:get(i.."speed_mod"))
  if track[i].rev == 1 then n = -n end
  if track[i].tempo_map == 1 then
    local bpmmod = params:get("clock_tempo") / clip[track[i].clip].bpm
    --print("bpmmod: "..bpmmod)
    n = n * bpmmod
  end
  softcut.rate(i,n)
end



gridkey_nav = function(x,z)
  if z==1 then
    if x==1 then
      if alt == 1 then softcut.buffer_clear() end
      set_view(vREC)
    elseif x==2 then set_view(vCUT)
    elseif x==3 then set_view(vCLIP)
    elseif x>4 and x<9 then
      local i = x - 4
      if alt == 1 then
        local e={t=ePATTERN,i=i,action="rec_stop"} event(e)
        local e={t=ePATTERN,i=i,action="stop"} event(e)
        local e={t=ePATTERN,i=i,action="clear"} event(e)
      elseif pattern[i].rec == 1 then
        local e={t=ePATTERN,i=i,action="rec_stop"} event(e)
        local e={t=ePATTERN,i=i,action="start"} event(e)
      elseif pattern[i].count == 0 then
        local e={t=ePATTERN,i=i,action="rec_start"} event(e)
      elseif pattern[i].play == 1 then
        local e={t=ePATTERN,i=i,action="stop"} event(e)
      else
        local e={t=ePATTERN,i=i,action="start"} event(e)
      end
    elseif x>8 and x<13 then
      local i = x-8
      if alt == 1 then
        --print("recall: clear "..i)
        recall[i].event = {}
        recall[i].recording = false
        recall[i].has_data = false
        recall[i].active = false
      elseif recall[i].recording == true then
        --print("recall: stop")
        recall[i].recording = false
      elseif recall[i].has_data == false then
        --print("recall: rec")
        recall[i].recording = true
      elseif recall[i].has_data == true then
        --print("recall: exec")
        recall_exec(i)
        recall[i].active = true
      end
    elseif x==15 and alt == 0 then
      quantize = 1 - quantize
      if quantize == 0 then quantizer:stop()
      else quantizer:start()
      end
    elseif x==15 and alt == 1 then
      set_view(vTIME)
    elseif x==16 then alt = 1
    end
  elseif z==0 then
    if x==16 then alt = 0
    elseif x==15 and view == vTIME then set_view(-1)
    elseif x>8 and x<13 then recall[x-8].active = false end
  end
  dirtygrid=true
end

gridredraw_nav = function()
  -- indicate view
  g:led(view,1,15)
  if alt==1 then g:led(16,1,9) end
  if quantize==1 then g:led(15,1,9) end
  for i=1,4 do
    -- patterns
    if pattern[i].rec == 1 then g:led(i+4,1,15)
    elseif pattern[i].play == 1 then g:led(i+4,1,9)
    elseif pattern[i].count > 0 then g:led(i+4,1,5)
    else g:led(i+4,1,3) end
    -- recalls
    local b = 2
    if recall[i].recording == true then b=15
    elseif recall[i].active == true then b=11
    elseif recall[i].has_data == true then b=5 end
    g:led(i+8,1,b)
  end
end

-------------------- REC
v.key[vREC] = function(n,z)
  if n==2 and z==1 then
    viewinfo[vREC] = 1 - viewinfo[vREC]
    redraw()
  end
end

v.enc[vREC] = function(n,d)
  if viewinfo[vREC] == 0 then
    if n==2 then
      params:delta(focus.."vol",d)
    elseif n==3 then
      params:delta(focus.."speed_mod",d)
    end
  else
    if n==2 then
      params:delta(focus.."rec",d)
    elseif n==3 then
      params:delta(focus.."pre",d)
    end
  end
  redraw()
end

v.redraw[vREC] = function()
  screen.clear()
  screen.level(15)
  screen.move(10,16)
  screen.text("REC > "..focus)
  local sel = viewinfo[vREC] == 0

  screen.level(sel and 15 or 4)
  screen.move(10,32)
  screen.text(params:string(focus.."vol"))
  screen.move(70,32)
  screen.text(params:string(focus.."speed_mod"))
  screen.level(3)
  screen.move(10,40)
  screen.text("volume")
  screen.move(70,40)
  screen.text("speed mod")

  screen.level(not sel and 15 or 4)
  screen.move(10,52)
  screen.text(params:string(focus.."rec"))
  screen.move(70,52)
  screen.text(params:string(focus.."pre"))
  screen.level(3)
  screen.move(10,60)
  screen.text("rec level")
  screen.move(70,60)
  screen.text("overdub")

  screen.update()
end

v.gridkey[vREC] = function(x, y, z)
  if y == 1 then gridkey_nav(x,z)
  else
    if z == 1 then
      i = y-1
      if x>2 and x<8 then
        if alt == 1 then
          track[i].tempo_map = 1 - track[i].tempo_map
          update_rate(i)
        elseif focus ~= i then
          focus = i
          redraw()
        end
      elseif x==1 and y<TRACKS+2 then
        track[i].rec = 1 - track[i].rec
        print("REC "..track[i].rec)
        set_rec(i)
      elseif x==16 and y<TRACKS+2 then
        if track[i].play == 1 then
          e = {}
          e.t = eSTOP
          e.i = i
          event(e)
        else
          e = {}
          e.t = eSTART
          e.i = i
          event(e)
        end
      elseif x>8 and x<16 and y<TRACKS+2 then
        local n = x-12
        e = {} e.t = eSPEED e.i = i e.speed = n
        event(e)
      elseif x==8 and y<TRACKS+2 then
        local n = 1 - track[i].rev
        e = {} e.t = eREV e.i = i e.rev = n
        event(e)
      end
      dirtygrid=true
    end
  end
end

v.gridredraw[vREC] = function()
  g:all(0)
  g:led(3,focus+1,7)
  g:led(4,focus+1,7)
  for i=1,TRACKS do
    local y = i+1
    g:led(1,y,3)--rec
    if track[i].rec == 1 then g:led(1,y,9) end
    if track[i].tempo_map == 1 then g:led(5,y,7) end -- tempo.map
    g:led(8,y,3)--rev
    g:led(16,y,3)--stop
    g:led(12,y,3)--speed=1
    g:led(12+track[i].speed,y,9)
    if track[i].rev == 1 then g:led(8,y,7) end
    if track[i].play == 1 then g:led(16,y,15) end
  end
  gridredraw_nav()
  g:refresh();
end

--------------------CUT
v.key[vCUT] = function(n,z)
  print("CUT key")
end

v.enc[vCUT] = function(n,d)
  if n==2 then
    params:delta(focus.."vol",d)
  end
  redraw()
end

v.redraw[vCUT] = function()
  screen.clear()
  screen.level(15)
  screen.move(10,16)
  screen.text("CUT > "..focus)
  if viewinfo[vCUT] == 0 then
    screen.move(10,32)
    screen.text(params:string(focus.."vol"))
    --screen.move(70,50)
    --screen.text(params:get("loop_mod"..focus))
    screen.level(3)
    screen.move(10,40)
    screen.text("volume")
    --screen.move(70,60)
    --screen.text("speed mod")
  else
    screen.move(10,50)
    screen.text(params:get(focus.."rec"))
    screen.move(70,50)
    screen.text(params:get(focus.."pre"))
    screen.level(3)
    screen.move(10,60)
    screen.text("rec level")
    screen.move(70,60)
    screen.text("overdub")
  end
  screen.update()
end

v.gridkey[vCUT] = function(x, y, z)
  if z==1 and held[y] then heldmax[y] = 0 end
  held[y] = held[y] + (z*2-1)
  if held[y] > heldmax[y] then heldmax[y] = held[y] end
  --print(held[y])

  if y == 1 then gridkey_nav(x,z)
  elseif y == 8 then return
  else
    i = y-1
    if z == 1 then
      if focus ~= i then
        focus = i
        redraw()
      end
      if alt == 1 and y<TRACKS+2 then
        if track[i].play == 1 then
          e = {} e.t = eSTOP e.i = i
        else
          e = {} e.t = eSTART e.i = i
        end
        event(e)
      elseif y<TRACKS+2 and held[y]==1 then
        first[y] = x
        local cut = x-1
        --print("pos > "..cut)
        e = {} e.t = eCUT e.i = i e.pos = cut
        event(e)
      elseif y<TRACKS+2 and held[y]==2 then
        second[y] = x
      end
    elseif z==0 then
      if y<TRACKS+2 and held[y] == 1 and heldmax[y]==2 then
        e = {}
        e.t = eLOOP
        e.i = i
        e.loop = 1
        e.loop_start = math.min(first[y],second[y])
        e.loop_end = math.max(first[y],second[y])
        event(e)
      end
    end
  end
end

v.gridredraw[vCUT] = function()
  g:all(0)
  gridredraw_nav()
  for i=1,TRACKS do
    if track[i].loop == 1 then
      for x=track[i].loop_start,track[i].loop_end do
        g:led(x,i+1,4)
      end
    end
    if track[i].play == 1 then
      g:led((track[i].pos_grid+1)%16, i+1, 15)
    end
  end
  g:refresh();
end



--------------------CLIP

clip_actions = {"load","clear","save"}
clip_action = 1
clip_sel = 1
clip_clear_mult = 3

function fileselect_callback(path, c)
  print("FILESELECT "..c)
  if path ~= "cancel" and path ~= "" then
    local ch, len = audio.file_info(path)
    if ch > 0 and len > 0 then
      print("file > "..path.." "..clip[track[c].clip].s)
      print("file length > "..len/48000)
      --softcut.buffer_read_mono(path, 0, clip[track[clip_sel].clip].s, len/48000, 1, 1)
      softcut.buffer_read_mono(path, 0, clip[track[c].clip].s, CLIP_LEN_SEC, 1, 1)
      local l = math.min(len/48000, CLIP_LEN_SEC)
      set_clip_length(track[c].clip, l)
      clip[track[c].clip].name = path:match("[^/]*$")
      -- TODO: STRIP extension
      set_clip(c,track[c].clip)
      update_rate(c)
      params:set(c.."file",path)
    else
      print("not a sound file")
    end

    -- TODO re-set_clip any tracks with this clip loaded
    screenredrawtimer:start()
    redraw()
  end
end

function textentry_callback(txt)
  if txt then
    local c_start = clip[track[clip_sel].clip].s
    local c_len = clip[track[clip_sel].clip].l
    print("SAVE " .. _path.audio .. "mlr/" .. txt .. ".wav", c_start, c_len)
    util.make_dir(_path.audio .. "mlr")
    softcut.buffer_write_mono(_path.audio.."mlr/"..txt..".wav",c_start,c_len,1)
    clip[track[clip_sel].clip].name = txt
  else
    print("save cancel")
  end
  screenredrawtimer:start()
  redraw()
end

v.key[vCLIP] = function(n,z)
  if n==2 and z==0 then
    if clip_actions[clip_action] == "load" then
      screenredrawtimer:stop()
      fileselect.enter(os.getenv("HOME").."/dust/audio",
        function(n) fileselect_callback(n,clip_sel) end)
    elseif clip_actions[clip_action] == "clear" then
      local c_start = clip[track[clip_sel].clip].s * 48000
      print("clear_start: " .. c_start)
      --softcut.clear_range(c_start, CLIP_LEN_SEC * 48000) -- two minutes
      clip[track[clip_sel].clip].name = '-'
      redraw()
    elseif clip_actions[clip_action] == "save" then
      screenredrawtimer:stop()
      textentry.enter(textentry_callback, "mlr-" .. (math.random(9000)+1000))
    end
  elseif n==3 and z==1 then
    clip_reset(clip_sel,60/params:get("clock_tempo")*(2^(clip_clear_mult-2)))
    set_clip(clip_sel,track[clip_sel].clip)
    update_rate(clip_sel)
  end
end

v.enc[vCLIP] = function(n,d)
  if n==2 then
    clip_action = util.clamp(clip_action + d, 1, 3)
  elseif n==3 then
    clip_clear_mult = util.clamp(clip_clear_mult+d,1,6)
  end
  redraw()
  dirtygrid=true
end

local function truncateMiddle (str, maxLength, separator)
  maxLength = maxLength or 30
  separator = separator or "..."

  if (maxLength < 1) then return str end
  if (string.len(str) <= maxLength) then return str end
  if (maxLength == 1) then return string.sub(str, 1, 1) .. separator end

  midpoint = math.ceil(string.len(str) / 2)
  toremove = string.len(str) - maxLength
  lstrip = math.ceil(toremove / 2)
  rstrip = toremove - lstrip

  return string.sub(str, 1, midpoint - lstrip) .. separator .. string.sub(str, 1 + midpoint + rstrip)
end

v.redraw[vCLIP] = function()
  screen.clear()
  screen.level(15)
  screen.move(10,16)
  screen.text("CLIP > TRACK "..clip_sel)

  screen.move(10,52)
  screen.text(truncateMiddle(clip[track[clip_sel].clip].name, 18))
  screen.level(3)
  screen.move(10,60)
  screen.text("clip "..track[clip_sel].clip .. " " .. clip_actions[clip_action])

  screen.move(100,52)
  screen.text(2^(clip_clear_mult-2))
  screen.level(3)
  screen.move(100,60)
  screen.text("resize")

  screen.update()
end

v.gridkey[vCLIP] = function(x, y, z)
  if y == 1 then gridkey_nav(x,z)
  elseif z == 1 and y < TRACKS+2 and x < MAX_CLIPS+1 then
    clip_sel = y-1
    if x ~= track[clip_sel].clip then
      set_clip(clip_sel,x)
    end
    redraw()
    dirtygrid=true
  end
end

v.gridredraw[vCLIP] = function()
  g:all(0)
  gridredraw_nav()
  for i=1,16 do g:led(i,clip_sel+1,4) end
  for i=1,TRACKS do g:led(track[i].clip,i+1,10) end
  g:refresh();
end




--------------------TIME
v.key[vTIME] = function(n,z)
  print("TIME key")
end

v.enc[vTIME] = function(n,d)
  if n==2 then
    params:delta("clock_tempo",d)
  elseif n==3 then
    params:delta("quant_div",d)
  end
  redraw()
end

v.redraw[vTIME] = function()
  screen.clear()
  screen.level(15)
  screen.move(10,30)
  screen.text("TIME")
  if viewinfo[vTIME] == 0 then
    screen.move(10,50)
    screen.text(params:get("clock_tempo"))
    screen.move(70,50)
    screen.text(params:get("quant_div"))
    screen.level(3)
    screen.move(10,60)
    screen.text("tempo")
    screen.move(70,60)
    screen.text("quant div")
  end
  screen.update()
end

v.gridkey[vTIME] = function(x, y, z)
  if y == 1 then gridkey_nav(x,z) end
end

v.gridredraw[vTIME] = function()
  g:all(0)
  gridredraw_nav()
  g:refresh();
end

function draw_grid_connected()
  dirtygrid=true
  gridredraw()
end

function cleanup()
  for i=1,4 do
    pattern[i]:stop()
    pattern[i] = nil
  end

  grid.add = function() end
end

I thought this would be super easy since there’s no overlap in functionality, and one of the scripts doesn’t have a UI. But it seems like I’m missing something, and I can’t figure out what it is. I’m assuming the error has something to do with clock because the error message I’m getting right now is

lua:

/home/we/norns/lua/core/clock.lua:59: bad argument #1 to ‘resume’ (thread expected)

stack traceback:

[C]: in function ‘coroutine.resume’

/home/we/norns/lua/core/clock.lua:59: in function ‘core/clock.resume’

Just a wild shot in the dark; is there an extra ‘end’ here?


end

end

local g = grid.connect()

local fileselect

1 Like

I think there might have been! That got the lua error to go away. Now MLR is running normally, but the other script isn’t doing anything, as far as I can tell. Nothing’s coming out of any of the crow outputs.

looks like you’re also defining two init scripts. if you combine them into one it might get you close(r) :crossed_fingers:

or you could rename the first one seq_init and call it at the end of the other/mlr init function.

3 Likes

untested with crow (it’s printing match to the REPL tho!), but i feel like it’d be best to create a standalone lied script file which could be included as a library within another script.

here’s a version of lied which can be placed in its own code/lied folder and can be called in another script with Lied = include 'lied/lied', then initialized with Lied.init(): lied.lua · GitHub

to give it a go:

  • making a code/lied folder
    • dropping that lied.lua file into it
  • at the top of the mlr script (line 24?) paste Lied = include 'lied/lied'
  • paste in Lied.init() at the end of mlr’s init

if this works, i’m happy to outline the key differences – if it doesn’t, lmk!
edit: i put in some additional debugging prints and it seems like it should work :crossed_fingers:

4 Likes

it definitely seems like it should work! I checked out your changes to the lied code, and it doesn’t seem to run on its own, but I’m not entirely sure whether it’s supposed to? Maiden isn’t throwing an error per se, but I do get this message when I run the script, and nothing comes out of crow’s outputs while the script is running

from maiden

reading PMAP /home/we/dust/data/lied/untitled/untitled.pmap

m.read: /home/we/dust/data/lied/untitled/untitled.pmap not read.

When I try to run the combo with mlr by calling lied from within mlr as you’ve described, I get this error from Maiden:

from maiden

lua:

/home/we/norns/lua/core/clock.lua:59: bad argument #1 to ‘resume’ (thread expected)

stack traceback:

[C]: in function ‘coroutine.resume’

/home/we/norns/lua/core/clock.lua:59: in function ‘core/clock.resume’

Not sure why this is. Something to do with the clock, it seems, but I’m not sure what. Any ideas?

Thank you so much for taking the time to get back to me on this!

happy to help!

nah, this is in a library-friendly form, sticking with the idea of combining it into other scripts :slight_smile:
once you confirm it’s all workin’, we can get it running on its own.

hmm, not seeing that on my end at all – just to confirm, here’s my version of mlr. if you copy/paste that over your current mlr.lua file, you should see things initialize correctly by simply running mlr. lmk where it goes weird!

if it helps, here’s how things look on my end (i added some additional playing note debugging prints to the gist to confirm stuff should be sending to crow):

2 Likes