[linux-audio-dev] Quasimodo parameters, etc.

New Message Reply About this list Date view Thread view Subject view Author view Other groups

Subject: [linux-audio-dev] Quasimodo parameters, etc.
From: Paul Barton-Davis (pbd_AT_Op.Net)
Date: su joulu  12 1999 - 23:00:16 EST


[ LAD folk: I forwared this because it occured to me after writing it
  that it might be of interest to some of you. Its something of an
  expansion on a post I made last week. If it bores the pants off you,
  please excuse the intrusion. --p ]

OK, I will try to clear up some misconceptions and confusion that
Stephane (and probably others) have about Parameters in
Quasimodo. I'll have to start with a basic summary of how Quasimodo
works.

The fundamental unit of execution in Quasimodo is an opcode
function. This is a regular C/C++ function which takes a
ptr-to-an-OpcodeArgument as its argument (more precisely, a pointer to
a class derived from OpcodeArgument). The members of this structure
contain the data needed by the opcode to do whatevers it is going to
do. Many, and often all, of these members are numeric values, either
scalars ("Control values"), or arrays ("Audio values"). To avoid
data copying, the members are actually pointers to numeric
values. Currently, they are almost all of type Number, which is
typedef'ed to be a float. There is a hack to encode string values into
pointers to a float, but its totally irrelevant and confusing for the
purposes of this message.

So, to execute an opcode successfully, we have to set up both its
OpcodeArgument-derived argument, some memory to hold the numeric
values, and then set the members of the OpcodeArgument to point to the
right locations in our memory space. Then we can just call the opcode
function with the argument and we're done. Easy.

Well, not quite. A Module contains a ProgramTemplate. A
ProgramTemplate is (put simply) a linked list of
DspInstructionTemplates, each of which contains an pointer to an
opcode information structure, and pointers to the Parameters that will
be used for the input and output arguments to be used by the opcode
function.

The task of setting up the OpcodeArgument members really comes down to
looking at the Parameters, and figuring out what address to use to set
the relevant member to.

If every Module was monophonic - that is, if there was only ever a
single DspProcess created for any module - and there was no
cross-module patching (like Csound), the process of mapping Parameters
to addresses would be simple. We wouldn't need a ProgramTemplate, just
its instantiated form (Program - see 2 paragraphs below). We would
just figure out at module compilation time how much data space the
module needed, allocate it, and then go through the Program,
fixing up each OpcodeArgument in turn to use pointers into the
allocated space.

However, neither of things is true - Modules are polyphonic by
default, and we do have cross-module patching. The polyphonic aspect
means that we can't allocate the memory for the data values on a
per-module basis. Why ? Because each "voice" must have its own
independent parameters, of which "pitch" is the most obvious. So we
instead defer this until we create an actual DspProcess. Thats why we
use a ProgramTemplate - its essentially a form a position-independent
code. However, a DspProcess doesn't contain a
ProgramTemplate. Instead, it contains an actual Program - a pair of
linked lists of DspInstructions - and a data memory area.

Each DspInstruction contains a pointers to the relevant opcode
function, and the argument structure for the opcode. So, when we
convert a ProgramTemplate into a Program, we convert all uses of the
Parameters into pointers to specific addresses. Most of these
addresses will be within the DspProcess data block. A few will point
to reserved variable locations (e.g. if you use "ksmps" or "sr"), or
to other "external" memory, such as MIDI controller values (if you use
"c23", "c45" etc).

How are the Parameters converted into addresses ? Each Parameter
contains a DspOffset member. A DspOffset is basically a number
combined with a type. If the type was "pointer", then the number is a
raw C/C++-level pointer value. If the type was "data", the number is
an offset in bytes from the start of the DspProcess' data area where
the Parameter's data can be found. And so on. There aren't many offset
types. Converting a DspOffset into a real C/C++ address is clearly
pretty easy once you know the address of the DspProcess data area.
 
So, this is the normal way things happen. Once this link-editing is
done, the DspProcess is ready to run: we have a 2 linked lists of
pointers to functions, plus a set of structure arguments whose members
point to real data. Why *2* linked lists: one of init-time functions,
and the other of run-time functions.

OK, when the DspProcess starts, the first thing that happens is that
the DSP object executing the Process calls DspProcess::init(). This
causes the DspProcess to traverse the init-time function pointer list,
calling each opcode function in turn, passing it the appropriate
argument structure, which in turns contains pointers to the relevant
data locations. Once this is successfully done, the DspProcess enters
the DSP object's work queue. For each iteration of the DSP main
loop, the DSP object will call DspProcess::execute(). This causes the
run-time function pointer list for the DspProcess to be executed once
per iteration, subject to any control-flow stuff done by the opcodes
themselves (e.g. kgoto, etc.)

Great, so now we're all clear on how things happen without patching,
right ? Well, lets just take a little example to make sure its clear.
Consider the statement:

         kvar init 0

This will be compiled down to:

        * a function pointer to "void kinit(AOP *)" in the init-time function
          ptr list
        * an AOP structure containing a

                 Number *r;
                 Number *v;
      
          "r" will point to the per-process data address corresponding
          to kvar; "v" will contain a pointer to an address
          containing 0.0f

When we run DspProcess::init(), we will call kinit(AOP *), and after
this, the per-process data corresponding to kvar will contain 0.0f.
          
All with me ? OK. Now lets suppose that the interface for the Module
containing the statement above defines a knob to set the value of
"kvar". Whenever the UI decides it is appropriate, it will call
Module::parameter_edit (Parameter &, Datum &newval). A Datum is a
basic class that can hold a float or a string value, and be used
interchangeably as either (more or less).

What does parameter_edit() do ? Well, it does 2 critical things:

     1) it traverses the set of currently active DspProcesses for the Module.
        For each DspProcess, it adjusts the numeric value stored in
        the part of per-process data area corresponding to the Parameter.

     2) it looks at the Parameter, which was set up during Module
        compilation with a pointer to the DspInstructionTemplate that
        is "responsible" for initializing the Parameter during
        DspProcess::init(). It takes the first input argument to that
        DspInstructionTemplate, and alters it to point to the value
        given by the Datum.

(it also drops every "zombie process" - DspProcesses that could
otherwise be reused by later requests for a DspProcess instantiating
this Module - because doing so is quicker and more efficient than
having to edit them as well; they contain the old value for the
Parameter, which would spell trouble (and did, before I realized this
a few months back).

Now, clearly, step 2 is problematic. It represents a very important
and not very general assumption that the initializing opcode for the
Parameter is something like "init", and that its argument is a
constant that can easily be replaced. Consider:

         kvar init 3/5

In this case, the constructor will be init, but init's first input
argument will be a "temporary" holding the value of "div 3, 5". In
this particular case, replace the DspOffset that refers to the
temporary is fine (though it will leave us with some "stranded"
init-time functions that are never executed anymore), but its not hard
(I think) to come up with some other examples that would not work so
well. This is particularly true of Csound-init-time variables.

Anyway, the net result is that the Module has just been edited so that
not only all currently running DspProcesses have the new value, but
any newly instantiated DspProcesses will also set the Parameter to the
new value during initialization.

Lets move on to patching, which is both simpler and more complex.

The fundamental goal of the patching system is to avoid data
copying. As far as I know, Quasimodo is unique in its approach to
this. Modules/DspProcesses do *NOT* pass data around - they share data
pointers, just like any sensible C/C++ programmer would do if they
were writing equivalent code. How do they do this ?

To be able to use a Parameter for patching, something (typically the
UI/front end) needs to call Parameter::mark_as_patchpoint(). This
gets the Parameter allocates some memory to hold its value. Recall
that before this, the Parameter would have probably had a DspOffset of
type "data", meaning that its memory location is somewhere in the
per-process data area of the executing DspProcess. After this step,
the DspOffset is of type "pointer", and the "number" part of the
DspOffset points directly to the memory allocated by the
Parameter. This means that we can share this data location across (1)
all DspProcesses instantiating the Module to which the Parameter
belongs, and (2) share it across all other DspProcesses.

(1) is obvious, I hope. (2) is probably not. How do we accomplish this?

We do a little trick called "run time editing". When a patch is
created, the destination Parameter causes its Module to traverse all
currently active DspProcesses, and check every DspInstruction in their
init-time and run-time lists. If the Instruction uses the Parameter as
an argument, then the argument is replaced with a pointer to the
originating Parameter's data location. Result ? the patched-into
Module's DspProcess' opcodes see the same data location as the
patched-from Module's DspProcess' opcodes. Whether they read or write,
they are sharing the data. There is no data copying.

All with me still ? So, now we have two DspProcesses running on a DSP,
and sharing data via a Parameter that had mark_as_patchpoint() called
upon it. Excellent.

But what happens when the originating DspProcess shuts down ? The data
storage associated with the Patch won't go away - its fundamentally
associated with the Parameter, not the DspProcess - but the value left
in it will be the last thing written (presumably by the originating
DspProcess). This can lead to some very ugly results. To stay roughly
in keeping with the analog model Quasimodo is emulating, when a
DspProcess shuts down, all Parameters that were marked as (output)
patchpoints are set to zero. However, this happens only once *PER
MODULE PER DSP MAIN LOOP*. This means that if the Module is
polyphonic, other still-running DspProcesses will still get their
chance to write *their* data into the Parameter/patchpoint memory
location, and the receiving Module/DspProcesses will get that
instead. If the Module had only a single DspProcess running, then the
zeroing has effectively turned off all power to the Module, and so
complete silence (or zero values for scalar "control" elements) is all
that we can "hear" on all patchcords originating from the that
Module. The effect is intended to be similar to what one would expect
if you could create a patch where the originating point was "nowhere"
- you'd get no signal at all.

Its also important to realize the that this zeroing of patchpoint data
is *vital* for polyphony. There is a page in the HTML manual about
this. Polyphonic modules are required to use additive methods of
writing to their output sockets, because otherwise, only the last
active DspProcess will get its output to be "audible". Because of
this, it is critical that the data associated with each Parameter
being used as an output patchpoint is zeroed on each iteration of the
DSP main loop; without it, we would end up with numerical overflow and
sonic distortion.

Lets take another look at a concrete example that Stephane asked
about:

>Could you explain me what happens exactly when executing a program ?
>
>look at this:
>
>Module 1
>kparam init 0; actualy controlled by a knob and reflect by an output
>socket
>
><knob param="kparam"/>
><socket param="kparam" direction="output"/>
>
>Module 2:
>kfreq init 0
>aout rtsin kfreq, 32000
>
><socket param="kfreq" direction="input"/>
><socket param="aout" direction="output"/>
>
>Module 3:
>outs aleft, aright
>
><socket...
>
>
>there is a patch between the kparam and kfreq
>and a patch between aout and aleft.
>
>what happens when the program is executed ?

1) something calls Module::turnon() (MIDI/the on-off button)
2) an Execute request is sent to the DSP object, naming the Module
3) the DSP calls Module::checkout_process(), which returns a new DspProcess
4) the DSP calls DspProcess::init()
5) the DSP puts the DspProcess into its Work queue
6) on every iteration, the DSP calls DspProcess::execute() until
   the DspProcess is no longer runnable.

>what buffers are created ?

for Module 1: a per-process data location (sizeof(float) for kparam (never used)
              a shared location for kparam (because it was marked as a
                                            patchpoint)

for Module 2: a per-process data location (sizeof(float) for kfreq (never used)
              a shared location for kparam (because it was marked as a
                                            patchpoint)
              a per-process data location (sizeof(float)*CycleFrames) for
                            aout (never used)
              a shared location for aout (because it was marked as a
                                            patchpoint)

for Module 3: a per-process data location (sizeof(float)*CycleFrames) for
                            aleft (never used)
              a shared location for aleft (because it was marked as a
                                            patchpoint)
              a per-process data location (sizeof(float)*CycleFrames) for
                            aright (never used)
              a shared location for aright (because it was marked as a
                                            patchpoint)

Once the patch aout -> aleft is set up, any DspProcess for Module 3
will be executing a DspProgram in which all pointers to the shared
location of Module 3/aleft now point to the shared location associated
with Module 2/aout.

Once the patch for kparam -> kfreq is set up, any DspProcess for
Module 2 will be executing a DspProgram in which all pointers to the
per-process location of kfreq now point to the shared location
associated with Module 1/kparam.

>when values are copied, if they are ever ?

No data copying.

>is there a buffer copy in the audio case ?

No data copying.

I hope this clears up some confusion. It may also help others to get
up to speed on some of the more complex aspects of Quasimodo's
internals. It may be that there is a fatal flaw in all this, but if
so, getting other people to critique it is the only way I'll ever know :)

--p

 


New Message Reply About this list Date view Thread view Subject view Author view Other groups

This archive was generated by hypermail 2b28 : pe maalis 10 2000 - 07:23:26 EST