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

Middle Layer SDK (aka patching with Lua)

feedback

#21

Hehe, I see where out interpretations differ :crazy_face:

@udbhav?


#22

Creating sample chains outside of the 301 is certainly a viable solution. However, it requires planning and forethought. I often want to have a rhythm with say 3 different sounds driven by a single trigger sequence, but I won’t know which 3 sounds until I have a musical context to help me make my decision.

Realize you can patch this up with existing units, but its a bit cumbersome, especially when you’re talking about > 2 samples. I do have one custom unit that does this that I use for doing closed and open hihats. Was thinking if there was a way for me to build something generic using the SDK so doing more traditional tasks like round robin sampling or sample “zones” was a possibility on the 301.


#23

For one there’s this bit on the feature request page: "many audio files for one slice file"
I would very welcome this.
Maybe a sample player that can address multiple samples via cv while keeping the slice functionality would be an alternative.


#24

True! I just figured I could use this need as an excuse to poke around the SDK, but it seems like not the right use case.


#25

I was reading the CustomUnit source code, and found CustomUnit:insertCustomization to be pretty interesting. Will Chain.Patch will let us programmatically define nested structures inside the units we make?


#26

I’ve added a Countdown unit to the testlib as an example at the request of @kel. I’ve tried to comment it a little bit more for clarity but obviously there is still more to be said.

-- GLOBALS: app, connect, tie

-- The Class module implements basic OOP functionality
local Class = require "Base.Class"
-- The base class for all units
local Unit = require "Unit"
-- A graphical control for the unit menu
local ModeSelect = require "Unit.MenuControl.ModeSelect"
-- A graphical control for comparator objects on the unit input
local InputComparator = require "Unit.ViewControl.InputComparator"
-- A graphical control for gainbias objects
local GainBias = require "Unit.ViewControl.GainBias"
-- A graphical control for comparator objects
local Comparator = require "Unit.ViewControl.Comparator"
-- Needed to get access to the pre-defined encoder maps
local Encoder = require "Encoder"

-- Create an empty class definition for the Countdown unit
local Countdown = Class{}
-- Use inclusion to effectively inherit from the Unit class
Countdown:include(Unit)

-- The constructor
function Countdown:init(args)
  -- This is the default title shown in the unit header.
  args.title = "Count- down"
  -- This is the 2 letter abbreviation for this unit.
  args.mnemonic = "Cd"
  -- Optionally, set a version number unique to this unit.
  args.version = 1
  -- Make sure to call the parent class constructor also.
  Unit.init(self,args)
end

-- This method will be called during unit instantiation. Create the DSP graph here.
-- pUnit : an object containing the unit's input and output ports.
-- channelCount: used to determine if we are building a mono or stereo version of this unit.
function Countdown:onLoadGraph(pUnit,channelCount)
  -- The createObject method is used to instantiate and name DSP objects.

  -- Create a Comparator object for digitizing the incoming signal into triggers.
  local input = self:createObject("Comparator","input")
  -- A comparator has trigger, gate and toggle output modes.  Configure the comparator to output triggers.
  input:setTriggerMode()

  -- Create a Counter object, turn off wrapping and set its initial parameter values.
  local counter = self:createObject("Counter","counter")
  counter:getOption("Wrap"):set(app.YesNoChoices.no)
  counter:hardSet("Gain",1)
  counter:hardSet("Step Size",-1)
  counter:hardSet("Start",0)

  -- Here we connect the output of the 'input' comparator to the input of the 'counter' object.
  connect(input,"Out",counter,"In")

  -- Since a comparator only fires when the threshold is exceeded from below, let's negate the output of the counter.
  local negate = self:createObject("ConstantGain","negate")
  negate:hardSet("Gain",-1)
  connect(counter,"Out",negate,"In")

  -- Create another Comparator object for the output.
  local output = self:createObject("Comparator","output")
  output:setTriggerMode()
  output:hardSet("Threshold",-0.5)
  connect(negate,"Out",output,"In")

  -- And yet another Comparator object for the reset control.
  local reset = self:createObject("Comparator","reset")
  reset:setTriggerMode()
  connect(reset,"Out",counter,"Reset")

  -- We need an external control for setting what value to start counting from.
  local start = self:createObject("ParameterAdapter","start")
  --Give it an initial value, otherwise it will be zero.
  start:hardSet("Bias",4)

  -- Unlike audio-rate signals, parameters are tied together like this slave parameter to master parameter.  Think of it as an assignment.
  -- Note: We need to use the counter's Finish parameter because our step size is negative.
  tie(counter,"Finish",start,"Out")

  -- Register sub-chains (internally called branches) for modulation.
  self:addBranch("start","Start",start,"In")
  self:addBranch("reset","Reset",reset,"In")

  -- Finally, connect the output of the 'output' Comparator to the unit output(s).
  connect(output,"Out",pUnit,"Out1")
  if channelCount > 1 then
    connect(output,"Out",pUnit,"Out2")
  end

  -- Force a reset to occur, so that the counter is ready to go.
  reset:simulateRisingEdge()
  reset:simulateFallingEdge()
end

-- Describe the layout of the menu in terms of its controls.
local menu = {
  "infoHeader","rename","load","save",
  "wrap",
  "rate"
}

-- Here we create each control for the menu.
function Countdown:onLoadMenu(objects,controls)
  controls.wrap = ModeSelect {
    description = "Wrap?",
    option = objects.counter:getOption("Wrap"),
    choices = {"yes","no"}
  }

  controls.rate = ModeSelect {
    description = "Process Rate",
    option = objects.counter:getOption("Processing Rate"),
    choices = {"frame","sample"}
  }

  return menu
end

-- Describe the layout of unit expanded and collapsed views in terms of its controls.
local views = {
  expanded = {"input","count","reset"},
  collapsed = {},
}

-- Here we create each control for the unit.
function Countdown:onLoadViews(objects,controls)

  -- An InputComparator control wraps a comparator object (passed into the edge argument).
  controls.input = InputComparator {
    button = "input",
    description = "Unit Input",
    unit = self,
    edge = objects.input,
  }

  -- A GainBias control wraps any object with a Bias and Gain parameter.
  controls.count = GainBias {
    button = "count",
    description = "Count",
    branch = self:getBranch("Start"),
    gainbias = objects.start,
    range = objects.start,
    biasMap = Encoder.getMap("int[1,256]"),
    biasUnits = app.unitInteger,
  }

  -- A Comparator control wraps a comparator object (passed into the edge parameter).
  controls.reset = Comparator {
    button = "reset",
    description = "Reset Counter",
    branch = self:getBranch("Reset"),
    edge = objects.reset,
    param = objects.counter:getParameter("Value"),
    readoutUnits = app.unitInteger
  }

  return views
end

-- Don't forget to return the unit class definition
return Countdown

Programming Lua
#27

This is amazing - thank you very very much :crazy_face:


#28

Are you ready to start taking questions about this? :slight_smile:

I am starting to get my head around it. So let’s start with some basics.

Do each DSP objects that get instantiated in onLoadGraph have only one input and one output regardless of the channel count?

I assume that a DSP object can only have one connection to its input. But can outputs of DSP objects be connected to multiple destinations?

Are there any parameters or methods that are common to all DSP objects?

I see where you are instantiating a builtin unit and setting it’s parameters:

local counter = self:createObject(“Counter”,“counter”)
counter:getOption(“Wrap”):set(app.YesNoChoices.no)
counter:hardSet(“Gain”,1)

Are the parameter names going to be the same as what we see in the UI? Are they case sensitive?


#29

Absolutely.

A DSP object may have any number of inputs, outputs, parameters and options. Some objects have separate mono and stereo versions, where others are only mono (like a Multiply object) because you can just use two, and others take an argument on creation that sets the number of channels (like the Delay object used in the Delay unit).

Only common methods. I believe the only one that is necessary for building units is getOption(name). I could be wrong.

Actually, I’m instantiating an object not a unit. The unit namespace and the object namespace are completely separate.

No and yes.


#30

Getting clearer. :slight_smile:

What is “app”?

In the count down CU it is never defined that I can see, only referenced:

counter:getOption(“Wrap”):set(app.YesNoChoices.no)

In some of the built-ins it appears to be defined by referencing a global or parent (perhaps from Base.Class or Unit)?

local app = app

Oh, and:

Can you instantiate units and work with them in the middleware? Or only the objects the units are built from?

EDIT: One more. I am kicking around the idea of trying to build the poly 4 round robin sample player I made the video about a few days ago as a custom LUA unit. There seems to be interest in it, and I could probably build it in the UI in 5-10 minutes if I weren’t explaining it as I went. Is that a reasonable choice or would it be too complicated in the middleware?

If it’s a reasonable choice, can I just start another thread and start taking a stab at the various pieces and ask the 100+ stupid questions that I’m sure to have as I go out in the open (since others will probably have the same stupid questions too? :slight_smile: )


#31

Please do! Perfect use for this forum in my opinion.

I’m away from the computer (using phone) but when I get back to my workspace I will answer your other questions too.


#32

I, for one, am very interested to read about this on the forum. I’m a programmer myself, so I’m curious about your experiences with lua and setting this unit up.


#33

“There are only two hard problems in computer science: naming things, cache invalidation, and off-by-one errors.”


#34

In a nutshell, “app” is a global table containing functions and constants exposed from the C++ application layer. I use a C/C++ wrapping code generator called swig. There is also “os” which is also a global table containing functions and constants exposed from the operating system layer (implemented in C).

There is a LOT of stuff in these app and os modules (every container is a table in Lua). So right now the example code in builtins is your best friend.

The line local app = app is really just a bit of (probably pointless) optimization. I should have left it out. If you are curious you can read about it here.

You can instantiate units but the requirements for doing so are kind of hidden from you at the moment so I can’t recommend it. However, instead of encapsulation you can use inheritance to reuse an existing unit’s code (for example, builtins.Player.BasePlayer). This way you don’t need to know how to properly instantiate a unit (which requires knowing how to manipulate chains, an object that is not exposed to you, yet).

I haven’t thought through this in complete detail but I believe it is possible given the current set of DSP objects available. In terms of complexity, it is a bit on the advanced side for a first attempt but I’m here to support you. So let’s see what happens. I expect you will probably build a few simpler proof-of-concept units before you get to the final desired result. The sample-related units are without a doubt the most complicated but also a lot of fun.


Middle Layer Adventure - Revolver
#35

I found some time to read through this properly and it all makes some kind of sense, the notes are good and really helpful, thank you; but I am struggling to see where the functionality is, i.e. which part of the code does the ‘count down’

What am I missing?


#36

Think it’s this object, but I don’t think we don’t have access to the source of objects.


#37

Thanks @udbhav :slight_smile:

I thought this too, but then I don’t really understand how it switches the behaviour to only output a trigger when the countdown is complete.

I am sure @odevices will explain all :smiley:


#38

The output of the counter flows into a Comparator that fires a trigger when its input exceeds a given threshold:

  -- Create another Comparator object for the output.
  local output = self:createObject("Comparator","output")
  output:setTriggerMode()
  output:hardSet("Threshold",-0.5)
  connect(negate,"Out",output,"In")

#39

Ahhhh penny dropped - thank you!

So to make sure I’ve understood completely, the count is set to a default of 4 - it counts down using a step of -1 until the threshold of -0.5 is reached and then outputs a trigger.

If that’s right, then I think I understand everything.


#40

99% right. There is the nitpicking part where I negate the output of the counter because a comparator will only fire when you cross the threshold from below.