【FFmpeg】SDL 音视频开发 ⑦ ( SDL 播放 PCM 音频数据 | 提取 PCM 格式数据 | 设置音频参数 | 打开音频设备 | 设置播放回调函数 | 播放音频数据 | 关闭音频设备 )

2024-09-17 11:13:24 浏览数 (3)

博客源码下载 : https://download.csdn.net/download/han1202012/89734548

一、PCM 格式简介

1、PCM 简介

PCM , Pulse Code Modulation , 脉冲编码调制 , 使用数字表示模拟信号 , 广泛应用于音频数字化 ;

  • 模拟信号 转 数字信号 : PCM 将 模拟信号 转换为 数字信号 , 对模拟信号进行 采样、量化 和 编码 生成 PCM 数据 ;
  • 采样 : 在特定 时间间隔 内对模拟信号的幅度进行测量 , 对声音来说就是测量声音的振幅 ;
  • 量化 : 将 测量的幅度值 映射 到 离散的数值 上 ;
  • 编码 : 将 量化后的值 转换为二进制格式 , 以便进行数字处理和存储 ;

声音 是 模拟信号的一种 , 将声音 通过麦克风 录制成 PCM 数据 , 然后将 PCM 数据传递给扬声器 就可以将声音播放出来 ;

PCM 音频数据没有经过压缩 , 是高保真数据 , 没有任何声音损失 , 一旦转为 aac / mp3 格式 , 就会不可逆的损失部分声音信息 , 如 : 高频信号 / 低频信号 / 时域掩盖信息 / 频域掩盖信息 等 都在音频压缩时被删除 ;

参考 【音视频原理】音频编解码原理 ① ( 声音特性 | 声音本质 | 声音频率 | 声音频率和响度本质分析 | 数字音频 |脉冲编码调制 PCM - 采样振幅值 | 奈奎斯特 Nyguist 采样定理 ) 博客 ;

2、PCM 参数

PCM 数据的参数 :

  • 采样率 : 每秒钟对信号进行采样的次数 , 常见的采样率有 44.1 kHz , 48 kHz 等 , 注意区分 采样率 与 声音频率 , 下面有声音频率分析 ;
  • 通道数 : 音频信号的通道数量 , 例如 : 单声道 ( Mono ) 、立体声 ( Stereo ) 或 多声道 ( 5.1 环绕声 ) ;
  • 位深度 : 每个样本的分辨率 , 通常为 16 位、24 位等 , 位深度决定了音频的动态范围和精确度 ;

3、声音频率 ( 注意与采样率区分 )

声音频率分析 : 声音的频率 就是 声音的 振幅 ;

声音的振幅实际上是 正弦 / 余弦 曲线 , 正弦的周期数就是声音的频率 , 比如 : 128 键钢琴中间的中央 C 音符 Do 频率为 声音频率为 262 Hz , 也就是主频率每秒钟震动 262 次 , 每秒钟有 262 个 正弦 / 余弦 曲线 周期 , 参考 【音频处理】音高 与 频率 对照表 ( 音符频率算法 ) ;

通过 傅里叶变换 , 可以从音频采样数据中分析出 声音频率 , 这就是 时域信息 转 频域信息 ;

4、使用 ffmpeg 获取 PCM 格式数据

PCM 数据没有经过压缩 , 占用很多空间 , 1 分钟的音频数据有 11MB 左右 , 如果压缩成 mp4 或 aac 格式 , 能压缩到 1MB 以内 ;

PCM 数据不容易找到 , 该数据没有任何的 文件头 描述信息 , 文件的第一个字节就是 第一个采样的数值数据 , 播放 PCM 数据时必须知道该音频的 采样率 通道数 采样位数 等参数 ;

这里使用 FFmpeg 命令行工具从视频中提取 PCM 数据 , 下面的命令 , 可以将 mp4 格式的视频中提取 pcm 数据 ;

代码语言:javascript复制
ffmpeg -i input.mp4 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
  • -i input.mp4 : 指定输入文件 input2.mp4 , -i 是用于指定输入文件的参数 ;
  • -codec:a pcm_s16le : 指定 音频编解码器 为 pcm_s16le , 这是一种 PCM 音频格式,使用 16 位小端字节序 s16le , 这个编解码器用于将音频数据以未压缩的形式存储 ;
  • -ar 44100 : 设置 音频采样率为 44100 Hz , 采样率 是 每秒钟采集多少个音频样本 ;
  • -ac 2 : 设置音频通道数为 2 , 双声道 立体声 ;
  • -f s16le : 指定输出格式为 s16le , 这是音频的原始 PCM 数据格式 , 其中 s16 代表 16 位有符号整数 , le 代表小端字节序 Little Endian ;

参考 【FFmpeg】ffmpeg 命令行参数 ③ ( ffmpeg 音频参数解析 | 设置音频帧数 | 设置音频码率 | 设置音频采样率 | 设置音频通道数 | 设置音频编解码器 | 设置音频过滤器 ) 博客 ;

5、使用 ffplay 播放 PCM 格式数据

得到输出文件后 , 执行

代码语言:javascript复制
ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm

命令 , 可以播放上述提取的 PCM 音频数据 ;

二、SDL 播放 PCM 流程

SDL 播放 PCM 音频 主要分为以下几个步骤

  • 初始化 SDL - SDL_Init 函数
  • 设置音频参数 - SDL_AudioSpec 结构体
  • 打开音频设备 - SDL_OpenAudio 函数
  • 设置音频回调函数 - SDL_AudioCallback 类型函数
  • 读取 PCM 数据 - fread 函数
  • 播放音频 - SDL_PauseAudio 函数
  • 播放完毕后 关闭音频设备 - SDL_CloseAudio 函数
  • 退出 SDL - SDL_Quit 函数

1、初始化 SDL

初始化 SDL 环境 , 就是调用 SDL_Init 函数 , 该函数用于初始化 SDL 系统上下文环境 , SDL 的任何操作之前都必须执行 初始化 SDL 步骤 ;

SDL_Init 函数原型如下 , 传入的 flags 参数用于设置要使用 SDL 中的哪个子系统 , 本篇博客中设置 SDL_INIT_AUDIO 音频子系统 , 用于 PCM 音频播放 ;

代码语言:javascript复制
int SDL_Init(Uint32 flags);

具体的函数原型参考 【FFmpeg】SDL 音视频开发 ① ( SDL 窗口绘制 | SDL 视频显示函数 | SDL_Window 窗口 | SDL_Renderer 渲染器 | SDL_Texture 纹理 ) 博客章节中第一章内容 ;

2、设置音频参数

在 SDL 中 , 使用 SDL_AudioSpec 结构体来设置音频参数 , 该结构体种包含了音频的多个关键属性 , 创建一个 SDL_AudioSpec 结构体 , 设置该结构体的各个成员参数 ;

  • 采样频率 ( freg ) : 整数 , 表示音频数据的采样频率 , 常见的采样率有44100Hz、48000Hz等 , 这决定了音频的播放质量 , 采样率越大质量越高 ;
  • 音频数据格式 ( format ) : SDL_AudioFormat 枚举类型 , 表示每个样本的格式 ;
    • AUDIO_S16SYS 表示 有符号 16 位 整数样本 ;
    • AUDIO_S8 表示 有符号 8 位 整数样本 ;
    • AUDIO_F32SYS 表示 32 位 浮点数 样本 ;
  • 声道数 ( channels ) : 1 表示单声道 , 2 表示立体声 ;
  • 静音值 ( silence ) : 无符号 8 位整数 , 表示音频数据中每个样本的静音字节值 ;
  • 音频缓冲区的总字节数 ( size ) : 无符号 32 位整数 , 这个值通常需要是 2 的幂次方 , 该参数 决定了音频回调函数的调用频率和每次需要处理的数据量 ;
    • 计算公式 : samples * channels * (SDL_AUDIO_BITSIZE(format) / 8) ;
  • 用户自定义数据指针 ( userdata ) : 指向开发者定义的数据的指针 , SDL 本身不会使用这个指针 , 开发者可以用它来存储与音频数据相关的自定义信息 ;

SDL_AudioSpec 结构体原型如下 :

代码语言:javascript复制
/*
	SDL_AudioSpec 结构体 由 SDL_OpenAudio() 函数计算得出 
	对于多声道音频,默认的 SDL 声道映射为:
	2: 左前(FL)右前(FR) (立体声)
	3: 左前(FL)右前(FR)低频增强(LFE) (2.1 环绕声)
	4: 左前(FL)右前(FR)左后(BL)右后(BR)(四声道)
	5: 左前(FL)右前(FR)中置(FC)左后(BL)右后(BR)(四声道   中置)
	6: 左前(FL)右前(FR)中置(FC)低频增强(LFE)左环绕(SL)右环绕(SR)(5.1 环绕声 - 最后两个也可以是左后 BL 和右后 BR)
	7: 左前(FL)右前(FR)中置(FC)低频增强(LFE)后置中置(BC)左环绕(SL)右环绕(SR)(6.1 环绕声)
	8: 左前(FL)右前(FR)中置(FC)低频增强(LFE)左后(BL)右后(BR)左环绕(SL)右环绕(SR)(7.1 环绕声)
*/
typedef struct SDL_AudioSpec {  
    int freq;                 // 采样频率(Sample Rate)  
    SDL_AudioFormat format;   // 音频数据格式  
    Uint8 channels;           // 声道数(1 = 单声道, 2 = 立体声, etc.)  
    Uint8 silence;            // 静音值(每个样本的静音字节值)  
    Uint16 samples;           // 音频缓冲区中的样本数  
    Uint16 padding;           // 必要的填充值,以保证结构体大小为偶数(用于某些平台的对齐)  
    Uint32 size;              // 音频缓冲区的总字节数(= samples * channels * (SDL_AUDIO_BITSIZE(format) / 8))  
    void *userdata;           // 用户自定义数据指针(可由开发者自由使用)  
    Uint8 *buffer;            // 指向实际音频数据的指针  
    unsigned int length;      // 音频缓冲区的长度(以字节为单位)(在 SDL 2.0.9 中已弃用,建议使用 size 字段)  
} SDL_AudioSpec;

SDL_AudioSpec 结构体设置示例 :

代码语言:javascript复制
#include <SDL2/SDL.h>  
  
int main() {  
    SDL_AudioSpec spec;  
  
    // 设置采样频率为 44100 Hz  
    spec.freq = 44100;  
  
    // 设置音频格式为 16-bit 签名整数,系统字节序  
    spec.format = AUDIO_S16SYS;  
  
    // 设置为立体声(2 个声道)  
    spec.channels = 2;  
  
    // 设置静音值为 0(对于 16-bit 签名整数,通常使用 0)  
    spec.silence = 0;  
  
    // 设置每个缓冲区的样本数为 1024  
    spec.samples = 1024;  
  
    // 计算音频缓冲区的总字节数  
    spec.size = spec.samples * spec.channels * (SDL_AUDIO_BITSIZE(spec.format) / 8);  
  
    // 用户数据指针设为 NULL(无自定义数据)  
    spec.userdata = NULL;  
  
    // 分配音频缓冲区(需要手动分配内存)  
    spec.buffer = (Uint8 *)SDL_malloc(spec.size);  
  
    // 确保内存分配成功  
    if (spec.buffer == NULL) {  
        SDL_LogError(SDL_LOG_CATEGORY_AUDIO, "Failed to allocate audio buffer");  
        return -1;  
    }  
  
    // 初始化缓冲区为静音(可选)  
    SDL_memset(spec.buffer, spec.silence, spec.size);  
  
    // ... 使用 spec 进行音频播放或捕捉 ...  
  
    // 释放分配的缓冲区  
    SDL_free(spec.buffer);  
  
    return 0;  
}

3、打开音频设备

SDL_OpenAudio 函数 用于 设置音频参数 并 打开音频设备 , 为后续的音频播放做准备 ;

SDL_OpenAudio 函数原型如下 :

代码语言:javascript复制
int SDL_OpenAudio(SDL_AudioSpec *desired, SDL_AudioSpec *obtained);
  • SDL_AudioSpec *desired 参数 : 设置用户期望的音频配置 ;
  • SDL_AudioSpec *obtained 参数 : 实际的音频设备的参数 , 在本篇博客中暂时设置为 NULL ;

4、设置播放回调函数

SDL_AudioCallback 是 SDL ( Simple DirectMedia Layer ) 库中的 PCM 音频播放 回调函数类型 , 当 SDL 播放完当前音频缓冲区中的数据后 , 会自动回调该函数 , 为音频设备提供后续音频播放数据 , 该函数的主要作用如下 :

  • 提供音频数据 : 每当音频设备需要更多的数据时 , SDL 会调用这个回调函数 , 向 stream 参数 指向的音频数据缓冲区 填充音频数据 ;
  • 处理音频数据 : 在回调函数中 , 可以根据应用程序的需要生成或处理音频数据 , 例如 : 从文件中读取数据、合成音频、或应用音效等 ;

几乎所有的 PCM 音频播放都需要提供一个回调函数 , OpenSL / AAudio 也有一个类似的回调函数 ;

SDL_AudioCallback 函数原型 :

代码语言:javascript复制
/**
 *  当音频设备需要更多数据时,将调用此函数。
 *
 *  param userdata  保存在 SDL_AudioSpec 结构中的应用程序特定参数
 *  param stream    指向音频数据缓冲区的指针
 *  param len       缓冲区的长度(以字节为单位)
 *
 *  一旦回调函数返回,缓冲区将不再有效。
 *  立体声音频样本以 LRLRLR 的顺序存储。
 *
 *  如果愿意,您可以选择避免使用回调函数,改用 SDL_QueueAudio()。
 *  只需使用 NULL 回调打开您的音频设备即可。
 */
typedef void (SDLCALL * SDL_AudioCallback) (void *userdata, Uint8 * stream,
                                            int len);
  • void *userdata 参数 : 指向用户自定义数据的指针 , 在回调函数中 可借助该指针 访问或存储额外的信息 ;
  • Uint8 * stream 参数 : 指向音频数据缓冲区的指针 , 将音频数据写入这个缓冲区 , 就可以被音频设备播放 ;
  • int len 参数 : 缓冲区的字节长度 , 使用时需要确保向缓冲区写入的数据长度不超过这个值 ;

在本示例中 , 实现的 SDL_AudioCallback 回调函数 如下 :

代码语言:javascript复制
// 一帧 PCM 数据有 1024 个采样点
// 每个采样 都是 2 通道 立体声 ( 左右声道 ) , 每个通道的采样都是 16 位 (bit) 也就是 2 字节 (Byte)
// 每次读取 2 帧 PCM 数据
// 1024 ( 采样数 ) * 2 ( 通道数 ) * 2 ( 2 字节 / 16 位 ) * 2 ( 帧数为 2 帧 )
#define PCM_BUFFER_SIZE (1024 * 2 * 2 * 2)

// 音频PCM数据缓存指针
static Uint8 *s_audio_buf = NULL;
// 当前读取的位置
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置
static Uint8 *s_audio_end = NULL;

// 音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
    SDL_memset(stream, 0, len); // 将流缓冲区初始化为0

    if(s_audio_pos >= s_audio_end) // 如果数据已读取完毕
    {
        return; // 退出回调函数
    }

    // 数据够了就读预设长度,数据不够时只读取剩余数据
    int remain_buffer_len = s_audio_end - s_audio_pos;
    len = (len < remain_buffer_len) ? len : remain_buffer_len;
    // 将数据拷贝到stream并调整音量
    SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
    printf("len = %dn", len); // 输出当前读取的数据长度
    s_audio_pos  = len;  // 移动缓存指针到下一个位置
}

5、播放音频数据

调用 SDL_PauseAudio 函数 可以 恢复 / 暂停 播放音频数据 ;

SDL_PauseAudio 函数原型如下 :

代码语言:javascript复制
void SDL_PauseAudio(int pause_on);

int pause_on 参数 是一个整数值 , 决定了音频设备的状态 ;

  • 0 : 恢复音频播放 , 如果音频设备之前是暂停的 , 调用此函数将会恢复音频播放 ;
  • 1 : 暂停音频播放 , 如果音频设备正在播放音频 , 调用此函数将会暂停音频播放 ;

部分代码示例 :

代码语言:javascript复制
#include <SDL2/SDL.h>

int main() {
    // 初始化 SDL
    if (SDL_Init(SDL_INIT_AUDIO) < 0) {
        // 错误处理
        return -1;
    }

    // 打开音频设备、设置音频回调等(略)

    // 开始播放音频
    SDL_PauseAudio(0);  // 传递 0 表示恢复音频播放

    // 在适当的时候暂停音频
    SDL_PauseAudio(1);  // 传递 1 表示暂停音频播放

    // 结束音频播放、清理资源等(略)

    // 清理 SDL
    SDL_Quit();

    return 0;
}

6、关闭音频设备

播放完毕后 调用 SDL_CloseAudio 函数 , 关闭音频设备 , 释放 PCM 播放时申请的系统资源 ;

SDL_CloseAudio 函数原型如下 , 该函数用于关闭音频设备 ;

代码语言:javascript复制
void SDL_CloseAudio(void);

部分代码示例 :

代码语言:javascript复制
#include <SDL/SDL.h>

int main() {
    // 初始化 SDL
    if (SDL_Init(SDL_INIT_AUDIO) < 0) {
        // 错误处理
        return -1;
    }

    // 设置音频参数和打开音频设备(略)

    // 关闭音频设备
    SDL_CloseAudio();

    // 清理 SDL
    SDL_Quit();

    return 0;
}

7、SDL 播放 PCM 音频数据的 关键步骤 代码示例

代码示例 :

代码语言:javascript复制
#include <SDL2/SDL.h>  
#include <stdio.h>  
  
// 音频回调函数  
void audio_callback(void *userdata, Uint8 *stream, int len) {  
    // 这里填充音频数据到 stream 中  
    // len 是需要填充的字节数  
    SDL_memset(stream, 0, len); // 简单地将缓冲区静音  
}  
  
int main(int argc, char *argv[]) {  
    // 初始化 SDL 音频子系统  
    if (SDL_Init(SDL_INIT_AUDIO) < 0) {  
        printf("SDL 无法初始化! SDL_Error: %sn", SDL_GetError());  
        return -1;  
    }  
  
    // 配置音频设备参数  
    SDL_AudioSpec desired;  
    desired.freq = 44100;           // 采样频率 44.1kHz  
    desired.format = SDL_AUDIO_S16SYS; // 音频格式:16 位系统字节顺序  
    desired.channels = 2;          // 立体声  
    desired.samples = 4096;        // 每次回调的样本数  
    desired.callback = audio_callback; // 音频回调函数  
    desired.userdata = NULL;       // 用户数据(这里没有使用)  
  
    SDL_AudioSpec obtained;  
    // 打开音频设备  
    if (SDL_OpenAudio(&desired, &obtained) < 0) {  
        printf("无法打开音频设备! SDL_Error: %sn", SDL_GetError());  
        SDL_Quit();  
        return -1;  
    }  
  
    // 在这里,你可以开始播放音频了  
    // 例如,你可以调用 SDL_PauseAudio(0) 来开始播放  

    // 开始播放音频  
    SDL_PauseAudio(0);  
    
    // 注意:在实际应用中,你需要一个循环或某种方式来持续调用回调函数  
    // 这里只是为了示例而简化了代码  
  
    // 当你完成音频播放后,记得关闭音频设备  
    SDL_CloseAudio();  
    SDL_Quit();  
  
    return 0;  
}

三、完整代码示例


1、完整代码示例

代码语言:javascript复制
// 导入标准 IO 库
#include <stdio.h>
// 导入 SDL 库的头文件
#include <SDL.h>

// 一帧 PCM 数据有 1024 个采样点
// 每个采样 都是 2 通道 立体声 ( 左右声道 ) , 每个通道的采样都是 16 位 (bit) 也就是 2 字节 (Byte)
// 每次读取 2 帧 PCM 数据
// 1024 ( 采样数 ) * 2 ( 通道数 ) * 2 ( 2 字节 / 16 位 ) * 2 ( 帧数为 2 帧 )
// 每次从 本地 PCM 数据文件中读取 1024 * 2 * 2 * 2 字节的 音频 数据
#define PCM_BUFFER_SIZE (1024 * 2 * 2 * 2)

// 音频 PCM 数据缓存指针 , 该指针指向的堆内存中包含了完整的 PCM 文件数据
static Uint8 *s_audio_buf = NULL;
// 当前读取的位置 , 开始播放时指向 s_audio_buf 指针指向数据的首地址
static Uint8 *s_audio_pos = NULL;
// 缓存结束位置 , 指向 s_audio_buf 指针指向数据的 尾地址 , 防止数据越界出现 未知错误
static Uint8 *s_audio_end = NULL;

// 音频设备回调函数
void fill_audio_pcm(void *udata, Uint8 *stream, int len)
{
    // 清空缓冲区 , 将流缓冲区初始化为 0 , 防止有干扰数据
    SDL_memset(stream, 0, len);

    // 确保读取数据时不会出现越界 , 读取到其它未知数据
    if(s_audio_pos >= s_audio_end) // 如果数据已读取完毕
    {
        return; // 退出回调函数
    }

    // 计算剩余数据 : 数据够了就读预设长度 , 数据不够时只读取剩余数据
    // 之前读取的数据都是 len 字节
    // 最后的部分不足 len 字节时 , 读取 remain_buffer_len 字节数据
    int remain_buffer_len = s_audio_end - s_audio_pos;
    len = (len < remain_buffer_len) ? len : remain_buffer_len;

    // 将数据拷贝到stream并调整音量
    SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME/8);
    printf("len = %dn", len); // 输出当前读取的数据长度
    s_audio_pos  = len;  // 移动缓存指针到下一个位置
}

// 使用 ffmpeg 命令 提取 PCM 数据 :
// ffmpeg -i input.mp4 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
// 使用 ffplay 命令 播放 PCM 数据 , 播放 PCM 数据必须指定 采样率 / 通道数 / 采样位数
// ffplay -ar 44100 -ac 2 -f s16le 44100_16bit_2ch.pcm
#undef main
int main(int argc, char *argv[])
{
    int ret = -1; // 返回值初始化为-1
    FILE *audio_fd = NULL; // 文件指针初始化为空
    SDL_AudioSpec spec; // SDL音频规格
    const char *path = "44100_16bit_2ch.pcm"; // PCM文件路径
    // 每次缓存的长度
    size_t read_buffer_len = 0;

    // 初始化SDL音频
    if(SDL_Init(SDL_INIT_AUDIO))    // 初始化SDL音频支持
    {
        fprintf(stderr, "Could not initialize SDL - %sn", SDL_GetError()); // 输出错误信息
        return ret; // 返回错误代码
    }

    // 打开PCM文件
    audio_fd = fopen(path, "rb"); // 以只读模式打开PCM文件
    if(!audio_fd)
    {
        fprintf(stderr, "Failed to open pcm file!n"); // 打开文件失败
        goto _FAIL; // 跳转到失败处理
    }

    s_audio_buf = (uint8_t *)malloc(PCM_BUFFER_SIZE); // 为音频缓冲区分配内存

    // 设置音频参数SDL_AudioSpec
    spec.freq = 44100;          // 采样频率为44100Hz
    spec.format = AUDIO_S16SYS; // 采样点格式为16位系统格式
    spec.channels = 2;          // 2通道
    spec.silence = 0;           // 静音值为0
    spec.samples = 1024;        // 每次读取1024个采样点
    spec.callback = fill_audio_pcm; // 设置音频回调函数
    spec.userdata = NULL;       // 用户数据为空

    // 打开音频设备
    if(SDL_OpenAudio(&spec, NULL))
    {
        fprintf(stderr, "Failed to open audio device, %sn", SDL_GetError()); // 打开音频设备失败
        goto _FAIL; // 跳转到失败处理
    }

    // 开始播放音频
    SDL_PauseAudio(0); // 取消音频暂停状态

    int data_count = 0; // 数据计数器初始化为0
    while(1)
    {
        // 从文件读取PCM数据
        read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd); // 读取PCM数据到缓存
        if(read_buffer_len == 0)
        {
            break; // 如果没有更多数据,则退出循环
        }
        data_count  = read_buffer_len; // 累加读取的数据总字节数
        printf("now playing d bytes data.n",data_count); // 输出当前播放的数据字节数
        s_audio_end = s_audio_buf   read_buffer_len;    // 更新缓存的结束位置
        s_audio_pos = s_audio_buf;  // 更新缓存的起始位置
        // 主线程等待PCM数据被消耗
        while(s_audio_pos < s_audio_end)
        {
            SDL_Delay(10);  // 等待10毫秒
        }
    }
    printf("play PCM finishn"); // 播放完成提示
    // 关闭音频设备
    SDL_CloseAudio(); // 关闭音频设备

_FAIL:
    // 释放资源
    if(s_audio_buf)
        free(s_audio_buf); // 释放音频缓存内存

    if(audio_fd)
        fclose(audio_fd); // 关闭文件

    // 退出SDL
    SDL_Quit(); // 退出SDL库

    return 0; // 返回成功代码
}

2、执行结果

由于播放的是音频 , 播放时没有窗口界面 ;

从视频中提取的 的 PCM 音频数据 , 拷贝到了 编译输出的可执行文件的根目录中 ;

0 人点赞