Easygrain

easygrain

a simple granulator, now with optional arc support

easygrain is a portable granulator built upon @artfwo’s Glut engine (renamed here as “EasyGlut” without modifications). This script can granulate a file using only the norns interface, making it great for exploring sounds while traveling. In this new version for norns 2.0, optional arc controls and displays have been added for faster parameter changes. Additionally, the speed and pitch ranges have been widened.

Requirements

  • norns 2.0
  • an audio file to granulate
  • a moment to think
  • arc is optional
  • restart norns after installing (new engine)

Documentation

Before starting, load a file from the PARAMS menu.

button 2 begins/stops playback
button 3 resets playback
knobs change screen parameters
hold button 1 to change parameter set

arc:
knob 1 selects active mode. left crescent is speed/size/pitch. right crescent is jitter/density/spread.

Please note that the Glut engine includes its own reverb. If you hear tons of reverb, either turn down the Glut reverb mix or turn off the system reverb. This is a waste of battery, so I will be removing it from the engine in an upcoming update.

Download

v1.0.0 - https://github.com/mhetrick/easygrain/archive/master.zip

Todo:

  • Remove reverb from EasyGrain engine.
  • Reduce number of voices in EasyGrain engine.
  • Find anything else extra that could contribute to battery usage.
35 Likes

reposting to the correct thread here:

getting a few errors on with easygrain on init, but it appears to work OK otherwise

# script run
loading engine: EasyGlut
<ok>
lua: 
/home/we/norns/lua/core/paramset.lua:154: attempt to index a nil value (local 'param')
stack traceback:
	/home/we/norns/lua/core/paramset.lua:154: in function 'core/paramset.get'
	/home/we/dust/code/easygrain/easygrain.lua:68: in global 'arc_redraw'
	/home/we/dust/code/easygrain/easygrain.lua:101: in field 'event'
	/home/we/norns/lua/core/metro.lua:165: in function </home/we/norns/lua/core/metro.lua:162>
lua: 
/home/we/norns/lua/core/paramset.lua:154: attempt to index a nil value (local 'param')
stack traceback:
	/home/we/norns/lua/core/paramset.lua:154: in function 'core/paramset.get'
	/home/we/dust/code/easygrain/easygrain.lua:68: in global 'arc_redraw'
	/home/we/dust/code/easygrain/easygrain.lua:101: in field 'event'
	/home/we/norns/lua/core/metro.lua:165: in function </home/we/norns/lua/core/metro.lua:162>
Engine.register_commands; count: 14

Good catch. I’m probably starting the arc’s redraw callback before the params are initialized. I’ll clean that up when I’m removing the reverb from the engine.

2 Likes

Loving Gasygrain for processing the output of other scripts…

Here’s a quick hack adding 4x LFOs to control speed, jitter, size, density, pitch, and spread - thanks to @Justmat for hnds.lua. All LFOs are controlled in the PARAMS menu.

Edit: note, this requires Otis to be installed!

-- easygrain
--
-- a simplified glut
-- for traveling with nothing
-- not even a grid
--
-- KEY 1: Alt-key
-- KEY 2: Start/Stop
-- KEY 3: Retrigger
--        Main    Alt
-- ENC 1: Speed Jitter
-- ENC 2: Size  Density
-- ENC 3: Pitch Spread

local lfo = include("otis/lib/hnds")

engine.name = 'EasyGlut'
local VOICES = 1
voiceGate = 0
shiftMode = 0
grainPosition = 0

arcMode = 0

lfo_targets = { "none", "speed", "jitter", "size", "density", "pitch", "spread" }


REFRESH_RATE = 0.03


key = function(n,z)
  if n==2 then hold = z==1 and true or false
  elseif n==3 and z==1 then mode = mode==1 and 2 or 1 end
  redraw()
end

a = arc.connect()

a.delta = function(n,d)
  if n == 1 then
    if d > 2 then arcMode = 1
    elseif d < -2 then arcMode = 0 end
  end
  
  if arcMode == 0 then
    if n == 2 then
      params:delta("1speed",d/2)
    elseif n == 3 then
      params:delta("1size",d/3) 
    elseif n == 4 then
      params:delta("1pitch",d/20) 
    end
  elseif arcMode == 1 then
    if n == 2 then
      params:delta("1jitter",d/3)
    elseif n == 3 then
      params:delta("1density",d/3) 
    elseif n == 4 then
      params:delta("1spread",d/3) 
    end
  end
  
  
end

arc_redraw = function()
  a:all(0)
  
  if arcMode == 0 then
    a:segment(1, -3, 0, 15)
    
    local speed = params:get("1speed") / 200
    if speed > 0 then
      a:segment(2,0.5,0.5+speed,15)
    else
      a:segment(2,speed-0.5,-0.5,15)
    end
  
    local size = params:get("1size") / 100
    a:segment(3, -2.5, -2.5 + size, 15)
    
    local pitch = params:get("1pitch") / 20
    if pitch > 0 then
      a:segment(4,0.5,0.5+pitch,15)
    else
      a:segment(4,pitch-0.5,-0.5,15)
    end
  elseif arcMode == 1 then
    a:segment(1, 0, 3, 15)
    local jitter = params:get("1jitter") / 100
    local density = params:get("1density") / 100
    local spread = params:get("1spread") / 20
    
    a:segment(2, -2.5, -2.5 + jitter, 15)
    a:segment(3, -2.5, -2.5 + density, 15)
    a:segment(4, -2.5, -2.5 + spread, 15)
  end
  
  a:refresh()
end

re = metro.init()
re.time = REFRESH_RATE
re.event = function()
  arc_redraw()
end
re:start()


function init()
  
  local phase_poll = poll.set('phase_1', function(pos) grainPosition = pos end)
  phase_poll.time = 0.05
  phase_poll:start()
  
  local sep = ": "

  params:add_taper("reverb_mix", "*"..sep.."mix", 0, 100, 50, 0, "%")
  params:set_action("reverb_mix", function(value) engine.reverb_mix(value / 100) end)

  params:add_taper("reverb_room", "*"..sep.."room", 0, 100, 50, 0, "%")
  params:set_action("reverb_room", function(value) engine.reverb_room(value / 100) end)

  params:add_taper("reverb_damp", "*"..sep.."damp", 0, 100, 50, 0, "%")
  params:set_action("reverb_damp", function(value) engine.reverb_damp(value / 100) end)

  for v = 1, VOICES do
    params:add_separator()

    params:add_file(v.."sample", v..sep.."sample")
    params:set_action(v.."sample", function(file) engine.read(v, file) end)

    params:add_taper(v.."volume", v..sep.."volume", -60, 20, 0, 0, "dB")
    params:set_action(v.."volume", function(value) engine.volume(v, math.pow(10, value / 20)) end)

    params:add_taper(v.."speed", v..sep.."speed", -400, 400, 100, 0, "%")
    params:set_action(v.."speed", function(value) engine.speed(v, value / 100) end)

    params:add_taper(v.."jitter", v..sep.."jitter", 0, 500, 0, 5, "ms")
    params:set_action(v.."jitter", function(value) engine.jitter(v, value / 1000) end)

    params:add_taper(v.."size", v..sep.."size", 1, 500, 100, 5, "ms")
    params:set_action(v.."size", function(value) engine.size(v, value / 1000) end)

    params:add_taper(v.."density", v..sep.."density", 0, 512, 20, 6, "hz")
    params:set_action(v.."density", function(value) engine.density(v, value) end)

    params:add_taper(v.."pitch", v..sep.."pitch", -48, 48, 0, 0, "st")
    params:set_action(v.."pitch", function(value) engine.pitch(v, math.pow(0.5, -value / 12)) end)

    params:add_taper(v.."spread", v..sep.."spread", 0, 100, 0, 0, "%")
    params:set_action(v.."spread", function(value) engine.spread(v, value / 100) end)

    params:add_taper(v.."fade", v..sep.."att / dec", 1, 9000, 1000, 3, "ms")
    params:set_action(v.."fade", function(value) engine.envscale(v, value / 1000) end)
  end
  
  for i = 1, 4 do
    lfo[i].lfo_targets = lfo_targets
  end
  
  lfo.init()

  params:bang()
  
  counter = metro.init(count, 0.01, -1)
  counter:start()
end

function count()
  redraw()
end

local function reset_voice()
  engine.seek(1, 0)
end

local function start_voice()
  reset_voice()
  engine.gate(1, 1)
  voiceGate = 1
end

local function stop_voice()
  voiceGate = 0
  engine.gate(1, 0)
end



function enc(n, d)
  if n == 1 then
    if shiftMode == 0 then
      params:delta("1speed", d)
    else
      params:delta("1jitter", d)
    end
  elseif n == 2 then
    if shiftMode == 0 then
      params:delta("1size", d)
    else
      params:delta("1density", d)
    end
  elseif n == 3 then
    if shiftMode == 0 then
      params:delta("1pitch", d)
    else
      params:delta("1spread", d)
    end
  end
end

function key(n, z)
  if n == 1 then
    shiftMode = z
  elseif n == 2 then
    if z == 1 then
      if voiceGate == 0 then start_voice() else stop_voice() end
    end
  elseif n == 3 then
    if z == 1 then 
      reset_voice() 
    end
  end
end

function printRound(num, numDecimalPlaces)
  local mult = 10^(numDecimalPlaces or 0)
  return math.floor(num * mult + 0.5) / mult
end

function redraw()
  -- do return end
  screen.clear()
  screen.level(15)

  rectHeight = 10
  rectPadding = 10

  screen.rect(rectPadding, rectHeight, 100, 10)
  screen.stroke()
  
  if voiceGate == 1 then
    screen.rect(rectPadding, rectHeight, 100*grainPosition, 10)
    screen.fill()
  end
  
  if shiftMode == 0 then
    screen.move(4, 40)
    screen.text(printRound(params:get("1speed"), 1))
    screen.move(0, 50)
    screen.text("Speed")
    
    screen.move(60, 40)
    screen.text(printRound(params:get("1size"), 1))
    screen.move(60, 50)
    screen.text("Size")
    
    screen.move(96, 40)
    screen.text(printRound(params:get("1pitch"), 1))
    screen.move(95, 50)
    screen.text("Pitch")
  else
    screen.move(6, 40)
    screen.text(printRound(params:get("1jitter"), 1))
    screen.move(0, 50)
    screen.text("Jitter")
    
    screen.move(60, 40)
    screen.text(printRound(params:get("1density"), 1))
    screen.move(53, 50)
    screen.text("Density")
    
    screen.move(97, 40)
    screen.text(printRound(params:get("1spread"), 1))
    screen.move(90, 50)
    screen.text("Spread")
  end

  screen.update()
end

function lfo.process()

  for i = 1, 4 do
    local target = params:get(i .. "lfo_target")
    if params:get(i .. "lfo") == 2 then
      -- speed
      if target == 2 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, -400, 400))
      -- jitter
      elseif target == 3 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, 0, 500))
      -- size
      elseif target == 4 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, 1, 500))
      -- density
      elseif target == 5 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, 0, 512.0))
      -- pitch
      elseif target == 6 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, -48.0, 48.0))
      -- spread
      elseif target == 7 then
        params:set("1" .. lfo_targets[target], lfo.scale(lfo[i].slope, -4, 3, 0.0, 100.0))
      end
    end
  end
end
10 Likes

Oh that’s awesome! I’m just now seeing this. Are you able to do a pull request on Github? hnds.lua could be copied to easygrain/lib so that there isn’t a dependency on otis (if that’s fine with @Justmat!)

2 Likes

That’s the way I would do it! :smiley:

I really like this script, it’s very playable and I could see it being really satisfying on an Arc.

this sounds amazing, thank you for sharing! here’s something i made, it’s a lyra drone with oooooo played back through easygrain. https://drive.google.com/file/d/1xWodYAu1cLPvaxwkgCOxiffU44uir79y/view?usp=sharing

any chance of this script working on live audio, or does anyone have a recommendation for a similar simple granulator for live audio, a la clouds/beads?

1 Like

Beautiful sounds! Check out the Silos script for live audio grains.

3 Likes

glad you liked em! i’ll give silos another shot. last time I felt it was too complex but I’m sure that just means I didn’t try hard enough lol.

You can totally make it complex and use the text commands or set it up from the params menu and just let it do its thing too. I know that I’m just scratching the surface with it.

3 Likes

i was going to say this, but didn’t want to derail :slight_smile:

you can midi map most everything and never need to touch a keyboard :keyboard: :smiley:

2 Likes