使用ALSA框架播放PCM音频

ALSA框架介绍:

ALSA概述:

ALSA是Advanced Linux Sound Architecture 的缩写,目前已经成为了linux的主流音频体系结构,想了解更多的关于ALSA的这一开源项目的信息和知识,请查看以下网址:http://www.alsa-project.org,也可以从该网站下载相关源码。在内核设备驱动层,ALSA提供了alsa-driver,同时在应用层,ALSA为我们提供了alsa-lib,应用程序只要调用alsa-lib提供的API,即可以完成对底层音频硬件的控制。其总体架构图示如下:

image.png

在用户空间中,由alsa-lib对应用程序提供统一的调用接口,这样可以隐藏驱动层的实现细节,简化应用程序的实现难度。在内核空间中,由alsa-soc对alsa-driver核心驱动提供进一步封装,通过采用这种分层模块,可以在驱动层实现强大的嵌入式设备增强支持。

alsa-lib相关概念:

样本(sample):sample即一次采样的样本,是记录音频数据最基本的单位。通常的sample bit指的是一个channnel上,一次采样的bit数(常见的sample bit 8/16/24/32bits)。
通道(channel):即我们熟知的声道数。左/右声道,5.1channel等等。
帧(frame):帧记录了一个声音单元,一个frame是一次采样时所有channel上的sample bit,即frame = channels * (sample bit)。
采样率(rate):每秒钟采样次数,该次数是针对帧而言。

除开帧不说,通常样本、通道、采样率组成了音频采样的三要素。

周期(period):音频设备一次处理所需要的帧数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。每当hardware buffer 中有peroid size个frame的空间时,硬件就产生中断,来通知alsa driver来往硬件写数据。
buffer size:hardware buffer size 是由多个peroid组成。buffer size = peroid size * peroids。buffer的内存格式如下:


image.png

上图演示的是一块有着16个周期数据的缓存,每个周期设定为8个帧,每帧包含2个通道,每通道包含2个字节(即16位采样),帧使用的是交错排布访问方式。

数据访问布局(Data access and layout):在一个周期内,对于多通道数据,通道数据排布有两种方式:
交错模式(interleaved):数据以连续帧的形式存放,即首先记录完帧1的左声道样本和右声道样本,再开始帧2的记录。
非交错模式:首先记录的是一个周期内所有帧的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。

硬件参数(Hardware parameter):作用于声卡硬件的相关参数,包括sample format, sample rate, interupt intervals, data access and layout, buffer size。

软件参数(Software parameter):作用于alsa core的相关参数,控制一些软件策略,比如控制驱动内存缓存到多少帧后再发数据到声卡,以及xrun状态触发的相关控制等等。

关于缓冲区:
每个声卡都有一个硬件缓存区来保存记录下来的样本。当缓存区足够满时,声卡将产生一个中断。内核声卡驱动然后使用直接内存(DMA)访问通道将样本传送到内存中的应用程序缓存区。类似地,对于回放,任何应用程序使用DMA将自己的缓存区数据传送到声卡的硬件缓存区中。硬件缓存区是环缓存。也就是说当数据到达缓存区末尾时将重新回到缓存区的起始位置。应用程序缓存区的大小可以通过ALSA库函数调用来控制。缓存区可以很大,一次传输操作可能会导致不可接受的延迟,我们把它称为延时(latency)。为了解决这个问题,ALSA将缓存区拆分成一系列周期(period)(OSS/Free中叫片断fragments)。ALSA以period为单元来传送数据。

alsa设备文件:

查看系统设备文件:

$ cd /dev/snd
$ ls -lrt
crw-rw----+ 1 root audio 116, 33 5月  25 14:00 timer
crw-rw----+ 1 root audio 116,  1 5月  25 14:00 seq
crw-rw----+ 1 root audio 116,  5 5月  25 14:00 pcmC0D1c
crw-rw----+ 1 root audio 116,  2 5月  25 14:00 controlC0
drwxr-xr-x. 2 root root       60 5月  25 14:00 by-path
crw-rw----+ 1 root audio 116,  4 5月  25 14:01 pcmC0D0c
crw-rw----+ 1 root audio 116,  3 5月  27 14:21 pcmC0D0p

说明:
timer ---- 定时器设备。
seq ---- 音序器设备。
controlC0 ---- 第1块声卡的控制设备。
pcmC0D0c ---- 第1块声卡的第1个录音设备。
pcmC0D0p ---- 第1块声卡的第1个播放设备。
pcmC0D1c ---- 第1块声卡的第2个录音设备。

alsa驱动通过向应用层抽象出设备,应用层通过读写这些设备,和驱动层进行交互。但是alsa-lib提供了对这些设备的封装,通常我们不需要直接访问他们。

PCM格式介绍:

PCM全称Pulse-Code Modulation,就是脉冲调制编码,简单来说就是一种用数字表示采样模拟信号的方法。
从声卡设备生成PCM数据需要三个阶段:采样、量化、编码,关于这三者的细节不多说,我们直接看下PCM的格式:

例如一段有符号的 8-bit 的 pcm 数据:

+---------+-----------+-----------+----
binary | 0010 0000 | 1010 0000 | ...
decimal | 32 | -96 | ...
+---------+-----------+-----------+----
其表示的采样范围是 -128 ~ 127. 当含有多通道时候 PCM 数据就会交叉排列(通常)以双声道为例:

+---------+-----------+-----------+-----------+-----------+----
FL | FR | FL | FR | FL |
+---------+-----------+-----------+-----------+-----------+----
对于 8-bit 有符号的 PCM 数据而言,上图表示第一个字节存放第一个左声道数据(FL),第二个字节放第一个右声道数据(FR),第三个字节放第二个左声道数据(FL)…

不同的驱动程序对于多声道数据的排列方式可能稍有区别,下面是常用的声道排列地图:

2: FL FR (stereo)
3: FL FR LFE (2.1 surround)
4: FL FR BL BR (quad)
5: FL FR FC BL BR (quad + center)
6: FL FR FC LFE SL SR (5.1 surround - last two can also be BL BR)
7: FL FR FC LFE BC SL SR (6.1 surround)
8: FL FR FC LFE BL BR SL SR (7.1 surround)

上面讲的是PCM交错排列,还有一种非交错排列,两者对比如下:


image.png

相关API介绍:

本文主要讲解PCM接口的相关操作,使用这块分接口可以实现PCM音频播放。

首先需要安装alsa-lib软件包:
sudo yum install alsa-lib alsa-lib-devel -y
安装完成后,可以/usr/include/alsa/pcm.h找到PCM操作的相关定义,下面介绍基本的数据结构和函数。

结构定义:

PCM操作句柄,这是一个成员不公开的结构:

/** PCM handle */
typedef struct _snd_pcm snd_pcm_t;

PCM硬件参数,这是一个成员不公开的结构:

typedef struct _snd_pcm_hw_params snd_pcm_hw_params_t;

PCM的流类型,主要指播放和录音两种:

/** PCM stream (direction) */
typedef enum _snd_pcm_stream {
        /** Playback stream */
        SND_PCM_STREAM_PLAYBACK = 0,
        /** Capture stream */
        SND_PCM_STREAM_CAPTURE,
        SND_PCM_STREAM_LAST = SND_PCM_STREAM_CAPTURE
} snd_pcm_stream_t;

PCM的访问类型,暂时主要关注SND_PCM_ACCESS_RW_INTERLEAVED交错排列和SND_PCM_ACCESS_RW_NONINTERLEAVED非交错排列:

/** PCM access type */
typedef enum _snd_pcm_access {
        /** mmap access with simple interleaved channels */
        SND_PCM_ACCESS_MMAP_INTERLEAVED = 0,
        /** mmap access with simple non interleaved channels */
        SND_PCM_ACCESS_MMAP_NONINTERLEAVED,
        /** mmap access with complex placement */
        SND_PCM_ACCESS_MMAP_COMPLEX,
        /** snd_pcm_readi/snd_pcm_writei access */
        SND_PCM_ACCESS_RW_INTERLEAVED, 
        /** snd_pcm_readn/snd_pcm_writen access */
        SND_PCM_ACCESS_RW_NONINTERLEAVED,
        SND_PCM_ACCESS_LAST = SND_PCM_ACCESS_RW_NONINTERLEAVED
} snd_pcm_access_t;

PCM的样本格式:

/** PCM sample format */
typedef enum _snd_pcm_format {
        /** Unknown */
        SND_PCM_FORMAT_UNKNOWN = -1,
        /** Signed 8 bit */
        SND_PCM_FORMAT_S8 = 0,
        /** Unsigned 8 bit */
        SND_PCM_FORMAT_U8,
        /** Signed 16 bit Little Endian */
        SND_PCM_FORMAT_S16_LE,
        /** Signed 16 bit Big Endian */
        SND_PCM_FORMAT_S16_BE,
        /** Unsigned 16 bit Little Endian */
        SND_PCM_FORMAT_U16_LE,
        /** Unsigned 16 bit Big Endian */
        SND_PCM_FORMAT_U16_BE,
        /** Signed 24 bit Little Endian using low three bytes in 32-bit word */
        SND_PCM_FORMAT_S24_LE,
        /** Signed 24 bit Big Endian using low three bytes in 32-bit word */
        SND_PCM_FORMAT_S24_BE,
        /** Unsigned 24 bit Little Endian using low three bytes in 32-bit word */
        SND_PCM_FORMAT_U24_LE,
        /** Unsigned 24 bit Big Endian using low three bytes in 32-bit word */
        SND_PCM_FORMAT_U24_BE,
        /** Signed 32 bit Little Endian */
        SND_PCM_FORMAT_S32_LE,
        /** Signed 32 bit Big Endian */
        SND_PCM_FORMAT_S32_BE,
        /** Unsigned 32 bit Little Endian */
        SND_PCM_FORMAT_U32_LE,
        /** Unsigned 32 bit Big Endian */
        SND_PCM_FORMAT_U32_BE,
        /** Float 32 bit Little Endian, Range -1.0 to 1.0 */
        SND_PCM_FORMAT_FLOAT_LE,
        /** Float 32 bit Big Endian, Range -1.0 to 1.0 */
        SND_PCM_FORMAT_FLOAT_BE,
        /** Float 64 bit Little Endian, Range -1.0 to 1.0 */
        SND_PCM_FORMAT_FLOAT64_LE,
        /** Float 64 bit Big Endian, Range -1.0 to 1.0 */
        SND_PCM_FORMAT_FLOAT64_BE,
} snd_pcm_format_t;

PCM无符号和有符号帧数量定义:

/** Unsigned frames quantity */
typedef unsigned long snd_pcm_uframes_t;
/** Signed frames quantity */
typedef long snd_pcm_sframes_t;
函数定义:

alsa-lib的函数的返回值,如果类型为int,通常情况下0表示成功,负数表示失败,可以使用snd_strerror(n)获取错误描述。

获取错误描述:
const char *snd_strerror(int errnum);

打开指定的设备,返回PCM句柄,参数name指定设备名称,通常为"default",stream指定流类型,mode通常传0:
int snd_pcm_open(snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream, int mode);
关闭PCM句柄指向的设备:
int snd_pcm_close(snd_pcm_t *pcm);
释放PCM句柄占用的内存:
int snd_pcm_hw_free(snd_pcm_t *pcm);

获取指定PCM句柄的设备名称:
const char *snd_pcm_name(snd_pcm_t *pcm);

分配PCM硬件参数内存:
int snd_pcm_hw_params_malloc(snd_pcm_hw_params_t **ptr);
释放PCM硬件参数内存:
void snd_pcm_hw_params_free(snd_pcm_hw_params_t *obj);

为PCM硬件参数初使化默认值:
int snd_pcm_hw_params_any(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);

从PCM句柄指向的设备获取当前的硬件参数:
int snd_pcm_hw_params_current(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);
向PCM句柄指向的设备设置新的硬件参数:
int snd_pcm_hw_params(snd_pcm_t *pcm, snd_pcm_hw_params_t *params);

读取指定PCM硬件参数的访问方式:
int snd_pcm_hw_params_get_access(const snd_pcm_hw_params_t *params, snd_pcm_access_t *_access);
为PCM硬件参数设置访问方式:
int snd_pcm_hw_params_set_access(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_access_t _access);

读取指定PCM硬件参数的样本格式:
int snd_pcm_hw_params_get_format(const snd_pcm_hw_params_t *params, snd_pcm_format_t *val);
为PCM硬件参数设置样本格式:
int snd_pcm_hw_params_set_format(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_format_t val);

读取指定PCM硬件参数的通道个数:
int snd_pcm_hw_params_get_channels(const snd_pcm_hw_params_t *params, unsigned int *val);
为PCM硬件参数设置通道个数:
int snd_pcm_hw_params_set_channels(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int val);

读取指定PCM硬件参数的采样率:
int snd_pcm_hw_params_get_rate(const snd_pcm_hw_params_t *params, unsigned int *val, int *dir);
为PCM硬件参数设置合适的采样率,val给出建议值,同时输出alsa选择的实际值:
int snd_pcm_hw_params_set_rate_near(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, unsigned int *val, int *dir);

读取指定PCM硬件参数的周期大小(单位为帧数):
int snd_pcm_hw_params_get_period_size(const snd_pcm_hw_params_t *params, snd_pcm_uframes_t *frames, int *dir);
为PCM硬件参数设置合适的周期大小(单位为帧数),val给出建议值,同时输出alsa选择的实际值:
int snd_pcm_hw_params_set_period_size_near(snd_pcm_t *pcm, snd_pcm_hw_params_t *params, snd_pcm_uframes_t *val, int *dir);

向PCM句柄指向的设备写入一段样本缓冲,size为帧数量:
snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size);
从PCM句柄指向的设备读取一段样本缓冲,size为缓冲大小:
snd_pcm_sframes_t snd_pcm_readi(snd_pcm_t *pcm, void *buffer, snd_pcm_uframes_t size);

通知PCM句柄指向的设备准备接收数据:
int snd_pcm_prepare(snd_pcm_t *pcm);
启动PCM句柄指向的设备:
int snd_pcm_start(snd_pcm_t *pcm);
暂停PCM句柄指向的设备,enable取值0表示恢复,1表示暂停:
int snd_pcm_pause(snd_pcm_t *pcm, int enable);
恢复PCM句柄指向的设备:
int snd_pcm_resume(snd_pcm_t *pcm);

排空PCM句柄指向的设备的样本缓冲:
int snd_pcm_drain(snd_pcm_t *pcm);

设置PCM句柄指向的设备的延迟帧数:
int snd_pcm_delay(snd_pcm_t *pcm, snd_pcm_sframes_t *delayp);
重置PCM句柄指向的设备的延迟帧数:
int snd_pcm_reset(snd_pcm_t *pcm);

设置PCM句柄指向的设备执行延迟硬同步:
int snd_pcm_hwsync(snd_pcm_t *pcm);

代码举例:

下面这个例子读取1个PCM白噪声文件,并且按FLOAT_LE格式、2通道交错排列、44.1K采样率来解析,代码如下:

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

#include <alsa/asoundlib.h>

int main(int argc, char* argv[])
{
    snd_pcm_t *hPCM = NULL;
    int rc = snd_pcm_open(&hPCM, "default", SND_PCM_STREAM_PLAYBACK, 0);
    if (rc < 0)
    {
        printf("snd_pcm_open() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_open() %s ok. \n", snd_pcm_name(hPCM));

    snd_pcm_hw_params_t *pHWParams = NULL;
    snd_pcm_hw_params_malloc(&pHWParams);
    snd_pcm_hw_params_any(hPCM, pHWParams);

    rc = snd_pcm_hw_params_set_access(hPCM, pHWParams, SND_PCM_ACCESS_RW_INTERLEAVED);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params_set_access() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params_set_access() SND_PCM_ACCESS_RW_INTERLEAVED ok. \n");

    rc = snd_pcm_hw_params_set_format(hPCM, pHWParams, SND_PCM_FORMAT_FLOAT_LE);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params_set_format() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params_set_format() SND_PCM_FORMAT_FLOAT_LE ok. \n");

    rc = snd_pcm_hw_params_set_channels(hPCM, pHWParams, 2);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params_set_channels() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params_set_channels() 2 ok. \n");

    unsigned int nSampleRate = 44100;
    rc = snd_pcm_hw_params_set_rate_near(hPCM, pHWParams, &nSampleRate, NULL);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params_set_rate_near() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params_set_rate_near() advise:[44100] real:[%d] ok. \n", nSampleRate);

    snd_pcm_uframes_t nFrames = 1024;
    rc = snd_pcm_hw_params_set_period_size_near(hPCM, pHWParams, &nFrames, NULL);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params_set_period_size_near() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params_set_period_size_near() advise:[1024] real:[%d] ok. \n", nFrames);

    rc = snd_pcm_hw_params(hPCM, pHWParams);
    if (rc < 0)
    {
        printf("snd_pcm_hw_params() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_params() ok. \n");

    snd_pcm_hw_params_free(pHWParams);

    rc = snd_pcm_prepare(hPCM);
    if (rc < 0)
    {
        printf("snd_pcm_prepare() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    rc = snd_pcm_start(hPCM);
    if (rc < 0)
    {
        printf("snd_pcm_start() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    int nFrameBuffSize = 4 * 2 * nFrames;
    char* pFrameBuff = (char*)malloc(nFrameBuffSize);

    FILE* pFile = fopen("test.pcm", "rb");

    while (!feof(pFile))
    {
        int nBytes = fread(pFrameBuff, 1, nFrameBuffSize, pFile);
        if (nBytes == nFrameBuffSize)
        {
            printf("read 1 period frames(%d) \n", nBytes/8);
        }
        else
        {
            if (nBytes % 8)
            {
                printf("read break! \n");
                break;
            }

            printf("no 1 period frames(%d) \n", nBytes/8);
        }

        rc = snd_pcm_writei(hPCM, pFrameBuff, nBytes/8);
        if (rc == -EPIPE)
        {
            printf("snd_pcm_writei() underrun occurred! \n");
            snd_pcm_prepare(hPCM);
        }
        else if (rc < 0)
        {
            printf("snd_pcm_writei() ret:[%d:%s] \n", rc, snd_strerror(rc));
            break;
        }
        else if (rc < nFrames)
        {
            printf("snd_pcm_writei() short write! \n");
        }
    }

    fclose(pFile);
    free(pFrameBuff);
    
    rc = snd_pcm_drain(hPCM);
    if (rc < 0)
    {
        printf("snd_pcm_drain() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_drain() ok \n");

    rc = snd_pcm_close(hPCM);
    if (rc < 0)
    {
        printf("snd_pcm_hw_close() ret:[%d:%s] \n", rc, snd_strerror(rc));
        return -1;
    }

    printf("snd_pcm_hw_close() ok \n");

    snd_pcm_hw_free(hPCM);

    return 0;
}

保存上面的代码为alsa1.c,编译生成可执行:
gcc -o alsa1 alsa1.c -lasound

生成PCM文件,执行:
cat /dev/urandom > test.pcm
生成很快,所以需要尽快CTRL+C。

插上耳机,播放PCM文件,可能需要root权限:
sudo ./alsa1
可以听到沙沙的噪声。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容

  • alsa_udp_r.c: #include "alsa/asoundlib.h" struct snd_pcm ...
    Sweets_JIE阅读 1,718评论 1 0
  • 一.声音参数基本概念: 声音是连续模拟量,计算机将它离散化之后用数字表示,就有了以下几个名词术语。 样本长度(sa...
    cs1001阅读 2,596评论 0 2
  • 一.声音参数基本概念: 声音是连续模拟量,计算机将它离散化之后用数字表示,就有了以下几个名词术语。 样本长度(sa...
    cs1001阅读 5,191评论 0 3
  • [toc] 本文尝试提供一些对 ALSA 音频 API 的介绍。它不是 ALSA API 的完整参考手册,它也不...
    hanpfei阅读 1,039评论 0 0
  • #include "alsa/asoundlib.h" /* this buffer holds the digi...
    Sweets_JIE阅读 1,579评论 0 0