Chapter 18

The Buffer Classes


CONTENTS


Introduction

Depending on the device, many data-streaming applications require your plug-in to internally buffer data. Consider the audio plug-in sample in Chapter 17. The audio driver requires multiple buffers on startup and a continuous feed of data. Audio buffers are cached up to a maximum buffer number. Buffers are then sent to the driver with a thread that is totally independent of the main Navigator thread. This scheme allows the plug-in to pre-read any amount of data.

Pre-reading data in a real-time streaming application is a must. Even with the fastest Internet connections, data flow can be halted for several seconds. How much should be pre-read is highly dependent on the device requiring the data. For example, setting up the audio sample to have 20 buffers at 16KB each allows the plug-in to cache 320KB of data, which translates to 30 seconds of playing time with 11khz 8-bit mono audio.

This chapter covers the CBufferCircular and CBufferFIFO classes. These classes are fully interchangeable into the sample audio plug-in and can also be used in other applications. Both classes have exactly the same methods, but they differ in the buffer management implementation. CBufferCircular uses a recycling circular buffer scheme and CBufferFIFO uses a First In First Out (FIFO) scheme.

It should be noted that these classes are intended as learning tools and are not commercial-grade source code. The methods are short and to the point, without much in the way of error checking. If you plan to use these classes in your code, pay special attention to the technical notes throughout this chapter, which warn of potential pitfalls.

The Methods

Although the audio sample is currently set up to use the CBufferCircular class, it is a very simple task to switch to the CBufferFIFO class. In npwave.cpp, you can find where CBufferCircular is allocated in the CWave::Open method:

pCBuffer = new CBufferCircular;

Then, you can change the line to look like this:

pCBuffer = new CBufferFIFO;

That's all there is to it. Notice that this is possible because the methods are interchangeable between classes. These methods are as follows:

Buffer Header

To effectively manage these buffers, a small header structure is attached to the first part of each buffer:

typedef struct _cbuff
{
    struct _cbuff* pNextBuffer;
    ULONG ulFlags;

} CBUFF;

This header can be expanded for additional needs. In the sample audio plug-in, the expanded version is as follows:

typedef struct _buffer
{
    struct _buffer* pNextBuffer;
    ULONG ulFlags;
    ULONG ulBufferSize;
    ULONG ulDataSize;
    WAVEHDR WaveHdr;
    void* pvData;
    ULONG ulData;

} BUFFER;

Make sure that the first part of the expanded structure contains a next pointer and flags in that order. The buffer classes only care about these two members.

The Circular Buffer

The audio sample's default configuration uses the circular buffer class, CBufferCircular. This class is contained in the files npbfcirc.cpp and npbfcirc.h.

The circular buffer allocates a linked list of buffers in which the last buffer allocated points to the first. All buffers are allocated at initialization and continuously recycled throughout the life of the stream. Because memory is allocated only once, performance of this technique is slightly faster than the FIFO class CBufferFIFO.

An allocated CBufferCircular might look something like the one in Figure 18.1 in memory.

Figure 18.1 : Memory blocks allocated with CBufferCircular.

As you can see in Figure 18.1, pFirstFullBuffer points to the first buffer with valid data and pFirstEmptyBuffer points to the first buffer available for use. As the data is streaming, full buffers are consumed by a device and empty buffers are filled by the Navigator.

CBufferCircular Methods

The circular buffer class uses the standard C runtime memory management routines: malloc and free. All memory is allocated at the same time and freed at the same time.

Constructor

The constructor initializes the buffer pointers:

//
// Constructor
//
CBufferCircular::CBufferCircular ()
{
    pFirstEmptyBuffer = pFirstFullBuffer = NULL;
}

Destructor

The destructor checks to make sure that memory is freed, and if it is not, frees the memory implicitly:

//
// Destructor
//
CBufferCircular::~CBufferCircular ()
{
    if (pFirstEmptyBuffer)
        this->FreeBuffers ();    // Forgot to free us.
}

CBufferCircular::AllocateBuffers

The first method allocates buffers and chains them together to form a circular linked list:

//
// AllocateBuffers
//
BOOL CBufferCircular::AllocateBuffers (ULONG ulBuffSize,
    USHORT usNumberOfBuffers)
{
    CBUFF*    pBuff;

    for (int i=1; i<usNumberOfBuffers; i++, pBuff=pBuff->pNextBuffer)
    {
        if (i == 1)
        {
             pFirstEmptyBuffer = pBuff = (CBUFF*) malloc (ulBuffSize);
             memset (pFirstEmptyBuffer, 0, ulBuffSize);
        }

        pBuff->pNextBuffer = (CBUFF*) malloc (ulBuffSize);
        memset (pBuff->pNextBuffer, 0, ulBuffSize);
    }

    // Last buffer points to first, a circular buffer scheme.

    pBuff->pNextBuffer = pFirstEmptyBuffer;

    return TRUE;
}

Note
CBufferCircular::AllocateBuffers only works if usNumberOfBuffers is greater than one. Also, there is no protection against a memory leak if the method is called twice. If you use this code, you might want to add these checks.

The first empty buffer pointer (pFirstEmptyBuffer) is set, and the last buffer allocated points to the first, completing the circle.

Notice that the buffer size and amount of buffers is passed to the method, allowing any number of configurations. You can also allocate a circular buffer with respect to the data type. For example, if you are streaming 11khz 8-bit mono audio data, 10 16KB buffers might be sufficient, but 44khz 16-bit stereo might require 20 64KB buffers. It depends entirely on your application and performance requirements.

CBufferCircular::FreeBuffers

When you are done with the circular buffer, call FreeBuffers. This method scans through the chain and frees all memory:

//
// FreeBuffers
//
BOOL CBufferCircular::FreeBuffers (void)
{
    CBUFF*     pBuff=pFirstEmptyBuffer, *pBuffNext;

    do
    {
        pBuffNext = pBuff->pNextBuffer;
        free (pBuff);
        pBuff = pBuffNext;

    } while (pBuff != pFirstEmptyBuffer);

     pFirstEmptyBuffer = pFirstFullBuffer = NULL;

     return TRUE;
}

Note
CBufferCircular::FreeBuffers has no protection against being called when there are no buffers. You might want to add this check to this method in order to avoid a potential memory protection fault.

CBufferCircular::AnyEmptyBuffers

While data is streaming, the plug-in continuously calls AnyEmptyBuffers, which returns TRUE if there are any empty buffers. This is analogous to an NPP_WriteReady called from the Navigator:

//
// AnyEmptyBuffers
//
BOOL CBufferCircular::AnyEmptyBuffers (void)
{
    if (pFirstEmptyBuffer->ulFlags & BUFFER_FULL)
        return FALSE;
    else
        return TRUE;
}

To determine whether any empty buffers are available, the flags are checked for the first empty buffer, pFirstEmptyBuffer. This buffer can be full in the case of data overrun. Streaming for a local disk drive almost always produces a situation in which all buffers are full.

CBufferCircular::GetNextEmptyBuffer

When AnyEmptyBuffers returns TRUE, a call to GetNextEmptyBuffer is made, which returns a pointer to the next empty buffer:

//
// GetNextEmptyBuffer
//
void* CBufferCircular::GetNextEmptyBuffer (void)
{
    CBUFF*     pBuff=pFirstEmptyBuffer;

    if (!(pBuff->ulFlags & BUFFER_FULL))
    {
        if (!pFirstFullBuffer)
            pFirstFullBuffer = pFirstEmptyBuffer;

           pBuff->ulFlags |= BUFFER_FULL;

        // Next buffer please...

        pFirstEmptyBuffer = pFirstEmptyBuffer->pNextBuffer;

        return pBuff;
    }
    else
        return NULL;
}

The method double-checks the flags on the first empty buffer, making sure it is not full. If pFirstFullBuffer is NULL, it is set to this buffer, making it the first buffer in the stream. The buffer is flagged as full, and pFirstEmptyBuffer is pointed to the next buffer.

CBufferCircular::AnyFullBuffers

The consumer of stream data, which could be another thread, calls AnyFullBuffers and returns TRUE if data is available:

//
// AnyFullBuffers
//
BOOL CBufferCircular::AnyFullBuffers (void)
{
    if (pFirstFullBuffer->ulFlags & BUFFER_FULL)
        return TRUE;
    else
        return FALSE;
}

Note
CBufferCircular::AnyFullBuffers should really make sure that pFirstFullbuffer is not NULL and does exist. You might want to add this check to your implementation of this code.

The first full buffer pointer is checked for data with the BUFFER_FULL flag.

CBufferCircular::GetNextFullBuffer

After determining that there is some data, GetNextFullBuffer is called and returns a pointer to a full data buffer:

//
// GetNextFullBuffer
//
void* CBufferCircular::GetNextFullBuffer (void)
{
    CBUFF*     pBuff=pFirstFullBuffer;

    if (pBuff->ulFlags & BUFFER_FULL)
    {
        // Next buffer please...

        pFirstFullBuffer = pFirstFullBuffer->pNextBuffer;

        return pBuff;
    }
    else
        return NULL;
}

Note
Your calling code should assure that CBufferCircular::AnyFullBuffers is called before CBufferCircular::GetNextFullBuffer.

GetNextFullBuffer verifies that pFirstFullBuffer contains data by checking the BUFFER_FULL flag. It then points pFirstFullBuffer to the next buffer and returns a valid buffer pointer.

CBufferCircular::GetLastFullBuffer

When a stream is finished, it calls NPP_DestroyStream to signal stream completion. The fact that the stream is complete does not mean your plug-in is done consuming buffers. The plug-in needs to flag the last full buffer so that the device knows to stop. GetLastFullBuffer is provided to retrieve the last full buffer for this purpose:

//
// GetLastFullBuffer
//
void* CBufferCircular::GetLastFullBuffer (void)
{
    CBUFF*     pBuff=pFirstFullBuffer;

    if (!(pBuff->ulFlags & BUFFER_FULL))
        return NULL;

    for (; pBuff->pNextBuffer->ulFlags & BUFFER_FULL; pBuff=pBuff->pNextBuffer)
    {
        if (pBuff->pNextBuffer->ulFlags & BUFFER_ACTIVE)
            return pBuff;    // Buffer is in the driver
                             // (already consumed, but still full)

        if (pBuff->pNextBuffer == pFirstFullBuffer)
            return pBuff;    // All buffers are full
    }

    return pBuff;
}

GetLastFullBuffer scans through the buffer circle starting with pFirstFullBuffer. It stops when an empty buffer is found or a complete circle has been made. The flag BUFFER_ACTIVE is used to denote a buffer that is technically full but is already sent to the device. For the purpose of GetLastFullBuffer, these are considered empty.

CBufferCircular::ReturnUsedBuffer

When a device is totally finished with a buffer, the buffer is returned for recycling with a call to ReturnUsedBuffer:

//
// ReturnUsedBuffer
//
BOOL CBufferCircular::ReturnUsedBuffer (void* pvBuffer)
{
    CBUFF*     pBuff=pFirstEmptyBuffer;

    do
    {
        if (pvBuffer == (void*)pBuff)
        {
            // Found it, flag as empty and return.

            pBuff->ulFlags &= ~(BUFFER_FULL | BUFFER_ACTIVE);
            return TRUE;
        }

        pBuff=pBuff->pNextBuffer;

   } while (pBuff != pFirstEmptyBuffer);

    return FALSE;
}

This method scans the list, finds the given buffer, and turns off the BUFFER_FULL and BUFFER_ACTIVE flags.

The FIFO Buffer

The other buffer class, CBufferFIFO, is located in the files npbffifo.cpp and npbffifo.h. These files are part of the sample audio plug-in. This class is not used, but it can easily be swapped in as discussed earlier in this chapter.

The FIFO buffer also uses a linked list of buffers. Unlike the circular buffer, the FIFO buffer is a chain of buffers with a NULL terminated end buffer. Buffers are not recycled but are allocated and freed as needed. This provides for a somewhat simpler implementation at a slight performance penalty.

An allocated CBufferFIFO might look something the one in Figure 18.2 in memory.

Figure 18.2 : Memory blocks allocated with CBufferFIFO.

In Figure 18.2, pFirstFullBuffer points to the first buffer with valid data and pLastBuffer points to the last buffer with valid data. No empty or active buffers are maintained.

CBufferFIFO Methods

The FIFO buffer class uses the standard C runtime memory management routines: malloc and free. Unlike with CBufferCircular, memory is allocated as needed and freed immediately after use.

Constructor

The constructor initializes pointers and counters:

//
// Constructor
//
CBufferFIFO::CBufferFIFO ()
{
    pFirstFullBuffer = pLastBuffer = NULL;
    usBufferCount = 0;
}

Destructor

As it did with CBufferCircular, the destructor ensures that memory has been freed:

//
// Destructor
//
CBufferFIFO::~CBufferFIFO ()
{
    if (pFirstFullBuffer)
        this->FreeBuffers ();    // Forgot to free us.
}

CBufferFIFO::AllocateBuffers

No buffers are really allocated in this method. The buffer size and count is stored for later reference during buffer allocation:

//
// AllocateBuffers
//
BOOL CBufferFIFO::AllocateBuffers (ULONG ulBuffSize, USHORT usNumberOfBuffers)
{
    this->ulBuffSize = ulBuffSize;
    this->usNumberOfBuffers = usNumberOfBuffers;

    return TRUE;
}

CBufferFIFO::FreeBuffers

FreeBuffers is almost identical to CBufferCircular::FreeBuffers. The real difference is that the starting pointer is pFirstFullBuffer instead of pFirstEmptyBuffer:

//
// FreeBuffers
//
BOOL CBufferFIFO::FreeBuffers (void)
{
    CBUFF*     pBuff=pFirstFullBuffer, *pBuffNext;

    if (!pBuff)
        return TRUE;

    do
    {
        pBuffNext = pBuff->pNextBuffer;
        free (pBuff);
        pBuff = pBuffNext;

    } while (pBuff);

    pFirstFullBuffer = NULL;

    return TRUE;
}

CBufferFIFO::AnyEmptyBuffers

This method is much simpler than CBufferCircular::AnyEmptyBuffers. The FIFO buffer class allocates memory on the fly. Therefore, as long as the maximum buffers limit is not exceeded, the method returns TRUE.

//
// AnyEmptyBuffers
//
BOOL CBufferFIFO::AnyEmptyBuffers (void)
{
    if (usBufferCount <= usNumberOfBuffers)
        return TRUE;
    else
        return FALSE;
}

CBufferFIFO::GetNextEmptyBuffer

A new buffer is allocated and attached to the end of the chain. If there are no buffers to chain to, pLastBuffer is NULL and a new chain is created by resetting pFirstFullBuffer.

The buffer is zeroed, it is flagged as full, and buffer count is incremented:

//
// GetNextEmptyBuffer
//
void* CBufferFIFO::GetNextEmptyBuffer (void)
{
    CBUFF*     pBuff=pLastBuffer;

    if (pBuff)
    {
        pBuff->pNextBuffer = (CBUFF*)malloc (ulBuffSize);
        pBuff = pBuff->pNextBuffer;
        pLastBuffer = pBuff;
    }
    else
    {
        pFirstFullBuffer = pLastBuffer = (CBUFF*)malloc (ulBuffSize);
        pBuff = pFirstFullBuffer;
    }

    if (!pBuff)
        return NULL;

    memset (pBuff, 0, ulBuffSize);

    pBuff->ulFlags = BUFFER_FULL;

    usBufferCount++;

    return pBuff;
}

CBufferFIFO::AnyFullBuffers

Check to make sure that the first full buffer is indeed full, and return TRUE if it is. pFirstFullBuffer is NULL if no buffers are available:

//
// AnyFullBuffers
//
BOOL CBufferFIFO::AnyFullBuffers (void)
{
    if (pFirstFullBuffer)
    {
        if (pFirstFullBuffer->ulFlags & BUFFER_FULL)
            return TRUE;
        else
            return FALSE;
    }
    else
        return FALSE;
}

CBufferFIFO::GetNextFullBuffer

Make sure that pFirstFullBuffer points to a full buffer. If it does, detach the buffer from the chain by moving pFirstFullBuffer onto the next buffer (if any):

//
// GetNextFullBuffer
//
void* CBufferFIFO::GetNextFullBuffer (void)
{
    CBUFF*     pBuff=pFirstFullBuffer;

    if (!pBuff)
        return NULL;

    if (pBuff->ulFlags & BUFFER_FULL)
    {
        // Detach this full buffer from the list and send it out

        if (!(pFirstFullBuffer = pFirstFullBuffer->pNextBuffer))
            pLastBuffer = NULL;

        return pBuff;
    }
    else
        return NULL;
}

CBufferFIFO::GetLastFullBuffer

The last full buffer is always maintained by the FIFO class. It just returns pLastBuffer:

//
// GetLastFullBuffer
//
void* CBufferFIFO::GetLastFullBuffer (void)
{
    return pLastBuffer;
}

CBufferFIFO::ReturnUsedBuffer

Finally, a returned buffer is freed by the C runtime. Also, the buffer counter is decremented to allow for more buffer allocations:

//
// ReturnUsedBuffer
//
BOOL CBufferFIFO::ReturnUsedBuffer (void* pvBuffer)
{
    // Just free the memory and decrement the counter

    if (pvBuffer)
    {
        free (pvBuffer);
        usBufferCount-;
        return TRUE;
    }

    return FALSE;
}

Conclusion

Use the CBufferFIFO class or its partner CBufferCircular for your plug-in streaming needs. Be sure to call the methods in the correct order. This order is best illustrated in the audio plug-in sample file, npwave.cpp.

Tune your buffer sizes to effectively manage the data stream. For example, a MIDI buffer size would be very small compared to a much larger video buffer size.

Add your plug-in specific data to the buffer header while still maintaining compatibility with the CBUFF structure found in npbfcirc.h or npbffifo.h.

Don't forget to protect these buffer objects with critical sections for multithreaded plug-ins.

What's Next?

The next chapter outlines the Server CPU Monitor plug-in. This plug-in is not a streaming plug-in. It uses NPN_GetURL to retrieve server CPU statistics that are determined via the UNIX command vmstat, running with a server CGI program.