PCM音频播放器的实现

前言

今天向大家介绍一下如何通过 SDL 实现一个PCM音频播放器。这是一个最简单的播放器,它不涉及到音频的解复用,解码等工作。我们只需要将音频原始数据喂给 SDL 音频接口就可以听到悦耳的声音了。在下面的列子中我将向你演示,使用 SDL 做这样一个播放器是何等的简音。

当然这个看似简单的播放器其实是由许多的理论基础在底层支持着的。如果在这方面没有什么基础的同学可以通过下面的链接去自行学习。

  • SDL 入门

  • SDL窗口渲染

  • SDL基本图形绘制

  • SDL事件处事

  • 彻底理解SDL纹理

  • SDL孙悟空与多线程

另外,我在慕课网分享了音视频免费入门课程,有兴趣的同学可以去观看。

播放音频的基本原则

如果我们要播放一段声音,想当然的认为直接将播放的声音发送给声卡,这样扬声器就会将声音播放出来。只要我们不断的送数据,声音就会不停的输出。

事实上真的是这样吗?当 然 不 是!!!

实际上,所有的音频播放都遵守着一个原则,就是当声卡将要播放的声音输出到扬声器时,它首先会通过回调函数,向你要它一部分声频数据,然后拿着这部分音频数据去播放。等播放完了,它会再向你要下一部分。

至于要的数据的多少,什么时候向你要,这些都是由声卡决定的。对于我们上层应用来说,这些都是由底层 API 决定的。

为什么会出现这种情况呢?为什么播放音频与我们一般的逻辑相反呢?这是因为声卡会严格按照音频的播放时间进行播放,不会多一秒,也不会少一秒。正因为它能准确的计算出时间来,而应用层是不知道这个时间的,所以我们必须按照声卡的要求给它喂数据,而不能依据自己的性子来。

那么有人会问,为什么声卡可以精准的计算出播放时间来呢?这是因为在播放之前我们给它设置了采样率、通道数、采样大小等参数,通过这些参数它就可以计算出时间来。

我们来做个计算,假设采样率是 48000, 双通道,采样大小是 16bit,那么一秒种的数据是多少呢? 48000*2*16=1536000. 反过来,如果我们有一段 8M 的数据,那么声卡就知道它能播放 5秒多的声音。

上面的一大段文字描述,实际上只是想说明一个道理,就是要播放的声音数据,是声卡主动要的,不能由上层直接设置。这是通过回调函数来实现的。后面会有具体的例子。

SDL如何处理音频

SDL是一个处理多媒体的开源库,我们来看看它是如何播放音频的,具体的操作步骤是啥?

  • 打开音频设备

  • 设置音频参数

  • 播放音频

  • 向声卡喂数据

  • 关闭音频设置

详细API介绍

  • 打开音频设备

    int SDL_OpenAudio(SDL_AudioSpec* desired,
     SDL_AudioSpec* obtained)
  • desired: 设置音频参数。

    参数说明
    freq每秒采频率
    SDL_AudioFormat音频数据存储格式
    channels通道数
    silence静音值
    samples采样个数
    size音频缓冲区大小
    SDL_AudioCallback回调函数
    userdata回调函数参数指针
  • obtained: 返回参数。

  • 关闭音频设备

    void SDL_CloseAudio(void)
  • 播放与暂停

    void SDL_PauseAudio(int pause_on)

    pause_on: 0, 暂停播放;1, 播放;

  • 喂数据

    void SDL_MixAudio(Uint8* dst, const Uint8* src,
     Uint32 len, int volume)
  • dst: 目的缓冲区

  • src: 源缓冲区

  • len: 音频数据长度

  • volume: 音量大小,0-128 之间的数。SDL_MIX_MAXVOLUME代表最大音量。

例子

这个例子主要为大家展示了一下如何使用 SDL 的音频 API 来播放声音。其基本流程是,从 pcm 文件一块一块的读数据。然后通过 read_audio_data 这个回调函数给声卡喂数据。如果一次没用完,SDL会再次调用回调函数读数据。

如果audio_buf中的数据用完了,则再次从文件中读一块数据,直到读到文件尾。

#include #include #define BLOCK_SIZE 4096000static Uint8 *audio_buf = NULL;static Uint8 *audio_pos = NULL;static size_t buffer_len = 0;//callback function for audio devcievoid read_audio_data(void *udata, Uint8 *stream, int len){ if(buffer_len == 0){ return;
 }
 SDL_memset(stream, 0, len);
 len = (len < buffer_len) ? len : buffer_len;
 SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
 audio_pos += len;
 buffer_len -= len;
}int main(int argc, char *argv[]){ int ret = -1;
 FILE *audio_fd = NULL;
 SDL_AudioSpec spec; char *path = "./test.pcm"; //SDL initialize
 if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){ fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError()); return ret;
 } //open pcm file
 audio_fd = fopen(path, "r"); if(!audio_fd){ fprintf(stderr, "Failed to open pcm file!\n"); goto __FAIL;
 }//SDL_AudioSpec
 spec.freq = 44100;;
 spec.format = AUDIO_S16SYS;
 spec.channels = 2;
 spec.silence = 0;
 spec.samples = 1024;;
 spec.callback = read_audio_data;;
 spec.userdata = NULL; //open audio devcie
 if(SDL_OpenAudio(&spec, NULL)){ fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError()); goto __FAIL;
 } //play audio
 SDL_PauseAudio(0); do{ //read data from pcm file
 buffer_len = fread(audio_buf, 1, BLOCK_SIZE, audio_fd); fprintf(stderr, "block size is %zu\n", buffer_len);
 audio_pos = audio_buf; //the main thread wait for a moment
 while(audio_pos < (audio_buf + buffer_len)) {
 SDL_Delay(1);
 }
 }while(buffer_len !=0); //close audio device
 SDL_CloseAudio();
 ret = 0;
__FAIL: //release some resources
 if(audio_buf){ free(audio_buf);
 } if(audio_fd){
 fclose(audio_fd);
 } 
 //quit SDL
 SDL_Quit(); return ret;
}

小结

本文向大家讲解了一下如何通过SDL库的音频处理 API 实现一个最简单的 PCM 播放器。通过个例子大家可以了解到,SDL的使用是如此简单。

当然这个播放器还是有点 Low,不过不要紧,随着后面文章的推出,你会逐渐看到一个完整的播放器是如何被打造出来的。

希望本文能对你有所帮助,谢谢!

隆重推荐

我的免费音视频入门课(慕课网)