Dspaudio & FreeBSD - huge delay [testcase]

Jessica Reznor wrote:

What sound driver you use? OSS , or somethin? let me know and may be I
can help you.

It appears that I’ve actually found why this happens.

It seems that FreeBSD’s native sound driver have a very long internal buffer (about
64KB or 3 seconds on 22KHz 8 bit sound), and all calls to select() after sending data
with write() erroneously returns immediately until this large buffer will be filled.
Thus, as the SDL uses separate thread to deal with sound, on application startup this
thread almost immediately fills this buffer (because select() do not block it from
doing this), so all next sounds in fact going first to the top of this buffer and
have to wait until previously filled data will be played.

To isolate this problem I’ve wrote a small testcase, which I believe could expose
this bug to the FreeBSD developers and now I’m looking for someone with Linux box who
will run this case on it and will send its output to me. This small program in
general behaves very similarly to the way the SDL’s audio works, however I’ve tried
to make as simple as possible.

Following is some results of my testcase running on FreeBSD box and what I think this
testcase should return in the case of correct driver implementation.

Sincerely,

Maxim

In theory output of this program should look like:

[n * Buffer written in 0 ms]
Buffer written in xxx ms
Buffer written in xxx ms
Buffer written in xxx ms
Buffer written in xxx ms
[… xxx ms]

where the number of “0 ms” strings will depend on number of fragments allocated in
the sound driver.

However, on the FreeBSD this program produced something like that:

Buffer written in 0 ms
Buffer written in 0 ms
[… 0 ms - repeated approx. 256 times]
Buffer written in 0 ms
Buffer written in xxx ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in xxx ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in xxx ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms
Buffer written in 0 ms

So, it is clear that there is unwanted buffer with size of 256*256 bytes (64KB)
located somewhere down the wire.

-------------- next part --------------
#include <stdio.h>
#include <stdlib.h>
#include <machine/soundcard.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>

#define BSIZE 256
unsigned char *buf;
static struct timeval *start = NULL;

void playbuf(int, unsigned char *);
void waitplay(int);
unsigned long getticks(void);

int main()
{
int audio_fd;
int i;
audio_buf_info info;
unsigned long curtime;
struct snd_size;

audio_fd = open("/dev/dsp", O_WRONLY | O_NONBLOCK);
i = 0x00020008;
ioctl(audio_fd, SNDCTL_DSP_SETFRAGMENT, &i);
ioctl(audio_fd, SNDCTL_DSP_SPEED, 11025);
ioctl(audio_fd, SNDCTL_DSP_SETFMT, AFMT_U8);
ioctl(audio_fd, SNDCTL_DSP_GETOSPACE, &info);
printf("num=%d tot=%d siz=%d by=%d\n", info.fragments, info.fragstotal,
info.fragsize, info.bytes);
buf = malloc(BSIZE);
bzero(buf, BSIZE);
for(i=0;i<400;i++) {
	curtime = getticks();
	playbuf(audio_fd, buf);
	waitplay(audio_fd);
	printf("Buffer written in %lu ms\n", getticks() - curtime);
}
exit(0);

}

void playbuf(int fd, unsigned char *buf)
{
write(fd, buf, BSIZE);
}

void waitplay(int fd)
{
fd_set fdset;
struct timeval timeout;

FD_ZERO(&fdset);
FD_SET(fd, &fdset);
timeout.tv_sec = 10;
timeout.tv_usec = 0;
select(fd+1, NULL, &fdset, NULL, &timeout);

}

unsigned long getticks(void)
{
struct timeval now;
long ticks;

if (start == NULL) {
	start = malloc(sizeof(struct timeval));
	gettimeofday(start, NULL);
	return(0);
}

gettimeofday(&now, NULL);
ticks=(now.tv_sec-start->tv_sec)*1000+(now.tv_usec-start->tv_usec)/1000;
return(ticks);

}

[ reformatted to fit ]

It seems that FreeBSD’s native sound driver have a very long internal
buffer (about 64KB or 3 seconds on 22KHz 8 bit sound), and all calls
to select() after sending data with write() erroneously returns
immediately until this large buffer will be filled.

SDL sets the number and size of fragments in DSP_ReopenAudio(), but perhaps
the BSD audio drivers refuse to behave.
(You are using OSS drivers, aren’t you?)

Have you tried SDL_DSP_NOSELECT? It should use timer-based audio delay instead
of selecting on the descriptor.

Mattias Engdeg?rd wrote:

[ reformatted to fit ]

It seems that FreeBSD’s native sound driver have a very long internal
buffer (about 64KB or 3 seconds on 22KHz 8 bit sound), and all calls
to select() after sending data with write() erroneously returns
immediately until this large buffer will be filled.

SDL sets the number and size of fragments in DSP_ReopenAudio(), but perhaps
the BSD audio drivers refuse to behave.
(You are using OSS drivers, aren’t you?)

Yes, it refuses to use provided parameters. I’ve just digged into kernel sources
and found that actually buffer for each driver is specified by using #define, and
size of it remains constant from reboot to reboot. I’ve also found confirmation of
my assumptions - buffer for my specific driver was set to 64KB. I’m using
OSS-compatable (well, partially in fact) FreeBSD’s pcm driver.

Have you tried SDL_DSP_NOSELECT? It should use timer-based audio delay instead
of selecting on the descriptor.

Yes, I’ve tried. It helps somewhat, but do not solve the problem completly, as in
this case the SDL does not know exactly how much audio is in buffer and tend to
send more audio that has been played (FUDGE_…), so after several minutes of
playing this case actually degrades to the case with non-working select().

-Maxim

Yes, it refuses to use provided parameters. I’ve just digged into
kernel sources and found that actually buffer for each driver is
specified by using #define, and size of it remains constant from
reboot to reboot. I’ve also found confirmation of my assumptions -
buffer for my specific driver was set to 64KB. I’m using
OSS-compatable (well, partially in fact) FreeBSD’s pcm driver.

Ah. Well, that sucks. Can you check if ioctl(SNDCTL_DSP_GETOSPACE) works?
If it does, we can use the same trick as on Solaris (which also has a
big non-adjustable buffer): sleep a while, then check if the buffer
is running low. If so, feed it. Repeat.

Mattias Engdeg?rd wrote:

Yes, it refuses to use provided parameters. I’ve just digged into
kernel sources and found that actually buffer for each driver is
specified by using #define, and size of it remains constant from
reboot to reboot. I’ve also found confirmation of my assumptions -
buffer for my specific driver was set to 64KB. I’m using
OSS-compatable (well, partially in fact) FreeBSD’s pcm driver.

Ah. Well, that sucks. Can you check if ioctl(SNDCTL_DSP_GETOSPACE) works?
If it does, we can use the same trick as on Solaris (which also has a
big non-adjustable buffer): sleep a while, then check if the buffer
is running low. If so, feed it. Repeat.

Sounds reasonably, but unfortunately situation even more complicated than
I’ve written previously. In fact there is two buffers in the driver - one
frontend, which behaves accordingly to theh OSS specs, and one (usually
large) backend, which is used to actually transfer data into card. And what
makes this situation even worse, is that all ioctl’s expected to return
current state of audio buffers in fact returns only state of the front
buffer, which in the most cases is empty, as the driver tend to transfer data
into its back end buffer ASAP. At the same time select() call deals with
backend buffer, so it will not block untill the whole backend buffer will be
filled with data.

Therefore audio application cannot get from the FreeBSD sound driver current
position of the playback and even cannot get from operating system
notification when buffer is getting reasonably empty (I do not think that
buffer with 64KB of data could be considered as an empty one).

I’ve notified FreeBSD developers about my findings and will try to persuage
them to fix audio driver for better conformance with OSS specs.

-Maxim

In fact there is two buffers in the driver - one
frontend, which behaves accordingly to theh OSS specs, and one (usually
large) backend, which is used to actually transfer data into card. And what
makes this situation even worse, is that all ioctl’s expected to return
current state of audio buffers in fact returns only state of the front
buffer, which in the most cases is empty, as the driver tend to transfer data
into its back end buffer ASAP.

If there is no way to resize or get status information of the backend buffer,
then you are stuck. You can use it for playing music, but nothing latency-
critical. It sounds brain dead to me.

I’ve notified FreeBSD developers about my findings and will try to persuage
them to fix audio driver for better conformance with OSS specs.

Is OSS the native audio API of FreeBSD, or is it just a compatibility layer?
If so, there could be a way to handle it. Do the BSD developers have the
ambition to use other APIs in the future (ALSA, OpenAL)? The OSS/SunOS
concept where an app talks directly to a device isn’t a very flexible design.

Mattias Engdeg?rd wrote:

In fact there is two buffers in the driver - one
frontend, which behaves accordingly to theh OSS specs, and one (usually
large) backend, which is used to actually transfer data into card. And what
makes this situation even worse, is that all ioctl’s expected to return
current state of audio buffers in fact returns only state of the front
buffer, which in the most cases is empty, as the driver tend to transfer data
into its back end buffer ASAP.

If there is no way to resize or get status information of the backend buffer,
then you are stuck. You can use it for playing music, but nothing latency-
critical. It sounds brain dead to me.

Yeh, you are absolutely right. Hovewer I’ve submitted small patch to make
SNDCTL_DSP_GETOSPACE ioctl return the maximum free buffer space available in
either of buffers. So, this would allow application to query driver about amount
of data currently in the buffers and to behave accordingly.

More general approach would require to make driver part of the select() call to
actually consider difficulties of this two-buffer organisation.

I’ve notified FreeBSD developers about my findings and will try to persuage
them to fix audio driver for better conformance with OSS specs.

Is OSS the native audio API of FreeBSD, or is it just a compatibility layer?
If so, there could be a way to handle it. Do the BSD developers have the
ambition to use other APIs in the future (ALSA, OpenAL)? The OSS/SunOS
concept where an app talks directly to a device isn’t a very flexible design.

Yes, OSS is the native audio API on FreeBSD and it is still some sort of “work in
progress” after recent transition from the old Voxware API. As for you second
question I couldn’t tell you much about it, maybe if one of these standards will
gain significant application support then someone will try to port it to the BSD.

-Maxim