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.
38 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.

4 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

Hi- Norns newbie and I have looked online but I really don’t understand “load the file through parameters”

Would appreciate some help please.

Thanks

from the script, press key 1. this will return you to the norns menu, use encoder 1 to scroll right to the PARAMETERS page. select the edit option and you’ll be presented with the scripts parameters (this is the same for every script, btw). one of them will be a sample slot, select it to load a sample.

2 Likes

Thanks for the response.

My parameters menu says
LEVELS
REVERB
COMPRESSOR
SOFTCUT
CLOCK

Should one of them say “Sample” ??

So sorry if this is me being an utter idiot :disappointed:

have you ran the script yet? loading easygrain, and then navigating to the parameters menu will get you what you want.

fwiw…

these are the “system” parameters. anything/everything below these options is set up by whichever script you are currently running. (or, if you aren’t running a script, there wont be anything below them)

And there we go!

Thanks so much for your patience and for the photo.

Really appreciated .

What a lovely community this is :slightly_smiling_face:

3 Likes

noticed this in matron when loading
wondering if its bad?


# script clear

# script load: /home/we/dust/code/easygrain/easygrain.lua

# script run

loading engine: EasyGlut

>> reading PMAP /home/we/dust/data/easygrain/easygrain.pmap

m.read: /home/we/dust/data/easygrain/easygrain.pmap not read, using defaults.

lua: /home/we/norns/lua/core/paramset.lua:455: invalid paramset index: 1speed

stack traceback:

[C]: in function 'error'

/home/we/norns/lua/core/paramset.lua:455: in function 'core/paramset.lookup_param'

/home/we/norns/lua/core/paramset.lua:356: 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:164: in function </home/we/norns/lua/core/metro.lua:160>

lua:

/home/we/norns/lua/core/paramset.lua:455: invalid paramset index: 1speed

stack traceback:

[C]: in function 'error'

/home/we/norns/lua/core/paramset.lua:455: in function 'core/paramset.lookup_param'

/home/we/norns/lua/core/paramset.lua:356: 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:164: in function </home/we/norns/lua/core/metro.lua:160>

Engine.register_commands; count: 14

___ engine commands ___

density if

envscale if

gate ii

jitter if

pitch if

read is

reverb_damp f

reverb_mix f

reverb_room f

seek if

size if

speed if

spread if

volume if

___ polls ___

amp_in_l

amp_in_r

amp_out_l

amp_out_r

cpu_avg

cpu_peak

level_1

level_2

level_3

level_4

level_5

level_6

level_7

phase_1

phase_2

phase_3

phase_4

phase_5

phase_6

phase_7

pitch_in_l

pitch_in_r

# script init

thanks

Oof, I haven’t touched Norns development in almost four years. Is the script still running and producing sound?

From what I can tell, it looks like param “1speed” is an invalid index.

yes - still works and makes what it makes happen
just wondering what the error meant