Chapter1.设计思路
对于一个wave文件,如果需要播放,涉及到几个方面
1.对于wave文件的解析
2.通过解析wave文件,将得到的参数(主要是sampfrequency, bitsperSample,channel)通过alsa api设下去
3.正确找到data的起始点
4.play alsa
1.1 detail:
1.对于wave文件的解析,需要知道wave文件的格式
注意几点,标准的是44byte的头,但是有些情况下会有additional info, 占据2字节。头信息参见下图,也可以参考wave 文件解析
endian |
field name |
Size |
|
big | ChunkID | 4 | 文件头标识,一般就是" RIFF" 四个字母 |
little | ChunkSize | 4 | 整个数据文件的大小,不包括上面ID和Size本身 |
big | Format | 4 | 一般就是" WAVE" 四个字母 |
big | SubChunk1ID | 4 | 格式说明块,本字段一般就是"fmt " |
little | SubChunk1Size | 4 | 本数据块的大小,不包括ID和Size字段本身 |
little | AudioFormat | 2 | 音频的格式说明 |
little | NumChannels | 2 | 声道数 |
little | SampleRate | 4 | 采样率 |
little | ByteRate | 4 | 比特率,每秒所需要的字节数 |
little | BlockAlign | 2 | 数据块对齐单元 |
little | BitsPerSample | 2 | 采样时模数转换的分辨率 |
big | SubChunk2ID | 4 | 真正的声音数据块,本字段一般是"data" |
little | SubChunk2Size | 4 | 本数据块的大小,不包括ID和Size字段本身 |
little | Data | N | 音频的采样数据 |
2.设置alsa的参数可以详见代码
3.通过解析wave file可以知道我们data的起始位置
4.通过alsa来play,详见代码
注意点:仔细点...指针操作别搞错了..
代码适合初学者看..
Chapter2.Alsa prepare
2.1.pre-prepare
alsa的软件体系结构
Alsa播放需要遵循一套规定,包括设置alsa的open,samplerate,bitrate(format,涉及到大端小端),period,access,channel,prepare,这是一套相对来说固定格式的准备工作。
在打开alsa 设备的时候需要指定具体打开哪个alsa设备,如同aplay的时候我们需要用 -D 去指定device,例如 -D hw:0,0, -D sd_out_16k
关于这个device,可以通过 aplay/arecord -l 去查看
(注意 -L 和-l 是两个命令一个是list pcms 一个是list device, 其中我们指定的sd_out_16k的别名就会在-L命令中被显示出来)
这是aplay -l
对于指定alsa device这个事情,可以在2个文件中配置,一个是asound.conf 一个是.asoundrc. 如果已经安装了alsa,那么打开/usr/share/alsa/alsa.conf就可以看到
对于这两个文件,是preload的,可以通过修改这两个文件来制定你想要的pcm别名.但是如果如果你没做过任何修改,这让两个文件是不存在的,也就是说你需要自己去创建.下面这个是我自己创建的在home目录下的.asoundrc文件,仅仅只是用来做这个测试,指定了device,plugin的hw,sample frequency是16000,具体可以百度
PS:在alsa说明文档有这么一句话
The most important ALSA interfaces to the PCM devices are the "plughw" and the "hw" interface. If you use the "plughw" interface, you need not care much about the sound hardware. If your soundcard does not support the sample rate or sample format you specify, your data will be automatically converted. This also applies to the access type and the number of channels. With the "hw" interface, you have to check whether your hardware supports the configuration you would like to use.
/* Name of the PCM device, like plughw:0,0 */ /* The first number is the number of the soundcard, */ /* the second number is the number of the device. */ char *pcm_name;
Then we initialize the variables and allocate a hwparams structure:
/* Init pcm_name. Of course, later you */ /* will make this configurable ;-) */ pcm_name = strdup("plughw:0,0");
/* Allocate the snd_pcm_hw_params_t structure on the stack. */ snd_pcm_hw_params_alloca(&hwparams);
指定了alsa device之后,你可以通过aplay来试一下是否可以使用
例如:
aplay -D hw:0,0 /home/root/left_1k_right_400hz.wav
aplay -r 16000 -f S16_LE -D sd_out_16k -c 2 left_1k_right_400hz.wav
2.2.open alsa device
接下去就是对于alsadevice 的打开,可以参见ALSA_HOWTO里面说的还是挺详细的.
注意access设置的时候有一段说明
/* Open PCM. The last parameter of this function is the mode. */ /* If this is set to 0, the standard mode is used. Possible */ /* other values are SND_PCM_NONBLOCK and SND_PCM_ASYNC. */ /* If SND_PCM_NONBLOCK is used, read / write access to the */ /* PCM device will return immediately. If SND_PCM_ASYNC is */ /* specified, SIGIO will be emitted whenever a period has */ /* been completely processed by the soundcard. */ if (snd_pcm_open(&pcm_handle, pcm_name, stream, 0) < 0) { fprintf(stderr, "Error opening PCM device %s\n", pcm_name); return(-1);
其他具体顺序参见代码 bool PrepareAlsaDevice(Alsa_Conf* palsaCfg,snd_pcm_t** phandler);
对于别的alsa代码,同样的一套方式可以去使用,因为里面我使用的都是palsaCfg传进来的configuration,所以以后如果使用则只需要修改configuration的值就好.
2.3.HW/SW的设置
未完待续
Chapter3 PlayAlsa
对于play alsa,有几种方式
3.1.读写传输(read/write)
可以用于传输interleaved/non-interleaved的数据,并且有阻塞和非阻塞两种模式。在代码里面我用的是这种,对于write,根据你的interleaved/non-interleaved来确定到底是用writei还是writen。
我用的是writei,只需要把数据灌给alsa设备就可以了。具体方式见代码。
3.2.直接读写传输(mmap)
可以用于传输interleaved/non-interleaved/complex的数据。正在写sample code.基于HOW_TO的samplecode自己改了改...可以直接用,因为wave文件本身LR两个channel都放好了,所以2中copy方式都可以,详见下面的示例代码
Chapter 4 alsa confugiration under linux
Chapter 5 Sound Generateor
Chapter 6 mix the wave by Sound Generateor
Chapter 7 pending
Chapter 8 pending
未完待续....
Chapter 9:Code
9.1 Sample code 1, parse wave file and play , use rw to play
/** @file TestAlsaPlayWave.cpp @brief This is a short example to play the audio wave, please define the path in the main func @par author: jlm @par pre env: alsa @todo */ #include <stdio.h> #include <stdlib.h> #include <string> #include <sched.h> #include <errno.h> #include <getopt.h> #include <iostream> #include <asoundlib.h> #include <sys/time.h> #include <math.h> using namespace std; /*********Type definition***********************/ typedef unsigned char uint8; typedef unsigned short uint16; typedef enum EBitsPerSample { BITS_UNKNOWN = 0, BITS_PER_SAMPLE_8 = 8, BITS_PER_SAMPLE_16 = 16, BITS_PER_SAMPLE_32 = 32 }EBitsPerSample_t; typedef enum ENumOfChannels { NUM_OF_CHANNELS_1 = 1, NUM_OF_CHANNELS_2 = 2 }ENumOfChannels_t; #if 0 /** PCM state */ typedef enum _snd_pcm_state { /** Open */ SND_PCM_STATE_OPEN = 0, /** Setup installed */ SND_PCM_STATE_SETUP, /** Ready to start */ SND_PCM_STATE_PREPARED, /** Running */ SND_PCM_STATE_RUNNING, /** Stopped: underrun (playback) or overrun (capture) detected */ SND_PCM_STATE_XRUN, /** Draining: running (playback) or stopped (capture) */ SND_PCM_STATE_DRAINING, /** Paused */ SND_PCM_STATE_PAUSED, /** Hardware is suspended */ SND_PCM_STATE_SUSPENDED, /** Hardware is disconnected */ SND_PCM_STATE_DISCONNECTED, SND_PCM_STATE_LAST = SND_PCM_STATE_DISCONNECTED } snd_pcm_state_t; #endif typedef struct ALSA_CONFIGURATION { std::string alsaDevice; std::string friendlyName; /// Read: Buffer size should be large enough to prevent overrun (read / write buffer full) unsigned int alsaBufferSize; /// Chunk size should be smaller to prevent underrun (write buffer empty) unsigned int alsaPeriodFrame; unsigned int samplingFrequency;//48kHz EBitsPerSample bitsPerSample;//16 ENumOfChannels numOfChannels; bool block; // false means nonblock snd_pcm_access_t accessType; snd_pcm_stream_t streamType; // Playback or capture unsigned int alsaCapturePeriod; // Length of each capture period }Alsa_Conf; typedef struct Wave_Header { uint8 ChunkID[4]; uint8 ChunkSize[4]; uint8 Format[4]; uint8 SubChunk1ID[4]; uint8 SubChunk1Size[4]; uint8 AudioFormat[2]; uint8 NumChannels[2]; uint8 SampleRate[4]; uint8 ByteRate[4]; uint8 BlockAlign[2]; uint8 BitsPerSample[2]; uint8 CombineWaveFileExtra2Bytes[2]; uint8 SubChunk2ID[4]; uint8 SubChunk2Size[4]; }Wave_Header_t; typedef struct Wave_Header_Size_Info { uint8 ChunkID[4]; uint8 ChunkSize[4]; uint8 Format[4]; uint8 SubChunk1ID[4]; uint8 SubChunk1Size[4]; }Wave_Header_Size_Info_t; typedef struct Wave_Header_Audio_Info { uint8 AudioFormat[2]; uint8 NumChannels[2]; uint8 SampleRate[4]; uint8 ByteRate[4]; uint8 BlockAlign[2]; uint8 BitsPerSample[2]; }Wave_Header_Audio_Info_t; typedef struct Wave_Header_Additional_Info { uint8 AdditionalInfo_2Bytes[2]; //this depends on the SubChunk1Size,normal if SubChunk1Size=16 then match the normal wave format, if SubChunk1Size=18 then 2 more additional info bytes }Wave_Header_Additional_Info_t; typedef struct Wave_Header_Data_Info { uint8 SubChunk2ID[4]; uint8 SubChunk2Size[4]; }Wave_Header_Data_Info_t; /*********Global Variable***********************/ snd_pcm_uframes_t g_frames; //just test purpose /*********Func Declaration***********************/ void TestAlsaDevice(snd_pcm_t** phandler); bool PrepareAlsaDevice(Alsa_Conf* palsaCfg,snd_pcm_t** phandler); bool closeAlsaDevice(snd_pcm_t** phandler); bool ParseWaveFile(const string wavepath,Alsa_Conf* palsaCfg); uint16 HandleLittleEndian(uint8* arr,int size); bool PlayWave(FILE** fp,snd_pcm_t** phandler,Alsa_Conf* palsaCfg); uint16 HandleLittleEndian(uint8* arr,int size) { uint16 value=0; for(int i=0;i<size;i++) { value=value+(arr[i]<<(8*i)); } return value; } #if 0 //this is the return value ChunkID = "RIFF" ChunkSize = 54310 Format = "WAVE" fmt = "fmt " SubChunk1Size = 18 AudioFormat = 1 NumChannels = 2 SampleRate = 16000 ByteRate = 64000 BlockAlign = 4 BitsPerSample = 16 SubChunk2ID = "data" SubChunk2Size = 54272 #endif //parse the wave file bool ParseWaveFile(const string wavepath,Alsa_Conf* palsaCfg,FILE** fp) { int ret=0; //FILE* fp=NULL; *fp=fopen(wavepath.c_str(),"rb"); if(*fp==NULL) { cout<<"Can parse the wave file-->need check the file name"<<endl; } /*********************size info parse begin*************************/ //read size info Wave_Header_Size_Info_t wav_size_info; memset(&wav_size_info,0,sizeof(Wave_Header_Size_Info_t)); ret=fread(&wav_size_info,sizeof(Wave_Header_Size_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } string ChunkID=""; for(int i=0;i<4;i++) { ChunkID+=wav_size_info.ChunkID[i]; } string riff="RIFF"; if(0!=strcmp(ChunkID.c_str(),riff.c_str())) { cout<<"Invalid the fist Chunk id"<<endl; return false; } uint16 ChunkSize=HandleLittleEndian(wav_size_info.ChunkSize,4); cout<<"The ChunSize is "<<ChunkSize<<endl; string Format=""; for(int i=0;i<4;i++) { Format+=wav_size_info.Format[i]; } if(0!=strcmp(Format.c_str(),"WAVE")) { cout<<"Invalid the format"<<endl; return false; } string SubChunk1ID=""; for(int i=0;i<4;i++) { SubChunk1ID+=wav_size_info.SubChunk1ID[i]; } string fmt="fmt "; if(0!=strcmp(SubChunk1ID.c_str(),fmt.c_str())) { cout<<"Invalid the SubChunk1ID "<<endl; return false; } uint16 SubChunk1Size=HandleLittleEndian(wav_size_info.SubChunk1Size,4); if(SubChunk1Size!=16 && SubChunk1Size!=18) { cout<<"Invalid the SubChunk1Size"<<endl; return false; } /*********************Audio info parse begin*************************/ Wave_Header_Audio_Info_t wav_audio_info; memset(&wav_audio_info,0,sizeof(Wave_Header_Audio_Info_t)); ret=fread(&wav_audio_info,sizeof(Wave_Header_Audio_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } //fseek(fp,sizeof(Wave_Header_Size_Info_t),0);//文件指针偏移3个字节到'2' because fread will shift the pointer uint16 AudioFormat =HandleLittleEndian(wav_audio_info.AudioFormat,2); uint16 NumChannels =HandleLittleEndian(wav_audio_info.NumChannels,2); uint16 SampleRate =HandleLittleEndian(wav_audio_info.SampleRate,4); uint16 ByteRate =HandleLittleEndian(wav_audio_info.ByteRate,4); uint16 BlockAlign =HandleLittleEndian(wav_audio_info.BlockAlign,2); uint16 BitsPerSample=HandleLittleEndian(wav_audio_info.BitsPerSample,2); palsaCfg->numOfChannels=(ENumOfChannels)NumChannels; palsaCfg->samplingFrequency=SampleRate; palsaCfg->bitsPerSample=(EBitsPerSample)BitsPerSample; /*********************Additional info parse begin if needed*************************/ if(SubChunk1Size==18) { Wave_Header_Additional_Info_t wav_additional_info; memset(&wav_additional_info,0,sizeof(Wave_Header_Additional_Info_t)); fread(&wav_additional_info,sizeof(Wave_Header_Additional_Info_t),1,*fp); cout<<"read wav_additional_info"<<endl; if(ret<1) { cout<<"read error"<<endl; return false; } uint16 AdditionalInfo=HandleLittleEndian(wav_additional_info.AdditionalInfo_2Bytes,2); cout<<"read AdditionalInfo value="<<AdditionalInfo<<endl; } /*********************Data info parse begin *************************/ Wave_Header_Data_Info_t wave_data_info; memset(&wave_data_info,0,sizeof(Wave_Header_Data_Info_t)); fread(&wave_data_info,sizeof(Wave_Header_Data_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } string SubChunk2ID=""; for(int i=0;i<4;i++) { SubChunk2ID+=wave_data_info.SubChunk2ID[i]; } string fact="fact"; string data="data"; if(0==strcmp(SubChunk2ID.c_str(),fact.c_str())) { cout<<"SubChunk2ID fact"<<endl; } else if(0==strcmp(SubChunk2ID.c_str(),data.c_str())) { cout<<"SubChunk2ID data"<<endl; } else { cout<<"Invalid SubChunk2ID "<<endl; return false; } uint16 SubChunk2Size=HandleLittleEndian(wave_data_info.SubChunk2Size,4); cout<<"End Parse"<<endl; return true; } bool PlayWave(FILE** fp,snd_pcm_t** phandler,Alsa_Conf* palsaCfg) { int err=0; bool ret=false; snd_pcm_uframes_t frames=palsaCfg->alsaPeriodFrame; int bytesPerFrame=(int)palsaCfg->numOfChannels*palsaCfg->bitsPerSample/8; //4bytes uint16 audio_data_size=frames*bytesPerFrame;//one period 10ms ,1600*10/1000*(2*16/8)=640bytes one period uint8* buffer=new uint8[audio_data_size]; cout<<"Start play wave"<<endl; if(*fp==NULL || *phandler==NULL || palsaCfg==NULL) { cout<<"End play wave because something is NULL"<<endl; return false; } //fseek(*fp,46,SEEK_SET); //no need to do fseek because already shifted cout<<"available frame "<<snd_pcm_avail(*phandler)<<"my frames is "<<frames<<endl; while(true) { if(feof(*fp)) { cout<<"Reach end of the file"<<endl; break; } else { if(snd_pcm_avail(*phandler)<frames) { continue; } else { memset(reinterpret_cast<void*>(buffer),0,sizeof(uint8)*audio_data_size); err=fread(buffer,sizeof(uint8)*audio_data_size,1,*fp); if(err=0) { cout<<"read error"<<endl; } if ( NULL != buffer ) { err = snd_pcm_writei(*phandler, buffer, frames); if (err < 0) { cout<<"Fail to write the audio data to ALSA. Reason: "<<(snd_strerror(err)); // recover ALSA device err = snd_pcm_recover(*phandler, err, 0); if (err < 0) { cout<<"Fail to recover ALSA device. Reason: "<<(snd_strerror(err)); ret = false; } else { cout<<"ALSA device is recovered from error state"<<endl; } } } else { cout<<"Write buffer is NULL!"<<endl; } } } usleep(palsaCfg->alsaCapturePeriod / (2 * 1000)); } delete[] buffer; buffer=NULL; return ret; } bool PrepareAlsaDevice(Alsa_Conf* palsaCfg,snd_pcm_t** phandler) { bool ret=false; bool success=true; int error=0; snd_pcm_format_t format; snd_pcm_hw_params_t *hw_params = NULL; int dir=0; if(palsaCfg!=NULL) { // open ALSA device error=snd_pcm_open(phandler,palsaCfg->alsaDevice.c_str(),palsaCfg->streamType,palsaCfg->block? 0:SND_PCM_NONBLOCK); if(error<0) //0 on success otherwise a negative error code { success=false; cout<<"Open Alsadevice error error code="<<snd_strerror(error)<<endl; } if(success) { //allocate hardware parameter structur error=snd_pcm_hw_params_malloc(&hw_params);//alsao can use snd_pcm_hw_params_alloca(&hwparams) if(error<0) { success=false; hw_params=NULL; cout<<"Set hw params error error code="<<snd_strerror(error)<<endl; } } if(success) { //Fill params with a full configuration space for a PCM. initialize the hardware parameter error=snd_pcm_hw_params_any(*phandler,hw_params); if (error < 0) { success=false; cout<<"Broken configuration for PCM: no configurations available: "<<snd_strerror(error)<<endl; } } if(success) { // set the access type error = snd_pcm_hw_params_set_access(*phandler, hw_params, palsaCfg->accessType); if (error < 0) { cout<<"Fail to set access type. Reason: "<<snd_strerror(error)<<endl; success = false; } } if(success) { switch (palsaCfg->bitsPerSample) { case BITS_PER_SAMPLE_8: { format = SND_PCM_FORMAT_U8; break; } case BITS_PER_SAMPLE_16: { format = SND_PCM_FORMAT_S16_LE; //indicate this was little endian break; } case BITS_PER_SAMPLE_32: { format = SND_PCM_FORMAT_S32_LE; break; } default: { format = SND_PCM_FORMAT_S16_LE; cout<<"Invalid format"<<endl; success=false; } } if(success) { error=snd_pcm_hw_params_set_format(*phandler,hw_params,format); if(error<0) { cout<<"set format not available for "<<snd_strerror(error)<<endl; success=false; } } } if(success) { error=snd_pcm_hw_params_set_rate_near(*phandler,hw_params,&palsaCfg->samplingFrequency,0); if(error<0) { cout<<"set rate not available for "<<snd_strerror(error)<<endl; success=false; } } if(success) { error=snd_pcm_hw_params_set_channels(*phandler,hw_params,palsaCfg->numOfChannels); if(error<0) { cout<<"set_channels not available for "<<snd_strerror(error)<<endl; success=false; } } if (success) { // set period size (period size is also a chunk size for reading from ALSA) snd_pcm_uframes_t alsaPeriodFrame = static_cast<snd_pcm_uframes_t>(palsaCfg->alsaPeriodFrame); // One frame could be 4 bytes at most // set period size error = snd_pcm_hw_params_set_period_size_near(*phandler, hw_params, &alsaPeriodFrame, &dir); if (error < 0) { cout<<"Fail to set period size. Reason: "<<snd_strerror(error)<<endl; success = false; } } if (success) { // set hardware parameters error = snd_pcm_hw_params(*phandler, hw_params); if (error < 0) { cout<<"Fail to set hardware parameter. Reason: "<<snd_strerror(error)<<endl; success = false; } } if (success) { error=snd_pcm_hw_params_get_period_size(hw_params, &g_frames, &dir); //get frame cout<<"Frame is "<<g_frames<<endl; // free the memory for hardware parameter structure snd_pcm_hw_params_free(hw_params); hw_params = NULL; // Prepare ALSA device error = snd_pcm_prepare(*phandler); if (error < 0) { cout<<"Fail to prepare ALSA device. Reason: "<<(snd_strerror(error))<<endl; success = false; } } if (success) { cout<<"ALSA device is ready to use"<<endl; } else { // fail to prepare ALSA device ==> un-initialize ALSA device if (hw_params != NULL) { snd_pcm_hw_params_free(hw_params); hw_params = NULL; } closeAlsaDevice(phandler); } } return success; } bool closeAlsaDevice(snd_pcm_t** phandler) { bool ret = true; snd_pcm_state_t state; int snd_ret; if (*phandler != NULL) { // drop the pending audio frame if needed state = snd_pcm_state(*phandler); cout<<"Alsa handler sate: "<<state<<endl; if ((SND_PCM_STATE_RUNNING == state) || (SND_PCM_STATE_XRUN == state) || (SND_PCM_STATE_SUSPENDED == state)) { snd_ret = snd_pcm_drop(*phandler); if (snd_ret < 0) { cout<<"Fail to drop ALSA device. Reason: "<<(snd_strerror(snd_ret))<<endl; } } // close ALSA handler snd_ret = snd_pcm_close(*phandler); if (snd_ret < 0) { cout<<"Fail to close ALSA device. Reason: "<<(snd_strerror(snd_ret))<<endl; ret = false; } *phandler = NULL; cout<<"CLOSE ALSA DEVICE"<<endl; } return ret; } int main() { bool ret=false; snd_pcm_t* m_phandler=NULL; Alsa_Conf* m_palsaCfg=new Alsa_Conf(); m_palsaCfg->alsaDevice = string("sd_out_16k"); //m_palsaCfg->samplingFrequency = 16000; m_palsaCfg->alsaCapturePeriod = 50; //m_palsaCfg->numOfChannels = NUM_OF_CHANNELS_1; m_palsaCfg->block = true; //block m_palsaCfg->friendlyName = "AlsaWave"; //m_palsaCfg->bitsPerSample = BITS_PER_SAMPLE_16; m_palsaCfg->alsaPeriodFrame = m_palsaCfg->samplingFrequency * m_palsaCfg->alsaCapturePeriod / 1000; // calculate the number of frame in one period m_palsaCfg->alsaBufferSize = m_palsaCfg->alsaPeriodFrame * 8; //means the whole buffer was perdion*8, e.g. 10ms means every 10ms we will get/send the data m_palsaCfg->accessType = SND_PCM_ACCESS_RW_INTERLEAVED; m_palsaCfg->streamType = SND_PCM_STREAM_PLAYBACK; FILE* fp=NULL; const string wavePath="/mnt/hgfs/0_SharedFolder/0_Local_Test_Folder/01_TestFolder/TestALSA/left_1k_right_400hz.wav"; //parse the wave file ret=ParseWaveFile(wavePath,m_palsaCfg,&fp); //update the value m_palsaCfg->alsaPeriodFrame = m_palsaCfg->samplingFrequency * m_palsaCfg->alsaCapturePeriod / 1000; // calculate the number of frame in one period if(ret) { //open alsa device ret=PrepareAlsaDevice(m_palsaCfg,&m_phandler); } if(ret) { PlayWave(&fp,&m_phandler,m_palsaCfg); } closeAlsaDevice(&m_phandler); if(fp!=NULL) { fclose(fp); fp=NULL; } delete m_palsaCfg; m_palsaCfg=NULL; return 0; }
9.2 Sample code 2, use mmap to play wave
/**@file TestAlsaPlayWave.cpp @brief This is a short example to play the audio wave, please define the path in the main func @par author: jlm @par pre env: alsa @todo*/ #include <stdio.h> #include <stdlib.h> #include <string> #include <sched.h> #include <errno.h> #include <getopt.h> #include "asoundlib.h" #include <sys/time.h> #include <math.h> #include <iostream> using namespace std; /*********Type definition***********************/ typedef unsigned char uint8; typedef unsigned short uint16; typedef unsigned int uint32; typedef enum EBitsPerSample { BITS_UNKNOWN = 0, BITS_PER_SAMPLE_8 = 8, BITS_PER_SAMPLE_16 = 16, BITS_PER_SAMPLE_32 = 32 }EBitsPerSample_t; typedef enum ENumOfChannels { NUM_OF_CHANNELS_1 = 1, NUM_OF_CHANNELS_2 = 2 }ENumOfChannels_t; #if 0 /** PCM state */ typedef enum _snd_pcm_state { /** Open */ SND_PCM_STATE_OPEN = 0, /** Setup installed */ SND_PCM_STATE_SETUP, /** Ready to start */ SND_PCM_STATE_PREPARED, /** Running */ SND_PCM_STATE_RUNNING, /** Stopped: underrun (playback) or overrun (capture) detected */ SND_PCM_STATE_XRUN, /** Draining: running (playback) or stopped (capture) */ SND_PCM_STATE_DRAINING, /** Paused */ SND_PCM_STATE_PAUSED, /** Hardware is suspended */ SND_PCM_STATE_SUSPENDED, /** Hardware is disconnected */ SND_PCM_STATE_DISCONNECTED, SND_PCM_STATE_LAST = SND_PCM_STATE_DISCONNECTED } snd_pcm_state_t; #endif typedef struct ALSA_CONFIGURATION { std::string alsaDevice; std::string friendlyName; /// Read: Buffer size should be large enough to prevent overrun (read / write buffer full) unsigned int alsaBufferSize; /// Chunk size should be smaller to prevent underrun (write buffer empty) unsigned int alsaPeriodFrame; unsigned int samplingFrequency;//48kHz EBitsPerSample bitsPerSample;//16 ENumOfChannels numOfChannels; bool block; // false means nonblock snd_pcm_access_t accessType; snd_pcm_stream_t streamType; // Playback or capture unsigned int alsaCapturePeriod; // Length of each capture period }Alsa_Conf; typedef struct Wave_Header { uint8 ChunkID[4]; uint8 ChunkSize[4]; uint8 Format[4]; uint8 SubChunk1ID[4]; uint8 SubChunk1Size[4]; uint8 AudioFormat[2]; uint8 NumChannels[2]; uint8 SampleRate[4]; uint8 ByteRate[4]; uint8 BlockAlign[2]; uint8 BitsPerSample[2]; uint8 CombineWaveFileExtra2Bytes[2]; uint8 SubChunk2ID[4]; uint8 SubChunk2Size[4]; }Wave_Header_t; typedef struct Wave_Header_Size_Info { uint8 ChunkID[4]; uint8 ChunkSize[4]; uint8 Format[4]; uint8 SubChunk1ID[4]; uint8 SubChunk1Size[4]; }Wave_Header_Size_Info_t; typedef struct Wave_Header_Audio_Info { uint8 AudioFormat[2]; uint8 NumChannels[2]; uint8 SampleRate[4]; uint8 ByteRate[4]; uint8 BlockAlign[2]; uint8 BitsPerSample[2]; }Wave_Header_Audio_Info_t; typedef struct Wave_Header_Additional_Info { uint8 AdditionalInfo_2Bytes[2]; //this depends on the SubChunk1Size,normal if SubChunk1Size=16 then match the normal wave format, if SubChunk1Size=18 then 2 more additional info bytes }Wave_Header_Additional_Info_t; typedef struct Wave_Header_Data_Info { uint8 SubChunk2ID[4]; uint8 SubChunk2Size[4]; }Wave_Header_Data_Info_t; static char *device = "sd_out_16k";//"plughw:0,0"; /* playback device */ static snd_pcm_format_t format = SND_PCM_FORMAT_S16; /* sample format */ static unsigned int rate = 44100; /* stream rate */ static unsigned int channels = 2; /* count of channels */ static unsigned int buffer_time = 50000; /* ring buffer length in us */ static unsigned int period_time = 10000; /* period time in us */ static double freq = 440; /* sinusoidal wave frequency in Hz */ static int verbose = 0; /* verbose flag */ static int resample = 1; /* enable alsa-lib resampling */ static int period_event = 0; /* produce poll event after each period */ static snd_pcm_sframes_t buffer_size; static snd_pcm_sframes_t period_size; static snd_output_t *output = NULL; uint16 HandleLittleEndian(uint8* arr,int size); //parse the wave file static bool ParseWaveFile(const string wavepath,Alsa_Conf* palsaCfg,FILE** fp); static int set_hwparams(snd_pcm_t *handle, snd_pcm_hw_params_t *params, snd_pcm_access_t access) { unsigned int rrate; snd_pcm_uframes_t size; int err, dir; /* choose all parameters */ err = snd_pcm_hw_params_any(handle, params); if (err < 0) { printf("Broken configuration for playback: no configurations available: %s\n", snd_strerror(err)); return err; } /* set hardware resampling */ err = snd_pcm_hw_params_set_rate_resample(handle, params, resample); if (err < 0) { printf("Resampling setup failed for playback: %s\n", snd_strerror(err)); return err; } /* set the interleaved read/write format */ err = snd_pcm_hw_params_set_access(handle, params, access); if (err < 0) { printf("Access type not available for playback: %s\n", snd_strerror(err)); return err; } /* set the sample format */ err = snd_pcm_hw_params_set_format(handle, params, format); if (err < 0) { printf("Sample format not available for playback: %s\n", snd_strerror(err)); return err; } /* set the count of channels */ err = snd_pcm_hw_params_set_channels(handle, params, channels); if (err < 0) { printf("Channels count (%i) not available for playbacks: %s\n", channels, snd_strerror(err)); return err; } /* set the stream rate */ rrate = rate; err = snd_pcm_hw_params_set_rate_near(handle, params, &rrate, 0); if (err < 0) { printf("Rate %iHz not available for playback: %s\n", rate, snd_strerror(err)); return err; } if (rrate != rate) { printf("Rate doesn't match (requested %iHz, get %iHz)\n", rate, err); return -EINVAL; } /* set the buffer time */ err = snd_pcm_hw_params_set_buffer_time_near(handle, params, &buffer_time, &dir); if (err < 0) { printf("Unable to set buffer time %i for playback: %s\n", buffer_time, snd_strerror(err)); return err; } err = snd_pcm_hw_params_get_buffer_size(params, &size); if (err < 0) { printf("Unable to get buffer size for playback: %s\n", snd_strerror(err)); return err; } buffer_size = size; /* set the period time */ err = snd_pcm_hw_params_set_period_time_near(handle, params, &period_time, &dir); if (err < 0) { printf("Unable to set period time %i for playback: %s\n", period_time, snd_strerror(err)); return err; } err = snd_pcm_hw_params_get_period_size(params, &size, &dir); if (err < 0) { printf("Unable to get period size for playback: %s\n", snd_strerror(err)); return err; } period_size = size; /* write the parameters to device */ err = snd_pcm_hw_params(handle, params); if (err < 0) { printf("Unable to set hw params for playback: %s\n", snd_strerror(err)); return err; } return 0; } static int set_swparams(snd_pcm_t *handle, snd_pcm_sw_params_t *swparams) { int err; /* get the current swparams */ err = snd_pcm_sw_params_current(handle, swparams); if (err < 0) { printf("Unable to determine current swparams for playback: %s\n", snd_strerror(err)); return err; } /* start the transfer when the buffer is almost full: */ /* (buffer_size / avail_min) * avail_min */ err = snd_pcm_sw_params_set_start_threshold(handle, swparams, (buffer_size / period_size) * period_size); if (err < 0) { printf("Unable to set start threshold mode for playback: %s\n", snd_strerror(err)); return err; } /* allow the transfer when at least period_size samples can be processed */ /* or disable this mechanism when period event is enabled (aka interrupt like style processing) */ err = snd_pcm_sw_params_set_avail_min(handle, swparams, period_event ? buffer_size : period_size); if (err < 0) { printf("Unable to set avail min for playback: %s\n", snd_strerror(err)); return err; } /* enable period events when requested */ if (period_event) { err = snd_pcm_sw_params_set_period_event(handle, swparams, 1); if (err < 0) { printf("Unable to set period event: %s\n", snd_strerror(err)); return err; } } /* write the parameters to the playback device */ err = snd_pcm_sw_params(handle, swparams); if (err < 0) { printf("Unable to set sw params for playback: %s\n", snd_strerror(err)); return err; } return 0; } /* * Underrun and suspend recovery */ static int xrun_recovery(snd_pcm_t *handle, int err) { if (verbose) printf("stream recovery\n"); if (err == -EPIPE) { /* under-run */ err = snd_pcm_prepare(handle); if (err < 0) printf("Can't recovery from underrun, prepare failed: %s\n", snd_strerror(err)); return 0; } else if (err == -ESTRPIPE) { while ((err = snd_pcm_resume(handle)) == -EAGAIN) sleep(1); /* wait until the suspend flag is released */ if (err < 0) { err = snd_pcm_prepare(handle); if (err < 0) printf("Can't recovery from suspend, prepare failed: %s\n", snd_strerror(err)); } return 0; } return err; } /* * Transfer method - direct write only */ static int direct_loop(snd_pcm_t *handle, signed short *samples ATTRIBUTE_UNUSED, snd_pcm_channel_area_t *areas ATTRIBUTE_UNUSED) { printf("JLM Test Direct Loop\r\n"); double phase = 0; const snd_pcm_channel_area_t *my_areas; snd_pcm_uframes_t offset, frames, size; snd_pcm_sframes_t avail, commitres; snd_pcm_state_t state; int err, first = 1; Alsa_Conf* m_palsaCfg=new Alsa_Conf(); FILE* fp=NULL; const string wavePath="/mnt/hgfs/0_SharedFolder/0_Local_Test_Folder/01_TestFolder/TestALSA/left_1k_right_400_60ms_16bit_SampleRate_1600_stero.wav"; //parse the wave file int ret=ParseWaveFile(wavePath,m_palsaCfg,&fp); while (1) { state = snd_pcm_state(handle); if (state == SND_PCM_STATE_XRUN) { err = xrun_recovery(handle, -EPIPE); if (err < 0) { printf("XRUN recovery failed: %s\n", snd_strerror(err)); return err; } first = 1; } else if (state == SND_PCM_STATE_SUSPENDED) { err = xrun_recovery(handle, -ESTRPIPE); if (err < 0) { printf("SUSPEND recovery failed: %s\n", snd_strerror(err)); return err; } } avail = snd_pcm_avail_update(handle); if (avail < 0) { err = xrun_recovery(handle, avail); if (err < 0) { printf("avail update failed: %s\n", snd_strerror(err)); return err; } first = 1; continue; } if (avail < period_size) { if (first) { first = 0; err = snd_pcm_start(handle); if (err < 0) { printf("Start error: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } else { printf("JLM start: %s with state=%d\n", snd_strerror(err),snd_pcm_state(handle)); } } else { err = snd_pcm_wait(handle, -1); if (err < 0) { if ((err = xrun_recovery(handle, err)) < 0) { printf("snd_pcm_wait error: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } first = 1; } } continue; } size = period_size; while (size > 0) { frames = size; err = snd_pcm_mmap_begin(handle, &my_areas, &offset, &frames); if (err < 0) { if ((err = xrun_recovery(handle, err)) < 0) { printf("MMAP begin avail error: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } first = 1; } //JLM edit { //printf("start to do jlm \r\n"); int bytesPerFrame=2*16/8; //4bytes uint16 audio_data_size=frames*bytesPerFrame;//one period 10ms ,1600*10/1000*(2*16/8)=640bytes one period //snd_pcm_channel_area_t *playWavareas=NULL; int stepsize=my_areas->step/8; uint8* buffer=new uint8[audio_data_size]; memset(reinterpret_cast<void*>(buffer),0,sizeof(uint8)*audio_data_size); if(fp!=NULL) { //printf("FP is NOT NULL jlm \r\n"); if(feof(fp)) { cout<<"Reach end of the file"<<endl; return 0; } err=fread(buffer,sizeof(uint8),audio_data_size,fp); if(err<0) { cout<<"read error"<<endl; fp=NULL; break; } #define method_2 #ifdef method_1 //printf("jlm audio_data_size=%d ,size=%d",audio_data_size,size); unsigned char* playAddress =(unsigned char*)my_areas->addr+my_areas->first/8+offset*stepsize; memcpy(playAddress,buffer,sizeof(uint8)*audio_data_size); #endif #ifdef method_2 // 0 stands for left, 1 stands for right, because 1 frame=4bytes and 1frame have 2samples ,left have 2 bytes, right have 2bytes uint8* playAddressLeft =(uint8*)my_areas[0].addr+my_areas[0].first/8+offset*stepsize; uint8* playAddressRight=(uint8*)my_areas[1].addr+my_areas[1].first/8+offset*stepsize; for(int i=0;i<frames;i++) { memcpy((playAddressLeft+stepsize*i),buffer+i*4,sizeof(uint8)*2); memcpy((playAddressRight+stepsize*i),buffer+i*4+2,sizeof(uint8)*2); } #endif } else { printf("FP is NULL jlm \r\n"); } delete [] buffer; buffer=NULL; } //generate_sine(my_areas, offset, frames, &phase); commitres = snd_pcm_mmap_commit(handle, offset, frames); if (commitres < 0 || (snd_pcm_uframes_t)commitres != frames) { if ((err = xrun_recovery(handle, commitres >= 0 ? -EPIPE : commitres)) < 0) { printf("MMAP commit error: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } first = 1; } size -= frames; } } } /* * */ struct transfer_method { const char *name; snd_pcm_access_t access; int (*transfer_loop)(snd_pcm_t *handle, signed short *samples, snd_pcm_channel_area_t *areas); }; static struct transfer_method transfer_methods[] = { //{ "write", SND_PCM_ACCESS_RW_INTERLEAVED, write_loop }, //{ "write_and_poll", SND_PCM_ACCESS_RW_INTERLEAVED, write_and_poll_loop }, //{ "async", SND_PCM_ACCESS_RW_INTERLEAVED, async_loop }, //{ "async_direct", SND_PCM_ACCESS_MMAP_INTERLEAVED, async_direct_loop }, { "direct_interleaved", SND_PCM_ACCESS_MMAP_INTERLEAVED, direct_loop }, //{ "direct_noninterleaved", SND_PCM_ACCESS_MMAP_NONINTERLEAVED, direct_loop }, //{ "direct_write", SND_PCM_ACCESS_MMAP_INTERLEAVED, direct_write_loop }, { NULL, SND_PCM_ACCESS_RW_INTERLEAVED, NULL } }; /****************************************************************************************** *************************************ALSA TEST *******************************************/ uint16 HandleLittleEndian(uint8* arr,int size) { uint16 value=0; for(int i=0;i<size;i++) { value=value+(arr[i]<<(8*i)); } return value; } //parse the wave file static bool ParseWaveFile(const string wavepath,Alsa_Conf* palsaCfg,FILE** fp) { int ret=0; //FILE* fp=NULL; *fp=fopen(wavepath.c_str(),"rb"); if(*fp==NULL) { cout<<"Can parse the wave file-->need check the file name"<<endl; } /*********************size info parse begin*************************/ //read size info Wave_Header_Size_Info_t wav_size_info; memset(&wav_size_info,0,sizeof(Wave_Header_Size_Info_t)); ret=fread(&wav_size_info,sizeof(Wave_Header_Size_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } string ChunkID=""; for(int i=0;i<4;i++) { ChunkID+=wav_size_info.ChunkID[i]; } string riff="RIFF"; if(0!=strcmp(ChunkID.c_str(),riff.c_str())) { cout<<"Invalid the fist Chunk id"<<endl; return false; } uint16 ChunkSize=HandleLittleEndian(wav_size_info.ChunkSize,4); cout<<"The ChunSize is "<<ChunkSize<<endl; string Format=""; for(int i=0;i<4;i++) { Format+=wav_size_info.Format[i]; } if(0!=strcmp(Format.c_str(),"WAVE")) { cout<<"Invalid the format"<<endl; return false; } string SubChunk1ID=""; for(int i=0;i<4;i++) { SubChunk1ID+=wav_size_info.SubChunk1ID[i]; } string fmt="fmt "; if(0!=strcmp(SubChunk1ID.c_str(),fmt.c_str())) { cout<<"Invalid the SubChunk1ID "<<endl; return false; } uint16 SubChunk1Size=HandleLittleEndian(wav_size_info.SubChunk1Size,4); if(SubChunk1Size!=16 && SubChunk1Size!=18) { cout<<"Invalid the SubChunk1Size"<<endl; return false; } /*********************Audio info parse begin*************************/ Wave_Header_Audio_Info_t wav_audio_info; memset(&wav_audio_info,0,sizeof(Wave_Header_Audio_Info_t)); ret=fread(&wav_audio_info,sizeof(Wave_Header_Audio_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } //fseek(fp,sizeof(Wave_Header_Size_Info_t),0);//文件指针å移3个å—节到'2' because fread will shift the pointer uint16 AudioFormat =HandleLittleEndian(wav_audio_info.AudioFormat,2); uint16 NumChannels =HandleLittleEndian(wav_audio_info.NumChannels,2); uint16 SampleRate =HandleLittleEndian(wav_audio_info.SampleRate,4); uint16 ByteRate =HandleLittleEndian(wav_audio_info.ByteRate,4); uint16 BlockAlign =HandleLittleEndian(wav_audio_info.BlockAlign,2); uint16 BitsPerSample=HandleLittleEndian(wav_audio_info.BitsPerSample,2); palsaCfg->numOfChannels=(ENumOfChannels)NumChannels; palsaCfg->samplingFrequency=SampleRate; palsaCfg->bitsPerSample=(EBitsPerSample)BitsPerSample; /*********************Additional info parse begin if needed*************************/ if(SubChunk1Size==18) { Wave_Header_Additional_Info_t wav_additional_info; memset(&wav_additional_info,0,sizeof(Wave_Header_Additional_Info_t)); fread(&wav_additional_info,sizeof(Wave_Header_Additional_Info_t),1,*fp); cout<<"read wav_additional_info"<<endl; if(ret<1) { cout<<"read error"<<endl; return false; } uint16 AdditionalInfo=HandleLittleEndian(wav_additional_info.AdditionalInfo_2Bytes,2); cout<<"read AdditionalInfo value="<<AdditionalInfo<<endl; } /*********************Data info parse begin *************************/ Wave_Header_Data_Info_t wave_data_info; memset(&wave_data_info,0,sizeof(Wave_Header_Data_Info_t)); fread(&wave_data_info,sizeof(Wave_Header_Data_Info_t),1,*fp); if(ret<1) { cout<<"read error"<<endl; return false; } string SubChunk2ID=""; for(int i=0;i<4;i++) { SubChunk2ID+=wave_data_info.SubChunk2ID[i]; } string fact="fact"; string data="data"; if(0==strcmp(SubChunk2ID.c_str(),fact.c_str())) { cout<<"SubChunk2ID fact"<<endl; } else if(0==strcmp(SubChunk2ID.c_str(),data.c_str())) { cout<<"SubChunk2ID data"<<endl; } else { cout<<"Invalid SubChunk2ID "<<endl; return false; } uint16 SubChunk2Size=HandleLittleEndian(wave_data_info.SubChunk2Size,4); cout<<"End Parse"<<endl; return true; } /*************************************ALSA TEST ******************************************* ******************************************************************************************/ #define ALSA_TEST_FROM_HOW_TO int main(int argc, char *argv[]) { //#ifdef ALSA_TEST_FROM_HOW_TO #if 1 struct option long_option[] = { {"help", 0, NULL, 'h'}, {"device", 1, NULL, 'D'}, {"rate", 1, NULL, 'r'}, {"channels", 1, NULL, 'c'}, {"frequency", 1, NULL, 'f'}, {"buffer", 1, NULL, 'b'}, {"period", 1, NULL, 'p'}, {"method", 1, NULL, 'm'}, {"format", 1, NULL, 'o'}, {"verbose", 1, NULL, 'v'}, {"noresample", 1, NULL, 'n'}, {"pevent", 1, NULL, 'e'}, {NULL, 0, NULL, 0}, }; snd_pcm_t *handle; int err, morehelp; snd_pcm_hw_params_t *hwparams; snd_pcm_sw_params_t *swparams; int method = 0; signed short *samples; unsigned int chn; snd_pcm_channel_area_t *areas; snd_pcm_hw_params_alloca(&hwparams); snd_pcm_sw_params_alloca(&swparams); morehelp = 0; while (1) { int c; if ((c = getopt_long(argc, argv, "hD:r:c:f:b:p:m:o:vne", long_option, NULL)) < 0) break; switch (c) { case 'h': morehelp++; break; case 'D': device = strdup(optarg); break; case 'r': rate = atoi(optarg); rate = rate < 4000 ? 4000 : rate; rate = rate > 196000 ? 196000 : rate; break; case 'c': channels = atoi(optarg); channels = channels < 1 ? 1 : channels; channels = channels > 1024 ? 1024 : channels; break; case 'f': freq = atoi(optarg); freq = freq < 50 ? 50 : freq; freq = freq > 5000 ? 5000 : freq; break; case 'b': buffer_time = atoi(optarg); buffer_time = buffer_time < 1000 ? 1000 : buffer_time; buffer_time = buffer_time > 1000000 ? 1000000 : buffer_time; break; case 'p': period_time = atoi(optarg); period_time = period_time < 1000 ? 1000 : period_time; period_time = period_time > 1000000 ? 1000000 : period_time; break; case 'm': for (method = 0; transfer_methods[method].name; method++) if (!strcasecmp(transfer_methods[method].name, optarg)) break; if (transfer_methods[method].name == NULL) method = 0; break; case 'o': { int tempformat; for (tempformat = 0; format < SND_PCM_FORMAT_LAST; tempformat++) { const char *format_name = snd_pcm_format_name((snd_pcm_format_t)tempformat); if (format_name) if (!strcasecmp(format_name, optarg)) break; } format=(snd_pcm_format_t)tempformat; if (format == SND_PCM_FORMAT_LAST) format = SND_PCM_FORMAT_S16; if (!snd_pcm_format_linear(format) && !(format == SND_PCM_FORMAT_FLOAT_LE || format == SND_PCM_FORMAT_FLOAT_BE)) { printf("Invalid (non-linear/float) format %s\n", optarg); return 1; } break; } case 'v': verbose = 1; break; case 'n': resample = 0; break; case 'e': period_event = 1; break; } } err = snd_output_stdio_attach(&output, stdout, 0); if (err < 0) { printf("Output failed: %s\n", snd_strerror(err)); return 0; } std::string alsaDevice="sd_out_16k"; printf("Playback device is %s\n", device); printf("Stream parameters are %iHz, %s, %i channels\n", rate, snd_pcm_format_name(format), channels); printf("Sine wave rate is %.4fHz\n", freq); printf("Using transfer method: %s\n", transfer_methods[method].name); if ((err = snd_pcm_open(&handle, alsaDevice.c_str(), SND_PCM_STREAM_PLAYBACK, 0)) < 0) { printf("Playback open error: %s\n", snd_strerror(err)); return 0; } if ((err = set_hwparams(handle, hwparams, transfer_methods[method].access)) < 0) { printf("Setting of hwparams failed: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } if ((err = set_swparams(handle, swparams)) < 0) { printf("Setting of swparams failed: %s\n", snd_strerror(err)); exit(EXIT_FAILURE); } if (verbose > 0) snd_pcm_dump(handle, output); samples =(signed short *) malloc((period_size * channels * snd_pcm_format_physical_width(format)) / 8); if (samples == NULL) { printf("No enough memory\n"); exit(EXIT_FAILURE); } areas = (snd_pcm_channel_area_t*)calloc(channels, sizeof(snd_pcm_channel_area_t)); if (areas == NULL) { printf("No enough memory\n"); exit(EXIT_FAILURE); } for (chn = 0; chn < channels; chn++) { areas[chn].addr = samples; areas[chn].first = chn * snd_pcm_format_physical_width(format); areas[chn].step = channels * snd_pcm_format_physical_width(format); } err = transfer_methods[method].transfer_loop(handle, samples, areas); if (err < 0) printf("Transfer failed: %s\n", snd_strerror(err)); free(areas); free(samples); snd_pcm_close(handle); #endif return 0; }
未完待续...
希望能帮到和我一样的菜鸟...