Dennis Thompson <yknot(a)cipic.ucdavis.edu> wrote:
I am working on a surround sound project. When
finished this code will
update a virtual acoustic environment based on a persons head motion. I am
hoping for some hints on solving a problem I have. Note! My background is
in signal processing, not software/hardware interfacing. So I am still
clueless on the process of moving data between user memory and the sound
card. Current development is done using ALSA.
My problem:
The current code can process the audio far faster then the sound card can
except/output it. Therefore, to reduce the latency between head motion and
changes in the sound output, I will need to keep the amount of data in the
output buffer to a minimum. For the sake of discussion say about a 1024
frames (the exact amount is flexible) Currently I try to control the flow
by pausing the output loop. My problem is that I keep getting buffer
underruns, even when I am writing another 1024 frames of data to the sound
card before 1024 frames have had time to play (which does not make any
sense to me).
What I want to do is poll the data buffer so that I can determine the
amount of data remaining in the buffer. So far I have only been able to
determine (by polling) that the buffer is willing to accept new data.
So my question. Does anyone have a idea on how I can determine the
percentage of fill in the output buffer?
I am also open to any ideas on keeping the amount of data queued for output
to a minimum, in a controlled fashion.
With ALSA it is difficult to do this robustly. ALSA is a very low-level
API that directly exposes you to the quirks and differences in your particular
audio interface. The effective latency is dependent on the interrelationship
between several different parameters you set for each stream. To get an
idea of the kind for the kind of code you need to write to use ALSA robustly,
browse
http://cvs.sourceforge.net/cgi-bin/viewcvs.cgi/jackit/jack/drivers/alsa/als…
In my opinion, a better choice for your situation would be to use PortAudio.
PortAudio is a higher-level API that can nonetheless give you all the
performance the underlying API can support. In addition, your code will be
portable to several platforms in case you ever want to run it somewhere else.
Unfortunately the currently released version of PortAudio does not support
ALSA, but the next version will. The next version will also give you a lot of
control over the latency, in a straighforward way (see
http://www.portaudio.com/docs/proposals/003-ImproveLatencySpecificationInte…
for more information on how this will be implemented). PortAudio is available
at
www.portaudio.com
The primary programming model for PortAudio is different than ALSA: instead
of calling a function like snd_pcm_write() that writes data to the interface,
you write a callback function that gets called whenever the interface needs
more data (and has data for you, if you are capturing). This will lift you
of the burden of trying to control the timing, since your callback will
automatically be called at the right time.
An alternative that would give you the same benefits as PortAudio is JACK,
which has the advantage of being available right now. JACK is a sound server
whose main goal is the low-latency interconnection of applications. However,
it could serve you as an audio interface engine with a simple API that can
give you the low-latency performance you desire. It also uses the callback
model. JACK is available at
jackit.sourceforge.net.
All that said, if you still want to use ALSA, here is what you need to do
to achieve low-latency. The two main parameters that control latency in
ALSA are the period size and the number of periods. Essentially, the
hardware buffer is broken up into several periods, where a period is the
number of frames the interface processes per interrupt.
frames per period * number of periods = buffer size
The hardware buffer is a ring buffer, so if your hardware buffer has 4
periods, it will read the first period, then the second, and so on until it
reaches the end when it will wrap around to the beginning.
The ideal situation for low-latency is when you have two periods, and
period sizes as small as will work with your interface. In that case,
the audio loop will essentially be:
while(1):
Your program (host CPU) Sound Interface
----------------------- ---------------
1. Fill period 2 with data Play sound in period 1
2. Wait until write is available Wait until period 1 finished
PERIOD 1 finishes playing
3. (still waiting) Generate interrupt
4. Fill period 1 with data Play sound in period 2
5. Wait until write is available Wait until period 2 finished
PERIOD 2 finishes playing
6. (still waiting) Generate interrupt
In this case, your latency is the time between 1 and 4 (or 4 and 1),
since it is the time between when the sound is *generated* and when it is
*audible*. And if you follow the "Sound interface" column, you can see
that the time it will take to complete 1, 2, and 3 is the amount of time
it takes to play the sound in period one (since the interrupt is essentially
instantaneous).
So the moral of the story: use two periods, the smallest number of frames
per period you can reliably sustain, and you will minimize the latency.
There's certainly no need to try to slow down your sound generation loop,
since the calls you make to write to the interface will block until more
space is available to write.
Your current strategy of trying to keep the buffer as empty as possible is
going about this the wrong way: the correct way is to simply make the buffer
smaller (by reducing period size and number of periods). The reason you are
getting underruns is probably because your period size was set to be greater
than 1024. For example, if it was 2048, ALSA would expect you to have the
entire 2048-frame buffer full and ready to go when it hit that period.
(To ALSA gurus: please correct any mistakes I may have made!)
Josh
--
"Only Angels can sing that high, and only dogs can hear it." -- James Evans