SDL基础使用06 (SDL播放pcm文件)

简介: 如何使用SDL库在C和C++中播放PCM音频文件,包括初始化SDL音频、设置音频参数、读取PCM数据、播放音频以及资源释放的完整流程。

SDL播放PCM文件

C语言文件打开方式

// 提取PCM文件
// ffmpeg -i input.mp4 -t 20 -codec:a pcm_s16le -ar 44100 -ac 2 -f s16le suiyue_44100_2_s16le.pcm
// 测试PCM文件
// ffplay -ar 44100 -ac 2 -f s16le suiyue_44100_2_s16le.pcm
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
extern "C"
{
#include <SDL.h>
}

// 每次读取2帧数据, 以1024个采样点一帧 2通道 16bit采样点为例
#define PCM_BUFFER_SIZE (2 * 1024 * 2 * 2)

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)
{
    // udata: 用户自定义数据
    SDL_memset(stream, 0, len);
    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;
    // 设置混音
    SDL_MixAudio(stream, s_audio_pos, len, SDL_MIX_MAXVOLUME / 8);  // 音量的值 SDL_MIX_MAXVOLUME 128
    printf("use %d bytes\n", len);                                    // 4096 bytes  samples * channels * format

    s_audio_pos += len;  // 移动缓存指针(当前pos)
}

#undef main
int main()
{
    int ret = -1;
    FILE *audio_fd = NULL;
    SDL_AudioSpec spec;                         // SDL音频设备
    const char *path = "./suiyue_44100_2_s16le.pcm";

    size_t read_buffer_len = 0;                    // 存储每次读取文件数据的长度

    // 1. SDL初始化AUDIO
    if (SDL_Init(SDL_INIT_AUDIO))    
    {
        fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
        return ret;
    }

    // 2. 打开PCM文件(以二进制方式打开)
    audio_fd = fopen(path, "rb");
    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;              // 采样频率 44.1k
    spec.format = AUDIO_S16SYS;     // 采样点格式 (交错模式16bit)
    spec.channels = 2;              // 2通道
    spec.silence = 0;               // 设置静音的值
    // samples * channels * format = 4096 bytes  (回调每次读取的数据)
    // 每次读取的采样数量,多久调用一次回调和samples有关系(回调间隔 1024 / 44100)23.2ms
    spec.samples = 1024;
    spec.callback = fill_audio_pcm; // 设置回调函数
    spec.userdata = NULL;

    // 3. 打开音频设备
    if (SDL_OpenAudio(&spec, NULL))
    {
        fprintf(stderr, "Failed to open audio device, %s\n", SDL_GetError());
        goto _FAIL;
    }

    // 4. 设置为0时开始播放,为1时播放静音数据
    SDL_PauseAudio(0);

    int data_count = 0;    // 存储读取的总字节数
    while (1)
    {
        // 从文件读取PCM数据到s_audio_buf中(每次读取两帧数据)
        read_buffer_len = fread(s_audio_buf, 1, PCM_BUFFER_SIZE, audio_fd);
        if (read_buffer_len == 0)
        {
            break;
        }
        data_count += read_buffer_len;                             // 统计读取的数据总字节数
        printf("read %10d bytes data\n", data_count);             // 每次加2帧数据 8192 bytes
        s_audio_end = s_audio_buf + read_buffer_len;             // 更新buffer的结束位置(buf位置 + 本次读取的长度)
        s_audio_pos = s_audio_buf;                                 // 更新buffer的起始位置

        while (s_audio_pos < s_audio_end)                // 等待回调函数将数据读取完毕
        {
            // 不能大于上面计算的23.2ms 否则会产生断音
            SDL_Delay(10);  // 等待PCM数据消耗
        }
    }
    printf("play PCM finish\n");

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

_FAIL:
    // 释放资源退出
    if (s_audio_buf)
        free(s_audio_buf);

    if (audio_fd)
        fclose(audio_fd);

    SDL_Quit();

    return 0;
}

C++文件打开方式

    1. 初始化SDL SDL_Init
    1. 创建音频播放回调函数,打开音频设备 SDL_OpenAudio
    1. 开始播放 SDL_PauseAudio
    1. 打开文件循环读取数据到缓冲
    1. 关闭音频设备 SDL_CloseAudio
    1. 释放资源,退出 SDL_Quit
#include <iostream>
#include <fstream>
#include <cmath>

// SDL 播放PCM数据
extern "C"
{
#include <SDL.h>
}
#pragma comment(lib, "SDL2.lib")


Uint8* g_audio_chunk;                // 下面读取两帧数据的缓存
Uint32 g_audio_len;                    // 读取的长度
Uint8* g_audio_pos;                    // 当前声卡消耗到的位置

// 接收音频数据的回调函数
void read_audio_data_cb(void* udata, Uint8* stream, int len)
{
    SDL_memset(stream, 0x00, len);
    if (g_audio_len == 0)
        return;
    len = SDL_min(len, static_cast<int>(g_audio_len));

    // 将音频数据 g_audio_pos 传给 stream
    std::cout << "use " << len << " bytes" << std::endl;
    SDL_MixAudio(stream, g_audio_pos, len, SDL_MIX_MAXVOLUME);    // (设置混音(也可直接拷贝))将声音喂给声卡(最后一个参数音量0-128)
    g_audio_pos += len;
    g_audio_len -= len;
}

#undef main
int main()
{
    // 1. 初始化SDL为audio
    int nRet = SDL_Init(SDL_INIT_AUDIO);
    if (nRet < 0)
    {
        std::cout << "SDL Error: " << SDL_GetError() << std::endl;
        return -1;
    }

    // 定义结构
    SDL_AudioSpec spec;
    spec.freq = 44100;
    spec.channels = 2;
    spec.format = AUDIO_S16SYS;                // s16le
    spec.samples = 1024;                    // 字节数(与回调间隔有关) 1024 / 44100 约 23.2ms (设置延时超过这个数会出现断音现象)
    spec.callback = read_audio_data_cb;        // 回调函数(回调函数缓冲len的长度为 samples * format * channels)
    spec.userdata = NULL;
    // 2. 根据参数打开音频设备
    if (SDL_OpenAudio(&spec, NULL) < 0)
    {
        return -1;
    }

    // 3. 打开文件(需要使用二进制方式打开)
    std::ifstream pcmFile("./suiyue_44100_2_s16le.pcm", std::ios::in | std::ios::binary);
    if (!pcmFile.is_open())
    {
        return -1;
    }

    // 4. 开始播放
    SDL_PauseAudio(0);            // 0 开始播放 1 播放静音数据

    char* buffer = (char*)malloc(2 * 1024 * 2 * 2);        // 每次读两帧数据,以1024个采样点 双声道 位深16 为一帧
    while (!pcmFile.eof())
    {
        // 将两帧音频数据读取到buffer中(8192 bytes)
        pcmFile.read(buffer, 1024 * 2 * 2 * 2);
        if (pcmFile.bad())
        {
            return -1;
        }

        g_audio_chunk = reinterpret_cast<Uint8*>(buffer);    // 真实读取的音频数据
        g_audio_len = pcmFile.gcount();                        // 真实读取的字节数
        g_audio_pos = g_audio_chunk;                        // 将音频位置设置到刚读取到的buffer位置

        std::cout << "read " << g_audio_len << " bytes" << std::endl;
        // 延时等待声卡消耗音频数据
        while (g_audio_len > 0)
        {
            SDL_Delay(10);
        }
    }

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

    // 6. 释放资源、退出
    free(buffer);
    buffer = NULL;
    SDL_Quit();

    return 0;
}
相关文章
|
27天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
4天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
438 17
|
7天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
20天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
7天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
379 2
|
22天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
24天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2600 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
|
6天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
286 2
|
4天前
|
编译器 C#
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
106 65
|
24天前
|
机器学习/深度学习 算法 数据可视化
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码
2024年中国研究生数学建模竞赛C题聚焦磁性元件磁芯损耗建模。题目背景介绍了电能变换技术的发展与应用,强调磁性元件在功率变换器中的重要性。磁芯损耗受多种因素影响,现有模型难以精确预测。题目要求通过数据分析建立高精度磁芯损耗模型。具体任务包括励磁波形分类、修正斯坦麦茨方程、分析影响因素、构建预测模型及优化设计条件。涉及数据预处理、特征提取、机器学习及优化算法等技术。适合电气、材料、计算机等多个专业学生参与。
1582 17
【BetterBench博士】2024年中国研究生数学建模竞赛 C题:数据驱动下磁性元件的磁芯损耗建模 问题分析、数学模型、python 代码