Sig

Sig

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.lua>>=
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.

<<sig>>=
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.

<<sig>>=
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.

<<sig>>=
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

<<sig>>=
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.

<<sig>>=
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.

<<sig>>=
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.

<<sig>>=
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".

<<sig>>=
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).

<<sig>>=
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.

<<sig>>=
function Sig:throw(gain)
    if self.reg < 0 then
        error("no signal")
    end

    lil("dup")
    self.send(self, gain)
end