ffplay播放器用SDL库来播放声音的,我们来看看SDL库打开音频设备函数接口

1. 初始化SDL库

int  SDL_Init(Uint32 flags);

flags: SDL_INIT_TIMER支持定时器子系统,SDL_INIT_AUDIO支持音频子系统,SDL_INIT_VIDEO支持视频子系统

SDL_AudioSpec是音频输出格式的结构体:

typedef struct SDL_AudioSpec
{
    int freq;                   //采样率(每秒采样数量)
    SDL_AudioFormat format;     //音频数据格式 ,AUDIO_U16SYS,AUDIO_S16SYS,AUDIO_S32SYS,AUDIO_F32SYS
    Uint8 channels;             //通道数量:1单声道,2立体声 
    Uint8 silence;              //默认填0
    Uint16 samples;             ///以采样帧为单位的音频缓冲区大小(总采样数除以通道数)
    Uint16 padding;             ///默认填0
    Uint32 size;                ///音频缓冲区大小,默认填0,用SDL_OpenAudioDevice创建成功后才知道具体大小
    SDL_AudioCallback callback; //当音频设备需要更多数据时调用的回调函数; 
    void *userdata;             //SDL_AudioCallback回调函数运行的参数;
} SDL_AudioSpec;

2. 打开一个特定的音频设备

 SDL_AudioDeviceID  SDL_OpenAudioDevice(const char *device,int iscapture
                                       ,const SDL_AudioSpec *desired
                                        ,SDL_AudioSpec *obtained,
										int allowed_changes);

	device[in]		默认填null							
    iscapture[in]  默认填0                                           
    desired[in]    输入期望参数                                      
    obtained[out]   实际输出参数                                           
   allowed_changes  0或组合值,
如SDL_AUDIO_ALLOW_FORMAT_CHANGE 0x00000002可以动态修改音频数据格式
   
 返回值:一个有效的设备ID,大于0时成功 否则失败;

3.关闭音频设备:


  使用关闭音频处理和关闭音频设备。
  void SDLCALL SDL_CloseAudioDevice(SDL_AudioDeviceID dev);

4.播放与暂停音频设备:

使用此函数暂停和播放的音频播放设备
void SDL_PauseAudioDevice(
        SDL_AudioDeviceID dev //有效音频设备 可用SDL_OpenAudioDevice()函数取得
        ,int pause_on //非0时为暂停,0时为播放
						 );

 用SDL库来播放音频的流程比较简单:

  Uint32 flags = SDL_INIT_AUDIO
  SDL_Init(flags); //1.初始化
  
  SDL_AudioSpec inParam;
  SDL_AudioSpec outParam;

  //2. 设置音频输出格式
  inParam.freq = 48000;
  inParam.format = AUDIO_S16SYS;
  inParam.channels = 2;
  
  inParam.callback = [](void *opaque, Uint8 *stream, int len)-> void
					 {
						//3. 设置SDL回调函数,让 SDL 来主动执行回调函数来取数据
					 }

 //4. 打开一个的音频设备。outParam音频设备信息			 
 SDL_AudioDeviceID devid= SDL_OpenAudioDevice(null,0,&inParam, &outParam,0);

 //5.播放音频,SDL回调函数生效
 SDL_PauseAudioDevice(devid,0); 
 .....
 SDL_CloseAudioDevice()//6.关闭

接下来我们看看ffpaly是如何使用SDL库来处理音频部份:

1. 设置播放器音量大小(代码有注释):

 static VideoState *stream_open(const char *filename, const AVInputFormat *iformat)
{
     //SDL库音量范围[0~SDL_MIX_MAXVOLUME(128)],播放器音量范围[0~%100], SDL音量大小 = SDL_MIX_MAXVOLUME*百分比
	// SDL音量大小 = SDL_MIX_MAXVOLUME*(startup_volume%100) 
	startup_volume = av_clip(startup_volume, 0, 100);
	startup_volume = av_clip(SDL_MIX_MAXVOLUME * startup_volume / 100, 0, SDL_MIX_MAXVOLUME);
	is->audio_volume = startup_volume;
	is->muted = 0;// 是否是静音
}  

 实现播放器音量控制,播放器音量100%则用memcpy(),否则用SDL_MixAudioFormat()函数:

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{
		//1. 如果 is->muted == 1, stream内容为空 
		
		if (!is->muted && is->audio_buf && is->audio_volume == SDL_MIX_MAXVOLUME)
		{ 
			// 播放器音量为100% 直接 memcpy
			memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
		}
		else
		{
			memset(stream, 0, len1);
			if (!is->muted && is->audio_buf)
			{
				// 播放器音量小于 100% ,用 SDL_MixAudioFormat()函数 
				SDL_MixAudioFormat(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, AUDIO_S16SYS, len1, is->audio_volume);
			}
		}
}

 2. 设置音频格式

//设置音频格式
static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{

	//wanted_spec 想要的音频格式, spec实际打开的音频格式
	SDL_AudioSpec wanted_spec, spec;
	
	wanted_spec.format = AUDIO_S16SYS;
	wanted_spec.channels = wanted_nb_channels;
	wanted_spec.freq = wanted_sample_rate;
	wanted_spec.silence = 0;
	wanted_spec.samples = FFMAX(SDL_AUDIO_MIN_BUFFER_SIZE, 2 << av_log2(wanted_spec.freq / SDL_AUDIO_MAX_CALLBACKS_PER_SEC));
	wanted_spec.callback = sdl_audio_callback; // 回调函数
	wanted_spec.userdata = opaque;//回调函数参数;
}

  注意1:ffpaly固定了只用AUDIO_S16SYS 格式。
  注意2:samples 设置比较难以理解。比如采样率为48000音频设备每秒播放 48000 个样本,如果回调函数一次只能取4800个样本,那么一秒内就需要调 10 次回调函数。因为音频采样率是由音频文件决定的,是动态的,所以ffpaly固定了回调函数次数,也就是一秒内回调30次,计算出每次取样本数量。  2 << av_log2() (等价于pow(2,n))目的样本数据值最终为2ⁿ。

3. 打开音频设备:

static int audio_open(void *opaque, AVChannelLayout *wanted_channel_layout, int wanted_sample_rate, struct AudioParams *audio_hw_params)
{

	static const int next_nb_channels[] = { 0, 0, 1, 6, 2, 6, 4, 6 };          // 通道数量组数
	static const int next_sample_rates[] = { 0, 44100, 48000, 96000, 192000 }; //采样率组数
	int next_sample_rate_idx = FF_ARRAY_ELEMS(next_sample_rates) - 1;
	
	while (next_sample_rate_idx && next_sample_rates[next_sample_rate_idx] >= wanted_spec.freq)
	{
		/// 找到音频采样率在next_sample_rates组数的下标,为打开音频设备失败时降低采样率
		next_sample_rate_idx--;
	}
	
	//尝试打开音频设备,失败时先调整通道数量,再去尝试,直到通道数量为0,还是失败时,再降低采样率
	while (!(audio_dev = SDL_OpenAudioDevice(NULL, 0, &wanted_spec, &spec, SDL_AUDIO_ALLOW_FREQUENCY_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE)))
	{
		wanted_spec.channels = next_nb_channels[FFMIN(7, wanted_spec.channels)];
		if (!wanted_spec.channels)
		{
			wanted_spec.freq = next_sample_rates[next_sample_rate_idx--];
			wanted_spec.channels = wanted_nb_channels;
			if (!wanted_spec.freq)
			{
				return -1;
			}
		}
	}

   return spec.size;
		
}

注意:next_nb_channels通道数量组数,当打开音频设备失败,调整通道数量。
      next_nb_channels[7] = 6,从7声道切换到6声道打开音频设备
      next_nb_channels[6] = 4,从6声道切换到4声道打开音频设备
      next_nb_channels[5] = 6,从5声道切换到6声道打开音频设备
      next_nb_channels[4] = 2,从4声道切换到2声道打开音频设备
      next_nb_channels[3] = 6,从3声道切换到6声道打开音频设备
      next_nb_channels[2] = 1,从双声道切换到单声道打开音频设备
      next_nb_channels[1] = 0,无法再切换,需要降低采样率打开音频设备
      next_nb_channels[0] = 0,无法再切换,需要降低采样率打开音频设备
      next_sample_rates[0] = 0,降低采样率到0了,不用打开音频设备了。

打开音频设备成功时spec附带音频设备更多信息。

 4.填充音频数据 ,让 SDL 来主动执行回调函数来取数据

static void sdl_audio_callback(void *opaque, Uint8 *stream, int len)
{

    
   /// SDL_PauseAudioDevice(devid,0); 
    //播放音频,SDL回调函数生效
    //填充数据 
    .......
}

到这里就结束啦,感谢大家的支持,如要问题欢迎提问指正。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐