[linux-audio-dev] a port/buffer proposal

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

Subject: [linux-audio-dev] a port/buffer proposal
From: Paul Davis (pbd_AT_Op.Net)
Date: Mon May 21 2001 - 16:59:09 EEST


Assumptions
-----------
In this model, a Port is an object representing audio input or
output. For any given port, there is a method of determining the
address of a memory region where the data associated with the port can
be read from or written to. All writing occurs via standard C operators
such as "=" or "+=".

A Connection is an object representing the notion of two ports being
connected, such that data written to an output port is available at
by reading from the input port. A Connection object consists of two
ports (one input, one output) and a gain value.

Every Port maintains a list of all the Connections it is involved in.

A Word On Language
------------------

In what follows below, I adopt some OOP language in which Ports are
active objects capable of "doing things". In a C API, this would be
handled via functions like port_do_something (ptr_to_port, ...).

The Model
---------

Even in the simplest signal graph, there are 3 kinds of connections
between Ports. Here, I propose

1) One-to-One

   P1 is an output port. P2 is an input port. P1 and P2 are connected.

   How it works:

   P1 requests a memory region from the object that manages
   such things. The plugin that owns P1 will determine this
   address, and store data there using "=".

   P2 will check the gain level for the connection. It will use the
   following pseudocode:

     if (gain == unity) {
           set P2's memory buffer location to be P1's
     } else {
           request a memory buffer [ see notes below ]
           copy the data from P1's buffer to P2's buffer, scaled by gain
     }
      
2) One-to-Many

    P1 is an output port. P2 and P3 are input ports. P1 is connected
    to P2 and P3.

    How it works:

    P1 does the same as P1 the One-to-One case.

    P2 and P3 do the same as P2 in the One-to-One case.

3) Many-to-One

    P1 and P2 are output ports. P3 is an input port. P1 and P2 are
    connected to P3.

    How it works:

    P1 and P2 do the same as P1 in the One-to-One case.

    P3 will check its connection count, and notice that its
    more than 1. It will use the following method:

    request a memory buffer
    copy data from first connected Port's buffer, using the relevant
             gain level
    foreach other connected Port {
         mix (+=) that Port's buffer using the relevant gain level
    }

Followup Comments
------------------

* "request a memory buffer"
---------------------------
This does not involve malloc(). Some other object (in my view, the
engine) will maintain a pool of buffers that are the correct size for
the current engine cycle. requesting a memory buffer just means
grabbing the head of the buffer free list, advancing the free list
pointer, and storing the retrieved head in the buffer pointer for the
Port. The engine allocates these buffers from the non-audio thread,
and can do so every time a Port is registered, ensuring that it always
has enough. It can also resize the buffers if the engine cycle
duration is changed (if this is supported by the engine).

* how about the connection between i/o ports inside a Plugin?
-------------------------------------------------------------

thats handled differently. this is always a 1:1 connection, and can
always use zero-copy if there is no data modification. so, a function
like:

void
port_tie (Port *dst, Port *src)
{
        dst->buffer = src->buffer;
}

will work for Plugin's that want their outputs wired directly to their
inputs. To fit this into the model below, its very slightly more complex:

* when does all this stuff happen?
----------------------------------
in a naive model, it happens on every engine cycle.

in a more sophisticated model, the memory buffers are requested
whenever the signal graph is modified. the rest of the stuff happens
on every engine cycle.

* what does it really look like?
--------------------------------
This is a mixture of pseudocode and real code for the naive model.

struct Connection {
      Connection *next;
      Port *input;
      Port *output;
      gain_t gain;
};

struct Port {
      sample_t *buffer;
      unsigned long connection_cnt;
      Connection *connections;
      unsigned long flags;
      Port *tied;
};

void
port_prepare (Port *port, nframes_t nframes)

{
        if (port->flags & PortIsOutput) {
            port->buffer = get_buffer ();
            return;
        }

        if (port->tied) {
            port->buffer = tied->buffer;
            return;
        }
        
        /* input port */

        if (port->connection_cnt == 1) {

             /* One-to-One or One-to-Many */

             Connection *connection = port_get_nth_connection (port, 0);

             if (connection->gain == 1.0) {

                  /* use zero-copy data transfer */

                  port->buffer = connection->output->buffer;

             } else {

                  /* transfer data into a local buffer */

                  sample_t *src, *dst;

                  port->buffer = get_buffer ();

                  dst = port->buffer();
                  src = connection->output->buffer;

                  while (nframes--) {
                     *dst++ = *src++ * connection->gain;
                  }
             }

        } else {

             /* Many-to-One */
        
             sample_t *src, *dst;

             /* Copy data from first connected port */

             Connection *connection = port_get_nth_connection (port, 0);

             port->buffer = get_buffer ();

                  dst = port->buffer();
             src = connection->output->buffer;

             if (connection->gain == 1.0) while (nframes--) *dst++ = *src++;
             else while (nframes--) *dst++ = *src++ * connection->gain;

             /* Mix data from all other connected ports */
               
             for (n = 1; n < port->connection_cnt; n++) {
                Connection *connection = port_get_nth_connection (port, n);

                     dst = port->buffer();
                src = connection->output->buffer;

                if (connection->gain == 1.0) while (nframes--) *dst++ += *src++;
                else while (nframes--) *dst++ += *src++ * connection->gain;
             }
        }
}

* so what?
----------
well, each time a Plugin's process(nframes) callback is executed it
first does:

      foreach port in all ports { port_prepare (port); }

after which it knows:

      * port->buffer is the memory location of the data associated with every port
      * if its an input port, the data that should be available now is
      * if its an output port it can use "=" to store data there

in the simple, optimizable cases, this supports zero-copy data
"motion". in the more complex cases, the costs are still fairly cheap,
and unavoidable one way or another.

you will notice the difference between the code executed in the naive
case and the more sophisticated case is rather small (just the
get_buffer() calls).

* what about more complex signal graphs?
----------------------------------------

the design above handles each stage of the connections between ports.

theoretically, zero-copy could be used throughout IF (and ONLY if) it
can be proved that there are no branches along the path. That is, the
pathway from the first output port to the final input port consists
only of 1:1 connections. in such cases, the buffer used by the first
output port can be overwritten at each step of the graph (given that
the processing involved can be done in place, which i take as a
given).

since this is a rather likely common form for the signal graph, it
would make sense to find a way to discover that this is true, and use
it. i haven't fully figured this part out yet, but i think its trivial
to use the list of Connections to do this. it probably has to be a
doubly-linked list.

this only makes sense in the "more sophisticated" version, since you
don't want to try proving this on every engine cycle, only when the
buffers are assigned as the graph is modified.

* your turn
-----------

--p


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

This archive was generated by hypermail 2b28 : Mon May 21 2001 - 17:17:04 EEST