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

Middle Layer SDK (aka patching with Lua)



@odevices - Do you have any thoughts on guidelines for naming conventions and category selection for bespoke units? Should the developer just use their best judgment?

We could use “Bespoke BPF” above as a concrete example. I figure you will make a us better bpf at some point, given that it’s on the wiki feature request list. But I made it to learn, and also figured having a bespoke bpf unit was better than not having a BPF at all, and also maybe better than a UI layer CU until that happens.

So I didn’t want to just necessarily call it “BPF” or “Ladder BPF” and potentially get into your namespace for builtins. Nor did I want to put it in the Filters category (not only due to namespace, but also because I’m not quite sure it’s ready to come out of Experimental yet).

Also in this case, trying to come up with too clever a name for a unit that does something very basic/fundamental seemed like it would only make it less clear what it does.


I’ve been wondering if is it possible to create a Custom Unit programatically?

edit: I would basically like to mix the functionality of the Custom Units with the ability to add parameter controls in the same way as you would with a Bespoke Unit.

Or to put it another way, build a Bespoke Unit that has the Custom Unit subchain :slight_smile:


Ahh, I think I got it… require Chain.Patch

  self.patch = Patch {
    title = self.title,
    depth = self.depth,
    channelCount = self.channelCount,
    unit = self

  controls.meter = PatchMeter {
    button = "patch",
    description = "Patch",
    patch = self.patch

…and then the serialise and deserialise methods!

I should probably test this at this point, but it’s late, maybe over the weekend :slight_smile:

I have to say, this is all very clever!!


Thinking aloud here and possibly demonstrating my ignorance of Lua programming.

Would it make sense to return some of the functions ultimately called by getMap() in Encoder.lua available directly for creating custom maps?



Right now I am thinking you would need to create your own function in the unit.lua, which is basically a copy of that code.


Those map-creation functions are not part of the external interface of the Encoder module. So I would just go ahead and create your own maps in your unit module and not worry about sharing code with the Encoder module.


Hey Brian,

Has Unit.ViewControl.ModeSelect been moved or renamed in 0.3.25? Or am I just doing something wrong and silly…?

local ModeSelect = require "Unit.ViewControl.ModeSelect"

And I get…

1:/ER-301/libs/Joe-s-Bespoke-ER-301-Units/Comparator.lua:5: module 'Unit.ViewControl.ModeSelect' not found:
	no field package.preload['Unit.ViewControl.ModeSelect']
	no file 'X:/Unit/ViewControl/ModeSelect.lua'
	no file 'X:/Unit/ViewControl/ModeSelect/init.lua'
	no file '1:/ER-301/libs/Unit/ViewControl/ModeSelect.lua'
	no file '1:/ER-301/libs/Unit/ViewControl/ModeSelect/init.lua'

Trying to enclose the Comparator object in a self-contained unit and put some controls on it to better understand it. :slight_smile:


Hi Joe. It was renamed! :bowing_man:

Unit.ViewControl.ModeSelect is now called Unit.ViewControl.OptionControl


That explains it. Glad I’m not going crazy. :stuck_out_tongue_winking_eye:


That worked. Next question, if I may. :thinking:

I’ve added an InputComparator and OutputScope, and connected the Comparator out to the unit out.

  -- create objects
  local compare = self:createObject("Comparator","compare")
  -- connect inputs/outputs

I can see the indicator behaving like I’d expect when I switch modes (just feeding a clock in). But the unit seems to not have any output. Is that what you’d expect?


Please disregard. Rookie mistake; all sorted.


Guess I will share this here for anyone who wants to learn along with me and compare notes. This Compare unit is just a straightforward unit-ized version of the Comparator class with the ModeSelect, Threshold, and Hysteresis exposed as controls. You find the Comparator used a lot in the builtins to create pieces of other units, like for example in the ADSR, Counter, etc. but there’s not a UI unit for it (well I guess there kind of is now…)


Joe-s-Bespoke-ER-301-Units.zip (21.5 KB)

Unzip to SD /ER-301/libs. It’s in the Experimental category.


Thanks for sharing these Joe :+1:


A few discussion points follow for me.

After reading the wikipedia article on hysteresis, I assumed it would be a time-based parameter. E.g. if a rising edge event was triggered, and the setting was 30msec, then another rising edge event triggered within 30msec would be ignored. But it doesn’t seem to work like this.

I put an offset as an input into the unit. With threshold of 0.1, hysteresis of .03, and mode=gate, it takes a rising edge reaching 0.13 to engage the gate, and a falling edge value of 0.07 to disengage it. So hysteresis seems to be more of a value parameter, added to and subtracted from threshold for rising and falling edge respectively. I guess this makes good sense, but not sure I’d have understood without creating the UI unit.

Here are a couple more general questions about controls in the middle layer. Assuming the following code:

  controls.hyst = GainBias {
    button = "hyst",
    description = "Hysteresis",
    branch = self:getBranch("Hyst"),
    gainbias = objects.hysteresis,
    range = objects.hysteresis,
    biasMap = Encoder.getMap("[0,0.1]"),
    biasUnits = app.unitSecs,
    initialBias = 0.03

  controls.thresh = GainBias {
    button = "thresh",
    description = "Threshold",
    branch = self:getBranch("Thresh"),
    gainbias = objects.threshold,
    range = objects.threshold,
    biasMap = Encoder.getMap("[0,1.0]"),
    initialBias = 0.10

For the first block, I was trying to set the initialBias to 0.03. It actually defaults to 29.00ms. I have seen this in other middle layer explorations, where the exact value specified does not get set. It seems to (as a guess) get rounded based on whether you’re in fine/coarse adjustment mode, and I think it always defaults to coarse? Is there a way to specify fine/coarse as the default adjustment mode for a control? Is there a way to set the initial value precisely?

In the second block, I would have thought this would have created a control that goes from 0 to 1.0. But it actually created a control that goes from -1.0 to 1.0. Is there a way to achieve the former?


In your second question, you are asking Encoder.getMap() for a map that does not exist. So it returns nil and in the end the default map is used. Look inside the Encoder module for a list of the pre-defined system maps. You
are free to also create your own.

The answer to your first question is quite complicated and probably requires adding the ability to temporarily disabled a control’s encoder map until you move the encoder.

For now though, you can create your own encoder map and just make sure that the desired value is one of the values in the map’s look up table. This also has the benefit that the user can always return to this default value that you have chosen.


Thanks Brian! :slight_smile: So, how does the interaction between the encoder maps and the fine/coarse adjustment modes work? Does coarse mode just skip some of the encoder map values, fine skips fewer, holding shift while in fine mode skips none? If so, is that relationship between how many map values are skipped in, say, coarse mode, define somewhere in the code, or is does the OS make the decision in real time?


Encoder maps are kind of like wavetables. Some maps are interpolated and some are not. Moving the encoder moves the read head by an amount that depends on the coarse/fine setting and is determined when creating the encoder map. The super fine phase increment is always 10% of the fine phase increment.

Warning. I’m not in front of my computer, so I’m answering this from my memory of the code which is not infallible. :wink:


Hehe, well that’s more complex than I thought it was. :slight_smile:

I have tried a couple times to create my own encoder maps, but I’m just missing something. (I guess maybe I need to actually buy that book on Lua… :thinking:). Here’s a current code snip:

  controls.thresh = GainBias {
    button = "thresh",
    description = "Threshold",
    branch = self:getBranch("Thresh"),
    gainbias = objects.threshold,
    range = objects.threshold,
    biasMap = self:linMap(0,1.0,0.001),
    initialBias = 0.10

  return views

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

It seems I can’t use linMap from Encoder.lua because it’s not exported/made public. So I am trying to copy/paste the function into my own lua file and reference it there. I’ve tried a few different ways. This one doesn’t return an error, but results in a control with a value of zero that can’t be changed. Like it’s not getting any map at all. Any sage advice?


Why are you calling your linMap with n=0.001?


Sometimes the old synapses fire on their own. Sometimes they need a push. Thanks. :blush:


biasMap = self:linMap(0,1,1000),
    -- biasUnits = app.unitSecs,
    initialBias = 0.03

This encoder map should include my intialBias value of 0.03, but the default is still 0.029 when the unit loads. Sounds like this is fairly complicated and of course it’s probably not a huge deal. Just exploring and trying to figure things out.


Most likely 0.03 is still not in the map. Are you familiar with the pitfalls of floating point arithmetic?

Anyway I’ll take a look when I get a chance. This kind of thing should and probably can be made easier.