8.声卡驱动03-自己实现alsa驱动-虚拟声卡-缓存

平台:ubuntu 16.04,kernel版本是4.15.0, 理论任何平台都可以,甚至是android,只要能编译通过。

需要完成的功能:传说中的回采,做过语音方案的童鞋应该能懂,就是播放的音频,录音录回去。因为是虚拟的声卡,不涉及硬件操作,也只能这样看点效果。

目的:当然是为了能更直观的理解alsa驱动框架。虚拟出一个声卡,不涉及复杂的硬件操作,不涉及复杂的硬件调试,只关心数据流怎么一步一步传给应用的。

1.数据是怎么交互的

以playback为例
在这里插入图片描述

  1. 驱动程序分配一个buffer

  2. APP不断写入一个period的数据到buffer
    一个period含有多个frame
    一个frame就是一个采样数据

  3. 驱动不断从buffer里取出一个period,并发送给codec

  4. app更新appl_ptr指针, 驱动更新hw_ptr指针,当指针更新到buffer尾部,从头开始。

设计思路

如下图,播放和录音有两个dma buf,分别是缓存播放数据和录音数据;
另外有一个timer,用于模拟硬件播放或录音所需要的时间,timer时间到的时候,将播放缓存的buf数据copy到录音缓存的buf,达到回采的效果;
在这里插入图片描述

2.分配DMA内存

分配dma内存的是.pcm_new

static int vplat_pcm_new(struct snd_soc_pcm_runtime *rtd) {
	struct snd_card *card = rtd->card->snd_card;
	struct snd_pcm *pcm = rtd->pcm;
	
	struct snd_pcm_substream *substream;
	struct snd_dma_buffer *buf;
	
	int ret = 0;

	if (!card->dev->dma_mask)
		card->dev->dma_mask = &dma_mask;
	if (!card->dev->coherent_dma_mask)
		card->dev->coherent_dma_mask = 0xffffffff;

	if (pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream) {

		playback_info.buf_max_size = vplat_pcm_hardware.buffer_bytes_max;
		substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream;
		playback_info.substream = substream;
		buf = &substream->dma_buffer;
		
		buf->area = dma_alloc_coherent(pcm->card->dev, playback_info.buf_max_size,
					&buf->addr, GFP_KERNEL);
		if (!buf->area) {
			printk(KERN_ERR"plaback alloc dma error!!!\n");
			return -ENOMEM;
		}

    	buf->dev.type = SNDRV_DMA_TYPE_DEV;
    	buf->dev.dev = pcm->card->dev;
    	buf->private_data = NULL;
        buf->bytes = playback_info.buf_max_size;
		
		playback_info.addr = buf->area;
	}

	if (pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream) {
		
		capture_info.buf_max_size = vplat_pcm_hardware.buffer_bytes_max;
		
		substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream;
		capture_info.substream = substream;
		buf = &substream->dma_buffer;
		
		buf->area = dma_alloc_coherent(pcm->card->dev, capture_info.buf_max_size,
					&buf->addr, GFP_KERNEL);
		if (!buf->area) {
			printk(KERN_ERR"catpure alloc dma error!!!\n");
			return -ENOMEM;
		}

    	buf->dev.type = SNDRV_DMA_TYPE_DEV;
    	buf->dev.dev = pcm->card->dev;
    	buf->private_data = NULL;
        buf->bytes = capture_info.buf_max_size;	
		
		capture_info.addr = buf->area;
	}
	return ret;
}

调用dma_alloc_coherent()分配dma buffer,其实就是一块连续的物理内存。

dma在声卡的使用可以看:
[RK3288][Android6.0] Audio的DMA调用实例流程
[IMX6DL][Android4.4] Linux dmaengine 使用方法

3.启动定时器,模拟数据传输中断

定时器在vplat_pcm_trigger()启动

static int vplat_pcm_trigger(struct snd_pcm_substream *substream, int cmd) {
	int ret = 0;
	static u8 is_timer_run = 0;
	
	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
			/* 启动定时器, 模拟数据传输 */
			printk("playback running...\n");
			playback_info.is_running = 1;
			if(!is_timer_run) {
				is_timer_run = 1;
				start_timer();
			}
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			/* 停止定时器 */
			printk("playback stop...\n");
			playback_info.is_running = 0;
			if(!capture_info.is_running){
				is_timer_run = 0;
				del_timer(&vtimer);
			}
			break;
		default:
			ret = -EINVAL;
			break;
		}
	} else {
		switch (cmd) {
		case SNDRV_PCM_TRIGGER_START:
		case SNDRV_PCM_TRIGGER_RESUME:
		case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
			/* catpure开始接收数据 */		
			printk("capture running...\n");
			capture_info.is_running = 1;
			if(!is_timer_run) {
				is_timer_run = 1;
				start_timer();
			}
			break;
		case SNDRV_PCM_TRIGGER_STOP:
		case SNDRV_PCM_TRIGGER_SUSPEND:
		case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
			/* catpure停止接收数据 */
			printk("capture stop...\n");
			capture_info.is_running = 0;
			if(!playback_info.is_running){
				is_timer_run = 0;
				del_timer(&vtimer);
			}
			break;
		default:
			ret = -EINVAL;
			break;
		}
	}
	return ret;
}

定时器处理函数就是模拟DMA传输完成的中断处理函数,如下:

static void vplat_timer_function(struct timer_list *t) {
	schedule_work(&vplat_work);
}

启动一个work,work的处理函数如下:

static void work_function(struct work_struct *work){
	
	struct snd_pcm_substream *pb_substream = playback_info.substream;
	
	if (capture_info.is_running) {
		load_buff_period();
	}
        
    // 更新状态信息
	if(playback_info.is_running){
		playback_info.buf_pos += playback_info.period_size;
		if (playback_info.buf_pos >= playback_info.buffer_size)
			playback_info.buf_pos = 0;
		
		// 更新hw_ptr等信息,
		// 并且判断:如果buffer里没有数据了,则调用trigger来停止DMA 
		snd_pcm_period_elapsed(pb_substream); 
	}

    if (playback_info.is_running || capture_info.is_running) {
        //再次启动定时器
        mod_timer(&vtimer, jiffies + HZ/10);
    }
}

如果此时正在录音,调用load_buff_period()将playback的buffer数据拷贝到capture的buffer。
调用snd_pcm_period_elapsed()更新hw_ptr指针;

思考
下面这段代码,timer的timeout时间为什么是100ms (HZ/10)?

if (playback_info.is_running || capture_info.is_running) {
        //再次启动定时器
        mod_timer(&vtimer, jiffies + HZ/10);
    }

音乐播放器播放一个字节的时长不是一个固定的值,因为音频的播放时间取决于许多因素,包括音频的比特率(位元率)、采样频率、声道数等。音乐文件一般以比特率表示,比如128kbps,表示每秒钟传输128千位(位元)的数据。所以,如果想要知道播放1字节(8位)的音频需要的时间,需要知道音频的比特率。例如,对于128kbps的MP3文件来说,播放1字节的数据大约需要0.0625秒。这只是一个理论上的估计,实际的播放时间可能会受到其他因素的影响。

播放2声道16bit采样率为16000的音频文件的一个字节需要多长时间?

在这个例子中,声道是2,位深是16bit(表示每个样本是16比特/位),采样率是16000Hz(每秒采样16000次)。

然后需要明确一点,一个样本包含的字节数 = 位深/8声道数,即,一个样本大小约为22 = 4字节。

所以,播放一个字节的音频(在此前提下),大概需要播放1/4个样本的时间。因为一个样本的时间就是1/16000秒,所以播放一个字节的音频大约需要(1/16000)*1/4秒,也就是大约0.000015625秒。

请注意,这只是一个顶多的估计,因为实际的播放时间还会受到很多其他因素的影响,包括解码时间、缓冲时间等等。

根据 period 计算字节数量
period = 2048
byte = period * (位深 bit)/ 8 * 声道数=2048*(16/8)*2=8192

使用之前计算的播放一个字节所需时间的结果,那么播放8192字节所需时间如下:
单个字节播放时间 = 0.000015625秒
所以,
8192字节播放时间 = 8192 * 0.000015625 = 0.128s
以上所述为理论上的计算,实际播放时间可能会受到解码时间、缓冲时间等其他因素的影响。
因此,timer的时间直接固定为100ms也是不太合适的,但是为了代码简单,也就这么实现了,有兴趣的童鞋,可以改一下代码,实现根据采样率来调节timeout时间;

load_buff_period()实现如下:

static int load_buff_period(void) {
	struct snd_pcm_substream *cp_substream = capture_info.substream;
	int size = 0;
	
	if(capture_info.addr == NULL) {
		printk(KERN_ERR"catpure addr error!!!\n");
		return -1;
	}

	if (playback_info.is_running) {
		if(capture_info.period_size != playback_info.period_size) {
			printk(KERN_ERR"capture_info.period_size(%d) != playback_info.period_size(%d)\n",
					capture_info.period_size,playback_info.period_size);
		}
		
		size = capture_info.period_size <= playback_info.period_size ?
				capture_info.period_size :
				playback_info.period_size;
		
		//复制playback的一帧数据到catpure
		memcpy(capture_info.addr+capture_info.buf_pos,
				playback_info.addr+playback_info.buf_pos,
				size);
	} else {
		memset(capture_info.addr+capture_info.buf_pos,0x00,capture_info.period_size);
	}
	
	//更新capture当前buffer指针位置
	capture_info.buf_pos += capture_info.period_size;
	if (capture_info.buf_pos >= capture_info.buffer_size)
		capture_info.buf_pos = 0;
	
	snd_pcm_period_elapsed(cp_substream);
	return 0;
}

通过memcpy()复制playback的一帧数据到catpure,完事之后调用snd_pcm_period_elapsed()更新hw_ptr同时唤醒等待的录音进程。

4.测试

和上一篇一样安装完驱动之后,
一个终端执行命令:aplay -D hw:1,0 play.wav

参数解析:
-D 指定了录音设备,1,0 是card 1 device 0的意思

不知道是card几,可以执行aplay -l查看

一个终端执行命令:arecord -D hw:1,0 -d 10 -r 48000 -f S16_LE test.wav

参数解析
-D 指定了录音设备,1,0 是card 1 device 0的意思
-d 指定录音的时长,单位时秒(如果不加,可以使用Ctrl + C结束录音)
-f 指定录音格式
-r 指定了采样率,单位时Hz
-t 指定生成的文件格式

注意:录音采样率、格式必须和play.wav相同!!
不然录出来的音频和play.wav的不一样,就是失真。

window可以用Audacity打开test.wav查看,如下图,Audacity同样可以导入原始的pcm数据,具体操作问度娘。
在这里插入图片描述

5.问题记录

(1) aplay: set_params:1403: Unable to install hw params
出现这问题原因是底层驱动不支持aplay设置的hw params,参照kernel其他asoc分别设置codec dai、cpu dai和platform格式相关的参数。

(2) underrun!!! (at least 660.013 ms long)
出现这问题原因是应用准备的音频数据不够,比如,驱动需要播放需要 1026 帧数据,但应用只准备好了 1024 帧。
这里设计的是虚拟声卡,不涉及硬件传输,所以用定时器模拟数据传输完成产生的中断,如果定时时间太快,就会产生此问题,时间加大点就好了,这样应用就能准备好更多音频数据。
但是笔者的电脑本身就卡,运行虚拟机跑ubuntu 16.04,再运行qemu,导致在qemu测试,也容易出现此问题。
在ubuntu 16.04直接insmod驱动,就不会出现此问题。

(3) pcm_read:2145: read error: Input/output error
这是aplay报的错,刚开始很诧异,因为驱动没有报任何错。
没有aplay源码,不好定位问题,怎么办呢?
使用strace跟踪一下系统调用,用法是: “strace -o strace.log [命令]”, 最终生成strace.log,打开看一下,发现

ioctl(4, SNDRV_PCM_IOCTL_READI_FRAMES, 0x7e9199d4) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x1a26cb0) = 0
write(3,“\377\377\377\377\377\377\377\377\377\3\377\377\377\377\377\377”…, 12000) = 12000
ioctl(4, SNDRV_PCM_IOCTL_READI_FRAMES, 0x7e9199d4) = -1 EIO (Input/output error)
write(2, "arecord: pcm_read:2145: ", 24) = 24
write(2, “read error: Input/output error”, 30) = 30

可见,执行了两次SNDRV_PCM_IOCTL_READI_FRAMES ioctl,第一次成功,第二次失败。
查看代码发现,在只执行录音操作时,定时器只执行了一遍。第二次驱动没有及时执行snd_pcm_period_elapsed()函数,导致应用等待超时。
最终修改看最新代码,链接在本文末。

(4)驱动获取不到playback数据
驱动代码基本完成,开一个终端aplay播放,开一个终端arecord录音,看能不能录到正在播放的音频。结果录出来的音频如下图:
在这里插入图片描述

这根本就不是aplay播放的音频数据,猜想是因为buf没有初始化,所以是乱码,那就给它个初始化吧。
emm…, 猜想是对的,改了之后直接是0了,录不到数据。
继续分析,查看内核打印,没报错,aplay和arecord也没报错。哦豁。
要么是playback有问题,要么是capture有问题。那就先检查playback,使用vfs_write()把应用传下来的数据写到一个pcm文件里,然后在电脑上使用Audacity工具导入,发现压根没数据,所以确定playback有问题。
playback怎么会有问题呢?内核又没报错。老规矩,strace跟踪一下系统调用,打开strace.log一看,发现一大堆这种打印:

ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0
ioctl(4, SNDRV_PCM_IOCTL_SYNC_PTR, 0x4e7220) = 0

怎么没有write呢?在往上走发现了一条这样的打印:

mmap2(NULL, 98304, PROT_READ|PROT_WRITE, MAP_SHARED, 4, 0) = 0x76d16000

由此看来aplay并没有调write系统调用往驱动写数据,而是通过mmap的方式,但是驱动并没有实现mmap,问题找到了。
具体修改看最新代码,链接在本文末。

(5)网友发现的问题
定时器的精度问题:Linux内核的定时器并不是实时的,它的精确度受到系统负载和调度策略的影响,可能会产生一定的偏差。如果这个偏差超出了音频处理的容忍范围,就可能导致音频数据错位,产生噪音或者音频数据丢失。

工作队列的执行时机:工作队列中的任务是在软中断上下文执行的,它的执行时机也受到系统负载和调度策略的影响。如果系统负载过高,工作队列的任务可能无法及时执行,造成音频数据的处理延迟。

以上两个问题可以自己思考解决,以便了解和学习Linux负载均衡和调度策略相关知识

6.代码

代码位置:https://gitcode.net/u014056414/myalsa
后续会增加其他的功能,完成本文的提交是:5.完成回采功能
初学者可以按照此提交学习,以免新功能干扰;
后续有新功能,一般是DAPM,会有相应的文章。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值