FFmpeg入门:最简单的音频播放器

FFmpeg入门:最简单的音频播放器

欢迎大家来到FFmpeg入门的第二章,今天只做一个最简单的FFmpeg音频播放器;同样,话不多说,先上流程图

流程图

在这里插入图片描述
以上流程和视频播放器的解码过程基本上是一致的;
不同点在于 SDL的渲染方式。下面我会重点说一下这个部分

SDL音频渲染

音频渲染的方式和视频不太一样的,我们对于音频的播放速度其实是根据采样率定义的(音频的采样率==视频的帧率),在初始化的时候SDL播放器就指定了这个参数,因此不需要向视频播放器那样手动去延迟来保持帧率。

如下是SDL音频播放器的初始化。

SDL_AudioSpec wanted_spec;
wanted_spec.freq = out_sample_rate;					// 采样率
wanted_spec.format = AUDIO_S16SYS;					// 采样格式 16bit
wanted_spec.channels = out_channels;				// 通道数
wanted_spec.silence = 0;
wanted_spec.samples = out_nb_samples;				// 单帧处理的采样点
wanted_spec.callback = fill_audio;					// 回调函数
wanted_spec.userdata = pCodecCtx;					// 回调函数的参数

其原理就是SDL音频播放器会不断从其缓冲区取出数据读取播放,因此我们只需要不断向其缓冲区中写入数据即可。(详见代码)

// 设置读取的音频数据
audio_info.audio_len = out_buffer_size;
audio_info.audio_pos = (Uint8 *) out_buffer;

但是有个点注意一下,就是在写入SDL播放器的缓冲区之前,我们要确保之前的数据已经被SDL播放器消化完了,不然会导致音频数据被覆盖,而没有读出来;(详见代码)

// 等待SDL播放完成
while(audio_info.audio_len > 0)
	SDL_Delay(0.5);

源代码

接下来看看源代码吧
tutorial03.h

//
//  tutorial03.h
//  learning
//
//  Created by chenhuaiyi on 2025/2/16.
//

#ifndef tutorial03_h
#define tutorial03_h

/**
 头文件
 */
#include <stdio.h>
// ffmpeg
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
// SDL
#include <SDL.h>
#include <SDL_thread.h>

/**
 宏定义
 */
#define USE_SDL 1

/**
 数据类型定义
 */
typedef struct {
	Uint32  audio_len;
	Uint8  *audio_pos;
} AudioInfo;

/**
 全局变量
 */
extern AudioInfo audio_info;

#endif /* tutorial03_h */

tutorial03.c

/**
//  tutorial03.c
//  learning
//
//  Created by chenhuaiyi on 2025/2/16.
 */

#include "tutorial03.h"

AudioInfo audio_info;

/* udata: 传入的参数
 * stream: SDL音频缓冲区
 * len: SDL音频缓冲区大小
 * 回调函数
 */
void fill_audio(void *udata, Uint8 *stream, int len){
	SDL_memset(stream, 0, len);			// 必须重置,不然全是电音!!!
	if(audio_info.audio_len==0){					// 有音频数据时才调用
		return;
	}
	len = (len>audio_info.audio_len ? audio_info.audio_len : len);	// 最多填充缓冲区大小的数据
	SDL_MixAudio(stream, audio_info.audio_pos, len, SDL_MIX_MAXVOLUME);
	audio_info.audio_pos += len;
	audio_info.audio_len -= len;
}


int main(int argc, char* argv[])
{
	AVFormatContext* pFormatCtx = avformat_alloc_context();
	int				 i, audioStream;
	AVCodecContext*  pCodecCtx = avcodec_alloc_context3(NULL);
	const AVCodec*	 pCodec;
	AVPacket		 packet;
	
	if(argc < 2) {
		fprintf(stderr, "Usage: test <file>\n");
		exit(1);
	}
	
	avformat_network_init();
	
	// 1. 打开视频文件,获取格式上下文
	if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0){
		printf("Couldn't open input stream.\n");
		return -1;
	}
	
	// 2. 对文件探测流信息
	if(avformat_find_stream_info(pFormatCtx, NULL) < 0){
		printf("Couldn't find stream information.\n");
		return -1;
	}
	
	// 打印信息
	av_dump_format(pFormatCtx, 0, argv[1], 0);
	
	// 3. 找到对应的音频流
	audioStream=-1;
	for(i=0; i < pFormatCtx->nb_streams; i++) {
		if(pFormatCtx->streams[i]->codecpar->codec_type==AVMEDIA_TYPE_AUDIO){
			audioStream=i;
			break;
		}
	}
	if(audioStream==-1){
		printf("Didn't find a audio stream.\n");
		return -1;
	}
	
	// 4. 将音频流编码参数写入上下文
	AVCodecParameters* pCodecParam = pFormatCtx->streams[audioStream]->codecpar;
	avcodec_parameters_to_context(pCodecCtx, pCodecParam);
	avcodec_parameters_free(&pCodecParam);
	
	// 5. 查找流的编码器
	pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
	if(pCodec==NULL){
		printf("Codec not found.\n");
		return -1;
	}
	
	// 6. 打开流的编解码器
	if(avcodec_open2(pCodecCtx, pCodec,NULL)<0){
		printf("Could not open codec.\n");
		return -1;
	}
	
	// 输出用到的信息
	AVChannelLayout out_channel_layout = AV_CHANNEL_LAYOUT_STEREO;	// 通道 layout
	int out_nb_samples = pCodecCtx->frame_size;	// 编解码器每个帧需要处理或者输出的采样点的大小 AAC:1024  MP3:1152
	enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;			// 采样格式
	int out_sample_rate = 44100;									// 采样率
	int out_channels = out_channel_layout.nb_channels;				// 通道数
	// 获取需要使用的缓冲区大小 -> 通道数,单通道样本数,位深 1024(单帧处理的采样点)*2(双通道)*2(16bit对应2字节)
	int out_buffer_size = av_samples_get_buffer_size(NULL, out_channels,
													 out_nb_samples, out_sample_fmt, 1);
	// 分配缓冲区空间
	uint8_t* out_buffer = NULL;
	av_samples_alloc(&out_buffer, NULL, out_channels, out_nb_samples, out_sample_fmt, 1);
	
	AVFrame* pFrame = av_frame_alloc();
	
	//	SDL 初始化
#if USE_SDL
	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
		printf( "Could not initialize SDL - %s\n", SDL_GetError());
		return -1;
	}
	
	SDL_AudioSpec wanted_spec;
	wanted_spec.freq = out_sample_rate;					// 采样率
	wanted_spec.format = AUDIO_S16SYS;					// 采样格式 16bit
	wanted_spec.channels = out_channels;				// 通道数
	wanted_spec.silence = 0;
	wanted_spec.samples = out_nb_samples;				// 单帧处理的采样点
	wanted_spec.callback = fill_audio;					// 回调函数
	wanted_spec.userdata = pCodecCtx;					// 回调函数的参数
	
	// 打开音频播放器
	if (SDL_OpenAudio(&wanted_spec, NULL)<0){
		printf("can't open audio.\n");
		return -1;
	}
	
#endif
	
	int ret = 0;
	int index = 0;
	
	// 上下文格式转换
	SwrContext *swr_ctx = NULL;
	swr_alloc_set_opts2(&swr_ctx,
						&out_channel_layout,			// 输出layout
						out_sample_fmt,					// 输出格式
						out_sample_rate,				// 输出采样率
						&pCodecCtx->ch_layout,			// 输入layout
						pCodecCtx->sample_fmt,			// 输入格式
						pCodecCtx->sample_rate,			// 输入采样率
						0, NULL);
	swr_init(swr_ctx);
	
	// 开始播放
	SDL_PauseAudio(0);
	
	AVRational time_base = pFormatCtx->streams[audioStream]->time_base;
	int64_t av_start_time = av_gettime();								// 播放开始时间戳
	
	// 循环1: 从文件中读取packet
	while(av_read_frame(pFormatCtx, &packet)>=0){
		if(packet.stream_index==audioStream){
			// 将packet写入编解码器
			ret = avcodec_send_packet(pCodecCtx, &packet);
			if ( ret < 0 ) {
				printf("send packet error\n");
				return -1;
			}
			
			while (!avcodec_receive_frame(pCodecCtx, pFrame)) {
				// 格式转化
				swr_convert(swr_ctx, &out_buffer, out_buffer_size,
							(const uint8_t **)pFrame->data, pFrame->nb_samples);
				index++;
				printf("第%d帧 | pts:%lld | 帧大小(采样点):%d | 实际播放点%.2fs | 预期播放点%.2fs\n",
					   index,
					   packet.pts,
					   packet.size,
					   (double)(av_gettime() - av_start_time)/AV_TIME_BASE,
					   pFrame->pts * av_q2d(time_base));
				
#if USE_SDL
				// 设置读取的音频数据
				audio_info.audio_len = out_buffer_size;
				audio_info.audio_pos = (Uint8 *) out_buffer;
				// 等待SDL播放完成
				while(audio_info.audio_len > 0)
					SDL_Delay(0.5);
#endif
				
			}
			av_packet_unref(&packet);
		}
	}
	
	// 打印参数
	printf("格式: %s\n", pFormatCtx->iformat->name);
	printf("时长: %lld us\n", pFormatCtx->duration);
	
	printf("音频持续时长为 %.2f,音频帧总数为 %d\n", (double)(av_gettime()-av_start_time)/AV_TIME_BASE, index);
	printf("码率: %lld\n", pFormatCtx->bit_rate);
	printf("编码器: %s (%s)\n", pCodecCtx->codec->long_name, avcodec_get_name(pCodecCtx->codec_id));
	printf("通道数: %d\n", pCodecCtx->ch_layout.nb_channels);
	printf("采样率: %d \n", pCodecCtx->sample_rate);
	printf("单通道每帧的采样点数目: %d\n", pCodecCtx->frame_size);
	printf("pts单位(ms*1000): %.2f\n", av_q2d(pFormatCtx->streams[audioStream]->time_base) * AV_TIME_BASE);
	
	
	// 释放空间
	swr_free(&swr_ctx);
#if USE_SDL
	SDL_CloseAudio();
	SDL_Quit();
#endif
	av_free(out_buffer);
	av_free(pFrame);
	avcodec_free_context(&pCodecCtx);
	avformat_close_input(&pFormatCtx);
	
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/980564.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Python Django的人脸识别上课考勤系统(附源码,部署)

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

【小羊肖恩】小羊杯 Round 2 C+K

题目链接&#xff1a;https://ac.nowcoder.com/acm/contest/100672#question C.是毛毛虫吗&#xff1f; 思路&#xff1a; 其实很简单&#xff0c;假设我们要满足题目所给条件&#xff0c;那么这个毛毛虫最坏情况下肯定是一条如下图所示的无向图 右端省略号为对称图形 &…

PyCharm怎么集成DeepSeek

PyCharm怎么集成DeepSeek 在PyCharm中集成DeepSeek等大语言模型(LLM)可以借助一些插件或通过代码调用API的方式实现,以下为你详细介绍两种方法: 方法一:使用JetBrains AI插件(若支持DeepSeek) JetBrains推出了AI插件来集成大语言模型,不过截至2024年7月,官方插件主要…

安装 Open WebUI

2025.03.01 早上 我已经安装了ollama 和deeseek模型 &#xff08;本地部署流水账之ollama安装Deepseek安装-CSDN博客&#xff09;&#xff0c;然后需要个与模型沟通的工具&#xff08;这么说不知道对不对&#xff09;。 刚开始用的chatbox&#xff0c;安装很方便&#xff0c;…

java后端开发day25--阶段项目(二)

&#xff08;以下内容全部来自上述课程&#xff09; 1.美化界面 private void initImage() {//路径分两种&#xff1a;//1.绝对路径&#xff1a;从盘符开始写的路径 D:\\aaa\\bbb\\ccc.jpg//2.相对路径&#xff1a;从当前项目开始写的路径 aaa\\bbb\\ccc.jpg//添加图片的时…

基于Spring Boot + Vue的常规应急物资管理系统设计与实现

大家好&#xff0c;今天要和大家聊的是一款基于Spring Boot Vue的“常规应急物资管理系统”的设计与实现。项目源码以及部署相关事宜请联系我&#xff0c;文末附上联系方式。 项目简介 基于Spring Boot Vue的“常规应急物资管理系统”设计与实现的主要使用者分为管理员、员工…

SpringCloud + Spring AI Alibaba 整合阿里云百炼大模型

一、前言 记录一次自己使用微服务整合阿里云的百炼大模型&#xff0c;需要用到Redis来记录最近五条信息&#xff0c;已能够保证上下文的连通性&#xff0c;Ai和用户之间的对话是使用的MongoDB来进行存储。然后我这篇文章是介绍了两种请求方式&#xff0c;一种是通过Http请求&a…

(贪心 合并区间)leetcode 56

思路来源&#xff1a;代码随想录--代码随想录_合并区间题解 首先用lambda 按照左界值升序排序 建立答案的二维数组&#xff0c;将第一个行区间放入&#xff0c;判断从第二行开始 第i行的左区间一定大于第i-1行的左区间&#xff08;排序过了&#xff09;&#xff0c;所以只判断…

CAN总线通信协议学习4——数据链路层之仲裁规则

CAN总线只有一对差分信号线&#xff0c;同一时间只能有一个设备操作总线发送数据若多个设备同时有发送需求&#xff0c;该如何分配总线资源? 解决问题的思路&#xff1a;制定资源分配规则&#xff0c;依次满足多个设备的发送需求&#xff0c;确保同一时间只有一个设备操作总线…

Kubespray部署企业级高可用K8S指南

目录 前言1 K8S集群节点准备1.1 主机列表1.2 kubespray节点python3及pip3准备1.2.1. 更新系统1.2.2. 安装依赖1.2.3. 下载Python 3.12源码1.2.4. 解压源码包1.2.5. 编译和安装Python1.2.6. 验证安装1.2.7. 设置Python 3.12为默认版本&#xff08;可选&#xff09;1.2.8. 安装pi…

互推机制在开源AI智能名片2+1链动模式S2B2C商城小程序源码推广中的应用探索

摘要&#xff1a; 在数字化营销时代&#xff0c;开源AI智能名片21链动模式S2B2C商城小程序源码作为一种创新的技术解决方案&#xff0c;正逐步成为企业数字化转型的重要工具。然而&#xff0c;面对激烈的市场竞争&#xff0c;如何高效推广这一前沿技术产品&#xff0c;成为开发…

波导阵列天线 学习笔记11双极化全金属垂直公共馈电平板波导槽阵列天线

摘要&#xff1a; 本communicaition提出了一种双极化全金属垂直公共馈电平板波导槽阵列天线。最初提出了一种公共馈电的单层槽平板波导来实现双极化阵列。此设计消除了传统背腔公共馈电的复杂腔体边缘的必要性&#xff0c;提供了一种更简单的天线结构。在2x2子阵列种发展了宽十…

腾讯游戏完成架构调整 IEG新设五大产品事业部

易采游戏网2月28日独家消息&#xff1a;继1月份腾讯天美工作室群完成内部组织架构调整后&#xff0c;腾讯旗下互动娱乐事业群&#xff08;IEG&#xff09;再次宣布对组织架构进行优化调整。此次调整的核心在于新设立了五大产品事业部&#xff0c;包括体育产品部、音舞产品部、V…

Vue3国际化开发实战:i18n-Ally + vue-i18n@next高效配置教程,项目中文显示

本文详解 Vue3 国际化开发全流程&#xff1a;从安装 vue-i18nnext 到配置多语言文件&#xff08;JSON/YAML&#xff09;&#xff0c;结合 i18n-Ally 插件实现高效翻译管理。重点涵盖&#xff1a; 工程配置&#xff1a;创建 i18n 实例、模块化语言文件结构&#xff08;支持命名…

【愚公系列】《Python网络爬虫从入门到精通》036-DataFrame日期数据处理

标题详情作者简介愚公搬代码头衔华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,CSDN商业化专家,阿里云专家博主,阿里云签约作者,腾讯云优秀博主,腾讯云内容共创官,掘金优秀博主,亚马逊技领云博主,51CTO博客专家等。近期荣誉2022年度…

计算机网络---TCP三握四挥

文章目录 TCPTCP 的核心特点TCP 与 UDP 特性对比TCP 标志位 TCP 的三次握手&#xff08;建立连接&#xff09;TCP 三次握手概述图解 TCP 三次握手为什么需要三次握手&#xff0c;而不是两次为什么要三次握手&#xff0c;而不是四次三次握手连接阶段&#xff0c;最后一次 ACK 包…

ubuntu服务器安装VASP.6.4.3

ubuntu服务器安装VASP.6.4.3 1 安装Intel OneAPI Base Toolkit和Intel OneAPI HPC Toolkit1.1 更新并安装环境变量1.2 下载Intel OneAPI Base Toolkit和Intel OneAPI HPC Toolkit安装包1.3 安装 Intel OneAPI Base Toolkit1.4 安装 Intel OneAPI HPC Toolkit1.5 添加并激活环境…

【大模型系列篇】DeepSeek开源周,解锁AI黑科技

&#x1f525; Day1&#xff1a;FlashMLA —— GPU推理加速器 专为处理长短不一的AI推理请求而生&#xff0c;就像给Hopper GPU装上了智能导航&#xff0c;让数据在芯片上跑出3000GB/s的"磁悬浮"速度。✅ 已支持BF16格式&#xff5c;580万亿次浮点运算/秒FlashMLA G…

【JSON2WEB】15 银河麒麟操作系统下部署JSON2WEB

【JSON2WEB】系列目录 【JSON2WEB】01 WEB管理信息系统架构设计 【JSON2WEB】02 JSON2WEB初步UI设计 【JSON2WEB】03 go的模板包html/template的使用 【JSON2WEB】04 amis低代码前端框架介绍 【JSON2WEB】05 前端开发三件套 HTML CSS JavaScript 速成 【JSON2WEB】06 JSO…

leetcode第40题组合总和Ⅱ

原题出于leetcode第40题https://leetcode.cn/problems/combination-sum-ii/题目如下&#xff1a; 给定一个候选人编号的集合 candidates &#xff08;candidate中有重复的元素&#xff09;和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合…