8.声卡驱动04-写一个alsa应用程序

本文介绍如何在Ubuntu 16.04系统中使用alsa库实现音频播放和录音功能,包括设备选择、参数设置、多进程协作处理,并解决音频同步问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

需要完成的功能:一个进程播放音频,一个进程录音。

直接贴代码

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <alsa/asoundlib.h>

#define PLAYBACK_FILE "R48K-16LE-CH1.pcm"
#define CAPTURE_FINE "capture.pcm"

#define PCM_NAME	"hw:CARD=mycodec,DEV=0"
#define RATE 		48000
#define FORMAT		SND_PCM_FORMAT_S16_LE
#define CHANNELS	1

snd_pcm_hw_params_t *hw_params;

int print_all_pcm_name(void) {
	char **hints;
	/* Enumerate sound devices */
	int err = snd_device_name_hint(-1, "pcm", (void***)&hints);
	if(err != 0)
	   return -1;

	char** n = hints;
	while(*n != NULL) {
		char *name = snd_device_name_get_hint(*n, "NAME");

		if(name != NULL && 0 != strcmp("null", name)) {
			printf("pcm name : %s\n",name);
			free(name);
		}
		n++;
	}

	snd_device_name_free_hint((void**)hints);
	return 0;
}

snd_pcm_t *open_sound_dev(snd_pcm_stream_t type,const char *name, unsigned int rate, int format,int channels,snd_pcm_uframes_t *period_frames) {
	int err;
	snd_pcm_t *handle;
	int dir = 0;
	
	printf("rate=%d, format=%d, channels=%d\n",rate,format,channels);

	if((err = snd_pcm_open(&handle, name, type, 0)) < 0) {
		return NULL;
	}
	   
	if((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) {
		printf("cannot allocate hardware parameter structure (%s)\n",
			 snd_strerror(err));
		return NULL;
	}
			 
	if((err = snd_pcm_hw_params_any(handle, hw_params)) < 0) {
		printf("cannot initialize hardware parameter structure (%s)\n",
			 snd_strerror(err));
		return NULL;
	}

	if((err = snd_pcm_hw_params_set_access(handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) {
		printf("cannot set access type (%s)\n",
			 snd_strerror(err));
		return NULL;
	}

	if((err = snd_pcm_hw_params_set_format(handle, hw_params, format)) < 0) {
		printf("cannot set sample format (%s)\n",
			 snd_strerror(err));
		return NULL;
	}

	if((err = snd_pcm_hw_params_set_rate_near(handle, hw_params, &rate, 0)) < 0) {
		printf("cannot set sample rate (%s)\n",
			 snd_strerror(err));
		return NULL;
	}

	if((err = snd_pcm_hw_params_set_channels(handle, hw_params, channels)) < 0) {
		printf("cannot set channel count (%s)\n",
			 snd_strerror(err));
		return NULL;
	}

	if((err = snd_pcm_hw_params(handle, hw_params)) < 0) {
		printf("cannot set parameters (%s)\n",
			 snd_strerror(err));
		return NULL;
	}
	
	if(period_frames != NULL) {
		//获取一个周期有多少帧数据
		if((err =snd_pcm_hw_params_get_period_size(hw_params, period_frames, &dir)) < 0){
			printf("cannot get period size (%s)\n",
				snd_strerror(err));
			return NULL;
		}
	}
	
	snd_pcm_hw_params_free(hw_params);
	return handle;
}

int main(void) {
	int p,err;
	snd_pcm_t *playback_handle;
	snd_pcm_t *capture_handle;
	int pfd,cfd;
	snd_pcm_uframes_t period_frames;
	int size2frames;
	
	printf("program running ...\n");
	
	//查看所有pcm的name
	//print_all_pcm_name();
	
	playback_handle = open_sound_dev(SND_PCM_STREAM_PLAYBACK,
                                     PCM_NAME,RATE,FORMAT,CHANNELS,&period_frames);
	if(!playback_handle) {
		printf("cannot open for playback\n");
        return -1;
    }
	
	usleep(5);
	
	capture_handle = open_sound_dev(SND_PCM_STREAM_CAPTURE,PCM_NAME,RATE,FORMAT,CHANNELS,NULL);
	if(!capture_handle) {
		printf("cannot open for capuure\n");
		snd_pcm_close(playback_handle);
        return -1;
    }
	
	if((err = snd_pcm_prepare(playback_handle)) < 0) {
		printf("cannot prepare audio interface for use (%s)\n",
			 snd_strerror(err));
		goto out;
	}

	if((err = snd_pcm_prepare(capture_handle)) < 0) {
		printf("cannot prepare audio interface for use (%s)\n",
			 snd_strerror(err));
		goto out;
	}
	
	//打开要播放的PCM文件
	pfd = open(PLAYBACK_FILE,O_RDONLY,0644);
	if(pfd < 0){
		printf("open %s error!!!\n",PLAYBACK_FILE);
		goto out;
	}
	
	//新建一个进程, 子进程播放, 父进程录音
	p = fork();
	if(p < 0) {
		printf("fork error!!!\n");
		goto out;
	}
	
	if(p==0) {
		char *pbuf;
		int size,period_bytes;
		period_bytes = snd_pcm_frames_to_bytes(playback_handle,period_frames);
		pbuf = malloc(period_bytes);
		while( size = read(pfd, pbuf, period_bytes)) {
			//解决最后一个周期数据问题
			if(size < period_bytes) {
				memset(pbuf+size, 0, period_bytes-size);
			}
			
			//size2frames = snd_pcm_bytes_to_frames(playback_handle,size);
			size2frames = period_frames;
			
			//向PCM写入数据,播放
			err = snd_pcm_writei(playback_handle, pbuf, size2frames);
			if(err != size2frames) {
				printf("write to audio interface failede err:%d (size2frames:%d)\n",err,size2frames);
				free(pbuf);
				close(pfd);
				exit(-1);
			}
			printf("process:playback wrote %d frames\n",size2frames);
			usleep(100);
		}
		
		free(pbuf);
		close(pfd);
		sleep(1);	//等待一下,给点时间父进程录音
		exit(0);
	}
	
	char *cbuf;
	const int frames_size = snd_pcm_frames_to_bytes(capture_handle,period_frames);
	cbuf = malloc(frames_size);
	memset(cbuf, 0, frames_size);
	
	//打开录音的保存文件
	cfd = open(CAPTURE_FINE,O_RDWR | O_TRUNC | O_CREAT,0644);
	if(cfd < 0){
		printf("open %s error!!!\n",CAPTURE_FINE);
		goto out;
	}
	
	while(waitpid(p, NULL, WNOHANG) == 0) {	//查看一下子进程是否已经退出
		//向PCM读一周期数据
		if((size2frames = snd_pcm_readi(capture_handle, cbuf, period_frames)) < 0) {
			printf("read from audio interface failed (%d)\n",size2frames);
			free(cbuf);
			close(cfd);
			goto out;
		}
		printf("--process:capture read %d frames\n",size2frames);
		write(cfd,cbuf,snd_pcm_frames_to_bytes(capture_handle,size2frames));
		memset(cbuf,0,frames_size);
		usleep(100);
	}
	free(cbuf);
	close(cfd);

out:
	snd_pcm_close(playback_handle);
	snd_pcm_close(capture_handle);
	printf("program finish ...\n");
	return 0;
}

几个函数需要说明一下:

  • snd_pcm_open

    int snd_pcm_open(snd_pcm_t **pcmp, const char *name, snd_pcm_stream_t stream, int mode)
    
    • pcmp: 打开的pcm句柄;

    • name: 要打开的pcm设备名字,默认default。 那么这个name怎么确定?你去百度发现都是default,但是我们要打开的是card1,怎么办?
      方法一:代码里定义了print_all_pcm_name()函数,可以调用此函数打印所有PCM的name,找到自己需要打开的;
      方法二:调用aplay -L命令同样能查看所有的PCM的name;

    • stream: SND_PCM_STREAM_PLAYBACK 或 SND_PCM_STREAM_CAPTURE,分别表示播放和录音的PCM流;

    • mode: 打开pcm句柄时的一些附加参数 SND_PCM_NONBLOCK 非阻塞打开(默认阻塞打开), SND_PCM_ASYNC 异步模式打开;

    • 返回值: 0 表示打开成功,负数表示失败,对应错误码;

  • snd_pcm_writei:

    snd_pcm_sframes_t snd_pcm_writei(snd_pcm_t *pcm, const void *buffer, snd_pcm_uframes_t size) 
    
    • pcm: 打开的pcm句柄;
    • buffer: 要写的数据;
    • size: size不是buffer数据的字节数, 而是帧数(采样数)1 frames = 位宽 * 通道数 / 8 bytes;
    • 返回值: 返回值也是帧数,而不是字节数;

    snd_pcm_readi()也一样。

  • 帧数与字节数之间需要通过 snd_pcm_bytes_to_frames(bytes)snd_pcm_frames_to_bytes(frames)来做转换;

测试

编译:gcc play_to_cap.c -o play_to_cap -lasound

运行:运行之前需要安装上一篇的驱动,然后执行./play_to_cap

用Audacity导入播放和录音的pcm原始数据,如下图:
在这里插入图片描述

遇到个问题

发现录音最后一period数据异常,看下图:
在这里插入图片描述

最后一period末端应该是0才对,不应该有波形,因为这时播放进程进程已经停止了。怎么回事?

查看打印知道,播放进程写最后一period时数据大小不足一period大小,导致剩下的数据乱码,那就把不足的数据补零就好了,修改如下:

//解决最后一个周期数据问题
if(size < period_bytes) {
    memset(pbuf+size, 0, period_bytes-size);
}

代码

代码位置:https://gitcode.com/u014056414/myalsa

应用代码是:play_to_cap.c
播放音频是:R48K-16LE-CH1.pcm

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值