Home | ER-101 | ER-102 | ER-301 | Wiki | Contact

Middle Layer SDK (aka patching with Lua)



By familiar, do you mean do I have a head-shaped dent on my desk that can be directly attributed to them? If so, then yes. I have a matching one on the other side resulting from date calculations involving multiple time zones. :smiley:


By the way, when creating custom encoder maps (aka DialMaps), I would recommend creating just one map at your unit’s module level and re-using it for all instances of your unit. This way all instances of your unit can share the memory and caching is more efficient.

local function linMap(min,max,n)
  local map = app.DialMap()
  local scale = (max - min)/n
  for i=0,n do
  return map

-- to be used by all instances of this unit
local sharedMap = linMap(0,1.0,1000)


Great tip! I will make this update in the units I just shared.

I am trying to get a little better handle on view management. We created a custom view in Scorpio so that when you switch to that view, it shows more controls. I was going to do a similar thing in Pingable Scaled Random, hiding the levels control if the GridQuantizer was bypassed. One thing I notice though. If you, for example, press Enter while on some control to go into the scope view, when you come back out to the unit view, it is automatically going back to “expanded” view. So in this case, the levels control vanishes.

I might be approaching this the wrong way. Or maybe you just didn’t design it it to work like this. I can’t think of any examples where the builtins have different control views with different visible controls.

Any thoughts?


Hey Brian - I discovered the Clocked Random Gate unit I built crashes the ER-301 when I focus press the header. I actually built this using the v0.3.18 builtin code posted above for Tap Tempo as a base. Since I have no idea what’s wrong and haven’t messed with the menu on that one, I went back and deployed the original code (after switching requre Unit.MenuControl.ModeSelect to Unit.ViewControl.OptionControl) and it crashes in the same place. Guess some update to this lib happened post v0.3.18? Any idea how to fix it?

Here is the crash log:

Error Message:
X:/Unit/ViewControl/OptionControl.lua:64: Unit.ViewControl.OptionControl.init: button is missing.
stack traceback:
	[C]: in function 'error'
	x:/startup/on-load-app-module.lua:38: in field 'error'
	X:/Unit/ViewControl/OptionControl.lua:64: in function 'Unit.ViewControl.OptionControl.init'
	X:/Base/Class.lua:78: in upvalue 'ModeSelect'
	1:/ER-301/libs/Joe-s-Bespoke-ER-301-Units/TapTempoTest.lua:74: in function 'Joe-s-Bespoke-ER-301-Units.TapTempoTest.onLoadMenu'
	X:/Unit/init.lua:541: in function 'Unit.showMenu'
	(...tail calls...)
	X:/Unit/ViewControl/Header.lua:102: in function 'Unit.ViewControl.Header.spotReleased'
	X:/SpottedStrip/Section.lua:227: in function 'FileRecorder.TrackSection.spotReleased'
	X:/SpottedStrip/init.lua:253: in function 'GlobalChains.GlobalChain.spotReleased'
	X:/SpottedStrip/init.lua:240: in function 'GlobalChains.GlobalChain.mainReleased'
	X:/Base/Widget.lua:151: in function 'Chain.MarkMenu.sendUpHelper'
	X:/Base/Widget.lua:141: in function 'Chain.MarkMenu.sendUp'
	X:/Context.lua:192: in function 'Context.notify'
	X:/Application.lua:142: in upvalue 'notify'
	X:/Application.lua:231: in upvalue 'dispatch'
	X:/Application.lua:318: in function 'Application.loop'
	x:/startup/start.lua:54: in function <x:/startup/start.lua:47>
	[C]: in function 'xpcall'
	x:/startup/start.lua:57: in main chunk
	[C]: in function 'dofile'
	[string "dofile('x:/startup/start.lua')"]:1: in main chunk

And here is the menu section from the 0.3.18 Tap Tempo:

local menu = {"infoHeader","rename","load","save","rational"}

function TapTempoUnit:onLoadMenu(objects,controls)
  controls.rational = ModeSelect {
    description = "Allowed Mult/Div",
    option = objects.clock:getOption("Rational"),
    choices = {"any","rational only"},
    boolean = true,
    onUpdate = function(choice)
      if choice=="any" then
  return menu


It’s the MenuControl vs ViewControl part. MenuControl is the parent for controls in the menu whereas ViewControl is the parent for controls in the unit view.


At the moment, the “view cycling” logic assumes that there are only two views: expanded and collapsed. So I just need to enhance the logic to allow for more than those two. I’ll add it to the list of bugs to be fixed in v0.4 :wink:


Is there a disconnect() command available in the middle layer? And if so, what parameters would you supply to it? (I’m guessing not as I can’t seem to find any definitions or examples)

The Spread Delay unit only appears on the Insert menu when you are on a stereo channel. Is there something to supply in the Unit.lua or Init.lua that would make a Unit only appear on the Insert menu for a mono or stereo chain?

In case you haven’t guessed, I’m trying to figure out mono vs. stereo handling.

Edit: Found the StereoToMono object. I’m not sure why I was thinking that was something very specific to the Player units. Looks like I might have been swimming upstream. :thinking:


Upping this thread because the FM operator in the testlib (first post) is very interesting and maybe not everyone has seen/installed it :slight_smile:

(just be sure to rename the .lua files in .unit, if you’re running 0.4)

EDIT: see the next comment because the renaming part is WRONG.


Do not do this :wink: The *.unit extension is for unit presets (including custom unit presets). The middle layer produces bespoke units which are automatically loaded and installed into the unit selection screen during boot-up and as such they are never selected from a file browser view.


@odevices Maybe you should put in your list that scripts in the Lib directory remain as .lua


hahah, I’m sorry :smiley:



Absolutely no problem. The ambiguity is entirely my fault :wink:


There is a disconnect command available. It just hasn’t received any syntax sugar like the connect command because I haven’t used it. So at the moment you have to access the disconnect call from the app object as app.disconnect(outlet,inlet). Both connect and disconnect are non-thread-safe calls. So if you call them on objects in a running unit you will need to lock the containing chain before calling them. From the point of view of the audio thread, chains are atomic. So you can lock a sub-chain and have the ancestor chains keep running but skipping the locked sub-chain (using mutex.tryEnter() and mutex.leave() semantics). In the loadGraph() callback, the unit is not yet active, so there is no need to lock the parent chain. Locking an active chain will potentially create clicks/pops in the output so you have to mute() it first and then lock() it. Either way you disrupt the audio output of the chain.

So now you see why I generally refrain from using connect/disconnect outside of the loadGraph callback. You can do it but I’ve been lucky so far and always found a way to get around it.

This information is provided in the init.lua file. Take a look for example at the Spread Delay entry and note the channelCount value:

  title="Spread Delay",
  keywords="delay, effect",

If the channelCount is not provided then the unit chooser screen assumes the unit will work for mono and stereo. If the channelCount is provided then the unit chooser will filter out units with channel counts that do not match the chain you are inserting into.


It occurred to me at one point and I forgot to post it. What do you think of Component?


Not sure what’s going on with this one. I’m trying to create a Schroeder All Pass filter in the middle layer. (I know, kind of a weird time to do so, given that this could now be done as a UI patch).

Line 50 is making it go kaboom :fire:. If I comment that out, the unit loads. I can’t see where I’m doing anything invalid here, but more than likely I just need another set of eyes.

Any help appreciated!

Edit: BTW, same error in 0.3.25, so doesn’t appear to be anything related to 0.4 changes.

SchroederAllPass.zip (1.1 KB)

Line 50:



Time Since Boot: 10.674s
Firmware Version: 0.4.01 (unstable)
Boot Count: 11
Mount Count: 1
Error Message: Failed to construct unit: SchroederAllPass
X:/Unit/init.lua:77: Unit(Schroeder All-Pass,aacc5a8a).init: Failed to compile the graph.
stack traceback:
	[C]: in function 'error'
	x:/startup/on-load-app-module.lua:38: in field 'error'
	X:/Unit/init.lua:77: in function 'Unit.init'
	...301/libs/Joe-s-Bespoke-ER-301-Units/SchroederAllPass.lua:20: in function 'Joe-s-Bespoke-ER-301-Units.SchroederAllPass.init'
	X:/Base/Class.lua:78: in function <X:/Base/Class.lua:76>
	[C]: in function 'xpcall'
	X:/Unit/Factory/init.lua:106: in function 'Unit.Factory.instantiate'
	X:/Chain/Base.lua:336: in function 'Chain.Branch.loadUnit'
	(...tail calls...)
	X:/Chain/EmptySection.lua:42: in local 'x'
	X:/Base/Observable.lua:50: in function 'Joe-s-Bespoke-ER-301-Units.SchroederAllPass.emitSignal'
	X:/Base/Widget.lua:151: in function 'Joe-s-Bespoke-ER-301-Units.SchroederAllPass.sendUpHelper'
	X:/Base/Widget.lua:141: in function 'Joe-s-Bespoke-ER-301-Units.SchroederAllPass.sendUp'
	X:/Base/Context.lua:221: in function 'Base.Context.notify'
	X:/Application.lua:127: in upvalue 'notify'
	X:/Application.lua:222: in upvalue 'dispatch'
	X:/Application.lua:307: in function 'Application.loop'
	x:/startup/start.lua:54: in function <x:/startup/start.lua:47>
	[C]: in function 'xpcall'
	x:/startup/start.lua:57: in main chunk
	[C]: in function 'dofile'
	[string "dofile('x:/startup/start.lua')"]:1: in main chunk


Oops. Sorry about that one :bowing_man:

From way back, I had set up the unit graph compiler to throw an error when it detects a feedback loop inside a unit. I’ll remove that error condition in v0.4.02.


This means that… :heart_eyes:


So feedback connections should be good to go in v0.4.02. I’ve taken your Schroeder Allpass unit and used it for it testing (also edited it slightly for correctness and took some liberties). I’ve kept it in the Experimental category of the builtins (where I had it for easy testing) if you want to take a look.

Here is the code of the Schroeder Allpass unit that I was testing with:

-- GLOBALS: app, os, verboseLevel, connect, tie
local app = app
local Class = require "Base.Class"
local Unit = require "Unit"
local Fader = require "Unit.ViewControl.Fader"
local GainBias = require "Unit.ViewControl.GainBias"
local Task = require "Unit.MenuControl.Task"
local MenuHeader = require "Unit.MenuControl.Header"
local ModeSelect = require "Unit.MenuControl.OptionControl"
local Encoder = require "Encoder"
local Utils = require "Utils"
local ply = app.SECTION_PLY

local SchroederAllPass = Class{}

function SchroederAllPass:init(args)
  args.title = "Schroeder Allpass"
  args.mnemonic = "SA"

function SchroederAllPass:onLoadGraph(pUnit,channelCount)
  local delay = self:createObject("Delay","delay",1)
  local delayAdapter = self:createObject("ParameterAdapter","delayAdapter")
  local gainAdapter = self:createObject("ParameterAdapter","gainAdapter")
  local feedBackMix = self:createObject("Sum","feedBackMix")
  local feedBackGain = self:createObject("ConstantGain","feedBackGain")
  local feedForwardMix = self:createObject("Sum","feedForwardMix")
  local feedForwardGain = self:createObject("ConstantGain","feedForwardGain")
  local limiter = self:createObject("Limiter","limiter")

  connect(feedBackMix,"Out",delay,"Left In")
  connect(delay,"Left Out",feedForwardMix,"Left")
  connect(delay,"Left Out",feedBackGain,"In")

  tie(delay,"Left Delay",delayAdapter,"Out")


local menu = {

local views = {
  expanded = {"delay","gain"},
  collapsed = {},

local function linMap(min,max,n)
  local map = app.DialMap()
  local scale = (max - min)/n
  for i=0,n do
  return map

local delayMap = linMap(0,0.05,100)

function SchroederAllPass:onLoadViews(objects,controls)
  controls.delay = GainBias {
    button = "delay",
    description = "Delay Time",
    branch = self:getBranch("Delay"),
    gainbias = objects.delayAdapter,
    range = objects.delayAdapter,
    biasMap = delayMap,
    biasUnits = app.unitSecs,
    initialBias = 0.005

  controls.gain = GainBias {
    button = "gain",
    description = "Loop Gain",
    branch = self:getBranch("Gain"),
    gainbias = objects.gainAdapter,
    range = objects.gainAdapter,
    biasMap = nil,
    biasUnits = app.unitNone,
    initialBias = 0.7

  return views

return SchroederAllPass


Thanks so much! Looking forward to trying it out and also learning from your corrections and liberties. :wink:


Why do I have the feeling I’m about to learn lua?:fearful: