本文仅仅是个人的一个学习过程总结,并没有细化下去,如果想要细化研究,可百度查找相关资料细看。
大概会按照:
概念 -> 硬件 -> 软件 -> 驱动 -> 用户空间 -> Android 这种流程介绍
下面展示了一般 Codec 芯片的框架及所可能拥有的模块图
仅简单介绍了常用功能,具体含义可百度,写太多会抓不住重点
供参考理解上面介绍的相关概念
下面是个更详细的例子,介绍了 WM9883 音频芯片逻辑通路及相关 Linux Codec 通路抽象
Linux 部分参考资料:
韦东山一期/三期声卡相关视频
http://blog.csdn.net/droidphone Alsa 相关博客
Android 部分参考:
深入理解Android内核设计思想_林学森./第 13 章 应用不再同质化 – 音频系统
深入剖析 Android 系统_杨长刚/第 14 章 Audio
深入理解 Android 卷1_邓凡平/ 第7章 深入理解 Audio 系统
不会写太细,提纲擎领而已
怎么写 ALSA 声卡驱动:
1. snd_card_create() // 创建 snd_card 的一个实例
snd_ctl_create()
2. 初始化:创建声卡的功能部件(逻辑设备)
snd_pcm_new()
3. snd_card_register() //注册声卡
1. struct snd_card 结构体
1.1. snd_card 是什么
snd_card 可以说是整个ALSA音频驱动最顶层的一个结构,整个声卡的软件逻辑结构开始于该结构,几乎所有与声
音相关的逻辑设备都是在 snd_card 的管理之下,声卡驱动的第一个动作通常就是创建一个 snd_card 结构体。正因
为如此,本节中,我们也从 struct cnd_card 开始吧。
1.2. snd_card的定义
snd_card的定义位于改头文件中: include/sound/core.h
/* main structure for soundcard */
struct snd_card {
int number; / * number of soundcard (index to snd_cards) * /
char id[16]; / * id string of this card * /
char driver[16]; / * driver name * /
char shortname[32]; / * short name of this soundcard * /
char longname[80]; / * name of this soundcard * /
char mixername[80]; / * mixer name * /
char components[128]; / * card components delimited with space * /
struct module *module; / * top‐level module * /
void *private_data; / *【声卡的私有数据,可以在创建声卡时通过参数指定数据的大小】/
void (*private_free) (struct snd_card *card); / * callback for freeing of private data * /
struct list_head devices; / *【记录该声卡下所有逻辑设备的链表】 /
unsigned int last_numid; / * last used numeric ID * /
struct rw_semaphore controls_rwsem; / * controls list lock * /
rwlock_t ctl_files_rwlock; / * ctl_files list lock * /
int controls_count; / * count of all controls * /
int user_ctl_count; / * count of all user controls * /
struct list_head controls; / *【记录该声卡下所有的控制单元的链表】 /
struct list_head ctl_files; / * active control files * /
struct snd_info_entry *proc_root; / * root for soundcard specific files * /
struct snd_info_entry *proc_id; / * the card id * /
struct proc_dir_entry *proc_root_link; / * number link to real id * /
struct list_head files_list; / * all files associated to this card * /
struct snd_shutdown_f_ops *s_f_ops; / * file operations in the shutdown
state * /
spinlock_t files_lock; / * lock the files for this card * /
int shutdown; / * this card is going down * /
int free_on_last_close; / * free in context of file_release * /
wait_queue_head_t shutdown_sleep;
struct device *dev; / * device assigned to this card * /
#ifndef CONFIG_SYSFS_DEPRECATED
struct device *card_dev; / * cardX object for sysfs * /
#endif
#ifdef CONFIG_PM
unsigned int power_state; / * power state * /
struct mutex power_lock; / * power lock * /
wait_queue_head_t power_sleep;
#endif
#if defined(CONFIG_SND_MIXER_OSS) || defined(CONFIG_SND_MIXER_OSS_MODULE)
struct snd_mixer_oss *mixer_oss;
int mixer_oss_change_count;
#endif
};
2. 声卡的建立流程
2.1.1. 第一步,创建snd_card的一个实例
struct snd_card *card;
int err;
....
err = snd_card_create(index, id, THIS_MODULE, 0, &card);
index :一个整数值,该声卡的编号
id :字符串,声卡的标识符
第四个参数 :该参数决定在创建 snd_card 实例时,需要同时额外分配的私有数据的大小,该数据的指针最
终会赋值给 snd_card 的 private_data 数据成员
card :返回所创建的 snd_card 实例的指针
2.1.2. 第二步,创建声卡的芯片专用数据
声卡的专用数据主要用于存放该声卡的一些资源信息,例如中断资源、 io资源、 dma资源等。可以有两种
创建方法:
1. 通过上一步中 snd_card_create() 中的第四个参数,让 snd_card_create() 自己创建
// struct mychip 用于保存专用数据
err = snd_card_create(index, id, THIS_MODULE, sizeof(struct mychip), &card);
// 从 private_data 中取出
struct mychip *chip = card‐>private_data;
2. 自己创建:
struct mychip {
struct snd_card *card;
....
};
struct snd_card *card;
struct mychip *chip;
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
......
err = snd_card_create(index[dev], id[dev], THIS_MODULE, 0, &card);
// 专用数据记录snd_card实例
chip‐>card = card;
.....
然后,把芯片的专有数据注册为声卡的一个低阶设备:
static int snd_mychip_dev_free(struct snd_device *device)
{
return snd_mychip_free(device‐>device_data);
}
static struct snd_device_ops ops = {
.dev_free = snd_mychip_dev_free,
};
....
snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip, &ops);
注册为低阶设备主要是为了当声卡被注销时,芯片专用数据所占用的内存可以被自动地释放。
2.1.3. 第三步,设置 Driver 的 ID 和名字
strcpy(card‐>driver, "My Chip");
strcpy(card‐>shortname, "My Own Chip 123");
sprintf(card‐>longname, "%s at 0x%lx irq %i",
card‐>shortname, chip‐>ioport, chip‐>irq);
snd_card 的 driver 字段保存着芯片的ID字符串, user 空间的 alsa-lib 会使用到该字符串,所以必须要保证该 ID 的唯
一性。 shortname 字段更多地用于打印信息, longname 字段则会出现在 /proc/asound/cards 中。
2.1.4. 第四步,创建声卡的功能部件(逻辑设备),例如PCM, Mixer, MIDI等
这时候可以创建声卡的各种功能部件了,还记得开头的 snd_card 结构体的 devices 字段吗?每一种部件的创建最终
会调用 snd_device_new() 来生成一个 snd_device 实例,并把该实例链接到 snd_card 的 devices 链表中。
通常, alsa-driver的已经提供了一些常用的部件的创建函数,而不必直接调用snd_device_new(),比如:
PCM -- snd_pcm_new()
RAWMIDI -- snd_rawmidi_new()
CONTROL -- snd_ctl_create()
TIMER -- snd_timer_new()
INFO -- snd_card_proc_new()
JACK -- snd_jack_new()
2.1.5. 第五步,注册声卡
err = snd_card_register(card);
if (err < 0) {
snd_card_free(card);
return err;
}
/sound/arm/pxa2xx-ac97.c的部分代码贴上来:
static int __devinit pxa2xx_ac97_probe(struct platform_device *dev)
{
struct snd_card *card;
struct snd_ac97_bus *ac97_bus;
struct snd_ac97_template ac97_template;
int ret;
pxa2xx_audio_ops_t *pdata = dev->dev.platform_data;
if (dev->id >= 0) {
dev_err(&dev->dev, "PXA2xx has only one AC97 port./n");
ret = -ENXIO;
goto err_dev;
}
(1) 第一步,创建snd_card的一个实例
ret = snd_card_create(SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1,THIS_MODULE, 0, &card);
if (ret < 0)
goto err;
card->dev = &dev->dev;
(3) 第三步,设置Driver的ID和名字
strncpy(card->driver, dev->dev.driver->name, sizeof(card->driver));
(4) 第四步,创建声卡的功能部件(逻辑设备)PCM
ret = pxa2xx_pcm_new(card, &pxa2xx_ac97_pcm_client, &pxa2xx_ac97_pcm);
if (ret)
goto err;
(2) 第二步,创建声卡的芯片专用数据
ret = pxa2xx_ac97_hw_probe(dev);
if (ret)
goto err;
(4) 第四步,创建声卡的功能部件(逻辑设备)AC97_BUS
ret = snd_ac97_bus(card, 0, &pxa2xx_ac97_ops, NULL, &ac97_bus);
if (ret)
goto err_remove;
memset(&ac97_template, 0, sizeof(ac97_template));
ret = snd_ac97_mixer(ac97_bus, &ac97_template, &pxa2xx_ac97_ac97);
if (ret)
goto err_remove;
(3) 第三步,设置Driver的ID和名字
snprintf(card->shortname, sizeof(card->shortname),
"%s", snd_ac97_get_short_name(pxa2xx_ac97_ac97));
snprintf(card->longname, sizeof(card->longname),
"%s (%s)", dev->dev.driver->name, card->mixername);
if (pdata && pdata->codec_pdata[0])
snd_ac97_dev_add_pdata(ac97_bus->codec[0], pdata->codec_pdata[0]);
snd_card_set_dev(card, &dev->dev);
(5) 第五步,注册声卡
ret = snd_card_register(card);
if (ret == 0) {
platform_set_drvdata(dev, card);
return 0;
}
err_remove:
pxa2xx_ac97_hw_remove(dev);
err:
if (card)
snd_card_free(card);
err_dev:
return ret;
}
static int __devexit pxa2xx_ac97_remove(struct platform_device *dev)
{
struct snd_card *card = platform_get_drvdata(dev);
if (card) {
snd_card_free(card);
platform_set_drvdata(dev, NULL);
pxa2xx_ac97_hw_remove(dev);
}
return 0;
}
static struct platform_driver pxa2xx_ac97_driver = {
.probe = pxa2xx_ac97_probe,
.remove = __devexit_p(pxa2xx_ac97_remove),
.driver = {
.name = "pxa2xx-ac97",
.owner = THIS_MODULE,
#ifdef CONFIG_PM
.pm = &pxa2xx_ac97_pm_ops,
#endif
},
};
static int __init pxa2xx_ac97_init(void)
{
return platform_driver_register(&pxa2xx_ac97_driver);
}
static void __exit pxa2xx_ac97_exit(void)
{
platform_driver_unregister(&pxa2xx_ac97_driver);
}
module_init(pxa2xx_ac97_init);
module_exit(pxa2xx_ac97_exit);
MODULE_AUTHOR("Nicolas Pitre");
MODULE_DESCRIPTION("AC97 driver for the Intel PXA2xx chip");
ASOC 是建立在 Alsa 驱动层之上的优化,模块化更好,个人感觉更方便偷懒了。。。
它在 ALSA 中又抽象出来三部分:
1. machine: 单板相关,表明 platform 是哪个,CPU DAI 是哪个,DMA 是哪个
表明 codec 是哪个,codec DAI 是哪个
代表结构体:snd_soc_card/snd_soc_dai_link
2. platform: CPU 相关,主要有两方面:
1> DAI: 控制接口,代表数据结构 snd_soc_dai_driver
2> DMA: 传数据,代表数据结构 snd_soc_platform_driver
3. codec: 声卡芯片相关,也分两方面:
1> DAI: 用于传输数据,代表结构 snd_soc_dai_driver
2> 控制接口:用于寄存器设置什么的,代表结构 snd_soc_codec_driver
相关的数据结构内容为:
1. machine:
snd_soc_dai_link{各子驱动的名字}
2. codec:
snd_soc_dai_driver{
属性:声道数,采样率,格式
ops: 启动、关闭、参数设置
}
snd_soc_codec_driver{函数}
3. platform:
dai: snd_soc_dai_driver{
属性:声道数,采样率,格式
ops: 启动、关闭、参数设置
}
dma: snd_soc_platform_driver{
函数
ops: 数据传输相关函数
}
具体可参考附件 【非常好】音频驱动框架总结.不带DPM.3.4.2.txt
// 【machine】 相关部分
// 主要任务:
// 注册一个平台设备,名为 "soc-audio"
// 设置平台设备的私有数据为 snd_soc_card 结构体
// snd_soc_card:
// 设置一个 snd_soc_dai_link 结构体,匹配各驱动
// snd_soc_dai_link:
// ################################################
// # Codec 驱动: snd_soc_codec_driver/snd_soc_dai_driver
// .codec_name : 指明 Codec 名称,用于设置 Codec 寄存器,注册 snd_soc_codec_driver 两个结构体
// .codec_dai_name : 指明 Codec Dai 名称,用于与 CPU 通信的接口操作,如 PCM/I2S, 注册 snd_soc_dai_driver
// ################################################
// # Platform 驱动:snd_soc_platform_driver/snd_soc_dai_driver
// .platform_name : 指明 DMA 驱动的名称,用于如何将音频数据传到 dai 接口, 注册 snd_soc_platform_driver 这个结构体
// .cpu_dai_name : 指明 CPU Dai 名称,用于与 codec 通信的接口,如 PCM/I2S 等,注册 snd_soc_dai_driver 结构体
// .ops
//S3c24xx_uda134x.c (sound\soc\samsung) |
module_platform_driver(s3c24xx_uda134x_driver); |//Mach-mini2440.c (arch\arm\mach-s3c24xx)
static struct platform_driver s3c24xx_uda134x_driver = { | static struct platform_device mini2440_audio = {
.probe = s3c24xx_uda134x_probe, | .name = "s3c24xx_uda134x",
.remove = s3c24xx_uda134x_remove, | .id = 0,
.driver = { | .dev = {
.name = "s3c24xx_uda134x", | .platform_data = &mini2440_audio_pins,
.owner = THIS_MODULE, | },
}, | };
}; |
左边:
s3c24xx_uda134x_probe()//【注册一个名为 "soc-audio" 平台设备,其私有数据保存了要注册的 snd_soc_card 】
s3c24xx_uda134x_snd_device = platform_device_alloc("soc-audio", -1);
platform_set_drvdata(s3c24xx_uda134x_snd_device, &snd_soc_s3c24xx_uda134x);
/* 整个驱动核心 */
static struct snd_soc_card snd_soc_s3c24xx_uda134x = {
.name = "S3C24XX_UDA134X",
.owner = THIS_MODULE,
.dai_link = &s3c24xx_uda134x_dai_link,
.num_links = 1,
};
/* 指明了用哪个 codec,codec dai,cpu,cpu dai */
static struct snd_soc_dai_link s3c24xx_uda134x_dai_link = {
.name = "UDA134X",
.stream_name = "UDA134X",
.codec_name = "uda134x-codec", // 【Codec】用哪一个 codec
.codec_dai_name = "uda134x-hifi", // 用 codec 芯片的哪一个 dai
.cpu_dai_name = "s3c24xx-iis", // 【Platform】指定 2440 的 dai
.ops = &s3c24xx_uda134x_ops,
.platform_name = "samsung-audio",// 2440 DMA 操作
};
static struct snd_soc_ops s3c24xx_uda134x_ops = {
.startup = s3c24xx_uda134x_startup, // 获得时钟
.shutdown = s3c24xx_uda134x_shutdown,// 释放时钟
.hw_params = s3c24xx_uda134x_hw_params,//设置 cpu dai 通信格式
};
右边:
// Soc-core.c (sound\soc) 匹配对应的驱动
static struct platform_driver soc_driver = {
.driver = {
.name = "soc-audio",
.owner = THIS_MODULE,
.pm = &snd_soc_pm_ops,
},
.probe = soc_probe,
.remove = soc_remove,
};
soc_probe()
// 注册左边声明 snd_soc_card 结构的 snd_soc_s3c24xx_uda134x
snd_soc_register_card(card);
card->rtd = devm_kzalloc(card->dev,...
card->rtd[i].dai_link = &card->dai_link[i]; // &s3c24xx_uda134x_dai_link
list_add(&card->list, &card_list);
snd_soc_instantiate_cards(); // 实例化声卡
snd_soc_instantiate_cards(card);
3.1 /* bind DAIs,确定使用 CPU 侧以及 Codec 侧的哪一个 DAI */
for (i = 0; i < card->num_links; i++)
soc_bind_dai_link(card, i);
3.1.1 /* find CPU DAI */
rtd->cpu_dai = cpu_dai; = //&s3c24xx_i2s_dai
3.1.2 /* find_codec */
rtd->codec = codec; = // codec, codec->driver=&soc_codec_dev_uda134x
3.1.3 /* find CODEC DAI */
rtd->codec_dai = codec_dai; // = &uda134x_dai
3.1.4 /* find_platform */
rtd->platform = platform; // = &samsung_asoc_platform
3.2 /* initialize the register cache for each available codec */
ret = snd_soc_init_codec_cache(codec, compress_type);
3.3 snd_card_create() // 标准声卡创建函数
3.4 /* early DAI link probe */
// 这个函数会调用各个匹配的驱动的 probe 函数, 会匹配哪些呢?就是 s3c24xx_uda134x_dai_link 指定的那些名字的驱动
soc_probe_dai_link()
/* probe the cpu_dai */
/* probe the CODEC */
/* probe the platform */
/* probe the CODEC DAI */
/* create the pcm */
ret = soc_new_pcm(rtd, num); // 创建 PCM 部件
struct snd_pcm_ops *soc_pcm_ops = &rtd->ops;
soc_pcm_ops->open = soc_pcm_open;
soc_pcm_ops->close = soc_pcm_close;
soc_pcm_ops->hw_params = soc_pcm_hw_params;
soc_pcm_ops->hw_free = soc_pcm_hw_free;
soc_pcm_ops->prepare = soc_pcm_prepare;
soc_pcm_ops->trigger = soc_pcm_trigger;
soc_pcm_ops->pointer = soc_pcm_pointer;
snd_pcm_new()
3.5 snd_card_register() // 标准声卡注册函数
// 【platform】的匹配 probe : 这是处理 CPU 的 DMA 的操作
// 主要作用:通过 snd_soc_register_platform() 注册平台相关的 DMA 相关操作
// 即注册 snd_soc_platform_driver 这个结构体
//
// Dma.c (sound\soc\samsung) |//Devs.c (arch\arm\plat-samsung)
static struct platform_driver asoc_dma_driver = { | struct platform_device samsung_asoc_dma = {
.driver = { | .name = "samsung-audio",
.name = "samsung-audio", | .id = -1,
.owner = THIS_MODULE, | .dev = {
}, | .dma_mask = &samsung_device_dma_mask,
| .coherent_dma_mask = DMA_BIT_MASK(32),
.probe = samsung_asoc_platform_probe, | }
.remove = __devexit_p(samsung_asoc_platform_remove), | };
}; |
samsung_asoc_platform_probe()
return snd_soc_register_platform(&pdev->dev, &samsung_asoc_platform);
// 平台相关操作驱动
static struct snd_soc_platform_driver samsung_asoc_platform = {
.ops = &dma_ops,
.pcm_new = dma_new, // 分配 DMA 内存
.pcm_free = dma_free_dma_buffers, // 释放 DMA 内存
};
// DMA 操作
static struct snd_pcm_ops dma_ops = {
.open = dma_open, // 打开 pcm 设备时调用,用于保存平台 dma 参数,获取 DMA 中断
.close = dma_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = dma_hw_params, // 获得对应的 dai 的 dma 参数
.hw_free = dma_hw_free,
.prepare = dma_prepare, // 正式开始数据传输时会调用该函数
.trigger = dma_trigger, // 数据传送的开始、暂停、恢复和停止时调用
.pointer = dma_pointer, // 返回传送数据的当前位置
.mmap = dma_mmap,
};
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 【CPU DAI】 的匹配 probe: 这是处理与声卡芯片的操作的接口设置的
// 主要作用:通过 snd_soc_register_dai() 注册 CPU Dai
// 即注册 snd_soc_dai_driver 结构体
//
// S3c24xx-i2s.c (sound\soc\samsung) |// Devs.c (arch\arm\plat-samsung)
static struct platform_driver s3c24xx_iis_driver = { | struct platform_device s3c_device_iis = {
.probe = s3c24xx_iis_dev_probe, | .name = "s3c24xx-iis",
.remove = __devexit_p(s3c24xx_iis_dev_remove), | .id = -1,
.driver = { | .num_resources = ARRAY_SIZE(s3c_iis_resource),
.name = "s3c24xx-iis", | .resource = s3c_iis_resource,
.owner = THIS_MODULE, | .dev = {
}, | .dma_mask = &samsung_device_dma_mask,
}; | .coherent_dma_mask = DMA_BIT_MASK(32),
| }
s3c24xx_iis_dev_probe() | };
return snd_soc_register_dai(&pdev->dev, &s3c24xx_i2s_dai);
static struct snd_soc_dai_driver s3c24xx_i2s_dai = {
.probe = s3c24xx_i2s_probe,
.suspend = s3c24xx_i2s_suspend,
.resume = s3c24xx_i2s_resume,
.playback = {
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.capture = {
.channels_min = 2,
.channels_max = 2,
.rates = S3C24XX_I2S_RATES,
.formats = SNDRV_PCM_FMTBIT_S8 | SNDRV_PCM_FMTBIT_S16_LE,},
.ops = &s3c24xx_i2s_dai_ops,
};
// cpu dai 操作,他与 codec dai 数据结构是一样的
static const struct snd_soc_dai_ops s3c24xx_i2s_dai_ops = {
.trigger = s3c24xx_i2s_trigger, // 在 PCM 数据开始,传输,唤醒时被调用,用来启动/停止 IIS 传输
.hw_params = s3c24xx_i2s_hw_params, // 根据传入的参数,配置 CPU 的 IIS 相关模块设置
///
// 以下这里函数在 Machine 驱动的 hw_params() 会调用
.set_fmt = s3c24xx_i2s_set_fmt,
.set_clkdiv = s3c24xx_i2s_set_clkdiv,
.set_sysclk = s3c24xx_i2s_set_sysclk,
};
// 【codec】 的匹配 probe
// 主要作用:通过 snd_soc_register_codec() 注册 Codec Dai /Code Driver
// 即 snd_soc_dai_driver 与 snd_soc_codec_driver 两个结构体
//
//Uda134x.c (sound\soc\codecs) |// Mach-mini2440.c (arch\arm\mach-s3c24xx)
static struct platform_driver uda134x_codec_driver = { | static struct platform_device uda1340_codec = {
.driver = { | .name = "uda134x-codec",
.name = "uda134x-codec", | .id = -1,
.owner = THIS_MODULE, | };
}, |
.probe = uda134x_codec_probe, |
.remove = __devexit_p(uda134x_codec_remove), |
}; |
uda134x_codec_probe()
return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_uda134x, &uda134x_dai, 1);
// codec 的寄存器操作: snd_soc_codec_driver
static struct snd_soc_codec_driver soc_codec_dev_uda134x = {
.probe = uda134x_soc_probe, // 配置硬件,通过 snd_soc_add_codec_controls() 注册 snd_kcontrol_new
.remove = uda134x_soc_remove,
.suspend = uda134x_soc_suspend,
.resume = uda134x_soc_resume,
// UDA1341的寄存器不支持读操作
// 要知道某个寄存器的当前值,
// 只能在写入时保存起来
.reg_cache_size = sizeof(uda134x_reg),
.reg_word_size = sizeof(u8),
.reg_cache_default = uda134x_reg,
.reg_cache_step = 1,
.read = uda134x_read_reg_cache,
.write = uda134x_write, // 写寄存器,通过 snd_soc_wirte() 调用到
.set_bias_level = uda134x_set_bias_level,
};
/
// codec 的 DAI 接口设置
static struct snd_soc_dai_driver uda134x_dai = {
.name = "uda134x-hifi",
/* playback capabilities */
.playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2,
.rates = UDA134X_RATES,
.formats = UDA134X_FORMATS,
},
/* capture capabilities */
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = UDA134X_RATES,
.formats = UDA134X_FORMATS,
},
/* pcm operations */
.ops = &uda134x_dai_ops,
};
/
// 以下这里函数在 Machine 驱动的 hw_params() 会调用
static const struct snd_soc_dai_ops uda134x_dai_ops = {
.startup = uda134x_startup,
.shutdown = uda134x_shutdown,
.hw_params = uda134x_hw_params,
.digital_mute = uda134x_mute,
.set_sysclk = uda134x_set_dai_sysclk,
.set_fmt = uda134x_set_dai_fmt,
};
################################################################################
Mixer 控件:混音器,
Mixer控件用于音频通道的路由控制,由多个输入和一个输出组成,多个输入可以自由
地混合在一起,形成混合后的
################################################################################
static const struct snd_kcontrol_new left_speaker_mixer[] = {
SOC_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
SOC_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
SOC_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
SOC_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
};
// 以上这个 mixer 使用寄存器 WM8993_SPEAKER_MIXER 的第 3,5,6,7 位来分别控制 4 个输入端的开启和关闭
################################################################################
Mux 控件:多路开关选择器
Mux 中能同时只有一路被选中输出,多路开关选择器嘛
################################################################################
第一步,定义字符串和values数组:输入端名字
###############################################
static const char *drc_path_text[] = {
"ADC",
"DAC"
};
###############################################
第二步,利用 ASoc 提供的辅助宏定义 soc_enum 结构,用于描述寄存器
###############################################
static const struct soc_enum drc_path = SOC_ENUM_SINGLE(WM8993_DRC_CONTROL_1, 14, 2, drc_path_text);
// 从左到右依次为: 【xreg】/【xshif】/【xmax】/【xtexts】
###############################################
第三步,利用 ASoc 提供的辅助宏,定义 soc_kcontrol_new 结构,该结构最后用于注册该 mux 控件
###############################################
static const struct snd_kcontrol_new wm8993_snd_controls[] = {
SOC_DOUBLE_TLV(......), // 定义简单型的控件,只控制一位
......
SOC_ENUM("DRC Path", drc_path), // 定义 Mux 控件
......
}
硬件示意图:见 WM9883 SPK 输出逻辑通路与硬件通路图
################################################################################
第一步,利用辅助宏定义 widget 所需要的 dapm kcontrol
################################################################################
########################################
# Mixer 控件:多选一,多个输入可同时选中
static const struct snd_kcontrol_new left_speaker_mixer[] = {
// 从右到右依次为:【name】/【reg】/【shift】/【max】/【invert】
SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 7, 1, 0),
SOC_DAPM_SINGLE("IN1LP Switch", WM8993_SPEAKER_MIXER, 5, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 3, 1, 0),
SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
};
static const struct snd_kcontrol_new right_speaker_mixer[] = {
SOC_DAPM_SINGLE("Input Switch", WM8993_SPEAKER_MIXER, 6, 1, 0),
SOC_DAPM_SINGLE("IN1RP Switch", WM8993_SPEAKER_MIXER, 4, 1, 0),
SOC_DAPM_SINGLE("Output Switch", WM8993_SPEAKER_MIXER, 2, 1, 0),
SOC_DAPM_SINGLE("DAC Switch", WM8993_SPEAKER_MIXER, 0, 1, 0),
};
########################################
# Mux 控件:多选一,多个输入同时只有一个可选中
// 1. 定义字符串和 values 数组:输入端名字
static const char *aif_text[] = {
"Left", "Right"
};
// 2. 利用 ASoc 提供的辅助宏定义 soc_enum 结构,用于描述寄存器,从左到右依次为: 【xreg】/【xshif】/【xmax】/【xtexts】
static const struct soc_enum aifinl_enum = SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 15, 2, aif_text);
static const struct soc_enum aifinr_enum = SOC_ENUM_SINGLE(WM8993_AUDIO_INTERFACE_2, 14, 2, aif_text);
// 3. 利用 ASoc 提供的辅助宏,定义 soc_kcontrol_new 结构,该结构最后用于注册该 mux 控件
static const struct snd_kcontrol_new aifinl_mux = SOC_DAPM_ENUM("AIFINL Mux", aifinl_enum);
static const struct snd_kcontrol_new aifinr_mux = SOC_DAPM_ENUM("AIFINR Mux", aifinr_enum);
################################################################################
第二步,定义真正的 widget,包含第一步定义好的 dapm 控件
################################################################################
static const struct snd_soc_dapm_widget wm8993_dapm_widgets[] = {
......
#########################################
# 指定 AIFINL/AIFINR 为输出流:
SND_SOC_DAPM_AIF_IN("AIFINL", "Playback", 0, SND_SOC_NOPM, 0, 0),
SND_SOC_DAPM_AIF_IN("AIFINR", "Playback", 1, SND_SOC_NOPM, 0, 0),
......
#########################################
# 创建 Mux, 不带电源管理: DACL Mux/DACR Mux
SND_SOC_DAPM_MUX("DACL Mux", SND_SOC_NOPM, 0, 0, &aifinl_mux),
SND_SOC_DAPM_MUX("DACR Mux", SND_SOC_NOPM, 0, 0, &aifinr_mux),
#########################################
# 创建 Mixer, SPKL/SPKR 带电源管理,指定电源管理寄存器及操作位
SND_SOC_DAPM_MIXER("SPKL", WM8993_POWER_MANAGEMENT_3, 8, 0,left_speaker_mixer, ARRAY_SIZE(left_speaker_mixer)),
SND_SOC_DAPM_MIXER("SPKR", WM8993_POWER_MANAGEMENT_3, 9, 0,right_speaker_mixer, ARRAY_SIZE(right_speaker_mixer)),
......
};
################################################################################
第三步,定义这些 widget 的连接路径:
################################################################################
static const struct snd_soc_dapm_route routes[] = {
......
// 模块 模块引脚 另一模块引脚
{ "DACL Mux", "Left", "AIFINL" }, # AIFINL 连接到 DACL Mux 的 Left 输入脚
{ "DACL Mux", "Right", "AIFINR" }, # AIFINL 连接到 DACR Mux 的 Left 输入脚
{ "DACR Mux", "Left", "AIFINL" }, # AIFINR 连接到 DACL Mux 的 Right 输入脚
{ "DACR Mux", "Right", "AIFINR" }, # AIFINR 连接到 DACR Mux 的 Right 输入脚
......
{ "SPKL", "DAC Switch", "DACL" }, # DACL 连接到 SPKL 的 DAC Switch 输入脚
{ "SPKL", NULL, "CLK_SYS" },
{ "SPKR", "DAC Switch", "DACR" }, # DACR 连接到 SPKR 的 DAC Switch 输入脚
{ "SPKR", NULL, "CLK_SYS" },
};
################################################################################
第四步,在 codec 驱动的 probe 回调中注册这些 widget 和路径
################################################################################
static int wm8993_probe(struct snd_soc_codec *codec)
{
......
snd_soc_dapm_new_controls(dapm, wm8993_dapm_widgets,ARRAY_SIZE(wm8993_dapm_widgets));
......
snd_soc_dapm_add_routes(dapm, routes, ARRAY_SIZE(routes));
......
}
安卓与内核相关音频交互是通过 tinyalsa 相关接口交互的,即通过 tinyalsa 相关命令,控制前面内核定义的用户接口
所以这里简单介绍下 tinyalsa 工具,了解下有哪些接口
tinymix 的使用:
# tinymix
Mixer name: 'audiocodec'
Number of controls: 12
ctl type num name value
0 INT 1 MIC1_G boost stage output mixer control 3
1 INT 1 MIC2_G boost stage output mixer control 3
2 INT 1 LINEIN_G boost stage output mixer control 3
3 INT 1 MIC1 boost AMP gain control 4
4 INT 1 MIC2 boost AMP gain control 4
5 INT 1 Lineout volume control 31
6 INT 1 ADC input gain ctrl 3
7 BOOL 1 Audio linein in On
8 BOOL 1 Audio lineout Off
9 BOOL 1 Audio adda drc Off
10 BOOL 1 Audio adda loop Off
11 ENUM 1 audio capture mode linein
一个 mixer 通常有多个 controler,像这个,里面有 12 个,然后就分别列出每一个 controller 的信息
首先看第一个:它的编号为 0,类型是 int 型,它目前的值是 3,它是用来控制 mic1 的放大倍数的
然后看第7个是一个 bool 型,也就是只有开关
使用方法:
tinymix [序号] <参数> // 写配置
tinymix [序号] // 读配置
如:
tinymix "RX1 MIX1 INP1" RX1
tinymix "RX2 MIX1 INP1" RX2
tinymix "RDAC2 MUX" RX2
tinymix "HPHL" Switch
tinymix "HPHR" Switch
tinypcminfo 的使用:获得支持格式类型
# tinypcminfo -D 0 -d 0
Info for card 0, device 0:
PCM out:
Access: 0x000009
Format[0]: 0x003ffc
Format[1]: 00000000
Format Name: S16_LE, S16_BE, U16_LE, U16_BE, S24_LE, S24_BE, U24_LE, U24_BE, S32_LE, S32_BE, U32_LE, U32_BE
Subformat: 0x000001
Rate: min=8000Hz max=192000Hz
Channels: min=1 max=2
Sample bits: min=16 max=32
Period size: min=0 max=24576
Period count: min=1 max=4
PCM in:
cannot open device '/dev/snd/pcmC0D0c'
Device does not exist.
tinyplay 的使用:
tinyplay file.wav [-D card] [-d device] [-p period_size] [-n n_periods]
目前 286 能用的使用方法:
插入耳机
tinymix 4 On // Headset_Speaker_Amp_Switch, 这里有个问题,就是增益为 0,奇怪,需要点一下 TP 才短暂有声
tinyplay 441.wav -D 0 -d 0 -p 2048 -n 2
由于 Android 中默认并没有使用标准 alsa,而是使用的是 tinyalsa,所以就算基于命令行的测试也要使用 libtinyalsa。
Android 系统在上层 Audio 千变万化的时候,可以能这些个工具实时查看到,比如音频通道的切换等等.
1.编译tinyalsa配套工具
$ mmm external/tinyalsa/
编译完后会产生tinyplay/tinymix/tinycap等等工具。
tinymix: 查看配置混音器
tinyplay: 播放音频
tinycap: 录音
2.查看当前系统的声卡
root@android:/ # cat /proc/asound/cards
0 [RKRK616 ]: RK_RK616 - RK_RK616
RK_RK616
1 [ROCKCHIPSPDIF ]: ROCKCHIP-SPDIF - ROCKCHIP-SPDIF
ROCKCHIP-SPDIF
root@android:/ #
3.tinymix 查看混响器
tinymix 使用方法
a.不加任何参数 - 显示当前配置情况
b.tinymix [ctrl id] [var] 不加 [var] 可以查看该 [ctrl id] 可选选项
root@android:/ # tinymix
Number of controls: 7
ctl type num name value
0 ENUM 1 Playback Path OFF
1 ENUM 1 Capture MIC Path MIC OFF
2 ENUM 1 Voice Call Path OFF
3 ENUM 1 Voip Path OFF
4 INT 2 Speaker Playback Volume 0 0
5 INT 2 Headphone Playback Volume 0 0
6 ENUM 1 Modem Input Enable ON
root@android:/ #
对应解释:
英文 中文 备注
Playback Path 音频输出通道
Capture MIC Path 音频输入通道
Voice Call Pah 通话音频通道 设备没有通话模块,暂无法测试
Voip Pah IP 电话音频通道 场景 Gtalk;值有: SPK/HP_NO_MIC/BT
Speaker Playback Volume 扬声器音量 和上层音量值无关
Headphone Playback Volume 耳机音量 同上
Modem Input Enable 暂不知何用 经测试不能控制音频输入输出
Playback Path有:
英文 中文 备注
OFF 关闭
RCV -
SPK 扬声器 常用
HP 耳机带麦
HP_NO_MIC 耳机无麦 常用
BT 蓝牙
SPK_HP -
RING_SPK -
RING_HP -
RING_HP_NO_MIC -
RING_SPK_HP -
例:将输出切换到扬声器
root@Android:/ # tinymix 0 SPK
这里只是简单根据各本书上的流程进行了相关高通 8.0 代码的追溯,比较多也比较乱,大概主要就追了 Audio 服务启动初始化,
以及从 App -> Linux 放音这两线,其他的只是针对书本上的流程进行了总结
Audio 系统是 Android 平台的重要组成部分,它主要包括三方面内容:
AudioRcorder 和 AudioTrack: 这两个类属于 Audio 系统对外提供的 API 类,通过
它们可以完成 Android 平台上音频数据的采集和输出任务。
AudioFlinger:它是 Audio 系统的工作引擎,管理着系统中的输入输出音频流,并承担
音频数据的混音,以及读写 Audio 硬件等工作以实现数据的输入输出功能。
AudioPolicyService:它是 Audio 系统的策略控制中心,具体掌管系统中声音设备的选择
和切换、音量控制等功能。
AudtioPolicyService 用于路由 AudioTrack 到 PlaybackThread 中,即对应的硬件
AudioPolicyService 通过 AudioPolicyManager 来与 AF/AS 交互的
AudioTrack 与 AudioFlinge 是通过 IAudioTrack 交互的,在 AudioFlinge 对应的即是 TrackHandle
相关小类介绍
------------- Java ------------------------------------
AudioSystem: Audio 的管理
AudioTrack: Audio 的 PCM 输出
AudioRecod: Audio 录制输入
AudioEffect: Audio 音效
AudioPolicy: 音频设备策略
Visualizer: Audio 可视化效果(Visualizer)
------------- C++ --------------------------------------
AudioRcorder/AudioTrack: 这两个类属于 Audio 系统对外提供的 API 类,通过
他们可以完成 Android 平台上的音频数据的采集和输出任务。
AudioSystem/AudioService: 封装的 AudioFlinger/AudioPolicyService 接口,提供给 AudioTrack
AudioFlinger: 它是 Audio 系统的工作引擎,管理着系统中的输入输出音频流,并
承担音频数据的混音,以及读写 Audio 硬件等工作以实现数据的输入/
输出功能。
AudioPolicyService: 它是 Audio 系统的策略控制中心,具体掌管系统中声音设备的
选择和切换、音量控制等功能。
AudioMixer: 混音核心类
注:此类用于将 AudioTrack 中的 32 路 track 数据,有选择的放到消费都缓冲区中?
AudioMixer 内部有一个 mState 成员变量
最多达 32 路的 Track 数据就存储在其中 state_t::tracks[MAX_NUM_TRACKS] 数组中
每个 PlaybackThread::TrackBase 在 AudioMixer 中对应 tracks 数组中的一个元素
AudioMixer 用于混音操作的缓冲区对象和 AudioTrack/AudioFlinger 中的数据区是一个
AudioTrack 与 AudioSystem / AudioService 关系:
AudioTrack 与底层服务间又提供了 AudioSystem 和 AudioService
通过这两个类来访问 AudioFlinger
前者同时提供了 Java 和 Native 两层的接口实现, 而 AudioService 则只有 Native 层的实现
这样就降低了使用者(AudioTrack)与底层服务(AudioPolicyService, AudioFlinger 等)间的耦合
只要 AudioSytem 和 AudioService 向上接口不变, 那么 AudioTrack 就不需要做任何修改
AudioTrack 通过 AudioSystem 来访问 AudioPolicyService
//
// AudioTrack ==>
// AudioSystem ==>: 是一个接口类,降低与 AudioPolicy 之间的耦合
// AudioPolicyService ==>
// AudioPolicyManager ==> 相关音频逻辑关系实现类
// AudioFlinge ==>
// MixerThread ==> 混音进程: 持有硬件接口
// AudioMixer: 混音器
// AudioStreamOut ==> HAL 硬件代表
// HAL ==>
// Kernel
//
//
# 调用经过模块原因解释:
在上面的创建 AudioTrack 流程中,经过 AS--APS--厂家实现策略
AudioSystem 想找到 AF 中的一个工作线程,会经过 AP 返回
原因是因为 Audio 系统需要:
根据流类型找到对应的路由策略
根据该策略找到合适的输出设备(指扬声器、听筒之类的)
根据设备选择 AF 中合适的工作线程
如蓝牙的 MixerThread,还是 DSP 的 MixerThread
或者是 DuplicatingThread
AT 根据得到的工作线程索引号,最终将在对应的工作线程中创建 Track
之后,AT 的数据将由该线程负责处理,因为
只有 MixerThread 与硬件设备输出相关
# 从目的反推开始原因:AudioTrack:set()
AT 的目的是把数据发送到对应的设备,如蓝牙、DSP 等
代表输出设备的 HAL 对象由 MixerThread 线程执有,所以要找到对应 MixerThread
AP 维护流类型和输出设备和输出设备(耳机、蓝牙耳机、听筒等)之间的关系
不同的输出设备使用不同的混音线程
AT 根据自己的流类型向 AudioSystem 查询,希望得到对应的混音线程号
> PlaybackThread 和 AudioStreamOutput
PlaybackThread 类中有一个 AudioStreamOutput 类型对象
例如:MixerThread 有个 AudioStreamOutput 用于硬件输出
这个对象提供了音频数据的输出功能
PlaybackThread 接收来自 AT 的数据,对这些数据进行混音
把混音的结果写到 AudioStreamOut 中,完成音频输出
> 工作线程介绍
RecordThread: 录音线程,用于音频输入
PlaybackThread: 回放线程,用于音频输出
两个 Track 数组:
mActiveTracks: 表示当前活跃的 Track
mTracks: 表示这个线程创建的所有 Track
DirectOutputThread: 直接输出线程,选择一路音频输出
MixerThread: 混音线程,用于将多个源音频数据混音后输出
DuplicatingThread: 多路输出,也能混音
mOutputTracks: 表示多路输出的目的端
音效类继承关系: frameworks/av/media/libeffects
AudioEffect
BassBoost: 重低音
EnvironmentalReverb:环境音混响
Equalizer:均衡器
PresetReverb:预置混响
Virtualizer:可视化
Virtualizer: 虚拟器
#################################################
# 3rd audio effect的实现
#################################################
# audio_effect_library_t: 定义了所有effect的一个统一接口
// Audio_effect.h (hardware\libhardware\include\hardware)
// 所有的音效库必须实现一个名为 AUDIO_EFFECT_LIBRARY_INFO_SYM 的 audio_effect_library_t 的结构
typedef struct audio_effect_library_s {
// tag must be initialized to AUDIO_EFFECT_LIBRARY_TAG
uint32_t tag;
// Version of the effect library API : 0xMMMMmmmm MMMM: Major, mmmm: minor
uint32_t version;
// Name of this library
const char *name;
// Author/owner/implementor of the library
const char *implementor;
create_effect(): 就是创建对应的音效引擎,得到引擎控制接口 effect_interface_t
release_effect): 释放对应的音效引擎
get_descriptor(): 通过 uuid 去获取音效描述符
}
# 音效处理引擎接口 effect_interface_s
包括四个函数指针:
process(): 音效处理
command(): 用于向音效引擎发送命令和接收对命令的回复
// Audio_effect.h (system\media\audio\include\system)
/
// Effect control interface
/
//
//--- Standardized command codes for command() function
//
enum effect_command_e {
EFFECT_CMD_INIT, // initialize effect engine
EFFECT_CMD_SET_CONFIG, // configure effect engine (see effect_config_t)
EFFECT_CMD_RESET, // reset effect engine
EFFECT_CMD_ENABLE, // enable effect process
EFFECT_CMD_DISABLE, // disable effect process
EFFECT_CMD_SET_PARAM, // set parameter immediately (see effect_param_t)
EFFECT_CMD_SET_PARAM_DEFERRED, // set parameter deferred
EFFECT_CMD_SET_PARAM_COMMIT, // commit previous set parameter deferred
EFFECT_CMD_GET_PARAM, // get parameter
EFFECT_CMD_SET_DEVICE, // set audio device (see audio.h, audio_devices_t)
EFFECT_CMD_SET_VOLUME, // set volume
EFFECT_CMD_SET_AUDIO_MODE, // set the audio mode (normal, ring, ...)
EFFECT_CMD_SET_CONFIG_REVERSE, // configure effect engine reverse stream(see effect_config_t)
EFFECT_CMD_SET_INPUT_DEVICE, // set capture device (see audio.h, audio_devices_t)
EFFECT_CMD_GET_CONFIG, // read effect engine configuration
EFFECT_CMD_GET_CONFIG_REVERSE, // read configure effect engine reverse stream configuration
EFFECT_CMD_GET_FEATURE_SUPPORTED_CONFIGS,// get all supported configurations for a feature.
EFFECT_CMD_GET_FEATURE_CONFIG, // get current feature configuration
EFFECT_CMD_SET_FEATURE_CONFIG, // set current feature configuration
EFFECT_CMD_SET_AUDIO_SOURCE, // set the audio source (see audio.h, audio_source_t)
EFFECT_CMD_OFFLOAD, // set if effect thread is an offload one,
// send the ioHandle of the effect thread
EFFECT_CMD_FIRST_PROPRIETARY = 0x10000 // first proprietary command code
};
get_desriptor(): 返回音效描述符
process_reverse(): 提供一个反向音频流,一般用于回声消除
###################
# 音效库实现例:
###################
// EffectDownmix.c (frameworks\av\media\libeffects\downmix)
// This is the only symbol that needs to be exported
__attribute__ ((visibility ("default")))
audio_effect_library_t AUDIO_EFFECT_LIBRARY_INFO_SYM = {
.tag = AUDIO_EFFECT_LIBRARY_TAG,
.version = EFFECT_LIBRARY_API_VERSION,
.name = "Downmix Library",
.implementor = "The Android Open Source Project",
.create_effect = DownmixLib_Create,
.release_effect = DownmixLib_Release,
.get_descriptor = DownmixLib_GetDescriptor,
// 这个结构体在Android4.3+时有发生变化,query_num_effects()和query_effect()被删除。
// 如果希望做库兼容性,需要检测EFFECT_LIBRARY_API_VERSION,当其为
// #ifEFFECT_API_VERSION_MAJOR(EFFECT_LIBRARY_API_VERSION) > 2 时 query_num_effects() 和 query_effect() 不复存在
};
// effect_handle_t interface implementation for downmix effect
// 音效处理引擎接口 effect_interface_s
const struct effect_interface_s gDownmixInterface = {
Downmix_Process,
Downmix_Command,
Downmix_GetDescriptor,
NULL /* no process_reverse function, no reference stream needed */
};
# audio_buffer_t: 定义了音效输入输出的数据格式
# effect_param_t: 定义了音效之间、系统上下之间的通信协议(数据、格式等等)
// audio_effects.conf 的结构如下:
libraries { // libraries 指明了库的加载路径,默认是在/system/lib/soundfx/目录下
...
downmix {
path /system/lib/soundfx/libdownmix.so
}
}
effects {// effects 包含了该系统支持的所有音效,音效所使用的库,以及音效的 uuid
...
downmix {
library downmix
uuid 93f04452-e4fe-41cc-91f9-e475b6d1d69f
}
}
# 音效引擎工厂:EffectFactory
主要是为了减少 AudioFlinger 与音效引擎库的耦合
根据配置文件中的信息装载引擎库,解析出引擎库符号(audio_effect_library_t)放在链表中,
再遍历链表,调用 audio_effect_library_t 的 get_descriptor 函数,进而得到音效描述符
effect_descriptor_t, 这样就可以得到系统中所有的音效引擎。
frameworks/av/media/libeffects/factory/EffectsFactory.c
gLibraryList: 管理音效链表
vendor/etc/audio_effects.conf 音效配置文件
# 音效使用:
会在 MixerThread 循环中中使用
App 音效处理流程
# AudioEffect的具体效果作用在音频数据上,MediaPlayer只管播放音频,二者通过AudioSessionId关联起来
# 可以说,音效的处理对 MediaPlayer 是透明的,具体的处理由 Android 框架进行
# 如果要应用全局音频输出的混响效果必须指定 audioSession=0,并且要求有 MODIFY_AUDIO_SETTINGS 权限
//创建 MediaPlayer,音频源为/res/raw/audio.mp3
mMediaPlayer = new MediaPlayer.create(this, R.raw.beautiful);
//创建Equalizer,通过AudioSessionId绑定到MediaPlayer
Equalizer mEqualizer = new Equalizer(0, mMediaPlayer.getAudioSessionId());
//启用、获取/设置参数
mEqualizer.setEnabled(true);
short bands = mEqualizer.getNumberOfBands();
mEqualizer.setBandLevel(band, level);
//MP3 播放,创建 Equalizer
mMediaPlayer.start();
// 1. 根据音频数据的特性来确定所要分配的缓冲区的最小 size
int bufsize = AudioTrack.getMinBufferSize(800, // 采样率:每秒 8000 个点
AudioTrack.CHANNEL_CONFIGURATION_STEREO,// 声道数:双声道
AudioTrack.ENCODING_PCM_16BIT // 采样精度:一个采样点 16 比特,相当于 2 个字节
// 2. 创建 AudioTrack
// 创建 AudioTrack 对象
// native_setup(): 创建一个本地 AudioTrack 与 AudioFlinger 建立联系
// AudioTrack 与底层服务间又提供了 AudioSystem 和 AudioService
// 前者同时提供了 Java 和 Native 两层的接口实现
// 而 AudioService 则只有 Native 层的实现
// 这样就降低了使用者(AudioTrack)与底层服务(AudioPolicyService, AudioFlinger 等)间的耦合
// 【只要 AudioSytem 和 AudioService 向上接口不变】
// 【那么 AudioTrack 就不需要做任何修改】
// AudioTrackThread: 用于给 AudioFlinger 发数据
// AudioTrack 在 AudioFlinger 内部以 Track 类来管理的
// AudioTrack 与 AudioFlinger 通过 IAudioTrack 通信
// AudioFlinger 服务名:media.audio_flinger
//
// AudioTrack::set()
// // 此线程用于主动从用户那里获取数据时用
// mAudioTrackThread = new AudioTrackThread(*this, threadCanCallJava);
// mAudioTrackThread->run("AudioTrack", ANDROID_PRIORITY_AUDIO, 0 /*stack*/);
//
// // create the IAudioTrack
// status_t status = createTrack_l();
// AudioSystem::getOutputForAttr()
// AudioPolicyService()::getOutputForAttr()
// AudioPolicyManager()::getOutputForAttr()
// 1. getStrategyForAttr(): 获取 stream 音频类型对应的 Strategy
// 每种 Stream 类型都有对应的路由策略(Routing Strategy)
// getDeviceForStrategy(): 进一步为这一 Strategy 匹配最佳的音频设备
// 2. getOutputForDevice(): 应用策略,判断哪些 Output 符合用户传入的 stream 类型
// 用于获得所有支持 device 设备的 output, 并添加到 outputs 中
// 3. selectOutput(): 选择最适合的 Output
// 1> 处理一些特殊情况
// 2> 开始择优判断,逐个处理所有 Outputs
// 3> 根据优先级做出最后的选择
// Flag 与要求相似度最高的 Output
// Primary Output
// 如果上面两种都找不到,则默认返回第一个 Output
//
// // 该函数返回 IAudioTrack(实现上是 BpAudioTrack)对象,后续 AF 和 AT 的交互就是围绕 IAudioTrack 进行的
// audioFlinger->createTrack()
// ----------- Binder --------------------------
// AudioFlinger::createTrack()
// # 他会通过 AS 查询 APS 获得 AF 中的对应硬件的播放线程
// # 然后创建一个 Track 添加到此播放线程中去
//
// # 根据索引号找到一个工作线程,PlaybackThread,例如 MixerThread 线程
// # 此工作线程与 HAL 输出对象相关,在 AudioPolicyService 中创建
// # 在新的工作线程对象中创建一个 Track 对象,用于写音频到硬件
// # 然后再创建一个 TrackHandler 的 Binder 代理对象
// # 用来通过 Binder 接收请求,让新工作线程 Track 处理
//
// 1> 选择工作线程:AudioFlinger::checkPlaybackThread_l()
// AF 会创建几个工作线程,AT 会找到对应的工作线程
// checkPlaybackThread_l(output);
// AudioFlinger::openOutput(): 产生唯一的 audio_io_handle_t
// 而 AudioTrack 调用 createTrack 时,需要传入这个全局标记值,从而找到匹配的 PlaybackThread
//
// 2> AudioFlinger::PlaybackThread::createTrack_l()
// 创建 Track 对象,添加到内部数组 mTracks 中
// createTrack_l()
// 找到匹配的 PlaybackThread 后,在其内部创建一个 PlaybackThread::Track 对象
//
// 3> Track 创建共享内存和 TrackHandler
// Track 创建了共享内存
// CB 对象通过 placement new 方法创建于这块共享内存中
// Track 没有基于 Binder 通信,所以不能接收远端请求
// 这里用其初始化一个代理对象 TrackHandle
// TrackHandle 能基于 Binder 通信,可接收远端通信
// 并能调用 Track 相应函数进行处理,这就是代理模式
//
// TrackHandle():实际就是 IAudioTrack
AudioTrack trackplayer = new AudioTrack(
AudioManager.STREAM_MUSIC, // 音频流类型
800, // 设置音频数据的采样率 32k,如果是44.1k就是44100
AudioFormat.CHANNEL_CONFIGURATION_STEREO, //
// 音频流的类型:和 Audio 系统对音频的管理策略有关
STREAM_ALARM: 警告声
STREAM_MUSIC: 音乐声,例如 music 等
STREAM_RING: 铃声
STREAM_SYSTEM: 系统声音,例如低电提示音、锁屏音等
STREAM_VOCIE_CALL: 通话声
AudioFormat.ENCODING_PCM_16BIT,// 设置音频数据块是8位还是16位,这里设置为16位。好像现在绝大多数的音频都是16位的了
bufsize,
AudioTrack.MODE_STREAM // 数据加载模式,在这里设置为流类型,另外一种MODE_STATIC
// AudioTrack 数据加载模式:
#MODE_STREAM: 在这种模式下,通过 write 一次次把音频数据写到 AudioTrack 中。这和平时通过 write 系统调用
往文件中写数据类似,但这种工作方式每次都需要把数据从用户提供的 Buffer 中拷贝到 AudioTrack
内部的 Buffer 中,这在一定程序上会引起延时。
#MODE_STATIC: 这种模式下,在 play 之前只需要将所有的数据通过一次 write 调用传递到 AudioTrack 的内部缓冲区
中,后续就不必再传递数据了。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。但它
也有一个缺点,就是一次 write 的数据不能太多,否则系统无法分配足够的内存来存储全部数据。
);
// 3. 开始播放
// AT 调用 IAudioTrack.start() 由于 TrackHandler 的代理作用
// 实际上会由具体的 Track 对象进行处理
// Track 代表一路音频流,他需要输出到 MixerThread 中的硬件设备中
// Track::start():
// 通过 AS 查询 APS 获得 AF 对应硬件的 MixerThread 线程
// 调用其 addTrack_l() 函数
// AudioFlinger::PlaybackThread::addTrack_l():
// 设置重试次数,等待数据写 write()
// 将此 Track 添加到 MixerThread 的活跃队列 mActiveTracks
// 广播一个事件,触发 MixerThread 线程,通知有活跃数组加入
//
// ----------- MixerThread 线程 -------------------
// 1> MixerThread 接收广播事件后( track.start() 在 apk 调用那块写了)
// Thread 类线程工作都是在 threadLoop() 中完成
// MixerThread::threadLoop()
// 首先处理通知信息或配置请求,比如向监听者通知 AF 信息
// 或者根据配置请求进行音量控制、声音设备切换等
//
// 然后调用 prepareTrack_l() 检查活跃的 Tracks 是否有数据
// 2> prepareTrack_l() 和 process() 分析
// prepareTrack_l():
// 一个混音器可支持 32 个 Track, 依次检查活跃 Track
// 对当前活跃 Track, 输入数据设置 AudioMixer 要进行混音处理
//
// 调用混音器对象 AudioMixer:process() 进行混音处理
// process():
// 对需要根据 prepareTrack_l() 设置混音参数
// 对活跃 Track 输入音频数据调用 hook() 函数进行处理
// 3> AudioMixer 对象分析
// 在其构造函数中初始化 32 路 Track 和其 hook() 函数
// 支持的 hook() 函数有:
// process__validate: 根据 Track 格式、数量选择其他处理函数
// process__nop: 什么也不做
// process__genericNoResampling: 普通无需重采样
// process__genericResamplinge: 普通需重采样
// process__OneTrack16BitsStereoNoResampling: 一路音频流,双声道,PCM16 格式,无需重采样
// process__TwoTrack16BitsStereoNoResampling: 两路音频流,双声道,PCM16,无重采样
// 4> 在 AF prepare_l() 会为每个准备好的 Track 使能混音标志
// AudioMixer->enbale() 使能混音,设置 hook() 函数为 procss_validate()
// AudioMixer::process__validate(): 会根据 Track 情况设置合适 hook() 函数
// 调用音频输出对象 AudioOutputStream.write() 输出音频到设备
trackplayer.play();
。。。
// 4. 调用 write 写数据
// 5> 怎么消费数据
// 数据是在 AT 中通过 ObtainBuffer() 得到缓冲区
// 然后 memcpy 写入,最后 releaseBuffer() 释放缓冲区得到的
// 消费数据就是在对应 hook() 中进行的,如在
// ------------ 消费数据 ------------------
// AudioMixer::process__oneTrack16BitsStereoNoResampling():
// 首先找到被激活的 Track,即本 hook() 绑定的 Track 对象
// 然后通过 getNextBuffer() 获得可读数据缓冲
//
// 再然后进行数据处理,即混音
//
// 最后调用数据复制到 out 缓冲,得到混音后数据
//
// 调用 Track 的 releaseBuffer() 释放缓冲区
//
// 6> getNextBuffer 和 releaseBuffer 分析
// getNextBuffer(): 从缓冲区中得到一块可读空间
// 即根据 CB 记录的读写位置等计算可读缓冲区位置
// 首先通过 frameReady() 得到可读帧数
// 然后根据可读帧数等信息等到可读空间首地址
// releaseBuffer(): 通过 stepServer() 更新读位置
trackplayer.write(bytes_pkg, 0, bytes_pkg.length); // 往 track 中写数据
。。。
// 5. 停止播放和释放资源
// 1> TrackHandle 和 Track 的回收
// 来自 AT 的 stop() 请求最终会通过 TrackHandle 这个代理
// 交给具体的 Track 的 stop 进行处理
// AudioFlinger::PlaybackThread::Track::stop():
// 如果 Track 最初牌活跃数组中,则
// 设置 Track 状态为停止 STOPPED 状态
// 因为在 MixerThread::prepareTrack_l() 中,如果 AT 写
// 数据快,而 AF 消耗快,则声音还是会在调用 stop() 后听到
// 所以这里还有其他处理操作
// 这就是 AT 端 stop 后会被很快 delete, 导致 AF 端的
// TrackHandle 也被 delete
// 所以会调用到 TrackHandle 的析构函数
// 这里会调用到 MixerThread->destroyTrack_l(),即 playbackThread->destroyTrack()
// 将不在活跃数组的 Track 从 mActiveTracks 数组移出
// deleteTrackName_l(): 由 PlaybackThread 子类实现,回收一些资源
// TrackHandle 的 delete 会导致所代理的 Track 对象也被删除
//
// 2> Client 的回收
// 前面说过,凡是使用 AT/AR 的线程,都会被 AF 当作 Client 对象
// Client 是 AudioFlinge 对客户端的封装, AF 的 Client,并且 Client 用它的进程 pid 为标识
// Track 的析构,会导致它的基类 TrackBase 析构函数被调用
// AudioFlinger::ThreadBse::TrackBase::~TrackBase()
// 释放 定位 new 声明的对象
// 如果 mClient 强弱引用计数都为 0,则导致该 Client 被 delete
trackplayer.stop(); // 停止播放
###########################################
# 声音路由切换实例分析:邓凡平 2.2
###########################################
// 这一切主要介绍耳机插入,详细介绍下声音路由切换实例
// #########################
// #1. 耳机插拔事件的处理
// #########################
// 耳机插入后,系统会发出一个广播,Java 层的 AudioService 会接收这个广播
// 调用内部类 AudioServiceBroadcastReceiver 处理该事件
//
// //1> 耳机插拔事件的接收
// AudioServiceBroadcastReceiver::onReceive()
// 判断耳机状态,是插入还是拔出,调用
// AudioSystem::setDeviceConnectionState() 设置设备连接状态
//
// //2> setDeviceConnectionState: 设置设备连接状态
// 直接调用 jni 的函数处理
// ------------ jni -------------------------
// android_media_AudioSystem_setDeviceConnectState()
// 调用 Native 的 AudioSystem::setDeviceConnectionState()
// 调用 AMB 处理,AMB 是需要厂家实现的,但一般直接用 AudioPolicyManagerBase 实现
// AudioPolicyManagerBase::setDeviceConnectionState()
// 一次只能设置一个设备
// 根据设备号判断是不是输出设备,耳机属于输出设备
// getNewDevice():得到一个 MixerThread 对应的硬件设备
//
// //3> getNewDevice()
// 根据索引号找到对应的 AudioOutputDescriptor
// AudioOutputDescriptor: 用来记录并维护与输出设备 DSP 相关的信息,
// 此对象在 AudioPolicyManagerBase 构造函数中创建的
// 如使用该设备硬件上,他代表的是 DSP 设备,
// 如 PMIC 中的 Audio Codec
// 此对象是 AMB 用来控制和管理音频输出设备的
// 的流个数,各个流音量该设备所支持的采样率,采样精度等
//
// 当前应用场景为正在听歌,会走 getDeviceForStrategy(STRATEGY_MEDIA)
// AudioPolicyManagerBase::getDeviceForStrategy()
// 重新计算策略所对应的输出设备
// 此函数会根据当前策略,以及通话、蓝牙状态,选择输出硬件设备
//
// updateDeviceForStrategy(): 更新各种策略使用的设备
// //4> AudioPolicyManagerBase::updateDeviceForStrategy()
// 重新计算每种策略使用的设备,保存到 mDeviceForStrategy[] 中,启 cache 作用
// mdeviceForStrategy[] = getDeviceForStrategy()
//
// setOutputDevice(): 设置新的输出设备
// //5> AudioPolicyManagerBase::setOutputDevice()
// 创建请求,需要发送到 AF 对应的工作队列中进行处理
// 比如 DSP 的 MixerThread 或者蓝牙的 MixerThread
// AudioParameter param = AudioParameter()
// 调用 AP 的对象进行发送处理
//
// mpClientInterface->setParameters()
// 最终调用到 APS 的 setParameters()
// AudioPolicyService::setParameters()
// 把这个请求加入到 AudioCommandThread 中处理
// 此线程在 AudioPolicyService 构造时创建
// 创建 AudioCommandThread 用于处理控制命令,例如路由切换、音量调节等
// #########################
// #2. AudioCommandThread
// #########################
// AudioCommandThread 有一个请求处理队列
// AP 负责往该队列提交请求,而 AudioCommandThread
// 在它的线程函数 threadLoop 中处理这些命令
// //1> AudioCommandThread::threadLoop()
// case STOP_TONE: TONE 处理
// case SET_VOLUME: 设备音量
// case SET_PARAMETERS: 处理路由设置请求
// 转到 AudioSystem 处理
// //2> AudioSystem::setParameters()
// 交给 AF 处理
// AudioFlinger::setParameters()
// 根据索引号找到对应的混音线程 MixerThread
// 将请求交由混音线程处理
// //3> MixerThread::treadLoop()
// MixerThread::checkForNewParameters()
// 路由设置需要硬件参数,直接交给代表
// 音频输出设备 HAL 对象处理
//
// //4> HAL 对象的处理例
// 以高通公司为例
// AudioHardware::AudioStreamOutMSM72xx:setParameters()
// AudioHardware::doRouting()
// AudioHardware::doAudioRouteOrMute()
// 硬件相关的代码
// do_route_audio_dev_ctrl()
// open(/dev/msm_audio_ctl)
// ioctl(): 通过 ioctl 切换设备
// applyStreamVolumes(): 设置音量
//
###########################################
# DuplicatingThread破解:邓凡平 2.2
###########################################
// 当一份数据同时需要发给 DSP 和蓝牙 A2DP 设备时使用 DuplicatingThread
//
// 1. DuplicatingThread 的来历
// 假设已经连接上了一个蓝牙耳机,耳机中断处理?
// 会调用到 AudioPolicyManagerBase::setDeviceConnectionState() 设置设备连接状态
// AudioPolicyManagerBase::setDeviceConnectionState()
// 专门处理 A2DP 设备的连接
// handleA2dpConnection()
// 先为 mA2dpOutput 创建一个蓝牙用的 MixerThread
// SONIFCATION 策略的音频流类型需要同时从蓝牙和 DSP 中传出
// 代表音频流有:来电铃声、短信通知等
// 所以要创建一个 Duplicateoutput 线程,传入参数为蓝牙 MixerThread
// mpClientInterface->openDuplicateOutput()
// # openDuplicateOutput() 结果示意图:
//
// [DuplicatingThread 线程]
// .mOutputTracks
// | |
// | |
// OutputTrack_0 OutputTrack_1
// | |
// | |
// [蓝牙 MT 线程] V V [DSP MT 线程]
// .mTracks .mTracks
//
//
// 蓝牙中有一个成员为 OutputTrack()
// DT 的 mOutputTracks 也有一个成员指向 OutputTrack()
//
// 【这就好像 DT 是 MT 的客户端一样】
// 与前面的 AT 是 AF 的客户端类似
//
//
// # 3. DT 的客户端 AT
// DT 是从 MT 中派生的,根据 AP 和 AT 的交互流程可知
// 当 AT 创建流类型对应策略为 SONIFACATION 时
// 他会从 AP 中得到代表 DT 的线程索引号
//
// 由于 DT 没有重载 createTrack_l(), 因此也会类似 MT 过程
// 创建一个 Track, 用于跟踪音频流
// 然后配合两个 OutputTrack 进程内缓冲
// 把来自 AT 的音频数据原封不动的发给蓝牙 MT 和 DSP MT
//
//
// # 有 AT 的 DT 全景图
//
// [AudioTrack]
// IAudioTrack
// /\
// ||
// \/
// mTracks
// [DuplicatingThread]
// mOutputTracks
// | |
// OutputTrack_0 OutputTrack_1
// | |
// mTracks mTracks
// [蓝牙 MT] [DSP MT]
//
// AT 通过 Track 将音频流数据发给 DT
// DT 通过 OutputTrack() 将数据分别发给蓝牙 MT 和 DSP MT
//
//
// # 4. DT 的线程函数
// AF/DuplicatingThread::threadLoop()
// 和 MT 处理一样,处理配置请求
// 如果 AT 的 Track 停止了,则需要停止和 MT 共享的 OutputTrack
// prepareTracks_l(): DT 派生自 MT, 功能一样,检查活跃的 Tracks 是否有数据
// outputsReady(): 检查 OutputTracks 对应的 MT 状态
// 调用 AudioMixer 进行混音处理
// outputTracks[]->write(): 将混音后的数据写到 outputTrack 中
// AT 调用 start() 将导致 DT 的 Track 加入到上面的活跃数组中
// 蓝牙 MT 与 DSP MT 的则在 write() 将 OutputTrack 加入对应线程活跃数组
// AF/PT/OutputTrack::write()
// start(): 如果此 Track 没有活跃,则调用 start() 激活
// 现在 AF 中的数据传递有三个线程:一个 DT, 两个 MT
// MT 作为二级消费者可能来不及消费数据
// 所以 DT 提供了一个缓冲区进行缓冲来不及处理的数据
// 最多可缓冲 10 组数据
// 数据就这样通过 DT 的帮助,从 AT
// 传输到蓝牙 MT 和 DSP 的 MT 中
// 缺点:数据传输比直接使用 MT 传输要缓慢
//
// # 最终的处理都是在 AF 中
// AudioFlinger::openDuplicateOutput()
// 获得对应蓝牙的 MixerThread
// 获得对应 DSP 的 MixerThread
// new DuplicatingThread(): 创建 DuplicatingThread, 传入第二个参数是 MixerThread
// 2. AF/DuplicatingThread::DuplicatingThread()
// DT 是 MT 的派生类,所以先要完成基类的构造,它会创建一个 AudioMixer
// addOutputTrack(): 然后把代表 DSP 的 MT 加入进来
// AF/DuplicatingThread::addOutputTrack()
// 构造一个 OutputTrack,第一个参数是 MT
// AF/PT/OutputTrack::OutputTrack():
// OutputTrack 从 Track 派生,所以先调用基类的构造,创建一块内存
// 内存的结构如图 7-4 所示,前面为 CB 对象,后面为数据缓冲
// 然后将这个 Track 加入到 MT 的 Track 中
// 表示 DT 将往 MT 中写数据
// 把这个 OutputTrack 加入到 mOutputTracks 数组保存
// 加入代表 DSP 的 MixerThread
// DuplicatingThread->addOutputTrack()
// 经过上面两步,DT 分别构造了两个 OutputTrack
// 一个对应蓝牙的 MT,另一个对应 DSP 的 MT
//
// 然后创建一个 new AudioOutputDescriptor 对象
// AudioOutputDescriptor: 用来记录并维护与输出设备 DSP 相关的信息,
// 此对象在 AudioPolicyManagerBase 构造函数中创建的
// 如使用该设备硬件上,他代表的是 DSP 设备,
// 如 PMIC 中的 Audio Codec
// 此对象是 AMB 用来控制和管理音频输出设备的
// 的流个数,各个流音量该设备所支持的采样率,采样精度等
###########################################
# 音量控制: 林学森 4.3
###########################################
// 1. AudioManager
// 当用户按下音量调节键
// public static KeyEvent extends InputEvent implements Parcelable
// 1. AudioManager.java
// handleKeyDown()
// adjustSuggestedStreamVolume()
// IAudioService service = getService()
// 服务名为 = audio
// service.adjustSuggestedStreamVolume()
// ------- AudioService.java ----------------------
// adjustSuggestedStreamVolume()
// getActiveStreamType(): 获得当前流类型
// adjustStreamVolume()
// 函数重点:
// 1> 计算 oldIndex(之前的音量值),index(要调整的音量值)和 flags
// 2> 调用 sendVolumeUpdate 和 sendMsg 把上一步的计算结果发送出去
// 1. 为各 StreamType 寻找 Alias 归类
// 获取当前streamType 对应的 alias
// 2. 为 Stream Alias 寻找匹配的 device
// 根据 stream alias 来查询匹配的输出设备
// 3. 获取对应 device 的 index
// 通过 VolumeStreamState() 获得音量值(index)
// VolumeStreamState(): 维护了一个 mIndex 数组来记录 device 对应的 index 值
// 4. 调节 index
// 为 UI 显示条做前期准备
// 5. 音量调节对于音量模式的影响
// 音量的调节还可能与手机的铃声模式(Ringer Mode)有关:
// 静音模式
// 震动模式
// 正常模式
// 6. 将音量调节事件发送给下一个处理者
// sendMsg():把命令投递到消息队列中
// 再由 AudioHandler 做进一步处理
// AudioHandler 除了将音量值保存到系统设置文件中外
// 还会调用 AudioPolicyService 的相关接口来真正调整音频设备音量
// ------------- AudioPolicyService.cpp -------------------
// AudioPolicyServiceService::setStreamVolumeIndex()
// AudioPolicyManagerBase::setStreamVolumeIndex()
// 循环查找所有匹配设备
// 通过 checkAndSetVolume() 进行音量设置
// ----------- AudioFlinger.cpp ----------------
// AudioFlinger::setStreamVolume()
// 首先根据 output 找到它对应的 PlaybackThread
// 而后交由这个线程具体处理流的音量
// AudioFlinger::PlaybackThread::setStreamVolume()
// mStreamTypes[stream].volume = value
// ----- 会在 PlaybackThread::threadLoop() 处理 ----------
// AudioFlinger::MixerThread::prepareTracks_l()
// 之前 setStreamVolume() 设置的音量值在这里会被提取出来
// 并和主音量等其他因素进行综合运算
// mAudioMixer->setParameter()
// 设置音量
// 同时把新的值记录到 mStreamTypes 中
// sendVolumeUpdate():用于产生音量调节提示音并显示系统音量条
// 2. [email protected]
// 对于部分重要的物理按键(比如 HOME 和音量调节键)
// 系统会先判断它们是否需要做预处理,即 interceptKeyBeforeQueueing
// 这个函数会根据当前的具体情况(是不是在通话状态,是不是在播放音乐等)
// 来决定需要调整的 STREAM 类型
// 而且最后它会调用 AudioService.adjustStreamVolume()
// 接下来的处理流程就和上面 AudioManager 中的情况一致了
Linux 部分参考资料:
UDA1341TS 芯片手册
wm8993 音频芯片手册
mini2440原理图
AudioCODEC基本知识及应用_百度
相关内核源码 2.6/3.4.2
韦东山一期/三期声卡相关视频及相关源码
http://blog.csdn.net/droidphone Alsa 相关博客
Android 部分参考:
高通安卓 8.0 源码
深入理解Android内核设计思想_林学森./第 13 章 应用不再同质化 – 音频系统
深入剖析 Android 系统_杨长刚/第 14 章 Audio
深入理解 Android 卷1_邓凡平/ 第7章 深入理解 Audio 系统