Intro

For an old assignment I was tasked to make PoC of a custom ALSA Driver. It seems to be very hard at the time to find a decent tutorials on how to write an ALSA driver which is not PCI based. Most pages just refer to 2 files in the kernel source tree sound/drivers/dummy.c and sound/drivers/aloop.c. While these are very useful as a reference, some more information could be useful when writing a driver yourself.

There is also some interesting material in the kernel documentation which is very helpful when trying to understand what’s going on. However the example used in the documentation leaves a lot of gaps between and assumes that the reader already has quite some knowledge about ALSA in general.

Note: Important note: All of this information has being gathered from multiple sources either old documentation, source code,… I’m not an expert on the matter by any means. This is a write-down of my understanding how it works. It might be completely wrong. With that out of the way, let’s go!

Credits

The tutorial Writing an ALSA Driver written by Takashi Iwai tiwai@suse.de served as huge guidance and source of information while writing this and developing the PoC. Which is available at multiple places. Writing an ALSA Driver โ€” The Linux Kernel documentation Writing an ALSA Driver - ALSA Project

What is ALSA?

ALSA stands for Advanced Linux Sound Architecture. Which is a subsystems within the Linux kernel that provides audio and midi functionality. It has support for all kinds of audio interfaces. ALSA also provides a Userspace library to simplify application development. This library is called alsa-lib. We will use this to write some test programs for our driver.

Terminology

  • frame: Number of bytes for one sample. e.g. 1 frame 16bit Stereo PCM is 4 bytes.
  • Period: Number of frames between each hardware interrupt. Poll() will return every period.
  • Buffer: … A buffer… But it must be bigger than 1 period. Usually 2 times the size of a period
  • Substream: An datastream either for capture or playback. Large parts of an ALSA driver revolve around substreams.
  • PCM: Pulse Code modulation. A form of digital audio data.
  • Rate: Samplerate, Number of datapoints per second.

Goal of the tutorial

Our goal is the have, by the end of this tutorial, a working ALSA driver which can generate a pattern and have some application which can read the data and check this pattern.

Since for this we don’t need any hardware, we will be using Qemu to test our driver. Since we don’t want to mess up our own system if something goes wrong.

Qemu setup

All commands were tested on Fedora 28. But should work on other distro’s as well (except for the installation of packages of course. There the commands should be substituted with the package manager of your own system). I’m using some scripts for the setup of qemu which can be found here: GitHub - bravl/qemu-virt-env at develop

# Install qemu
sudo dnf install qemu qemu-user deboostrap bc flex bison

# Generate a debian rootfs
./create-img.sh -t debian
# Set a password
mkdir debian-mount
mount -o loop debian.img debian-mount
sudo chroot debian-mount
passwd
exit
umount debian-mount

# Get a kernel
git clone https://github.com/torvalds/linux.git
cd linux
make x86_64_defconfig
make kvmconfig
make -j4

# Run Qemu
cd ../
./run-qemu.sh -k local linux/ -i debian.img

# Once in qemu debian instance
mount -o rw,remount /
dhclient eth0

# Now we are ready to go

ALSA Driver

Platform device

Platform devices are (mostly) devices that is not discoverable by the hardware. Because we still want to be able to register these with the kernel this done in a name based way.

So since there is no actual hardware for our sound driver we will be using a platform driver.

So let’s setup our platform device. Linux provides a pretty straight forward API for this kind of drivers.

Platform skeleton

Creating a platform device is done with a only a few functions. We start with our init and exit function which are needed because it’s a kernel module.

#define SND_SOUNDGEN_DRIVER "snd_soundgen"

static struct platform_driver snd_soundgen_driver = {
        .probe = snd_soundgen_driver_probe,
        .remove = snd_soundgen_driver_remove,
        .driver = {
                .name = SND_SOUNDGEN_DRIVER,
        },
};

static int __init alsa_soundgen_card_init(void)
{
        int err;

        err = platform_driver_register(&snd_soundgen_driver);
        if (err < 0) {
                pr_err("Faild to register platform driver\n");
                return err;
        }
        pr_info("Sound Generator platform device registered\n");

        device = platform_device_register_simple(SND_SOUNDGEN_DRIVER,
                                                 0, NULL, 0);
        if (IS_ERR(device)) {
                pr_err("Failed to register platform device\n");
                platform_driver_unregister(&snd_soundgen_driver);
                return PTR_ERR(device);
        }

        return 0;
}

static void __exit alsa_soundgen_card_exit(void)
{
        platform_driver_unregister(&snd_soundgen_driver);
}

module_init(alsa_soundgen_card_init)
module_exit(alsa_soundgen_card_exit)

If we take a closer look at our init function we can see that we are going to create 2 things. First the platform driver and then a device that will register itself to the driver. The platform driver defines the probe and remove callbacks that will be called for each device that is registered with the driver.

static struct platform_driver snd_soundgen_driver = {
        .probe = snd_soundgen_driver_probe,
        .remove = snd_soundgen_driver_remove,
        .driver = {
                .name = SND_SOUNDGEN_DRIVER,
        },
};
device = platform_device_register_simple(SND_SOUNDGEN_DRIVER,
                                          0, NULL, 0);

Since there is no discovery as mentioned before we match devices to drivers on a name basis. Therefore it’s important that the .name in the platform_driver struct should be the same as the one being used when registering the device.

To complete the platform driver we need to add 2 more things.

  • The probe and remove functions
  • module_init/exit to define the functions that get called at module insertion and removal. These macro’s also make it easier when the driver is compiled into the kernel instead of as a module.
static int snd_soundgen_driver_probe(struct platform_device *devptr)
{
        pr_info("Sound gen driver probed");
        return 0;
}

static int snd_soundgen_driver_remove(struct platform_device *devptr)
{
        pr_info("Sound gen driver removed");
        return 0;
}
module_init(alsa_soundgen_card_init)
module_exit(alsa_soundgen_card_exit)

Soundcard setup

After creating our platform device we need to register our device to the ALSA subsystem. This is done by creating a sound card. Just like our platform device this is done using some very simple helper functions. But first we will create a structure that will hold all our driver specific data. At first this won’t contain much but it will become more and more important as the driver progresses.

struct snd_card_soundgen {
        struct snd_card *card;
};

We want to register a sound card every time a device gets probed. So all of this functionality is added to the probe function of our platform driver.

First we create a new sound card. The first parameter is the device that is linked to the soundcard. In our case this is our platform device. Second and third parameters are id’s, one in the form of an integer and the other a string. For now we will just put some static values. However if our driver would support multiple devices we would need to dynamically iterate these values. The fourth parameter is used to link the soundcard to our module. Second to last we have a size parameter which is used to allocate some extra private_data inside the snd_card struct which we can use to store our own structure in. Lastly we have the a pointer to the address of our snd_card used to return an allocated snd_card structure.

static int snd_soundgen_driver_probe(struct platform_device *devptr)
{
        int err;
        struct snd_card *card;
        struct snd_card_soundgen *soundgen;

        dev_info(&devptr->dev, "Sound gen driver probed\n");
        err = snd_card_new(&devptr->dev, 0, NULL,
                           THIS_MODULE, sizeof(struct snd_card_soundgen),
                           &card);
        if (err < 0) {
                dev_err(&devptr->dev, "Failed to create new soundcard\n");
                return err;
        }

Next we link our snd_card and own struct together and setup some names that will be visible in userspace and used to distinguish different cards.

        /* Link new snd_card and our snd_card_soundgen together */
        soundgen = card->private_data;
        soundgen->card = card;

        strcpy(card->driver, "Sound Gen");
        strcpy(card->shortname, "Sound Gen");
        sprintf(card->longname, "Virtual Sound generator");

At this point we need to register the soundcard with the ALSA subsystem. Just as the platform device this done with a function snd_card_register.

        err = snd_card_register(card);
        if (err < 0) {
                dev_err(&devptr->dev, "Failed to register soundcard\n");
                goto error;
        }

Finally we need to make sure that we can retrieve or card data through the device interface. We do this by setting the drvdata of the device.

        platform_set_drvdata(devptr, card);
        return 0;

error:
        snd_card_free(card);
        return err;
}

If we remove our card we need to cleanup all data correctly. Otherwise it might not be possible to load the module a second time or it could cause corruption over time.

Here we see why it’s important that we set the drvdata. The remove function is called through the driver interface but we have to cleanup the soundcard. We can accomplish this using platform_get_drvdata and using the return value (our snd_card) to free the soundcard.

static int snd_soundgen_driver_remove(struct platform_device *devptr)
{
        dev_info(&devptr->dev,"Sound gen driver removed\n");
        snd_card_free(platform_get_drvdata(devptr));
        return 0;
}

ALSA-Lib

The userspace side of ALSA is mainly implemented under the for of ALSA-Lib. We are going to use some parts of it to test our driver but I won’t go into details about it. Will start by trying to find our card and read the driver names. Which thanks to the libraries is pretty simple.

All we have to do is iterate over the available soundcards using snd_card_next. If id is set to -1 ALSA will return the first available card. If the function returns id -1 it means there are no more available cards. Retrieving the names we can do using *snd_card_get_(long)name*.

#include <stdio.h>
#include <stdlib.h>

#include <alsa/asoundlib.h>

int main()
{
        int id = -1, err;
        char *name, *longname;

        for (;;) {
                err = snd_card_next(&id);
                if (id < 0 || err < 0) {
                        printf("No next sound card\n");
                        break;
                }
                if ((err = snd_card_get_name(id, &name)) < 0) {
                        printf("Failed to get name\n");
                        continue;
                }
                if ((err = snd_card_get_longname(id, &longname)) < 0) {
                        printf("Failed to get longname\n");
                        continue;
                }
                printf("Soundcard: %s\n", name);
                printf("longname: %s\n", longname);
        }
        return 0;
}

Back to the driver

PCM Setup

Now we have soundcard but we don’t have any way of providing actual audio data to the user. ALSA support multiple formats to pass and audio stream to userspace. However the most common one is PCM. Since the final driver for the costumer will retrieve 8bit PCM samples from some external line we will only focus on the PCM layer. But should this not be what you are looking for there is also MIDI support for example.


struct snd_card_soundgen {
        struct snd_card *card;
        struct snd_pcm *pcm;
};

static int snd_soundgen_new_pcm(struct snd_card_soundgen *soundgen_card)
{
        struct snd_pcm *pcm;
        int err;

        err = snd_pcm_new(soundgen_card->card, "Soundgen PCM", 0, 0,
                          1, &pcm);
        if (err < 0) {
                return err;
        }
        pcm->private_data = soundgen_card;
        strcpy(pcm->name, "Soundgen PCM");
        soundgen_card->pcm = pcm;
        snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
                        &snd_soundgen_capture_ops);
        snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
                                              snd_dma_continuous_data(GFP_KERNEL),
                                              0, (64*1024));
        return 0;
}

Creating a PCM device is again the same old as all other devices in the kernel. We create a new device, fill in a struct that contains some function pointers and somehow link this to our new device.

So let’s start by creating our new device. The main structure for PCM is the snd_pcm struct. Just like the soundcard, we use one of the builtin ALSA functions called snd_pcm_new which takes some parameters (that are very similar to the snd_card_new function). First parameter is the parent for the PCM device in the form of a snd_card structure. Second and third parameter are again ID’s. One in the form of a string, the other as an integer. Next we have 2 integer signaling the number of playback and recording substreams. In our case just 1 recording substream. Lastly a pointer to the address of the pcm device, which will hold an allocated PCM if everything worked out.

static int snd_soundgen_new_pcm(struct snd_card_soundgen *soundgen_card)
{
        struct snd_pcm *pcm;
        int err;

        err = snd_pcm_new(soundgen_card->card, "Soundgen PCM", 0, 0,
                          1, &pcm);
        if (err < 0) {
                return err;
        }

Next up, just as with the soundcard we want to be able to access our soundcard even if some functions are called through the PCM layer so we have to link our soundcard and our PCM device. Note that we added a struct snd_pcm to our struct snd_card_soundgen structure in order to keep track of our PCM device.

        pcm->private_data = soundgen_card;
        strcpy(pcm->name, "Soundgen PCM");
        soundgen_card->pcm = pcm;

Now we set the snd_pcm_ops for our recording substreams. We’ll have a closer look a this structure in a minute.

       snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE,
                        &snd_soundgen_capture_ops);

Finally we need some memory to store our PCM data. We have no dedicated hardware DMA or memory so let the kernel preallocate some memory that we can use later on. Using the snd_pcm_lib_preallocate_page_for_all we can preallocate a given size of memory and restrict to what size this block of memory is allowed to grow. These to sizes are controlled with the last 2 parameters. So in our case we don’t preallocate any memory yet but we allow the buffer to grow as large as 64Kb. Later on we can easily access this memory using the snd_pcm_lib_malloc_page.

        snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
                                              snd_dma_continuous_data(GFP_KERNEL),
                                              0, (64*1024));
        return 0;
}

Now let’s have a closer look at the snd_pcm_ops struct. It contains a lot of functions we won’t need but it’s good to just know what’s there. The important ones for us are: open, close, ioctl, hw_params, hw_free, prepare, trigger and pointer.

struct snd_pcm_ops {
        int (*open)(struct snd_pcm_substream *substream);
        int (*close)(struct snd_pcm_substream *substream);
        int (*ioctl)(struct snd_pcm_substream * substream,
               unsigned int cmd, void *arg);
        int (*hw_params)(struct snd_pcm_substream *substream,
                         struct snd_pcm_hw_params *params);
        int (*hw_free)(struct snd_pcm_substream *substream);
        int (*prepare)(struct snd_pcm_substream *substream);
        int (*trigger)(struct snd_pcm_substream *substream, int cmd);
        snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
        int (*get_time_info)(struct snd_pcm_substream *substream,
            struct timespec *system_ts, struct timespec *audio_ts,
            struct snd_pcm_audio_tstamp_config *audio_tstamp_config,
            struct snd_pcm_audio_tstamp_report *audio_tstamp_report);
        int (*copy)(struct snd_pcm_substream *substream, int channel,
                    snd_pcm_uframes_t pos,
                    void __user *buf, snd_pcm_uframes_t count);
        int (*silence)(struct snd_pcm_substream *substream, int channel,
                       snd_pcm_uframes_t pos, snd_pcm_uframes_t count);
        struct page *(*page)(struct snd_pcm_substream *substream,
                             unsigned long offset);
        int (*mmap)(struct snd_pcm_substream *substream,
                    struct vm_area_struct *vma);
        int (*ack)(struct snd_pcm_substream *substream);
};

Let’s go over the ones that are important for us one by one.

static const struct snd_pcm_ops snd_soundgen_capture_ops = {
        .open = soundgen_pcm_open,
        .close = soundgen_pcm_close,
#ifdef DEBUG
        .ioctl = soundgen_pcm_ioctl_wrap,
#else
        .ioctl = snd_pcm_lib_ioctl,
#endif
        .hw_params = soundgen_pcm_hw_params,
        .hw_free = soundgen_pcm_hw_free,
        .prepare = soundgen_pcm_prepare,
        .trigger = soundgen_pcm_trigger,
        .pointer = soundgen_pcm_pointer
};

open

The PCM open function gets called whenever a substream is opened. For example when recording or playback is started. It’s important that we setup the hardware correctly during this function. As we see below this is done at line 38 and 39 by copying our defined snd_pcm_hardware struct into runtime->hw and chip->pcm_hw structures. We can also allocate some device specific private data here. In our case we allocate our timer here since this is devices specific (line 30).

static const struct snd_pcm_hardware snd_soundgen_hw = {
        .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED |
                 SNDRV_PCM_INFO_BLOCK_TRANSFER |
                 SNDRV_PCM_INFO_MMAP_VALID),
        .formats = SNDRV_PCM_FMTBIT_S8,
        .rates = SNDRV_PCM_RATE_8000,
        .rate_min = 8000,
        .rate_max = 8000,
        .channels_min = 2,
        .channels_max = 2,
        .buffer_bytes_max = 32768,
        .period_bytes_min = 64,
        .period_bytes_max = 32768,
        .periods_min = 1,
        .periods_max = 1024,
        .fifo_size = 0,
};

static int soundgen_pcm_open(struct snd_pcm_substream *substream)
{
        int err;
        struct snd_card_soundgen *chip = snd_pcm_substream_chip(substream);
        if (!chip) {
                pr_info("Failed to retrieve chip\n");
                return -1;
        }
        struct snd_pcm_runtime *runtime = substream->runtime;
        pr_info("Opening PCM\n");

        err = snd_pcm_timer_ops.create(substream);
        if (err < 0) {
                pr_err("Failed to create timer\n");
                return -1;
        }

        get_timer_ops(substream) = &snd_pcm_timer_ops;

        runtime->hw = snd_soundgen_hw;
        chip->pcm_hw = runtime->hw;

        if (substream->pcm->device & 1) {
                runtime->hw.info &= ~SNDRV_PCM_INFO_INTERLEAVED;
                runtime->hw.info |= SNDRV_PCM_INFO_NONINTERLEAVED;
        }
        if (substream->pcm->device & 2)
                runtime->hw.info &= ~(SNDRV_PCM_INFO_MMAP |
                                SNDRV_PCM_INFO_MMAP_VALID);

        return 0;
}

close

This will of course be called when we close a substream. Here we will cleanup the private data we’ve allocated in the open function. Again for our example the timers will be free’d here.

static int soundgen_pcm_close(struct snd_pcm_substream *substream)
{
        pr_info("Closing PCM\n");
        get_timer_ops(substream)->free(substream);
        return 0;
}

ioctl

We can implement a custom ioctl function if we want. However usually this is just set to snd_pcm_lib_ioctl. Although in our example there is a small function wrapping the IOCTL because an issue popped up and I needed some way to validate some data seen in strace with what was actually coming from the kernel.

ifdef DEBUG
int soundgen_pcm_ioctl_wrap(struct snd_pcm_substream *substream,
                unsigned int cmd, void *arg)
{
        int err;
        pr_info("PCM IOCTL Wrapper");
        pr_debug("IOCTL Command: %d", cmd);
        err = snd_pcm_lib_ioctl(substream, cmd, arg);
        pr_debug("Return: %d",err);
        return err;
}
#endif

hw_params

While all params are being setup by the hardware, this function will get called multiple times. There are multiple macros available to retrieve information from the snd_pcm_hw_params struct. It’s important that we configure the hardware correctly here if necessary.

We just allocate some memory using the snd_pcm_lib_malloc_page function. Be careful, this will only work if some buffers were preallocated already. In our case we used snd_pcm_lib_preallocate_pages_for_all during the snd_card_pcm_new function to preallocate some memory. Note: This function can get called multiple times during initialisation so if we use custom memory allocation we must be careful not to allocate to much memory.

static int soundgen_pcm_hw_params(struct snd_pcm_substream *substream,
                struct snd_pcm_hw_params *params)
{
        pr_info("HW Params\n");
        pr_debug("Buffer size %d\n", params_buffer_bytes(params));
        return snd_pcm_lib_malloc_pages(substream,
                        params_buffer_bytes(params));
}

hw_free

As the name suggests, this callback is intended to free the data allocated by the hw_params function.

static int soundgen_pcm_hw_free(struct snd_pcm_substream *substream)
{
        pr_info("HW free\n");
        return snd_pcm_lib_free_pages(substream);
}

prepare

This is called when we are preparing the PCM setup. Here we set formats, rates,… or in our case we setup our timers. Note: Difference with hw_params is that this function gets called everytime we call snd_pcm_prepare.

static int snd_pcm_timer_prepare(struct snd_pcm_substream *substream)
{
        struct snd_pcm_runtime *runtime = substream->runtime;
        struct snd_pcm_timer *dpcm = runtime->private_data;

        dpcm->frac_pos = 0;
        dpcm->rate = runtime->rate;
        dpcm->frac_buffer_size = runtime->buffer_size * HZ;
        dpcm->frac_period_size = runtime->period_size * HZ;
        dpcm->frac_period_rest = dpcm->frac_period_size;
        dpcm->elapsed = 0;
        return 0;
}

trigger

When a stream gets started or stopped, this is done through the trigger callback. If supported here we can also handle pause and resume.

static int soundgen_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
        pr_info("PCM Trigger\n");
        pr_debug("Command: %d\n",cmd);
        switch (cmd) {
                case SNDRV_PCM_TRIGGER_START:
                        /* do something to start the PCM engine */
                        return get_timer_ops(substream)->start(substream);
                case SNDRV_PCM_TRIGGER_STOP:
                        return get_timer_ops(substream)->stop(substream);
        }
        return -EINVAL;
}

pointer

This callback gets run when snd_pcm_period_elapsed is called. Usually snd_pcm_period_elapsed gets called by some kind of interrupt. In our case we don’t have a hardware interrupt so we will generate one using a timer. Our pointer offset also get calculated depending on the elapsed time on the timer.

static snd_pcm_uframes_t soundgen_pcm_pointer(struct snd_pcm_substream *substream)
{
        pr_info("PCM Pointer\n");
        return get_timer_ops(substream)->pointer(substream);
}

callback

The final part of our initial driver is the interrupt that drives the entire driver. Usually this is an interrupt driven by the hardware. We don’t have any hardware interrupt so as already mentioned before we are using a timer to generate our interrupts. The following code will rearm the timer, update the counters used to calculate the position and if a period has elapsed it will signal a elapsed period to the PCM mid layer. This is also where we are going to update our data.

static void snd_pcm_timer_callback(struct timer_list *t)
{
        pr_info("Systimer callback\n");
        struct snd_pcm_timer *dpcm = from_timer(dpcm, t, timer);
        unsigned long flags;
        int elapsed = 0;

        spin_lock_irqsave(&dpcm->lock, flags);
        snd_pcm_timer_update(dpcm);
        snd_pcm_timer_rearm(dpcm);
        elapsed = dpcm->elapsed;
        dpcm->elapsed = 0;
        pr_debug("elapsed = %d\n",elapsed);

        spin_unlock_irqrestore(&dpcm->lock, flags);
        if (elapsed)
                snd_pcm_period_elapsed(dpcm->substream);
}

ALSA Lib (Continue’d)

Some full examples of ALSA Userspace programming can be found on the following Github page: OPS ALSA Examples. THese can help you test your driver. We’ll go into more detail about ALSA Userspace programming in an upcoming post. Having spend the last 1,5 years developing a bunch of audio software for a customer, I’ve learned that their is a lot of undocumented parts of ALSA which you just have to know.

Conclusion

The ALSA stack mostly follows the driver structure as other subsystems. However it has some quirks that you really know about. Combined with old documentation and no straight forward tutorials it takes some time to get going. However as mentioned in the intro there is a good tutorial on the ALSA page but it misses some parts especially if you’re not using PCI.

All code is available on github: Alsa Sound Gen - bravl/module-dev ยท GitHub