toga: TouchOSC grid and arc controller for monome norns

Awake doesn’t require the grid to be vertical - mine isn’t using Awake and Toga. If you extend Awake to 16 steps then you’ll want it horizontal.

If it’s helpful, he’s my code for Awake, which also has Mx.Samples added as a voice.


*> *
> – awake-mx.samples: time changes
> – 2.4.0 @tehn
> – mx.samples @infinitedigits
> – plumbing @tomw
> –
> – top loop plays notes
> – transposed by bottom loop
> –
> – (grid optional)
> –
> – E1 changes modes:
> – STEP/LOOP/SOUND/OPTION
> –
*> – K1 held is alt **
> –
> – STEP
> – E2/E3 move/change
*> – K2 toggle clear
*> – K3 morph rand
> –
> – LOOP
> – E2/E3 loop length
> – K2 reset position
> – K3 jump position
> –
> – SOUND
> – K2/K3 selects
> – E2/E3 changes
> –
> – OPTION
*> – toggle
> – E2/E3 changes
*> *
> mxsamples=include(“mx.samples/lib/mx.samples”)
> engine.name=“MxSamples”
> skeys=mxsamples:new()
*> *
> hs = include(‘lib/halfsecond’)
*> *
> MusicUtil = require “musicutil”
*> *
> options = {}
> options.OUTPUT = {“audio”, “midi”, “audio + midi”, “crow out 1+2”, “crow ii JF”}
> local grid = util.file_exists(_path.code…“midigrid”) and include “midigrid/lib/mg_128” or grid
> local arc = util.file_exists(_path.code…“toga”) and include “toga/lib/togaarc” or arc
> local grid = util.file_exists(_path.code…“toga”) and include “toga/lib/togagrid” or grid
> g = grid.connect()
*> *
> alt = false
*> *
> mode = 1
> mode_names = {“STEP”,“LOOP”,“SOUND”,“OPTION”}
*> *
> one = {
> pos = 0,
> length = 8,
> data = {1,0,3,5,6,7,8,7,0,0,0,0,0,0,0,0}
> }
*> *
> two = {
> pos = 0,
> length = 7,
> data = {5,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
> }
*> *
*> function add_pattern_params() *
> params:add_separator()
> params:add_group(“pattern 1”,17)
*> *
*> params:add{type = “number”, id = “one_length”, name = “length”, min=1, max=16, *
> default = one.length,
> action=function(x) one.length = x end }
*> *
> for i=1,16 do
*> params:add{type = “number”, id= (“one_data_”…i), name = ("data "…i), min=0, max=8, *
> default = one.data[i],
> action=function(x)one.data[i] = x end }
> end
*> *
> params:add_group(“pattern 2”,17)
*> *
*> params:add{type = “number”, id = “two_length”, name = “length”, min=1, max=16, *
> default = two.length,
> action=function(x)two.length = x end}
*> *
> for i=1,16 do
*> params:add{type = “number”, id= “two_data_”…i, name = "data "…i, min=0, max=8, *
> default = two.data[i],
> action=function(x) two.data[i] = x end }
> end
*> *
> end
*> *
> set_loop_data = function(which, step, val)
> params:set(which…“data”…step, val)
> end
*> *
*> *
> local midi_out_device
> local midi_out_channel
*> *
> local scale_names = {}
> local notes = {}
> local active_notes = {}
*> *
> local edit_ch = 1
> local edit_pos = 1
*> *
> local instruments = {}
*> *
> snd_sel = 1
> snd_names = {“inst”,“amp”,“lpf”,“hpf”,“atk”,“dec”,“sus”,“rel”,“fb”,“rate”, “pan”, “delay_pan”}
*> *
> snd_params = {“mxsamples_instrument”,“mxsamples_amp”,“mxsamples_lpf_mxsamples”,“mxsamples_hpf_mxsamples”,“mxsamples_attack”, “mxsamples_decay”,“mxsamples_sustain”,“mxsamples_release”,“delay_feedback”,“delay_rate”, “mxsamples_pan”, “delay_pan”}
*> *
> NUM_SND_PARAMS = #snd_params
*> *
> notes_off_metro = metro.init()
*> *
> function build_scale()
> notes = MusicUtil.generate_scale_of_length(params:get(“root_note”), params:get(“scale_mode”), 16)
> local num_to_add = 16 - #notes
> for i = 1, num_to_add do
> table.insert(notes, notes[16 - num_to_add])
> end
> end
*> *
> function all_notes_off()
> if (params:get(“output”) <= 3) then
> for _, a in pairs(active_notes) do
> skeys:off({name=params:get(“mxsamples_instrument_txt”),midi=a})
> midi_out_device:note_off(a, nil, midi_out_channel)
> end
> end
> active_notes = {}
> end
*> *
> function morph(loop, which)
> for i=1,loop.length do
> if loop.data[i] > 0 then
*> set_loop_data(which, i, util.clamp(loop.data[i]+math.floor(math.random()3)-1,1,8))
> end
> end
> end
*> *
> function random()
*> for i=1,one.length do set_loop_data(“one”, i, math.floor(math.random()9)) end
*> for i=1,two.length do set_loop_data(“two”, i, math.floor(math.random()9)) end
> end
*> *
> function step()
> while true do
> clock.sync(1/params:get(“step_div”))
*> *
> all_notes_off()
*> *
> one.pos = one.pos + 1
> if one.pos > one.length then one.pos = 1 end
> two.pos = two.pos + 1
> if two.pos > two.length then two.pos = 1 end
*> *
> if one.data[one.pos] > 0 then
> local note_num = notes[one.data[one.pos]+two.data[two.pos]]
> local freq = MusicUtil.note_num_to_freq(note_num)
> – Trig Probablility
> if math.random(100) <= params:get(“probability”) then
> – Audio engine out
> if params:get(“output”) == 1 or params:get(“output”) == 3 then
> skeys:on({name=params:get(“mxsamples_instrument_txt”), midi=note_num, velocity=120})
> elseif params:get(“output”) == 4 then
> crow.output[1].volts = (note_num-60)/12
> crow.output[2].execute()
> elseif params:get(“output”) == 5 then
> crow.ii.jf.play_note((note_num-60)/12,5)
> end
*> *
> – MIDI out
> if (params:get(“output”) == 2 or params:get(“output”) == 3) then
> midi_out_device:note_on(note_num, 96, midi_out_channel)
> end
*> *
> – note off
> if params:get(“output”) <= 3 then
> table.insert(active_notes, note_num)
> – Note off timeout
> if params:get(“note_length”) < 4 then
> notes_off_metro:start((60 / params:get(“clock_tempo”) / params:get(“step_div”)) * params:get(“note_length”) * 0.25, 1)
> end
> end
> end
> end
*> *
> if g then
> gridredraw()
> end
> redraw()
> end
> end
*> *
> function stop()
> all_notes_off()
> end
*> *
*> *
> function init()
> for i = 1, #MusicUtil.SCALES do
> table.insert(scale_names, string.lower(MusicUtil.SCALES[i].name))
> end
*> *
> midi_out_device = midi.connect(1)
> midi_out_device.event = function() end
*> *
> notes_off_metro.event = all_notes_off
*> *
> – mx.samples init
> local startup = true
> instruments = skeys:list_instruments()
> – hidden text param for the instrument name because list IDs will change if instruments change
> params:add_text(“mxsamples_instrument_txt”, “mxsamples_instrument_txt”, “”)
> params:hide(“mxsamples_instrument_txt”)
*> *
*> params:add{type = “option”, id = “mxsamples_instrument”, name = “instrument”, options = instruments, *
> action = function(value)
> all_notes_off()
> if not startup then
> params:set(“mxsamples_instrument_txt”, instruments[value])
> end
> end
> }
*> *
> params:add{type = “option”, id = “output”, name = “output”,
> options = options.OUTPUT,
> action = function(value)
> all_notes_off()
> if value == 4 then crow.output[2].action = “{to(5,0),to(0,0.25)}”
> elseif value == 5 then
> crow.ii.pullup(true)
> crow.ii.jf.mode(1)
> end
> end}
> params:add{type = “number”, id = “midi_out_device”, name = “midi out device”,
> min = 1, max = 4, default = 1,
*> *
> action = function(value) midi_out_device = midi.connect(value) end}
> params:add{type = “number”, id = “midi_out_channel”, name = “midi out channel”,
> min = 1, max = 16, default = 1,
> action = function(value)
> all_notes_off()
> midi_out_channel = value
> end}
> params:add_separator()
*> *
> params:add{type = “number”, id = “step_div”, name = “step division”, min = 1, max = 16, default = 4}
*> *
> params:add{type = “option”, id = “note_length”, name = “note length”,
> options = {“25%”, “50%”, “75%”, “100%”},
> default = 4}
*> *
> params:add{type = “option”, id = “scale_mode”, name = “scale mode”,
> options = scale_names, default = 5,
> action = function() build_scale() end}
> params:add{type = “number”, id = “root_note”, name = “root note”,
> min = 0, max = 127, default = 60, formatter = function(param) return MusicUtil.note_num_to_name(param:get(), true) end,
> action = function() build_scale() end}
> params:add{type = “number”, id = “probability”, name = “probability”,
> min = 0, max = 100, default = 100,}
*> *
> hs.init()
*> *
> add_pattern_params()
> params:default()
*> *
> – set the instrument option param based on the text param value
> local inst_txt = params:get(“mxsamples_instrument_txt”)
> if inst_txt ~= “” then
> for i = 1, #instruments do
> if instruments[i] == inst_txt then
> params:set(“mxsamples_instrument”, i)
> end
> end
> else
> params:set(“mxsamples_instrument_txt”, instruments[1])
> end
*> *
> startup = false
*> *
> clock.run(step)
*> *
> norns.enc.sens(1,8)
> end
*> *
> function g.key(x, y, z)
> local grid_h = g.rows
> if z > 0 then
> if (grid_h == 8 and edit_ch == 1) or (grid_h == 16 and y <= 8) then
> if one.data[x] == 9-y then
> set_loop_data(“one”, x, 0)
> else
> set_loop_data(“one”, x, 9-y)
> end
> end
> if (grid_h == 8 and edit_ch == 2) or (grid_h == 16 and y > 8) then
> if grid_h == 16 then y = y - 8 end
> if two.data[x] == 9-y then
> set_loop_data(“two”, x, 0)
> else
> set_loop_data(“two”, x, 9-y)
> end
> end
> gridredraw()
> redraw()
> end
> end
*> *
> function gridredraw()
> local grid_h = g.rows
> g:all(0)
> if edit_ch == 1 or grid_h == 16 then
> for x = 1, 16 do
> if one.data[x] > 0 then g:led(x, 9-one.data[x], 5) end
> end
> if one.pos > 0 and one.data[one.pos] > 0 then
> g:led(one.pos, 9-one.data[one.pos], 15)
> else
> g:led(one.pos, 1, 3)
> end
> end
> if edit_ch == 2 or grid_h == 16 then
> local y_offset = 0
> if grid_h == 16 then y_offset = 8 end
> for x = 1, 16 do
> if two.data[x] > 0 then g:led(x, 9-two.data[x] + y_offset, 5) end
> end
> if two.pos > 0 and two.data[two.pos] > 0 then
> g:led(two.pos, 9-two.data[two.pos] + y_offset, 15)
> else
> g:led(two.pos, 1 + y_offset, 3)
> end
> end
> g:refresh()
> end
*> *
> function enc(n, delta)
> if n==1 then
> mode = util.clamp(mode+delta,1,4)
> elseif mode == 1 then --step
> if n==2 then
> if alt then
> params:delta(“probability”, delta)
> else
> local p = (edit_ch == 1) and one.length or two.length
> edit_pos = util.clamp(edit_pos+delta,1,p)
> end
> elseif n==3 then
> if edit_ch == 1 then
> params:delta(“one_data_”…edit_pos, delta)
> else
> params:delta(“two_data_”…edit_pos, delta)
> end
> end
> elseif mode == 2 then --loop
> if n==2 then
> params:delta(“one_length”, delta)
> elseif n==3 then
> params:delta(“two_length”, delta)
> end
> elseif mode == 3 then --sound
> if n==2 then
> params:delta(snd_params[snd_sel], delta)
> elseif n==3 then
> params:delta(snd_params[snd_sel+1], delta)
> end
> elseif mode == 4 then --option
> if n==2 then
> if alt==false then
> params:delta(“clock_tempo”, delta)
> else
> params:delta(“step_div”,delta)
> end
> elseif n==3 then
> if alt==false then
> params:delta(“root_note”, delta)
> else
> params:delta(“scale_mode”, delta)
> end
> end
> end
> redraw()
> end
*> *
> function key(n,z)
> if n==1 then
> alt = z==1
*> *
> elseif mode == 1 then --step
> if n==2 and z==1 then
> if not alt==true then
> – toggle edit
> if edit_ch == 1 then
> edit_ch = 2
> if edit_pos > two.length then edit_pos = two.length end
> else
> edit_ch = 1
> if edit_pos > one.length then edit_pos = one.length end
> end
> else
> – clear
> for i=1,one.length do params:set(“one_data_”…i, 0) end
> for i=1,two.length do params:set(“two_data_”…i, 0) end
*> *
> end
> elseif n==3 and z==1 then
> if not alt==true then
> – morph
> if edit_ch == 1 then morph(one, “one”) else morph(two, “two”) end
> else
> – random
> random()
> gridredraw()
> end
> end
> elseif mode == 2 then --loop
> if n==2 and z==1 then
> one.pos = 0
> two.pos = 0
> elseif n==3 and z==1 then
*> one.pos = math.floor(math.random()one.length)
> two.pos = math.floor(math.random()two.length)
> end
> elseif mode == 3 then --sound
> if n==2 and z==1 then
> snd_sel = util.clamp(snd_sel - 2,1,NUM_SND_PARAMS-1)
> elseif n==3 and z==1 then
> snd_sel = util.clamp(snd_sel + 2,1,NUM_SND_PARAMS-1)
> end
> elseif mode == 4 then --option
> if n==2 then
> elseif n==3 then
> end
> end
> *
> redraw()
> end
> *
> function redraw()
> screen.clear()
> screen.line_width(1)
> screen.aa(0)
> – edit point
> if mode==1 then
> screen.move(26 + edit_pos6, edit_ch==1 and 33 or 63)

> screen.line_rel(4,0)
> screen.level(15)
> if alt then
> screen.move(0, 30)
> screen.level(1)
> screen.text(“prob”)
> screen.move(0, 45)
> screen.level(15)
> screen.text(params:get(“probability”))
> end
> screen.stroke()
> end
> – loop lengths
> screen.move(32,30)
> screen.line_rel(one.length6-2,0)

> screen.move(32,60)
> screen.line_rel(two.length6-2,0)

> screen.level(mode==2 and 6 or 1)
> screen.stroke()
> – steps
> for i=1,one.length do
> screen.move(26 + i6, 30 - one.data[i]3)
> screen.line_rel(4,0)
> screen.level(i == one.pos and 15 or ((edit_ch == 1 and one.data[i] > 0) and 4 or (mode==2 and 6 or 1)))
> screen.stroke()
> end
> for i=1,two.length do
> screen.move(26 + i6, 60 - two.data[i]3)
> screen.line_rel(4,0)
> screen.level(i == two.pos and 15 or ((edit_ch == 2 and two.data[i] > 0) and 4 or (mode==2 and 6 or 1)))
> screen.stroke()
> end
*> *
> screen.level(4)
> screen.move(0,10)
> screen.text(mode_names[mode])
*> *
> if mode==3 then
> screen.level(1)
> screen.move(0,30)
> screen.text(snd_names[snd_sel])
> screen.level(15)
> screen.move(0,40)
> screen.text(params:string(snd_params[snd_sel]))
> screen.level(1)
> screen.move(0,50)
> screen.text(snd_names[snd_sel+1])
> screen.level(15)
> screen.move(0,60)
> screen.text(params:string(snd_params[snd_sel+1]))
> elseif mode==4 then
> screen.level(1)
> screen.move(0,30)
> screen.text(alt==false and “bpm” or “div”)
> screen.level(15)
> screen.move(0,40)
*> screen.text(alt==false and params:get(“clock_tempo”) or params:string(“step_div”)) *
> screen.level(1)
> screen.move(0,50)
> screen.text(alt==false and “root” or “scale”)
> screen.level(15)
> screen.move(0,60)
> screen.text(alt==false and params:string(“root_note”) or params:string(“scale_mode”))
> end
*> *
*> *
*> *
> screen.update()
> end
*> *
> function cleanup ()
> end

1 Like

@JaxD, I am running into some of the same issues with toga and MLR you report to have resolved, but like hellzapop below, I am not quite sure where to find the code/GitHub link you reference in your post. Do you happen to still have that available?

In the meantime, I tried my best to resolve the issue myself based on your comments re: LED brightness levels and the issues they seem to cause in toga. I am definitely NOT a programmer, but after some research (googling), what I tried was to locate all instances of g:led() and screen.level() that included the number 15 (which I believe to correspond to max grid button brightness) and reduced that number to one slightly lower (I tried 13 and 12).

This actually got my partway there, i.e., I could kinda/sorta get gesture recording to work whereas it wouldn’t previously. However, it’s still not working quite as Peter Bark shows in his excellent tutorial (My Favorite Norns Scripts Episode 5: MLR - YouTube) and I feel it must be due to my kludge of a “fix” (see “I’m not a programmer” disclaimer noted previously).

Anyway, I just wanted to thank you for getting me this far in a (hopeful) fix, but was wondering if you (or anyone else) has managed to get this working better than I am able.

Any help appreciated. :slight_smile:

1 Like

has anyone gotten toga working with Fall: generative synth/sequencer - Library - lines (llllllll.co) ?

Hi! I’m really new to the monome ecosystem so sorry if my problem is only caused by my stupidity. I’ve got a monome Norns shield and touchosc, I’ve installed toga and everything works quite well but when I’m using granchild I’ve got a bit of a problem, I can start the recording but I cannot stop it! I have to use the shield, the iPad seams not to catch the “stop recording” function. Any ideas on I can make it work? Thank you!

I’ve found the same problem with being able to start recording but not stop in mlre (not mlr). Thanks to @JaxD identifying the reason above (it’s because a fully-lit LED won’t send a value of 1.0) I think I’ve found a fix - although I’ve only tested it for about 5 minutes.

In line 200 of lib/togagrid.lua change the value of 15.0 to 15.1 so that the line becomes this:

      osc.send(dest, addr, {z / 15.1})

This makes theLEDs fractionally less bright, and so they’re never instructed to reach their maximum of 1.0. Therefore a touch of the button will be able to send a “new” value of 1.0.

If you do make that code change you’ll need to restart your norns (at least, that’s what I’ve found).

Maybe this will also fix @nimk94’s issue with granchild, but I’ve not tried that.

This doesn’t fix the other problem that @JaxD also mentioned, which is that mlr tries to light an LED off the grid. This generates an error in toga. However, I’m guessing that’s an mlr bug, not a toga bug. And at least we’ve got mlre to play with.

I hope this change does actually work for people. :crossed_fingers:

3 Likes

Wow, thank you so much for finding this fix! I am currently without a usable tablet, but I just put TouchOSC on my phone (Android), made your edit to togagrid.lua, and loaded up mlr (not mlre). Although I haven’t done a thorough test (only had a chance to play with it for a few minutes), everything that was not working for me previously now is!

Thanks again for tracking this down and posting your fix. I can’t wait to dig deeper into mlr now.