Proposed Enhancements to PortAudio API

005 - Blocking Read/Write Interface

Enhancement Proposals Index, PortAudio Home Page

Updated: October 19, 2002

Status

This proposal is sufficiently well defined to be implemented. This proposal is implemented in the v19-devel infrastructure, but not yet in any of the host api implementations.

Background

Many PortAudio users have requested a blocking read()/write() API that will be supported in addition to the current callback based API. A blocking read/write API would allow a more natural style of multi-threaded programming, and facilitate single-threaded reactive applications, while insulating clients from platform-specific thread synchronisation facilities.

A blocking read/write API would also be useful when binding PortAudio to languages that don't easily support callbacks such as Python, Java, Lisp and Smalltalk. However in this case it has been noted that a blocking API is not sufficient - the host language also needs to support native threads to interact efficiently with blocking. Dannenberg observes that native thread support cannot be added without major redesign (based on a study of Python and Squeak), but given blocking calls, there are several ways to structure programs using non-native threads.

Adding a blocking Read/Write interface to PortAudio has been discussed on a number of occasions, including the following threads:

http://techweb.rfa.org/pipermail/portaudio/2001-August/000063.html a long thread about blocking calls.

http://techweb.rfa.org/pipermail/portaudio/2001-August/000137.html this is Roger Dannenberg's proposal and a subsequent discussion

http://techweb.rfa.org/pipermail/portaudio/2001-August/000144.html is a thread discussing using blocking APIs with other languages

Proposal

If a NULL callback parameter is passed to Pa_OpenStream() then the stream will be opened in blocking mode. This enables users to call Pa_WriteStream() and Pa_ReadStream() to read and write sample data. (The PaErrorNum code "paNullCallback" becomes obsolete.)

Pa_WriteStream() writes a buffer of frames to a stream. The length of the buffer is arbitrary and specified by the frames parameter. High performance applications will want to match the length of the buffer to framesPerBuffer, but this is not a requirement. Pa_WriteStream() returns when all samples have been copied from the buffer. If necessary, Pa_WriteStream() will wait until buffer space becomes available. (Waiting on Unix will be the by-product of an I/O system call, waiting in Win32 will be implemented by waiting on an Event object, and waiting on MacOS 9 will probably require a busy wait.)

The buffer parameter has the same semantics and format as the inputbuffer and outputbuffer parameters of a PortAudioCallback function. In particular, non-interleaved data is handled in the same way.

PaError Pa_WriteStream( PaStream* stream,
                        void *buffer,
                        unsigned long frames );

Pa_ReadStream() is similar, but it reads rather than writes.

PaError Pa_ReadStream( PaStream* stream,
                       void *buffer,
                       unsigned long frames );

Pa_ReadStream() returns paInputOverflowed if input data was discarded by PortAudio after the previous call and before this call. Pa_WriteStream() returns paOutputUnderflowed if output data was inserted after the previous call and before this call. The mode flag paNeverDropInput is ignored because Pa_ReadStream() and Pa_WriteStream() are not synchronized.

There are two functions to determine the number of frames available for writing and reading. These functions may be called to determine whether calls to Pa_WriteStream() or Pa_ReadStream() will return immediately or will wait. The return value, if non-negative, is the maximum number of frames that can be written or read without blocking or busy waiting. A negative value is a PaErrorNum.

signed long Pa_GetStreamWriteAvailable( PaStream* stream );

signed long Pa_GetStreamReadAvailable( PaStream* stream );

The stream functions Pa_CloseStream(), Pa_StartStream(), Pa_StopStream(), Pa_AbortStream(), and Pa_GetStreamTime() work with the blocking API as well as with callbacks. Pa_GetStreamCpuLoad() does not work with the blocking API and will return 0 when called on a blocking stream. PortAudio might be extended to give applications access to the internal routines that compute Pa_GetStreamCpuLoad(). Applications using blocking calls could then bracket audio computation with these calls to determine the CPU load. However, this additional functionality is not being proposed here.

Discussion

A rejected alternative is to allow Pa_WriteStream() and Pa_ReadStream() to return the number of frames actually written or read so that a Mac implementation could return immediately and avoid blocking. This would require applications to be prepared to handle partial read/writes. It seems simpler and more consistent to use "Available" to determine in advance whether blocking or busy waiting will occur if that is a concern. Also, note that data is almost certainly copied; however, it seems likely that the copy will be folded into any format conversion.

Implementations may want to provide a way for applications to be notified when data can be written or read. For example, one might want to know the file ID of an ALSA or OSS stream for use in a select() system call. Since this sort of information will be platform-specific and non-portable, no interface is defined here, but implementations can include a device-model-specific access function. If applications commonly need this information, we can think about how to make this more standardized.

Implementation Notes

Implementing blocking i/o will be quite simple for host APIs which are natively blocking-based. Under Windows (MME), the arrival of a buffer will signal an Event passed to waveOutOpen(). Pa_WriteStream() and Pa_ReadStream() will do all the work (no server threads necessary). Writes will make waveOutWrite calls. When no buffer is available, the writer will wait on the event and try again. Reading is similar. On the Mac, a double-buffer scheme can be set up where the Mac callbacks pick up data placed in buffers by Pa_WriteStream(). The double-buffer adds to the latency. Alternatively (and preferably) callbacks can be used only for notification, and Pa_WriteStream() can issue all the calls to write samples.

PABLIO currently contains a busy-wait ring buffer in "ringbuffer.c" which is generic, used in many projects and is pretty solid. This code could be a useful starting point for implementing the new blocking API on some platforms.

Pa_WriteStream() and Pa_ReadStream() are not thread safe. Applications wanting to call these from multiple threads should manage their own mutual exclusion. [Roger: Is any of PortAudio thread safe? I don't think so. This is good because it avoids many system calls for mutual exclusion.]

At one time it was suggested that implementations only implement blocking calls and that callbacks would be required to implement callbacks in terms of the blocking API. The current direction is that this decision should be made independently for each host API.

Impact Analysis

This proposal would extend the functionality of PortAudio without requiring any changes to client code with the exception that the PaErrorNum paNullCallback will no longer be defined. As noted above, implementation complexity is dependent on the target platform.