ALSA应用层编程播放音乐

关于ALSA,网上也有介绍,但是我在看的时候看的也是一脸懵逼,不是介绍的不好,是因为我之前对于嵌入式软件这一块实在没什么了解,之前一直学的JAVA,整个体系跟JAVA还是有很大的区别,要学的也完全是我之前没了解过的,所以以下有说错的请及时纠正。

功能

实现在linux中通过编程.C文件播放一个.wav格式的音频文件

播放: 将音频文件进行解码(Decode)生成PCM数据, 并将其送入音频设备中播出.

录音: 本程序暂时不涉及录音功能

ALSA

关于ALSA我不过多介绍,这篇笔记主要是记录我如何成功播放音乐,主要是怕误导别人,在我看来就是向上提供了接口供我们使用,向下控制了硬件播放音乐,跟JAVA中的JDK提供的接口函数类似,你只管使用就可以了。

还有一个是ALSA的官方的教程好像是,播放音乐整体顺序我也是参考的这篇文章来写的。

术语

关于ALSA中有一些术语着实是让我懵逼的一批,有几个术语我到现在还不知道理解的对不对,所以在正式编程之前一定要先知道,等自己编程的时候再理解一下。

因为声音是连续模拟量,计算机将它离散化之后用数字表示,就有了以下几个名词术语

  • 样本长度(sample):样本是记录音频数据最基本的单位,计算机对每个通道采样量化时数字比特位数,常见的有8位和16位。

这个样本长度后面编程时会用到的,按照字面意思理解的话,就是取出来8bit或者16bit的数据做样本;就理解成一个样本就可以,只不过一个样本的大小是8bit或者16bit,或者其他大小。

  • 通道(channel):该参数为1表示单声道,2则是立体声。

这个术语也好理解,单声道应该就是只有一个左耳机或右出声音,而立体就是左右耳机都出声的意思;每一个通道都有一个样本长度,单声道的数据就是一个样本长度(样本),立体声道的话2个样本长度(样本)。

  • 帧(frame):帧记录了一个声音单元,其长度为样本长度与通道数的乘积,一段音频数据就是由苦干帧组成的。

帧单位,把所有通道中的数据加在一起叫做一帧,所以单声道:一帧 = 样本长度 * 1;双声道:一帧 = 样本长度 * 2

  • 采样率(rate):每秒钟采样次数,该次数是针对帧而言,常用的采样率如8KHz的人声,44.1KHz的mp3音乐, 96Khz的蓝光音频。

这个在编程时也比较好设置,没什么混淆的,固定的值也就那么几个,一般没有随便乱设置的数值

  • 周期(period):音频设备一次处理所需要的桢数,对于音频设备的数据访问以及音频数据的存储,都是以此为单位。

一个周期用来存放若干个帧的单元,ALSA函数是以一个周期为单位来读取音频数据的,其实这个周期现在我也没很好的理解到底什么个意思,
是说ALSA向硬件输入数据时,是以时间为周期,在这个时间周期中输入若干个帧呢?
还是说就是一个固定存储空间大小叫做周期,然后这个周期大小的空间放入若干个帧? 真是让人头大!!!!!!!

  • 缓冲区(buffer): 用来存放将要被输入到硬件的数据,我这么理解也不知道对还是不对

一个缓冲区一般有两个周期,缓冲区是循环读取的,比如一个缓冲区有两个周期,那么硬件在读取一个缓冲区时便会产生两次中断,当第一个周期的音频数据被取走就准备取第二个周期的音频数据,同时第三个周期的音频数据会填充到第一个音频数据的位置,以此循环

  • 交错模式(interleaved):是一种音频数据的记录方式,分为交错模式和非交错模式

交错模式与非交错模式只是音频数据存放在缓冲区时的一种方式,
在交错模式下,数据以连续桢的形式存放,即首先记录完桢1的左声道样本和右声道样本(假设为立体声格式),再开始桢2的记录。
而在非交错模式下,首先记录的是一个周期内所有桢的左声道样本,再记录右声道样本,数据是以连续通道的方式存储。

看到上面的术语真是让人头大,但是不理解这些,等编程的时候也是瞎子摸象–一通乱摸,所以务必要先理解了这些术语,如果实在看不下去这术语,可以编程的时候走到哪一步时再回过来一一对照理解也可以。

ALSA编程

在用户空间使用ALSA需要安装依赖

alsa-lib: 用户空间函数库, 封装驱动提供的抽象接口, 通过文件libasound.so提供API给应用程序使用

alsa-utils: 实用工具包,通过调用alsa-lib实现播放音频(aplay)、录音(arecord) 等工具

安装

ubuntu 安装

$ sudo apt-get update
$ sudo apt-get install alsa-lib alsa-utils

Arch 安装

$ sudo pacman -Sy alsa-lib alsa-utils glibc

安装好以后需要运行 aplay -l 确认当前用户可以使用声卡设备

$ aplay -l

应用层声卡设备

如果没有显示图片中的内容,可以切换到root用户试一下,或者把当前用户加入到音频组也可以

$ gpasswd -a [user_name] audio
# 记得注销一下,或者切换用户后再切换回来,通过下面的命令可以查看当前用户是否已经加入audio组
$ groups [user_name]
# 再运行查看声卡设备是否有了
$ aplay -l

编程

文件分为:

  • Makefile:make 的配置文件
  • alsa-play-music.c:C源码文件
  • const.h:C头文件

alsa-play-music.c

/**
	ALSA音频应用层编程
	auth:	ywh
	date:	2019-9-4
	idea:	先打开一个普通wav音频文件,从定义的文件头前面的44个字节中,取出文件头的定义消息,置于一个文件头的结构体中。
			然后打开alsa音频驱动,从文件头结构体取出采样精度,声道数,采样频率三个重要参数,利用alsa音频驱动的API设置好参数,
			最后打开wav文件,定位到数据区,把音频数据依次写到音频驱动中去,开始播放,当写入完成后,退出写入的循环。
	rel:	- [参考的想法](https://www.bbsmax.com/A/n2d9x2D05D/)
			- [alsa的函数库](https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html#ga8340c7dc0ac37f37afe5e7c21d6c528b)
**/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <string.h>
#include <stdbool.h>
#include <alsa/asoundlib.h>
#include "const.h"


int main(int argc, char *argv [])
{

	int ret, rate_arg, format_arg;
	bool flag = true;


#if 0
	// 判断命令参数中是携带音乐文件
	if(argc != 2){
		printf("err: not found music file \n");
		exit(1);
	}
	// 打开音乐文件
	open_music_file(argv[1]);

#endif

	while((ret = getopt(argc,argv,"m:f:r:")) != -1){
		flag = false;
		switch(ret){
			case 'm':
				printf("打开文件 \n");
				open_music_file(optarg);
				break;
			case 'f':
				
				format_arg = atoi(optarg);
				
				// 判断是哪种采样位
				switch(format_arg){
					case 161:
						printf("format_arg value is : S16LE \n");
						pcm_format = SND_PCM_FORMAT_S16_LE;
						break;
					
					case 162:
						printf("format_arg value is : S16BE \n");
						pcm_format = SND_PCM_FORMAT_S16_BE;
						break;
					
					case 201:
						printf("format_arg value is : S20LE \n");
						pcm_format = SND_PCM_FORMAT_S20_LE;
						break;
					
					case 202:
						printf("format_arg value is : S20BE \n");
						pcm_format = SND_PCM_FORMAT_S20_BE;
						break;

					case 241:
						printf("format_arg value is : S24LE \n");
						pcm_format = SND_PCM_FORMAT_S24_LE;
						break;

					case 242:
						printf("format_arg value is : S24BE \n");
						pcm_format = SND_PCM_FORMAT_S24_BE;
						break;

					case 2431:
						printf("format_arg value is : S243LE \n");
						pcm_format = SND_PCM_FORMAT_S24_3LE;
						break;
					
					case 2432:
						printf("format_arg value is : S243BE \n");
						pcm_format = SND_PCM_FORMAT_S24_3BE;
						break;

					case 321:
						printf("format_arg value is : S32LE \n");
						pcm_format = SND_PCM_FORMAT_S32_LE;
						break;

					case 322:
						printf("format_arg value is : S32BE \n");
						pcm_format = SND_PCM_FORMAT_S32_BE;
						break;
						

				}
				break;
				
			case 'r':
				
				rate_arg = atoi(optarg);
				
				if(rate_arg == 44){
					
					printf("rate_arg value is : 44.1HZ \n");
					rate = 44100;
				
				}else if(rate_arg == 88){
					
					printf("rate_arg value is : 88.2HZ \n");
					rate = 88200;
					
				}else{
					
					printf("rate_arg value is : 8HZ \n");
					rate = 8000;
					
				}
				break;
		}
	}

	if(flag){
		printf("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
		printf("Either 1'st, 2'nd, 3'th or all parameters were missing \n");
		printf("\n");
		printf("1'st : -m [music_filename] \n");
		printf("		music_filename.wav \n");
		printf("\n");
		printf("2'nd : -f [format 241bit or 16bit or 32bit] \n");
		printf("		161 for S16_LE, 162 for S16_BE \n");
		printf("		241 for S24_LE, 242 for S24_BE \n");
		printf("		2431 for S24_3LE, 2432 for S24_3BE \n");
		printf("		321 for S32_LE, 322 for S32_BE \n");
		printf("\n");
		printf("3'th : -r [rate,44 or 88] \n");
		printf("		44 for 44100hz \n");
		printf("		82 for 88200hz \n");
		printf("\n");
		printf("For example: ./alsa -m 1.wav -f 161 -r 44 \n");
		printf("+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n");
		exit(1);
	}
	

	
	// 在堆栈上分配snd_pcm_hw_params_t结构体的空间,参数是配置pcm硬件的指针,返回0成功
	debug_msg(snd_pcm_hw_params_malloc(&hw_params), "分配snd_pcm_hw_params_t结构体");

	

	// 打开PCM设备 返回0 则成功,其他失败
	// 函数的最后一个参数是配置模式,如果设置为0,则使用标准模式
	// 其他值位SND_PCM_NONBLOCL和SND_PCM_ASYNC 如果使用NONBLOCL 则读/写访问, 如果是后一个就会发出SIGIO(没太懂什么意思)
	pcm_name = strdup("hw:0,0");
	debug_msg(snd_pcm_open(&pcm_handle, pcm_name, stream, 0), "打开PCM设备");



	// 在我们将PCM数据写入声卡之前,我们必须指定访问类型,样本长度,采样率,通道数,周期数和周期大小。
	// 首先,我们使用声卡的完整配置空间之前初始化hwparams结构
	debug_msg(snd_pcm_hw_params_any(pcm_handle, hw_params), "配置空间初始化");




	
	// 设置交错模式(访问模式)
	// 常用的有 SND_PCM_ACCESS_RW_INTERLEAVED(交错模式) 和 SND_PCM_ACCESS_RW_NONINTERLEAVED (非交错模式) 
	// 参考:https://blog.51cto.com/yiluohuanghun/868048
	debug_msg(snd_pcm_hw_params_set_access(pcm_handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED), "设置交错模式(访问模式)");




	

	// 设置采样位数 根据音乐文件返回的采样位数设置
#if 0
	if(wav_header.bits_per_sample == 8){
		pcm_format = SND_PCM_FORMAT_S8;
	}else if(wav_header.bits_per_sample == 16){
		pcm_format = SND_PCM_FORMAT_S16_LE;
	}

	
	S是有符号	U是无符号
	BE是大端(低地址存高位)
	LE是小端(低地址存低位)

	BE 还是 LE 都是相对于软件层面来说的

#endif
	debug_msg(snd_pcm_hw_params_set_format(pcm_handle, hw_params, pcm_format), "设置样本长度(位数)");




	

	// 设置采样率为44.1KHz dir的范围(-1,0,1) 88.2
	//rate = wav_header.sample_rate;
	debug_msg(snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &rate, 0), "设置采样率");
	printf("rate value is : %d \n", rate);

	

	// 设置通道数
	debug_msg(snd_pcm_hw_params_set_channels(pcm_handle, hw_params, wav_header.num_channels), "设置通道数");



	// 设置periods: 一个缓冲区所包含的周期数.
	debug_msg(snd_pcm_hw_params_set_periods(pcm_handle, hw_params, periods, 0), "设置周期数");


	// 设置缓冲区 buffer_size = period_size * periods 一个缓冲区的大小可以这么算,我上面设定了周期为2,
	// 周期大小我们预先自己设定,那么要设置的缓存大小就是 周期大小 * 周期数 就是缓冲区大小了。
	buffer_size = period_size * periods;
	
	// 为buff分配buffer_size大小的内存空间
	buff = (unsigned char *)malloc(buffer_size);
	
	if(format_arg == 161 || format_arg == 162){

		/**
			snd_pcm_hw_params_set_buffer_size() 最后一个参数是要设置的缓冲区有多大,单位是帧(frames_size),
			因为snd_pcm_hw_params_set_buffer_size第三个参数需要传进的是以帧为单位,所以要先把字节大小先转换为帧

			一个帧的大小 = 采样位数(长度) * 通道数,例如:我上面的采样位数位16bit,通道数为2,那么一个帧的大小就是16bit * 2 / 8 = 4个字节
			所以要除以4以后就算出来缓冲区的大小了,其实缓冲区的大小还是 period_size * periods 个字节,只不过把这些数据变成了另一种单位传进去而已

			就好像 1美元 = 7人民币,虽然钱的数量变少了,但是价值和购买力没变,只是换成了另一种
		**/
		frames = buffer_size >> 2;
	
		debug_msg(snd_pcm_hw_params_set_buffer_size(pcm_handle, hw_params, frames), "设置S16_LE OR S16_BE缓冲区");
		
	}else if(format_arg == 2431 || format_arg == 2432){

		frames = buffer_size / 6;
		
		/*
			那么当位数为24时,就需要除以6了,因为是24bit * 2 / 8 = 6
		*/
		debug_msg(snd_pcm_hw_params_set_buffer_size(pcm_handle, hw_params, frames), "设置S24_3LE OR S24_3BE的缓冲区");
		
	}else if(format_arg == 321 || format_arg == 322 || format_arg == 241 || format_arg == 242){

		frames = buffer_size >> 3;
		/*
			那么当位数为32时,就需要除以8了,因为是32bit * 2 / 8 = 8
		*/
		debug_msg(snd_pcm_hw_params_set_buffer_size(pcm_handle, hw_params, frames), "设置S32_LE OR S32_BE OR S24_LE OR S24_BE缓冲区");

		
		
	}


	// 设置的硬件配置参数,加载,并且会自动调用snd_pcm_prepare()将stream状态置为SND_PCM_STATE_PREPARED
	debug_msg(snd_pcm_hw_params(pcm_handle, hw_params), "设置的硬件配置参数");


#if 0
	/*
		snd_pcm_hw_params_get_period_size返回的是以帧为单位的数,如果想换算成字节数,如果是16位乘以4,32位乘以8就是字节数
	 	正好是一个周期大小的字节数
	*/
	debug_msg(snd_pcm_hw_params_get_period_size(hw_params, &frames, 0), "获取返回的近似周期大小");
	printf("frames value is : %ld \n", frames * 4);



	// snd_pcm_hw_params_get_buffer_size函数也是返回的以帧为单位的数,想换算成字节数,也需要根据不同bit乘以不同的数字
	debug_msg(snd_pcm_hw_params_get_buffer_size(hw_params, &period_size), "获取缓冲区大小");
	printf("buffer_size value is : %ld \n", period_size * 4);

	exit(1);
#endif


	// feof函数检测文件结束符,结束:非0, 没结束:0 !feof(fp)
	while(1){
		// 读取文件数据放到缓存中
		ret = fread(buff, 1, buffer_size, fp);
		
		if(ret == 0){
			
			printf("end of music file input! \n");
			exit(1);
		}
		
		if(ret < 0){
			
			printf("read pcm from file! \n");
			exit(1);
		}

		// 向PCM设备写入数据 snd_pcm_writei()函数第三个是帧单位,意思是这些个(buffer_size)大小缓存中有多少个frames(左右声道 * 一个采样位数)
		while((ret = snd_pcm_writei(pcm_handle, buff, frames)) < 0){
			if (ret == -EPIPE){
				
                  /* EPIPE means underrun 基本上-32  的错误就是缓存中的数据不够 */
                  printf("underrun occurred -32, err_info = %s \n", snd_strerror(ret));
				  //完成硬件参数设置,使设备准备好
                  snd_pcm_prepare(pcm_handle);
			
            } else if(ret < 0){
				printf("ret value is : %d \n", ret);
				debug_msg(-1, "write to audio interface failed");
			}
		}
	}

	fprintf(stderr, "end of music file input\n");
	
	// 关闭文件
	fclose(fp);
	snd_pcm_close(pcm_handle);
	
	return 0;

}

// 想要定义函数必须要先声明函数,在头文件中声明了
void open_music_file(const char *path_name){
	// 通过fopen函数打开音乐文件
	fp = fopen(path_name, "rb");
	// 判断文件是否为空
	if(fp == NULL){
		printf("music file is NULL \n");
		fclose(fp);
		exit(1);
	}
	// 把文件指针定位到文件的开头处
	fseek(fp, 0, SEEK_SET);

	// 读取文件,并解析文件头获取有用信息
	wav_header_size = fread(&wav_header, 1, sizeof(wav_header), fp);
	printf("wav文件头结构体大小:	%d 			\n", wav_header_size);
	printf("RIFF标志:		\t 		%c%c%c%c 	\n", wav_header.chunk_id[0], wav_header.chunk_id[1], wav_header.chunk_id[2], wav_header.chunk_id[3]);
	printf("文件大小: 		\t 		%d			\n", wav_header.chunk_size);
	printf("文件格式: 		\t 		%c%c%c%c 	\n", wav_header.format[0], wav_header.format[1], wav_header.format[2], wav_header.format[3]);
	printf("格式块标识:	\t\t\t 	%c%c%c%c	\n", wav_header.sub_chunk1_id[0], wav_header.sub_chunk1_id[1], wav_header.sub_chunk1_id[2], wav_header.sub_chunk1_id[3]);
	printf("格式块长度:	\t\t\t 	%d 			\n", wav_header.sub_chunk1_size);
	printf("编码格式代码: 	\t\t\t 	%d 			\n", wav_header.audio_format);
	printf("声道数:			\t 		%d			\n", wav_header.num_channels);
	printf("采样频率:		\t 		%d			\n", wav_header.sample_rate);
	printf("传输速率: 		\t\t 	%d 			\n", wav_header.byte_rate);
	printf("数据块对齐单位: \t\t\t 	%d			\n", wav_header.block_align);
	printf("采样位数(长度):		\t 		%d			\n", wav_header.bits_per_sample);


}


bool debug_msg(int result, const char *str)
{
	if(result < 0){
		printf("err: %s 失败!, result = %d, err_info = %s \n", str, result, snd_strerror(result));
		exit(1);
	}
	return true;
}

const.h

// 定义音乐全局结构体,具体该定义什么参考 https://www.cnblogs.com/ranson7zop/p/7657874.html 表3
// int 由uint32_t代替,short 由uint16_t代替,因为在跨平台后有可能不兼容,类型长度不一致,使用统一的类型
struct WAV_HEADER
{

	char 		chunk_id[4]; // riff 标志号
	uint32_t 	chunk_size; // riff长度
	char 		format[4]; // 格式类型(wav)
	
	char 		sub_chunk1_id[4]; // fmt 格式块标识
	uint32_t 	sub_chunk1_size; // fmt 长度 格式块长度。
	uint16_t 	audio_format; // 编码格式代码									常见的 WAV 文件使用 PCM 脉冲编码调制格式,该数值通常为 1
	uint16_t 	num_channels; // 声道数 									单声道为 1,立体声或双声道为 2
	uint32_t  	sample_rate; // 采样频率 									每个声道单位时间采样次数。常用的采样频率有 11025, 22050 和 44100 kHz。
	uint32_t 	byte_rate; // 传输速率 										该数值为:声道数×采样频率×每样本的数据位数/8。播放软件利用此值可以估计缓冲区的大小。
	uint16_t	block_align; // 数据块对齐单位									采样帧大小。该数值为:声道数×位数/8。播放软件需要一次处理多个该值大小的字节数据,用该数值调整缓冲区。
	uint16_t 	bits_per_sample; // 采样位数								存储每个采样值所用的二进制数位数。常见的位数有 4、8、12、16、24、32

	char 		sub_chunk2_id[4]; // 数据  不知道什么数据
	uint32_t 	sub_chunk2_size; // 数据大小
	
} wav_header;
int wav_header_size; // 接收wav_header数据结构体的大小


/**
	ALSA的变量定义
**/
// 定义用于PCM流和硬件的 
snd_pcm_hw_params_t *hw_params;
// PCM设备的句柄  想要操作PCM设备必须定义
snd_pcm_t *pcm_handle;
// 定义pcm的name snd_pcm_open函数会用到,strdup可以直接把要复制的内容复制给没有初始化的指针,因为它会自动分配空间给目的指针,需要手动free()进行内存回收。
char *pcm_name;
// 定义是播放还是回放等等,播放流 snd_pcm_open函数会用到 可以在 https://www.alsa-project.org/alsa-doc/alsa-lib/group___p_c_m.html#gac23b43ff55add78638e503b9cc892c24 查看
snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;
// 定义采样位数
snd_pcm_format_t pcm_format;


// 缓存大小
#define BUF_LEN 1024
//char buf[BUF_LEN];
unsigned char *buff;
unsigned char *buff1;


// 周期数
int periods = 2;
// 一个周期的大小,这里虽然是设置的字节大小,但是在有时候需要将此大小转换为帧,所以在用的时候要换算成帧数大小才可以
snd_pcm_uframes_t period_size = 12 * 1024;
snd_pcm_uframes_t frames;
snd_pcm_uframes_t buffer_size;





// 初始化采样率
unsigned int rate;

// 音乐文件指针变量
FILE *fp;


// 函数声明
bool debug_msg(int result, const char *str);
void open_music_file(const char *path_name);

Makefile

# 本机编译器,如果需要可以改成交叉编译器 -Wall -werror -g 都是在编译的时候有打印调试信息的参数,最好在正式编译的时候都带上
GCC	= gcc -Wall -Werror -g

# Makefile的内置规则,文件最前的为最高目标,执行make命令会自动编译此目标
main: clean alsa

.PHONY: clean alsa
# $@ 符号是Makefile中内置变量,代表目标文件
# $^ 符号是Makefile中内置变量,代表依赖文件,即源文件
alsa: alsa-play-music.c
	@echo "=== compile start! ==="
	$(GCC) $^ -o $@ -lasound
	@echo "=== compile edn! ==="


clean:
	@rm -rf *.o alsa
	@echo "clean success!"

C源码文件中我每一步都有注释,使用的函数都可以在参考中的函数接口文档中找到。

在源码中我有一些自己的理解,如果觉得我的注释对你起不到辅助作用可以删除。

buff_size 和 frame 的关系

alsa中有这么两个函数 snd_pcm_hw_params_set_buffer_size函数 和 snd_pcm_writei 函数,在这之前我对这两个函数的第三个参数就特别混淆,不知道该设置什么,现在我有点明白了

首先这两个函数的第三个参数都是传入以 帧 为单位的值,所以根本就是同一个值,第一个函数设置的是每次硬件可以向多大的缓存中拿数据,第二个是应用层向多大的缓存输入多少数据,这里的数据都是 帧,所以我输入的就是你拿的数据,那可不就是一个东西嘛

所以buff_size大小就无所谓了,通常为 buffer_size = period_size * periods

最主要的是第三个参数frame如何计算,也特别好计算,就好像 RMB和美元的换算是一个道理

buffer_size = RMB

frame(帧) = 美元

钱的是1:7,在计算机中按照字节换算,首先要知道一帧(frame)等于到少字节,才能换算,以下使用16bit采样长度换算,如果是其他采样长度换掉采样长度即可。

# 这里除以8 是因为 1字节 = 8bit
1 帧(frame) = 采样长度 * 通道数 = 16 * 2 / 8 = 4
# 知道一帧的大小后,就可以计算总共的帧大小了,右移两位相当于除以4
frames = buffer_size >> 2;

经过计算的frames 就是要传入两个函数的第三个参数的值了。

使用

我自己是在树莓派4上编译使用的,进入到文件夹后,直接运行 make 命令即可在本目录出现可执行文件 alsa

$ cd ALSA
$ make
# -m 参数的意思音乐文件,-f 是术语中提到的样本长度,-r 是术语中提到的采样率
$ ./alsa -m 1.wav -f 161 -r 44
# 161 是 S16_LE 162 是 S16_BE,以此类推,或者直接运行 ./alsa 查看都可以设置什么值
$ ./alsa
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Either 1'st, 2'nd, 3'th or all parameters were missing 

1'st : -m [music_filename] 
		music_filename.wav 

2'nd : -f [format 241bit or 16bit or 32bit] 
		161 for S16_LE, 162 for S16_BE 
		241 for S24_LE, 242 for S24_BE 
		2431 for S24_3LE, 2432 for S24_3BE 
		321 for S32_LE, 322 for S32_BE 

3'th : -r [rate,44 or 88] 
		44 for 44100hz 
		82 for 88200hz 

For example: alsa -m 1.wav -f 161 -r 44 
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

一般不报错的话把耳机插入树莓派的耳机插口就可以听到音乐了

参考

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值