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;
}
相关文章
|
11月前
|
XML 编译器 API
|
12月前
|
计算机视觉
(15)Qt绘图(two)
Qt框架中QPainter类的多种绘图功能,包括坐标变换、基本图形绘制、文本和图片绘制、图像保存以及碰撞检测等。
144 1
(15)Qt绘图(two)
|
12月前
SDL事件处理以及线程使用(2)
SDL库中事件处理和多线程编程的基本概念和示例代码,包括如何使用SDL事件循环来处理键盘和鼠标事件,以及如何创建和管理线程、互斥锁和条件变量。
153 1
SDL事件处理以及线程使用(2)
|
11月前
|
数据采集 Cloud Native Java
10 倍性能提升, GraalVM 应用可观测实践
本文介绍了 GraalVM 静态编译技术在云原生环境下的应用:ARMS 发布了支持 GraalVM 应用的 Java Agent 探针,可为 GraalVM 应用提供开箱即用的可观测能力。同时,文章还提供了使用 ARMS 对 GraalVM 应用进行可观测的详细步骤。
1026 157
|
12月前
|
JSON 数据处理 Go
一文教会你如何使用 iLogtail SPL 处理日志
iLogtail 作为日志、时序数据采集器,在 2.0 版本中,全面支持了 SPL 。本文对处理插件进行了梳理,介绍了如何编写 SPL 语句,从插件处理模式迁移到 2.0 版本的 SPL 处理模式,帮助用户实现更加灵活的端上数据处理。
775 133
|
12月前
|
人工智能 Java API
阿里云开源 AI 应用开发框架:Spring AI Alibaba
近期,阿里云重磅发布了首款面向 Java 开发者的开源 AI 应用开发框架:Spring AI Alibaba(项目 Github 仓库地址:alibaba/spring-ai-alibaba),Spring AI Alibaba 项目基于 Spring AI 构建,是阿里云通义系列模型及服务在 Java AI 应用开发领域的最佳实践,提供高层次的 AI API 抽象与云原生基础设施集成方案,帮助开发者快速构建 AI 应用。本文将详细介绍 Spring AI Alibaba 的核心特性,并通过「智能机票助手」的示例直观的展示 Spring AI Alibaba 开发 AI 应用的便利性。示例源
8116 133
|
11月前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
12065 17
|
11月前
|
消息中间件 监控 数据可视化
ROS Terraform 托管服务与原生 Terraform 对比:选择最适合你的 IaC 工具
本文详细介绍了阿里云资源编排服务(ROS)提供的Terraform托管服务,对比了ROS与Terraform的原生能力,帮助用户根据需求选择合适的IaC工具。
818 54
|
11月前
|
人工智能 前端开发 API
一种基于通义千问prompt辅助+Qwen2.5-coder-32b+Bolt.new+v0+Cursor的无代码对话网站构建方法
本文介绍了当前大模型应用的趋势,从单纯追求参数量转向注重实际应用效果与效率,重点探讨了结合大模型的开发工具,如Bolt.new、v0、Cursor等,如何形成完整的AI工具链,助力开发者高效构建、优化和部署应用。通过实例演示了从项目创建、前端优化到后端代码改写的全过程,强调了提示词设计的重要性,并推荐了适用于不同场景的工具组合方案。
|
11月前
|
缓存 监控 Linux
Python 实时获取Linux服务器信息
Python 实时获取Linux服务器信息