ALSA’s file plugin for playing back a raw pipe file

This post was written by eli on April 15, 2014
Posted Under: Linux,Linux sound

Motivation

On an embedded system, I have a device file /dev/xillybus_audio, which can be opened for read and/or write. One can write raw (signed 16 bit Little Endian Rate 48000 Hz stereo) samples to this file, and they’re played on a “headphones out” plug, and one can read samples of the same time, which are captured from a “mic input” plug. Clean and simple. Now let’s use that as an ALSA sound interface. This is where it doesn’t get all that simple.

How about a kernel driver for that interface? Nice idea, but the stream interface is already there. Besides, this is useful for piping with programs etc.

Attempt I

To make along story short, making /etc/asound.conf read like this, and playing back works (but capturing doesn’t!).

pcm.xillybus {
    type asym
    playback.pcm {
        type plug
        slave {
            pcm {
                type file
                file "/dev/xillybus_audio"
                slave.pcm null
                format raw
            }
            rate 48000
            format s16_le
            channels 2
        }
    }
    capture.pcm {
        type plug
        slave {
            pcm {
                type file
                file "/dev/null"
                infile "/dev/xillybus_audio"
                slave.pcm null
            }
            rate 48000
            format s16_le
            channels 2
        }
    }
}

Playback works with rates other than 48000 Hz (and other formats), because of the wrapping with the “plug” plugin.

# aplay -D "xillybus" rate8000.wav
Playing WAVE 'rate8000.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo

Note that “file” — which defines the output file — must be defined or arecord (or whatever program is used) quits on a segmentation fault. Not very polished.

Capturing doesn’t work at all, however. It’s just a silence file, which grows way too fast. It has been said that the slave of the capturing device shouldn’t be null, and indeed this probably the issue.

Diving into it

The problem seems to lie in the implementation of the file capture routine. Taken from alsa-lib-1.0.27.2/src/pcm/pcm_file.c:

static snd_pcm_sframes_t snd_pcm_file_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size)
{
    snd_pcm_file_t *file = pcm->private_data;
    snd_pcm_channel_area_t areas[pcm->channels];
    snd_pcm_sframes_t n;

    n = snd_pcm_readi(file->gen.slave, buffer, size);
    if (n <= 0)
         return n;
    if (file->ifd >= 0) {
        n = read(file->ifd, buffer, n * pcm->frame_bits / 8);
        if (n < 0)
             return n;
        return n * 8 / pcm->frame_bits;
    }
    snd_pcm_areas_from_buf(pcm, areas, buffer);
    snd_pcm_file_add_frames(pcm, areas, 0, n);
    return n;
}

This is the method, which the plugin exposes for reading samples. Note that it attempts to read the desired amount of samples from the slave first, and then attempts to fetch the same number of samples it got from the slave, from the file. This probably makes sense when reading from a plain file, because it would otherwise slurp the entire file in no-time. The slave is used as a data rate controller. Great.

Attempt II

To come around this, I changed /etc/asound.conf to this:

pcm.xillybus_raw {
                type file
                file "/dev/xillybus_audio"
                slave.pcm null
                format raw
}
pcm.xillybus_play {
        type plug
        slave {
            pcm "xillybus_raw"
            rate 48000
            format s16_le
            channels 2
        }
}

pcm.xillybus {
    type asym
    playback.pcm "xillybus_play"
    capture.pcm {
        type plug
        slave {
            pcm {
                type file
                file "/dev/null"
                infile "/dev/xillybus_audio"
                slave.pcm "xillybus_play"
            }
            rate 48000
            format s16_le
            channels 2
        }
    }
}

This isn’t perfect either. When attempting

# arecord -D "xillybus" --rate 48000 --channels 2 --format s16_le try.wav

sound is indeed recorded into try.wav. The captured sound is echoed in the headphones (with a delay), so the output interface is now busy and noisy. But worst of all, this only works if the parameters are set exactly to the sound interface’s. So I could have read directly from /dev/xillybus_audio as well.

Changes in pcm_file.c

Based upon alsa-lib-1.0.25, the following functions were changed in pcm_file.c. The intention of these changes is to detach the I/O operations from the slave, which is null in the setting of Attempt I above.

static int snd_pcm_file_drop(snd_pcm_t *pcm)
{
	return 0;
}

static int snd_pcm_file_drain(snd_pcm_t *pcm)
{
	return 0;
}

static snd_pcm_sframes_t snd_pcm_file_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size)
{
	snd_pcm_file_t *file = pcm->private_data;
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_sframes_t n;

	n = read(file->ifd, buffer, size * pcm->frame_bits / 8);
	if (n < 0)
		return n;
	return n * 8 / pcm->frame_bits;
}

static snd_pcm_sframes_t snd_pcm_file_readn(snd_pcm_t *pcm, void **bufs, snd_pcm_uframes_t size)
{
	snd_pcm_file_t *file = pcm->private_data;
	snd_pcm_channel_area_t areas[pcm->channels];
	snd_pcm_sframes_t n;

	SNDERR("DEBUG: Noninterleaved read not yet implemented.\n");
	return 0;	/* TODO: Noninterleaved read */
}

(these functions don’t appear one after the other in the source file)

Compiling to obtain libasound.so

After making the changes in pcm_file.c, compiled natively on the embedded board

# ./configure
# make -j 2

and then copy the result to the library directory:

# cp src/.libs/libasound.so.2.0.0 /usr/lib/arm-linux-gnueabihf/

This overwrites the previous file.

Plugin library issue

When attempting to use a sound interface with the new libasound, the following error occurs:

# aplay -D "xillybus" snip.wav
ALSA lib conf.c:3314:(snd_config_hooks_call) Cannot open shared library libasound_module_conf_pulse.so
ALSA lib pcm.c:2217:(snd_pcm_open_noupdate) Unknown PCM xillybus
aplay: main:682: audio open error: No such file or directory

this is because the plugin loader looks in the wrong directories: It does look in /usr/lib/arm-linux-gnueabihf/, but not in /usr/lib/arm-linux-gnueabihf/alsa-lib/.

Trying configure parameters didn’t help:

# ./configure --libdir=/usr/lib/arm-linux-gnueabihf/
# ./configure --with-plugindir=/usr/lib/arm-linux-gnueabihf/alsa-lib/

The dirty solution was to create symbolic links to all files in alsa-lib/ that aren’t symbolic links themselves with

# cd /usr/lib/arm-linux-gnueabihf
# for i in `find alsa-lib/ -type f -a ! -type l` ; do ln -s "$i" ; done

Not the most elegant solution, but after spending a couple of hours on trying to figure this out, at least it works.

This is the list of files that were symlinked:

alsa-lib/libasound_module_pcm_speex.so
alsa-lib/libasound_module_ctl_oss.so
alsa-lib/libasound_module_ctl_pulse.so
alsa-lib/libasound_module_pcm_usb_stream.so
alsa-lib/libasound_module_pcm_pulse.so
alsa-lib/libasound_module_rate_samplerate.so
alsa-lib/libasound_module_ctl_bluetooth.so
alsa-lib/libasound_module_pcm_jack.so
alsa-lib/libasound_module_pcm_upmix.so
alsa-lib/libasound_module_pcm_bluetooth.so
alsa-lib/libasound_module_conf_pulse.so
alsa-lib/libasound_module_pcm_oss.so
alsa-lib/libasound_module_rate_speexrate.so
alsa-lib/libasound_module_ctl_arcam_av.so
alsa-lib/libasound_module_pcm_vdownmix.so
alsa-lib/smixer/smixer-sbase.so
alsa-lib/smixer/smixer-ac97.so
alsa-lib/smixer/smixer-hda.so

Current position

Both record and playback work with the first asound.conf above (a.k.a. Attempt I), as long as the parameters for recording are the same. For playback, the parameters must be the 48000 Hz, s16_le but it’s fine to work in mono and stereo. If other parameters are attempted, a huge file which is filled with click sounds is created.

So

# arecord -D "xillybus" --rate 48000 --format s16_le --channels 2 good.wav
Recording WAVE 'good.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo

makes a nice sound file, but

# arecord -D "xillybus" --format s16_le --channels 2 justclicks.wav
Recording WAVE 'justclicks.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo

creates a huge file with just clicks. The intriguing thing about those failing just-click scenarios, is that snd_pcm_file_readi() is never called when this happens. It looks like something in the data flow goes wrong when a rate resampler is pushed into the system. When the rate is the same, snd_pcm_file_readi() has been observed to be called in a steady way.

Both of the following are OK:

# aplay -D "xillybus" good.wav
Playing WAVE 'good.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo
# aplay -D "xillybus" rate8000.wav
Playing WAVE 'rate8000.wav' : Signed 16 bit Little Endian, Rate 8000 Hz, Stereo

 

Add a Comment

required, use real name
required, will not be published
optional, your blog address