Sig
- 1. Overview/Top
- 2. Getting in the weeds
- 3. New
- 4. Hold
- 5. Hold (to external buffer via cabnew)
- 6. Hold Data
- 7. Unhold
- 8. Get
- 9. Getstr
- 10. zero
- 11. Send
- 12. Throw
1. Overview/Top
This module contains helper functions that assist in signal management in sndkit.
signal management
refers to any non-trivial use of a signal that
requires manual intervention. In sndkit, signals become "non-trivial"
quite quickly; any signal that wants to be used more than once in a way
that can't be done using stack operations can be considered non-trivial.
Sig = {}
function lil_default(s)
if (type(s) == "table") then
s = table.concat(s, " ")
end
lil(s)
end
<<sig>>
return Sig
2. Getting in the weeds
Here's some low-level details about how signal management works in sndkit.
Buffers, blocks of signal that unit generator nodes can read/write to, are finite, and managed in a pre-allocated resource pool. Any time a signal is used more than once in a patch, special care must be put into ensuring that the underyling buffer the signal is in is marked as in use throughout the lifetime of the signal, then returned back to the pool when the signal is no longer being used.
Buffers are stored using a register system that sndkit employs, which are referenced by index. Similar to buffers, registers can be marked as in-use and then cleared when they are no longer used, which allows for a process to go in and find the next available free register without worrying about exaclty which register it is.
So, the process of storing a signal is a matter of finding an available buffer to write to from the buffer pool, marking the buffer as in-use (unavailable to others), writing the signal to that buffer, finding an available register to write to, marking the register as in-use, and then storing a reference to the buffer in the register. Using the signal is a matter of looking up the buffer from the register, and the pushing it onto the the stack that sndkit uses to pass around data. A signal is successfully released by marking both the buffer and the signal as available for use again.
A the sndkit layer of abstraction, finding an available buffer from
the buffer pool is pretty transparent. It is a process handled
by another low level component of sndkit called graforge
.
3. New
Creates a new instance of a signal.
function Sig:new(o)
o = o or {}
o.reg = -1
setmetatable(o, self)
self.__index = self
return o
end
4. Hold
Takes the last item off the buffer stack, holds it, and then stores it in a free register.
lil_eval
is an optional callback that can replace the
default lil
evaluator. It can be used for debugging
purposes. Usually something like print
would be used.
Not that in order for the register marking to work,
actual lil code must be evaluated. These bits of code
are always actually called by lil
, and will also
be called by lil_eval
if they are different functions.
function Sig:hold(lil_eval)
-- can be a callback used to simulate holding
lil_eval = lil_eval or lil_default
if self.reg >= 0 then
error("can't hold, already holding")
end
-- regnxt actually has to be called to see if it is
-- working
local lstr = "param [regnxt 0]"
-- if lil_eval ~= lil then
-- lil_eval(lstr)
-- end
lil(lstr)
local reg = pop()
if reg < 0 then
error("invalid index")
end
-- hold/regset can be simulated without issue
lil_eval({"hold", "zz"})
-- lil_eval("hold zz # sig")
lil_eval({"regset", "zz",reg})
-- regmrk actually has to be called for it to work
-- local lstr = string.format("regmrk %d", reg)
local lstr = {"regmrk", reg}
if lil_eval ~= lil then
lil_eval(lstr)
end
lil(table.concat(lstr, " "))
-- lil(string.format("regset zz %d; regmrk %d", reg, reg))
self.reg = reg
end
5. Hold (to external buffer via cabnew)
TODO: refactor repeated code logic.
function Sig:hold_cabnew(lil_eval)
-- can be a callback used to simulate holding
lil_eval = lil_eval or lil_default
if self.reg >= 0 then
error("can't hold, already holding")
end
-- regnxt actually has to be called to see if it is
-- working
local lstr = "param [regnxt 0]"
-- if lil_eval ~= lil then
-- lil_eval(lstr)
-- end
lil(lstr)
local reg = pop()
if reg < 0 then
error("invalid index")
end
-- cabnew: allocates to extbuf
lil_eval({"cabnew", "zz"})
-- hold/regset can be simulated without issue
lil_eval({"hold", "zz"})
-- lil_eval("hold zz # sig")
lil_eval({"regset", "zz",reg})
-- regmrk actually has to be called for it to work
-- local lstr = string.format("regmrk %d", reg)
local lstr = {"regmrk", reg}
if lil_eval ~= lil then
lil_eval(lstr)
end
lil(table.concat(lstr, " "))
-- lil(string.format("regset zz %d; regmrk %d", reg, reg))
self.reg = reg
end
6. Hold Data
function Sig:hold_data(lil_eval)
-- can be a callback used to simulate holding
lil_eval = lil_eval or lil_default
if self.reg >= 0 then
error("can't hold, already holding")
end
-- regnxt actually has to be called to see if it is
-- working
local lstr = "param [regnxt 0]"
-- if lil_eval ~= lil then
-- lil_eval(lstr)
-- end
lil(lstr)
local reg = pop()
if reg < 0 then
error("invalid index")
end
lil_eval({"regset", "zz",reg})
local lstr = {"regmrk", reg}
if lil_eval ~= lil then
lil_eval(lstr)
end
lil(table.concat(lstr, " "))
self.reg = reg
self.is_data = true
end
7. Unhold
Unholds the underlying signal (buffer), if there is one to be unheld.
Just like hold
, lil_eval
is a an optional function
that overrides the default lil
evaluator, and was
originally used for debugging purposes.
function Sig:unhold(lil_eval)
lil_eval = lil_eval or lil_default
if self.reg < 0 then
error("no signal to unhold")
end
-- lil_eval(string.format("unhold [regget %d]; regclr %d",
-- self.reg, self.reg))
if self.is_data ~= true then
lil_eval({"regget", self.reg})
lil_eval({"unhold", "zz"})
end
lil_eval({"regclr", self.reg})
self.reg = -1
end
8. Get
Gets the signal and pushes it onto the buffer stack.
function Sig:get(eval)
if self.reg < 0 then
error("no signal")
end
if eval == nil then
eval = lil
end
-- eval(string.format("regget %d", self.reg))
local s = {"regget", self.reg}
if eval == lil and type(s) ~= "string" then
s = table.concat(s, " ")
end
eval(s)
end
9. Getstr
This returns the string of LIL code that, once evaluated, would push the signal onto the stack.
function Sig:getstr()
if self.reg < 0 then
error("no signal")
end
--return string.format("[regget %d]", self.reg)
return {"regget", self.reg}
end
10. zero
Creates and holds an auxilliary cable to be used for sends and throws. It starts of with no signal, hence the name "zero".
function Sig:zero()
if self.reg >= 0 then
error("A signal is already being held")
end
lil("zero")
self.hold(self)
end
11. Send
Pops the last signal off the stack and mixes it into the internal cable.
"Gain" is a attenuation value in db units. By default it is 0 (full scale).
function Sig:send(gain)
if self.reg < 0 then
error("no signal")
end
gain = gain or 0
lil(string.format("mix zz [regget %d] [dblin %g]",
self.reg, gain))
end
12. Throw
Like send, but instead of popping the signal off the stack, it dups it first, keeping a copy of the signal on the stack.
function Sig:throw(gain)
if self.reg < 0 then
error("no signal")
end
lil("dup")
self.send(self, gain)
end