投屏开发调试技能-pcm数据转wav格式文件源码实战分享

news2024/11/26 11:34:13

背景

在学习投屏相关音视频开发时候,经常验证一些声音卡顿问题时候,需要对音频数据可能需要保存到本地,一般可能是pcm格式的数据,但是pcm格式的数据是不可以用音乐播放器直接进行播放,需要专门的工具,而且你还需要知道pcm详细的具体参数,具体如下参数:
在这里插入图片描述
需要知道采样的位数格式,采样率,声道数目,字节顺序,因为只有知道这些参数播放器才知道怎么播放。

在这里插入图片描述
所以pcm播放还是比较麻烦,有需要考虑使用更加简单的文件格式,那就是下面要带大家进行手把手实战的wav格式。

Wav格式详细介绍

Wav简单介绍

WAV即波形声音文件格式 (Waveform Audio File Format,简称WAVE,因后缀为*.wav故简称WAV文件),其采用RIFF(Resource Interchange File Format,资源互换文件格式)结构,并符合(RIFF)规范,用于保存Windows平台的音频信息资源,被Windows平台及其应用程序所广泛支持。Wave格式支持MSADPCM、CCITT A律、CCITT μ律和其他压缩算法,支持多种音频位数、采样频率和声道,是PC机上最为流行的声音文件格式;但由于“无损”的特点,WAV文件格式所占用的磁盘空间相对较大(每分钟的音乐大约需要12MB磁盘空间),故此文件格式多用于存储简短的声音片段。同时WAV文件格式通常用来保存PCM格式的原始音频数据,所以通常被称为无损音频(相对aac,mp3压缩格式来说,因为模拟到数字需要采样,无论如何都有失真)。但是严格意义上来讲,WAV也可以存储其它压缩格式的音频数据,但大部分都是pcm数据。

wav文件格式
pcm直接播放需要手动输入额外一些参数,wav格式就可以直接播放,就是因为wav有一个额外的文件头,文件头可以把这些参数进行放置,这样播放器就可以从wav文件头中获取pcm相关参数,实现直接播放wav的pcm数据
在这里插入图片描述
具体文件头格式如下表所示:
在这里插入图片描述
图中提到的RIFF 是 Resource Interchange File Format(资源交换文件格式)的简称。RIFF 是一种文件格式规范,用于在计算机系统之间交换和存储多媒体资源。WAV 文件格式是 Microsoft 的 RIFF 规范的一个子集。

格式说明总结:
上图可以看出来,wav文件格式都是由 chunk 组成,chunk 的格式如下:

在这里插入图片描述
在这里插入图片描述
里面了上面图后,再去写这个wav文件的head那么就变成非常简单了。
这里在重点介绍一下fmt部分的chunk数据,它们是pcm的格式参数的赋值部分

在这里插入图片描述

  • 音频格式(audio format):2个字节,表示音频数据的格式,具体可以对照下表,一般都是pcm就行
    在这里插入图片描述

  • 声道数(num channels):2个字节,表示音频数据的声道数。

  • 采样率(sample rate):4个字节,表示音频数据的采样率。

  • 每秒字节数(byte rate):4个字节,表示音频数据的数据速率。

  • 数据块对齐(block align):2个字节,表示数据块的对齐方式。

  • 位深度(bits per sample):2个字节,表示音频数据的位深度。

注意:同时注意左边字节顺序,一般字符都是大端模式,数字相关的都是小端模式,如上面的chunk名字都是大端一个个字符,其他数据大小都是小端。

编写代码实战

最重要要编写出一个wav头来

public static  byte[] generateWavFileHeader(long pcmAudioByteCount, long longSampleRate, int channels) {
        long totalDataLen = pcmAudioByteCount + 36; // 不包含前8个字节的WAV文件总长度
        long byteRate = longSampleRate * 2 * channels;
        byte[] header = new byte[44];
         //RIFF Chunk
        header[0] = 'R'; // RIFF
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';

        header[4] = (byte) (totalDataLen & 0xff);//数据大小
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);

        header[8] = 'W';//WAVE
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        
        //FMT Chunk
        header[12] = 'f'; // 'fmt '
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';//过渡字节
        //数据大小
        header[16] = 16; // 4 bytes: size of 'fmt ' chunk
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        //编码方式 10H为PCM编码格式
        header[20] = 1; // format = 1
        header[21] = 0;
        //通道数
        header[22] = (byte) channels;
        header[23] = 0;
        //采样率,每个通道的播放速度
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        //音频数据传送速率,采样率*通道数*采样深度/8
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数
        header[32] = (byte) (2 * channels);
        header[33] = 0;
        //每个样本的数据位数
        header[34] = 16;
        header[35] = 0;
        
        //Data chunk
        header[36] = 'd';//data
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (pcmAudioByteCount & 0xff);
        header[41] = (byte) ((pcmAudioByteCount >> 8) & 0xff);
        header[42] = (byte) ((pcmAudioByteCount >> 16) & 0xff);
        header[43] = (byte) ((pcmAudioByteCount >> 24) & 0xff);
        return header;
    }

有了generateWavFileHeader这个方法后,针对固定大小的pcm转成wav文件已经完全可以搞定了,但是往往录音等pcm数据都是不断产生,pcm数据刚开始大小并不确定,所以这里可以采用种解决方法:
1、等完全录音完毕再把pcm写入到wav
2、因为wav的head一般是固定的大小44字节,这里可以先生成pcm大小size为0的head,这样可以站位44字节,等录制完成,重新生成head再覆盖原来head

在录音时候文件:

package com.example.remotesubmix;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Environment;
import android.util.Log;

import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class AudioRecordBussiness  extends Thread {
    private static final int AUDIO_RATE = 44100;
    static String PATH =null;
        private AudioRecord record;
        private int minBufferSize;
        private boolean isDone = false;

        public AudioRecordBussiness(Context context) {
            PATH = context.getExternalCacheDir().getAbsolutePath() ;
            /**
             * 获取最小 buffer 大小
             * 采样率为 44100,双声道,采样位数为 16bit
             */
            minBufferSize = AudioRecord.getMinBufferSize(AUDIO_RATE, AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT);
            //使用 AudioRecord 去录音
            record = new AudioRecord(
                    MediaRecorder.AudioSource.REMOTE_SUBMIX,
                    AUDIO_RATE,
                    AudioFormat.CHANNEL_IN_STEREO,
                    AudioFormat.ENCODING_PCM_16BIT,
                    minBufferSize
            );
        }

        @Override
        public void run() {
            super.run();
            FileOutputStream fos = null;
            FileOutputStream wavFos = null;
            RandomAccessFile wavRaf = null;
            try {
                //没有先创建文件夹
                File dir = new File(PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                //创建 pcm 文件
                File pcmFile = getFile(PATH, "test.pcm");
                //创建 wav 文件
                File wavFile = getFile(PATH, "test.wav");
                fos = new FileOutputStream(pcmFile);
                wavFos = new FileOutputStream(wavFile);

                //先写头部,刚才是,我们并不知道 pcm 文件的大小
                byte[] headers = SaveToWaveFile.generateWavFileHeader(0, AUDIO_RATE, record.getChannelCount());
                wavFos.write(headers, 0, headers.length);

                //开始录制
                record.startRecording();
                byte[] buffer = new byte[minBufferSize];
                while (!isDone) {
                    //读取数据
                    int read = record.read(buffer, 0, buffer.length);
                    if (AudioRecord.ERROR_INVALID_OPERATION != read) {
                        //写 pcm 数据
                        fos.write(buffer, 0, read);
                        //写 wav 格式数据
                        wavFos.write(buffer, 0, read);
                    }

                }
                //录制结束
                record.stop();
                record.release();

                fos.flush();
                wavFos.flush();

                //修改头部的 pcm文件 大小
                wavRaf = new RandomAccessFile(wavFile, "rw");
                //pcmFile.length()只有pcm的数据大小,没有wav的head大小
                byte[] header = SaveToWaveFile.generateWavFileHeader(pcmFile.length(), AUDIO_RATE, record.getChannelCount());
                wavRaf.seek(0);
                wavRaf.write(header);

            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                close(fos, wavFos,wavRaf);
            }
        }

        public void done() {
            isDone = true;
            interrupt();
        }
    private File getFile(String path, String name) {
        File file = new File(path, name);
        if (file.exists()) {
            file.delete();
        }
        try {
            file.createNewFile();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void close(Closeable... closeables){
        if (closeables != null) {
            for (Closeable closeable : closeables) {
                if (closeable != null) {
                    try {
                        closeable.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

更多framework详细代码和资料参考如下链接

hal+perfetto+surfaceflinger

https://mp.weixin.qq.com/s/LbVLnu1udqExHVKxd74ILg
在这里插入图片描述

其他课程七件套专题:在这里插入图片描述
点击这里
https://mp.weixin.qq.com/s/Qv8zjgQ0CkalKmvi8tMGaw

视频试看:
https://www.bilibili.com/video/BV1wc41117L4/

参考相关链接:
https://blog.csdn.net/zhimokf/article/details/137958615

更多framework假威风耗:androidframework007

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

14种实际上有效的AI营销方法(专业推荐)

当有人提到人工智能时,你会感到头晕目眩吗?这是可以理解的。LinkedIn和Twitter(好吧……现在叫X)充斥着刚刚涌现的AI专家和科技达人们,他们在分享最新的27步算法攻略和自动化整个市场营销程序的操作。 这对大多数基层…

一款rust语言AI神器cursor在ubuntu环境下的安装启动教程

虽然cursor目前只支持英文但是它强大的代码联想能力以及问答能力,可以高效的提高编码效率。 如下步骤所有的前提是你的ubuntu上面已经安装了rust以及其必须的extensions。 1 下载 到官网https://www.cursor.com下载指定版本的软件。 下载到本地以后会生成如下软件…

如何通过网络找到自己想要的LabVIEW知识?

学习LabVIEW或其他编程技术时,无法依赖某一篇文章解决所有问题。重要的是通过多种途径获取灵感,并学会归纳总结,从而逐渐形成系统性的理解。这种持续学习和总结的过程是技术提升的基础。通过网络找到所需的LabVIEW知识可以通过以下几个步骤进…

WEB渗透权限维持篇-MSSQL后门

往期文章WEB渗透权限维持篇-DLL注入\劫持-CSDN博客 WEB渗透权限维持篇-CLR-Injection-CSDN博客 WEB渗透权限维持篇-计划任务-CSDN博客 WEB渗透权限维持篇-DLL注入-修改内存中的PE头-CSDN博客 WEB渗透权限维持篇-DLL注入-进程挖空(MitreT1055.012)-CSDN博客 WEB渗透权限维…

轻松上手LangChain:新手必读的入门指南

导语 在人工智能领域的不断发展中,语言模型扮演着重要的角色。特别是大型语言模型(LLM),如ChatGPT,已经成为科技领域的热门话题,并受到广泛认可。在这个背景下,LangChain作为一个以LLM模型为核…

打造高效业务架构:价值流在企业转型中的应用指南

从流程到价值流的业务架构转型 随着企业面对数字化转型带来的激烈市场竞争,优化业务架构成为每个企业管理者必须面对的核心挑战。传统的业务流程优化方法往往难以应对复杂的客户需求和日益增加的业务复杂性。《价值流指南》由The Open Group发布的企业数字化转型专…

K-Means算法详解与实战应用.

在数据分析的众多工具中,K-Means聚类算法以其简单、直观和高效的特点,成为了探索数据集结构的常用方法。本文将带你深入了解K-Means算法的原理,并展示如何在实际项目中运用这一强大的聚类工具。 一 算法原理 K-Means是一种迭代聚类算法&…

共享旅游卡,客户旅游云南,真实反馈,全程无删减!

​这是团队伙伴袁总的客户,也是袁总的朋友,使用千益畅行旅游卡,亲身带家人去旅游云南后体验反馈。 从抗拒旅游卡,报付费团旅游,到了解旅游卡,使用旅游卡去体验,中途的担忧顾虑,到结…

计算机网络(五) —— 自定义协议简单网络程序

目录 一,关于“协议” 1.1 结构化数据 1.2 序列化和反序列化 二,网络版计算器实现准备 2.1 套用旧头文件 2.2 封装sock API 三,自定义协议 3.1 关于自定义协议 3.2 实现序列化和反序列化 3.3 测试 三,服务器实现 3.1…

【C++ 面试题】构造函数和析构函数你了解多少呢?

文章目录 1. 什么是构造函数和析构函数2. 构造函数和析构函数可以是虚函数吗3. 构造函数有哪几种4. 深拷贝和浅拷贝的区别 1. 什么是构造函数和析构函数 🐧 构造函数: 构造函数是在创建对象时自动调用的特殊成员函数。 目的:初始化对象的成…

【Redis】主从复制 - 源码

因为主从复制的过程很复杂, 同时核心逻辑主要集中在 replication.c 这个文件中, 避免篇幅过大, 所以将主从复制中涉及这个文件的代码集中到了另一篇文章。 在当前文章主要分析主从复制的大体代码逻辑, 如果需要了解整体的过程, 可以配合 Redis 主从复制 - relication 源码分析 …

中非合作打开非洲14亿人的市场新空间,非洲电商平台有哪些?

中非合作论坛自2000年成立以来,中非贸易额从105亿美元增至2821亿美元,增长近26倍。中国对非投资也从4亿多美元增长至400多亿美元,增幅超过100倍。此次中非合作论坛开幕式上,中国更是宣布未来将为非洲提供约500亿美元的融资&#x…

水库大坝安全监测方案,双重守护,安全无忧

水库作为重要的水利设施,在防洪、灌溉及供水等方面发挥着重要作用。然而随着时间的推移,大坝面临着自然老化、设计标准不足及极端天气等多重挑战,其安全性与稳定性日益受到关注。水库堤坝险情导致的洪涝灾害给人民生命财产和经济社会发展带来…

运动耳机选哪种好用?六条绝妙选购要点避免踩坑

​开放式耳机目前非常流行,它们的设计不侵入耳道,长时间佩戴也不会感到不适,同时还能维护耳部卫生,这使得它们特别受到运动爱好者和耳机发烧友的喜爱。然而,市场上的开放式耳机品牌众多,质量参差不齐&#…

乡村振兴/乡村风貌 乡村建设改造方案设计

[若愚文化STUDIO] 乡村振兴/乡村建设/风貌改造/产业策划 深度参与GD省“百千万”工程, 助力乡村建设。 根据现状实景,充分保留主体建筑物,快速出改造意向图。

vue的学习之路(Vue中组件(component )

注意:其中添加div的意义就是让template标签有一个根标签 ,否则只展示“欢迎进入登录程序” 不加div效果图 (2)两种开发方式 第一种开发方式 //局部组件登录模板声明 let login { //具体局部组件名称 template:‘ 用户登录 ’…

网络安全工程师能赚多少钱一个月?

🤟 基于入门网络安全/黑客打造的:👉黑客&网络安全入门&进阶学习资源包 网络安全工程师的月薪取决于多种因素,包括他们的经验、技能、学历、所在地区和行业的需求等。因此,很难给出一个确切的数字作为所有网络安…

STM32的GPIO使用

一、使用流程 1.使用RCC开启GPIO时钟 2.使用GPIO_Init 函数初始化GPIO 3.使用输出或输入函数控制GPIO口 二、RCC的常用函数 函数内容可通过这两个文件进行查看: RCC常用函数如下: void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalS…

掌握Python自动化:探索keymousego库的无限可能!

文章目录 掌握Python自动化:探索keymousego库的无限可能!背景:为什么选择keymousego?简介:keymousego是什么?安装指南:如何安装keymousego?快速入门:5个简单函数的使用实…

Java中校验导入字段长度与数据库字段长度一致性

需求:使用EasyExcel导入数据时,根据数据库字段长度校验导入字段的长度。使用的数据库是mysql。若是一般的校验需求, Spring Validation 或 Hibernate Validator 即可满足。 实现步骤: 获取需要校验的表,查询出字段相…