当前位置: 首页 > news >正文

【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下)

公众号回复:OpenGL,领取学习资源大礼包

音视频学习入门技术文章连载:

  • 技术开发故事会连载

  • 【音视频连载-001】基础学习篇-SDL 介绍以及工程配置

  • 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色

  • 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应

  • 【音视频连载-004】基础学习篇-SDL 加载图片并显示

  • 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示

  • 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件

  • 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上)

接上篇 SDL 播放 PCM 音频文件,已经实现了 的模式去播放,接下来看看 的模式如何实现。

PCM 文件素材准备

前面的文章中已经准备好了相关素材,这里就不重复了,还是用同样的 PCM 文件作为这次实验素材。

代码实践

首先还是要通过 SDL_OpenAudioDevice 方法打开一个音频设备。

    SDL_AudioSpec audioSpec;
    audioSpec.freq = 44100;
    audioSpec.format = AUDIO_S16SYS;
    audioSpec.channels = 2;
    audioSpec.silence = 0;
    audioSpec.samples = 1024;
    // 拉的模式,这里要传一个函数
    audioSpec.callback = fill_audio;

    SDL_AudioDeviceID deviceId;
    if ((deviceId = SDL_OpenAudioDevice(nullptr, 0, &audioSpec, nullptr, SDL_AUDIO_ALLOW_ANY_CHANGE)) < 2) {
        cout << "open audio device failed " << endl;
        return -1;
    }

不同的是,这里 callback 参数不能是 nullptr 了,要传一个函数指针。这个函数在 模式下会不断回调,从而将音频数据填充给设备缓冲区。

函数声明如下:

typedef void (SDLCALL * SDL_AudioCallback) (
    // 传用户自定义的数据
    void *userdata,
    // 指向要填充给设备缓冲区的音频数据Buffer的指针
    Uint8 * stream,
    // 音频数据Buffer的长度
    int len);

参数 stream 是个指针类型,它指向要填充给设备缓冲区的音频数据 Buffer ,而 len 就是 Buffer 的长度。userdata 是我们自定义的数据,需要的时候可以用到。

在这个函数中我们要做的就是将读取的 PCM 音频数据传给 stream 指向的 Buffer ,而且还不能超出 len 的长度,如果超出了截断一下,下次回调时传剩下的部分。

因此就有了如下的实现:

// 读取出 pcm 数据长度
static Uint32 audio_len;
// 读取出的音频数据 Buffer
static Uint8 *audio_pos;

// 函数实现
void fill_audio(void *udata, Uint8 *stream, int len) {
    SDL_memset(stream, 0, len);
    if (audio_len == 0) {
        return;
    }
    // 数据大小不能超过 len
    len = len > audio_len ? audio_len : len;
    
    // 将 stream 和 audio_pos 进行混合播放
//    SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);

    // 单独播放 audio_pos,也就是解码出来的音频数据
    memcpy(stream, audio_pos, len);

    audio_pos += len;
    audio_len -= len;

    if (audio_len <= 0){
        // 读取完了,通知继续读取数据
        notifyGetAudioFrame();
    }
}

首先将 stream 数据清空。然后比较读出的 pcm 数据长度 audio_lenlen 的大小,保证数据大小不超过 len 的要求。

在播放时,也就是给 stream 写数据时有两种方式。一种是直接 memcpy 将音频数据 audio_pos 拷贝到 Buffer 上就好了。另一种是通过 SDL_MixAudio 方法。

SDL_MixAudio 方法顾名思义就是混音了,将 stream 和音频数据 audio_pos 混合播放,由于一开始就将 stream 数据清空为 0 了,所以看似混音,实际上和直接播放音频数据效果一致的。

最后,如果读出的 pcm 数据长度大于 len,那说明数据还没有全部填充完,下一次回调把剩下的填充到缓冲区,同时移动相应的指针位置。

如果小于,就得通知继续读取数据了,这里自定义了一个事件去通知应用读取音频数据。

// 自定义事件,通知读取音频数据
void notifyGetAudioFrame(){
    SDL_Event sdlEvent;
    sdlEvent.type = SDL_EVENT_BUFFER_END;
    SDL_PushEvent(&sdlEvent);
}

// 在程序事件循环中去响应事件,读取音频 Buffer
 while (!bQuit) {
        while (SDL_PollEvent(&windowEvent)) {
            switch (windowEvent.type) {
                case SDL_EVENT_BUFFER_END:
                    // 读取音频数据
                    if (fread(buffer, 1, bufferSize, pFile)) {
                        data_count += bufferSize;
                        audio_chunk = reinterpret_cast<Uint8 *>(buffer);
                        audio_len = bufferSize;
                        audio_pos = audio_chunk;
                    }
                default:
                    break;
            }
        }
    }

在事件的消息循环中进行响应,读取音频 Buffer 。如果读取的到的长度等于 0 了,也可以通过 fseek 方法将指针 seek 到 0,循环读取。

最后运行一下程序,就会播放出和原来 mp3 一样的音乐了。

总结

以上就是音视频基础学习连载的 008 篇。

通过两篇文章讲解了 SDL 播放音频的两种方式,后续会主要以 的模式进行开发。

本文具体代码见仓库:

https://github.com/glumes/av-beginner

本篇文章对应的提交 tagav-beginner-004,可切换至对应源码查看。

能力有限,文中有不对之处,欢迎加我微信 ezglumes 进行交流~~


技术交流,欢迎加我微信:ezglumes ,拉你入技术交流群。

扫码关注公众号【音视频开发进阶】,一起学习多媒体音视频开发~~~

喜欢就点个「在看」吧 ▽

相关文章:

  • memcpy速度太慢?掌握这个技术让内存拷贝效率成倍提升
  • DXOMark是如何评价音频质量的
  • 【每周一记-001】~~~
  • 【音视频连载-009】第二季 FFmpeg 打造简易播放器
  • 【每周一记-002】
  • iOS中使用OpenGL 实现增高功能
  • 五分钟用C++11实现Android系统的Handler机制
  • 从入门到进阶|如何基于WebRTC搭建一个视频会议
  • 【音视频连载-010】第二季 FFmpeg 日志打印
  • 如何将ijkplayer引入AS工程中进行二次开发
  • Android 性能优化必知必会
  • 音视频面试基础题
  • 2020年中国音频产业生态发展分析
  • 疫情下的网络视频行业观察 | 从疫情看网络视频领域竞争方向
  • 为什么小姐姐能摇一晚上不倒?
  • Android Studio:GIT提交项目到远程仓库
  • CentOS 7 防火墙操作
  • Effective Java 笔记(一)
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • java8 Stream Pipelines 浅析
  • javascript 总结(常用工具类的封装)
  • Java教程_软件开发基础
  • Laravel深入学习6 - 应用体系结构:解耦事件处理器
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • mac修复ab及siege安装
  • Mysql优化
  • UMLCHINA 首席专家潘加宇鼎力推荐
  • Web Storage相关
  • 二维平面内的碰撞检测【一】
  • 工作手记之html2canvas使用概述
  • 关于Flux,Vuex,Redux的思考
  • 关于使用markdown的方法(引自CSDN教程)
  • 基于Dubbo+ZooKeeper的分布式服务的实现
  • 前端每日实战:61# 视频演示如何用纯 CSS 创作一只咖啡壶
  • 三分钟教你同步 Visual Studio Code 设置
  • d²y/dx²; 偏导数问题 请问f1 f2是什么意思
  • Hibernate主键生成策略及选择
  • ​低代码平台的核心价值与优势
  • ​什么是bug?bug的源头在哪里?
  • #鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行
  • $.proxy和$.extend
  • (2020)Java后端开发----(面试题和笔试题)
  • (arch)linux 转换文件编码格式
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (翻译)Quartz官方教程——第一课:Quartz入门
  • (一)插入排序
  • (一)使用IDEA创建Maven项目和Maven使用入门(配图详解)
  • (转)从零实现3D图像引擎:(8)参数化直线与3D平面函数库
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .NET/C# 在代码中测量代码执行耗时的建议(比较系统性能计数器和系统时间)
  • @RunWith注解作用
  • [ MSF使用实例 ] 利用永恒之蓝(MS17-010)漏洞导致windows靶机蓝屏并获取靶机权限
  • [20150707]外部表与rowid.txt