Re: [linux-audio-dev] Problems and Solutions: events v. signals

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

Subject: Re: [linux-audio-dev] Problems and Solutions: events v. signals
From: Roger Larsson (roger.larsson_AT_norran.net)
Date: ke tammi  12 2000 - 17:49:34 EST


David Olofson wrote:
>
> On Sun, 09 Jan 2000, Roger Larsson wrote:
> > (David, you have received another one - delete that one)
> >
> >
> > Problem: events and signals are treated in different ways. Events may
> > update signal buffer locations (as suggested). But a plugin have to
> > handle [other] events and signals in different ways.
>
> It has to handle signals and events differently, no matter the API.
> Signals are pointers into "endless" arrays of data, and my "new
> buffer pointer" events are used to move the pointers around to keep
> the plugin or client reading and writing in the right places.
>

Ok, I was not clear.
I suggest only one event type == signal update.
 
> > Observation:
> > What is the difference between a signal and an event?
> > Not much!
> > Events happens rarely.
> > There might be a lot more event destinations than signal/connectors.
>
> IMHO, an event tells something about an "unusual" change, while a
> signal is a continous stream of samples, approximating a continous
> signal. You never know when you're going to receive an event (unless
> you peek in the buffer ;-), but you do know that there is signal data
> to process on all channels until the time of the next event.

Ok, but lets take a pitch control.
Should moving it it result in events or a sampled signal?
Usually nothing happens then suddenly it really is used.
When would you then create events? every time its not the same as the
previous? or difference bigger than D?
With sampling you decide the frequency once!
The same amount of data is sent all the time, plugins get the same
amount
of data all the time - no risk of overload due to external activity!

The externally sampled data can be stored and played back later with the
same result.

Sampling once per fragment would allow cheap processing - for the plugin
it would almost look like constant values, check assign once.

Problem:
* complex net with many rarely changed inputs might waste a lot of
work.
* how to handle a keyboard? one output signal per key?

>
> > Solution: [to original problem]
> > All events can be treated as an input signal with per fragment varying
> > sample rate, if everything that an event might change are assigned to
> > a signal/connector of its own.
> > This way all parameters used by a plugin may be a signals too :-)
>
> Cool in theory, but there are some serious problems IRL...

I have used a image processing system that does this, WiT by
LogicalVision
(http://www.logicalvision.com), and it is more than cool - it is
close to necessary!

>
> > normal signal event chain:
> > {timeN, signal#A, *buffA1} {timeN+time(buffA1), signal#A,*buffA2}
> >
> > event, parameter change, signal chain:
> > {timeM, param#P, value1} {timeN, param#P, value2}
> >
> > If 'time' is treated as described earlier in "P&S: time" there is no
> > longer any need to distinguish between signals and parameters. They get
> > with different time formats.
>
> ...and these time formats have to be handled, by hosts as well
> plugins and clients. Great fun having to keep track of N signals that
> can assume any sample rate the engine considers suitable, right? :-)
>

Not really!

The engine can only choose among those combinations that the plugin
accepts!
Thus, the plugin may require that all signals(in/out) uses the same
frequency.

> > Note:
> > * a signal that is event driven. Can normally be driven with a constant.
> > and only during fragments where something happens it changes to a
> > sampled
> > signal format. (even if sampled all the time)
> > {durationA1, variable#A, *buffA1} {durationA2, variable#A,*buffA2}
>
> Who tells you that the signal has changed it's rate? Do you have to
> check all ports on every call to find out?
>

The sample rate is a part of each signal fragment.
 
> And the engine has to generate lots of data just to get that one
> control signal change in the right place. And what about two
> changes, or three? That requires the signal sample rate to be the
> lowest common denominator of the change times...

>
> > * event/signal queue gains by sorting it in variable# as first key.
> > Times are always deltas - not sorted explicitly. Arrive sorted!
>
> This is no different from my system; you are required to send events
> in order, and the timestamps are taken in account when merging
> events from multiple sources for a plugin or client.
>
> > * The plugin _may_ chose different implementation depending on input
> > 'times'.
>
> Is that a runtime choice, or what exactly are you referring to
> here...?
>

When you get a new fragment you check if it has the same frequency as
the
previous, true continue with the current implementation.
False update plugins instance process pointer with a better version, and
call it.

> > * Several outputs can not be connected to one input can not be connected
> > directly.
>
> This is good from the engine POV, but it does mean that plugins get
> multiple input ports. That situation is a complexity nightmare right
> where things should be kept simple, especially if these ports can
> have different sample rates.
>
You have to store a pointer to the buffer anyway before returning.
=> all buffer pointers needs to be able to store in the instance.
=> they are the signals!
When a signal has no remaining samples you check your event buffer
for events for that signal. Update the buffer pointers accordingly.
Check changes in sample frequency. Continue processing.

In fact I am planning this slightly different.

When a signal fragment ends (several would end at the same time
if the same fragment size is used) the plugin returns. It has
processed a fragment. Signals are not stored in the instance
they are shared with the engine.

The code should look something like this (inlining done):

void fload_add_process(this, connectors)
{
  // if allowing frequency changes
  float frequency = connectors.signals[1].frequency;
  if (frequency != connectors.signal[2].frequency ||
      frequency != connectors.signal[3].frequency) {
    float_add_dispatch(this, signals);
    return;
  }

  int remaining = connectors.signals[1].remaining;
  assert(remaining == connectors.signals[2].remaining);
  assert(remaining == connectors.signals[3].remaining);

  float *pA = connectors.signals[1].pBuffer;
  float *pB = connectors.signals[2].pBuffer;
  float *pR = connectors.signals[3].pBuffer;

  while (remaining--) { // or some SMD(sic? = MMX2) instruction
    *pR++ = *pA++ + *pB++;
  }

  connectors.signals[1].remaining = 0;
  connectors.signals[2].remaining = 0;
  connectors.signals[3].remaining = 0;
}

// The engine then checks signals:
// - outputs with nothing remaining are full and can be
// forwarded to next plugin.
// - plugins with something on all signals can be run.

Lets see - what is the overhead:
Suppose very short fragments: remaining < 10
* indirect call
    R0 = read stack relative #this
    R0 = read R0 relative #common.process
    call R0
  => always done
     3 instructions,
     2 reads
     1 indirect call (expensive)

* The frequency check is not needed if this implementation only
  accepts equal frequencies.
* Get remaining (R4):
    R0 = read stack relative #connectors
    R4 = read R0 relative #(signals+k*sizeof(signal)+remaining)
  => equals 2 instructions, and 2 memory references
* Get buffer pointers (R1..R3)
    R0 already read
    R1..R3 = read R0 relative #(signals+k*sizeof(signal)+pBuffer)
  => gives 3 instructions, and 3 memory references

* Processing: always done, no _extra_ cost
    test R4
    jump if zero to end // should never happen
  again:
    R5 = read R1 relative; inc R1
    R6 = read R2 relative; inc R2
    write R3 relative = R6; inc R3
    dec R4
    jump if not zero to again
  end:
  => gives remaining*(5..8) instructions,
           remainging*2 reads,
           remaining writes
           remaining jumps

* Write back data
    R0 already read (unless used)
  3*write R0 relative #(signals+k*sizeof(signal)+remaining)
  => gives 3 (or 4) instructions

* Cache and chipset utilization can be improved by changing
  connector layout to
    connector.signal.remaining[k]
  gives only one cache miss and write combining.

=> 2+3+4=9 instructions overhead compared to 5 * fragment size
   instructions processing.
   => with fragment sizes of two are overhead and processing
      comparable...

> > * One output can be connected to several inputs.
>
> Good - no data copying.
>
> > Remaining problems:
> > Will be expensive to check every fragment if there are
> > lots of channels/signals...
>
> Yep, very expensive, I'm afraid. Some smart optimization will be
> needed just to find out which channels to check in the start of each
> plugin call, or client "cycle".
>

Maybe not that expensive... You only need to check the one expiring.
And you can specify that your plugin does not handle frequency changes
or even not handling different frequencies.
[I refined my solution above]

--
Home page:
  http://www.norran.net/nra02596/


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