博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
About a full duplex ALSA application
阅读量:4059 次
发布时间:2019-05-25

本文共 10078 字,大约阅读时间需要 33 分钟。

The tutorials on the web don't talk too much about how to write an effect processor using ALSA. They do talk about recording program, and a playback program, but the combination is a kind of heresy. One tutorial even explains that writing a full duplex program is fairly involved and suggests forgetting about the whole thing and usingjack instead. Gee, thanks. A "go away" message in tutorial was exactly what I wanted to hear. Well, jack support is probably in the future forgnuitar, but right now ALSA will have to do.

So, I struggled with it a while and managed to make it work at least on 3 soundcards, all using ALSA. It brings no great confidence to report that each time I managed to make it work on one, it seemed to break on the others... I suspect there are still bugs I don't know about.

Stage 1: opening up the capture and playback devices

This is fairly subtle, as is everything else. First of all, do not use the raw hw:0,0 device, but use plughw:0,0. (Except use surround40:0,0 if you need 4-channel output.) This is so that you get automatic adaptation of hardware to your requirements. For instance, you can use sample formats not directly supported by hardware, or do mono output on systems that only support stereo output in hardware. (I have such a system, and I was not amused to discover that mono output was "impossible".)

So, firstly, open up devices and handle errors:

/* you allocate these in heap */int             restarting;int             nchannels = 1;int             buffer_size = 512;int             sample_rate = 48000;int             bits = 16;char           *snd_device_in  = "plughw:0,0";char           *snd_device_out = "plughw:0,0";snd_pcm_t      *playback_handle;snd_pcm_t      *capture_handle;if ((err = snd_pcm_open(&playback_handle, snd_device_out, SND_PCM_STREAM_PLAYBACK, 0)) < 0) {    fprintf(stderr, "cannot open output audio device %s: %s\n", snd_device_in, snd_strerror(err));    exit(1);}if ((err = snd_pcm_open(&capture_handle, snd_device_in, SND_PCM_STREAM_CAPTURE, 0)) < 0) {    fprintf(stderr, "cannot open input audio device %s: %s\n", snd_device_out, snd_strerror(err));    exit(1);}configure_alsa_audio(snd_device_in,  nchannels);configure_alsa_audio(snd_device_out, nchannels);restarting = 1;

I wrote a small configuring function that sets identical setup on both handles. I copypasted these from the tutorials at the site, developer manuals.

intconfigure_alsa_audio(snd_pcm_t *device, int channels){    snd_pcm_hw_params_t *hw_params;    int                 err;    int                 tmp;    snd_pcm_uframes_t   frames;    int                 fragments = 2;    /* allocate memory for hardware parameter structure */     if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {        fprintf (stderr, "cannot allocate parameter structure (%s)\n",                 snd_strerror(err));        return 1;    }    /* fill structure from current audio parameters */    if ((err = snd_pcm_hw_params_any(device, hw_params)) < 0) {        fprintf (stderr, "cannot initialize parameter structure (%s)\n",                 snd_strerror(err));        return 1;    }    /* set access type, sample rate, sample format, channels */    if ((err = snd_pcm_hw_params_set_access(device, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {        fprintf (stderr, "cannot set access type: %s\n",                 snd_strerror(err));        return 1;    }    // bits = 16    if ((err = snd_pcm_hw_params_set_format(device, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) {        fprintf (stderr, "cannot set sample format: %s\n",                 snd_strerror(err));        return 1;    }    tmp = sample_rate;        if ((err = snd_pcm_hw_params_set_rate_near(device, hw_params, &tmp, 0)) < 0) {        fprintf (stderr, "cannot set sample rate: %s\n",                 snd_strerror(err));        return 1;    }    if (tmp != sample_rate) {        fprintf(stderr, "Could not set requested sample rate, asked for %d got %d\n", sample_rate, tmp);        sample_rate = tmp;    }    if ((err = snd_pcm_hw_params_set_channels(device, hw_params, channels)) < 0) {        fprintf (stderr, "cannot set channel count: %s\n",                 snd_strerror(err));        return 1;    }

Now, at this point of setup we are mostly done, but here comes the important bit that I discovered: many soundcards haveweird buffer sizes. For instance, I have a Hoontech Digital XG-I card which refuses to use 2^n buffer sizes. The buffers had crazy sizes like 608 or 1346 bytes or something like that. What is even more interesting is that it doesn't support operating with just 2 fragments. (Fragments are the number of equal-sized chunks the full audio buffer gets divided to, and alsa calls them periods.) The code that reads will get fragment-sized chunks. The code that writes needs to write a fragment-sized chunk. Many devices support just 2 fragment operation, but not this one. So, I have to use the _nearvariant of the function that will allocate whatever amount the soundcard hardware can do, and use that instead. So:

if ((err = snd_pcm_hw_params_set_periods_near(device, hw_params, &fragments, 0)) < 0) {        fprintf(stderr, "Error setting # fragments to %d: %s\n", fragments,                snd_strerror(err));        return 1;    }

The buffer_size variable holds the size of a fragment. It needs to be converted to frames, which are one sample data for all channels involved. If we are in 16-bit 2-channel operation, a frame is 4 bytes wide. So, I multiply the frames with the number of fragments supported by hardware, and hope that the hardware can give me that buffer size. Or if it can't, I certainly hope it chooses a buffer size that yields equal-sized fragments... I don't really care, I'll just update my variable with whatever the sound card saw fit to give.

frame_size = channels * (bits / 8);    frames = buffer_size / frame_size * fragments;    if ((err = snd_pcm_hw_params_set_buffer_size_near(device, hw_params, &frames)) < 0) {        fprintf(stderr, "Error setting buffer_size %d frames: %s\n", frames,                snd_strerror(err));        return 1;    }    if (buffer_size != frames * frame_size / fragments) {        fprintf(stderr, "Could not set requested buffer size, asked for %d got %d\n", buffer_size, frames * frame_size / fragments);        buffer_size = frames * frame_size / fragments;    }

The final part: just set the parameters:

if ((err = snd_pcm_hw_params(device, hw_params)) < 0) {        fprintf(stderr, "Error setting HW params: %s\n",                snd_strerror(err));        return 1;    }    return 0;}

Stage 2. The event loop

The code enters a while loop that looks like this:

int             frames, inframes, outframes, frame_size;while (! exit_program) {    frame_size = channels * (bits / 8);    frames = buffer_size / frame_size;    if (restarting) {        restarting = 0;        /* drop any output we might got and stop */        snd_pcm_drop(capture_handle);        snd_pcm_drop(playback_handle);        /* prepare for use */        snd_pcm_prepare(capture_handle);        snd_pcm_prepare(playback_handle);    /* fill the whole output buffer */    for (i = 0; i < fragments; i += 1)    snd_pcm_writei(playback_handle, rdbuf, frames);    }    while ((inframes = snd_pcm_readi(capture_handle, rdbuf, frames)) < 0) {        if (inframes == -EAGAIN)            continue;    // by the way, writing to terminal emulator is costly if you use    // bad emulators like gnome-terminal, so don't do this.        fprintf(stderr, "Input buffer overrun\n");        restarting = 1;        snd_pcm_prepare(capture_handle);    }    if (inframes != frames)        fprintf(stderr, "Short read from capture device: %d, expecting %d\n", inframes, frames);    /* now processes the frames */    do_something(rdbuf, inframes);    while ((outframes = snd_pcm_writei(playback_handle, rdbuf, inframes)) < 0) {        if (outframes == -EAGAIN)            continue;        fprintf(stderr, "Output buffer underrun\n");        restarting = 1;        snd_pcm_prepare(playback_handle);    }    if (outframes != inframes)        fprintf(stderr, "Short write to playback device: %d, expecting %d\n", outframes, frames);

There's nothing especially strange about this -- this is a read-process-write loop... apart from one thing: the restarting variable. Whenever a buffer overrun or underrun occurs, or when we are initializing, the restarting is set to 1 and that kicks the sound device back into orderly shape. I prefer shock treatment to accomplish this: first I tell it to dump all audio data it was carrying, then I prepare it back for operation with the prepare() call, and now the important bit: I prefill 2 fragments worth of data with some gibberish that happened to already linger in the buffers. (Yes, I'll init the buffers with silence one day.)

This last part is essential. When entering a read-process-write loop, these buffers must be prefilled with data. 1 fragment write would not do for my SB Live!, it had to be 2 fragments. (I have no idea whether the count should be scaled up to greater number of fragments in case the sound hardware don't support 2 fragment operation. The soundcard that doesn't seems to not rattle with 2 fragments write, though.)

Some comments about ALSA

  • It's a bit too difficult to use. Too many function calls, too low level. A few utility functions to open a device and set the most common parameters at one swoop would be great.

  • There seems to be no good manuals. No wonder audio software tends to be a bit crappy, it's extremely hard to puzzle this stuff out.

  • Why do I need to write 2 fragments before I start my read-process-write loop? Why doesn't the audio driver even seem to notice it's outputting garbage?

  • Why, with SB Live!, if I set the system to use 128 frames fragments and it tells me it can only do 192 frames fragments the driver will use 128 frame fragments for read and write loops. Is it a bug? What's going on?

For updates to the code introduced in the blog entry, check out gnuitar's src/main.c. There's the full glory with locking and threading details that I chose to skip.

转载地址:http://ebzji.baihongyu.com/

你可能感兴趣的文章
第03课:调整执行计划
查看>>
第02课:解读执行计划
查看>>
第01课:SQL 语句的历程
查看>>
导读:物理优化篇
查看>>
导读:逻辑优化篇
查看>>
开篇词:翻过数据库优化器这座山峰
查看>>
堆排序之MAX-HEAPIFY注释
查看>>
数据库之并发控制调度
查看>>
快速排序
查看>>
堆排序完整版,含注释
查看>>
鹰蛋问题解析之动态规划
查看>>
查询求解概述读书笔记
查看>>
二叉树非递归先序遍历、中序遍历、后序遍历
查看>>
二叉树深度优先遍历和广度优先遍历
查看>>
生产者消费者模型,循环队列实现
查看>>
PostgreSQL代码分析,查询优化部分,process_duplicate_ors
查看>>
PostgreSQL代码分析,查询优化部分,canonicalize_qual
查看>>
PostgreSQL代码分析,查询优化部分,pull_ands()和pull_ors()
查看>>
ORACLE权限管理调研笔记
查看>>
移进规约冲突一例
查看>>