Qt下实现一个音频播放器
项目介绍
在Qt下实现一个音频播放器,声音来源可以是本地文件或者网络播放地址。使用ffmpeg来实现音频数据的解码,使用SDL库来实现解码后音频数据的播放。
解码流程
通过ffmpeg来实现对音频数据解码,需要实现以下几个流程:
1.使用ffmpeg初始化音频相关实现
int FFPlayer::openAudio(){ int ret = 0; if(audio_stream_index streams[audio_stream_index]; //初始化解码器 AVCodecParameters* codec_param = audio_stream->codecpar; AVCodec* codec = nullptr; codec = (AVCodec*)avcodec_find_decoder(codec_param->codec_id); if(codec == nullptr){ return -1; } audio_codec_ctx = avcodec_alloc_context3(codec); if(audio_codec_ctx == nullptr){ return -1; } ret = avcodec_parameters_to_context(audio_codec_ctx,codec_param); if(ret != 0){ return -1; } ret = avcodec_open2(audio_codec_ctx,codec,nullptr); if(ret != 0){ return -1; } //初始化音频输出 initSDLAudio(); audioFrame = av_frame_alloc(); audio_buffer.data = (char*)malloc(9182); audio_buffer.size = 0; audio_buffer.index = 0; //创建音频解码线程 _audioDecodeThread = std::thread(decodeAudioFunc,this); return ret;}
2.使用读包线程获取音频数据包
void FFPlayer::doTask(){ while (!quit) { av_init_packet(packet);#if 1 fmt_ctx->interrupt_callback.callback = interrupt_callback; fmt_ctx->interrupt_callback.opaque = this; block_starttime = time(nullptr);#endif int ret = av_read_frame(fmt_ctx, packet); fmt_ctx->interrupt_callback.callback = nullptr; if (ret != 0) { if (!quit) { if (ret == AVERROR_EOF || avio_feof(fmt_ctx->pb)) { eof = 1; event_callback(HPLAYER_EOF); } else { error = ret; event_callback(HPLAYER_ERROR); } } return; } if(packet->stream_index == audio_stream_index){#if 1 { std::lock_guard lock(audioQueueMutex); AVPacket* pkt = av_packet_clone(packet); _audioPktList.push_back(*pkt); }#endif defer (av_packet_unref(packet);) } }}
3.使用解码线程对音频数据包解码
void decodeAudioFunc(void*ctx){ FFPlayer* ptx = (FFPlayer*)ctx; if(ptx == nullptr) return; while(!ptx->quit){ AVPacket pkt; if(ptx->_audioPktList.size()>0){ std::lock_guard lock(ptx->audioQueueMutex); pkt = ptx->_audioPktList.front(); ptx->_audioPktList.pop_front(); }else{ std::this_thread::sleep_for(std::chrono::milliseconds(20)); continue; }#if 1 //解码 int ret = 0; ret = avcodec_send_packet(ptx->audio_codec_ctx, &pkt); if (ret != 0) { hloge(\"avcodec_send_packet error: %d\", ret); return; } while(avcodec_receive_frame(ptx->audio_codec_ctx, ptx->audioFrame)>=0){ // 重采样 uint8_t* output; int out_samples = swr_get_out_samples(ptx->swr_ctx, ptx->audioFrame->nb_samples); av_samples_alloc(&output, NULL, ptx->audio_codec_ctx->channels, out_samples, AV_SAMPLE_FMT_S16, 0); out_samples = swr_convert(ptx->swr_ctx, &output, out_samples, (const uint8_t**)ptx->audioFrame->data, ptx->audioFrame->nb_samples); if(out_samples audio_codec_ctx->channels * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);#else aDataSize = av_samples_get_buffer_size( NULL, // 行大小(可忽略) ptx->audio_codec_ctx->channels, // 声道数 out_samples, // 实际样本数 AV_SAMPLE_FMT_S16, // 目标格式 0 // 字节对齐 );#endif { QByteArray aData((char*)output,aDataSize); { std::lock_guard lck(ptx->_audioDataMutex); ptx->_audioDataList.push_back(aData); } // 开始播放 if(ptx->_audioDataList.size() >4){//如果音频数据大于4帧,则打开音频播放 SDL_PauseAudioDevice(ptx->audio_dev, 0); } }// logger()->info(\"audio data size:%d\",ptx->_audioDataList.size()); av_freep(&output); }#endif av_packet_unref(&pkt); } logger()->info(\"quit decodeAudioFunc\");}
声音播放流程
通过sdl库来实现对解码后音频数据的播放,需要实现以下几个流程:
1.初始化SDL音频相关实现
int HFFPlayer::initSDLAudio(){ // 初始化SDL音频 SDL_Init(SDL_INIT_AUDIO); SDL_AudioSpec obtained_spec; SDL_AudioSpec desired_spec = { .freq = audio_codec_ctx->sample_rate, .format = AUDIO_S16SYS, .channels = (uint8_t)audio_codec_ctx->channels, .silence = 0, .samples = (Uint16)g_audioBuf, .callback = audio_callback, .userdata = this }; audio_dev = SDL_OpenAudioDevice(NULL, 0, &desired_spec, &obtained_spec, 0); if (!audio_dev) { return -5; } loger()->info(\"SDL_OpenAudioDevice succ obtained_spec.samples:%d\",obtained_spec.samples); // 初始化重采样器 swr_ctx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(obtained_spec.channels), AV_SAMPLE_FMT_S16, obtained_spec.freq, av_get_default_channel_layout(audio_codec_ctx->channels), audio_codec_ctx->sample_fmt, audio_codec_ctx->sample_rate, 0, NULL); loger()->info(\"swr_alloc_set_opts()sample_fmt:%d \",audio_codec_ctx->sample_fmt); if(!swr_ctx || swr_init(swr_ctx)info(\"重采样初始化失败\"); return -1; } return 0;}
2.实现SDL定义的回调函数
static void audio_callback(void* userdata, Uint8* stream, int len) { FFPlayer* player = (FFPlayer*)userdata; AudioBuffer *buffer = &player->audio_buffer; buffer->index = 0; buffer->size = 0; memset(stream, 0, len); int len1; while(!player->quit){#if 0 logger()->info(\"audio_callback len:%d index:%d buffer size:%d _audioDataList size:%d\", len, buffer->index, buffer->size, player->_audioDataList.size());#endif if(len index>=buffer->size){ QByteArray aData; { std::lock_guard lock(player->_audioDataMutex); if(!player->_audioDataList.empty()){ aData = player->_audioDataList.front(); player->_audioDataList.pop_front(); memcpy(buffer->data,aData.data(),aData.length()); buffer->size = aData.length(); buffer->index = 0; } } } len1 = buffer->size - buffer->index; if(len1 > len){ len1 = len; }#if 0 logger()->info(\"audio_callback len1:%d index:%d buffer size:%d\",len1,buffer->index,buffer->size);#endif if (buffer->size > 0) { memcpy(stream, buffer->data + buffer->index, len1); buffer->index += len1; len -= len1; stream += len1; }else{ memset(stream, 0, len); } }}
需要注意的地方
注意一:SDL实现播放音频时有刺耳的噪音,在回调实现中首先要静音处理。
memset(stream, 0, len);//对声卡缓存做静音处理
注意二:SDL实现播放音频时一直有杂音,这是因为声卡中数据一直没有填充满就被下一次的音频数据覆盖导致的。要在回调中使用while()语句,将声卡缓存大小len使用完后,才能再次执行回调。
if (buffer->size > 0) { memcpy(stream, buffer->data + buffer->index, len1); buffer->index += len1; len -= len1; stream += len1; }
注意三:解码后的音频数据大小计算公式
音频数据大小= 输出的采样大小*通道数*采样格式
int aDataSize = 0;#if 0 aDataSize = out_samples * ptx->audio_codec_ctx->channels * av_get_bytes_per_sample(AV_SAMPLE_FMT_S16);#else aDataSize = av_samples_get_buffer_size( NULL, // 行大小(可忽略) ptx->audio_codec_ctx->channels, // 声道数 out_samples, // 实际样本数 AV_SAMPLE_FMT_S16, // 目标格式 0 // 字节对齐 );#endif