Android使用AudioTrack播放WAV音频文件

news2025/1/20 16:19:25

目录

1、wav文件格式

2、wav文件解析

3、wav文件播放

QA:


开始播放wav的时候使用了系统的播放器mediaplayer进行播放,但是无奈mediaplayer支持的实在不好。

好些年前自己做过pcm播放使用的是audiotrack,参考:CSDN

其实两者之间只差了一个wav文件头而已,所以实现了一套audiotrack播放wav的功能。

1、wav文件格式

参考了:wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式

wav文件一般结构

 其中对我们比较重要的字段:

  1. NumChannels : 声道(一般1-8)
  2. SampleRate:采样频率(常见的有8000,16000,44100,48000)
  3. BitsPerSample:采样精度(常见的有8、16、32,分别代表着一个采样占据1、2、4个字节)

其余字段解释,详见wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式

wav文件头共44个字节,文件头后紧跟着的就是pcm数据,也就是真正的播放数据了。

2、wav文件解析

package com.macoli.wav_player

import java.io.DataInputStream
import java.io.InputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
class Wav(private val inputStream : InputStream) {
    val wavHeader : WavHeader = WavHeader()
    init {
        parseHeader()
    }
    private fun parseHeader() {
        val dataInputStream : DataInputStream = DataInputStream(inputStream)

        val intValue = ByteArray(4)
        val shortValue = ByteArray(2)

        try {
            wavHeader.mChunkID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            ) + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            )
            dataInputStream.read(intValue)
            wavHeader.mChunkSize = byteArrayToInt(intValue)
            wavHeader.mFormat = "" + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            ) + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            )
            wavHeader.mSubChunk1ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            ) + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            )
            dataInputStream.read(intValue)
            wavHeader.mSubChunk1Size = byteArrayToInt(intValue)
            dataInputStream.read(shortValue)
            wavHeader.mAudioFormat = byteArrayToShort(shortValue)
            dataInputStream.read(shortValue)
            wavHeader.mNumChannel = byteArrayToShort(shortValue)
            dataInputStream.read(intValue)
            wavHeader.mSampleRate = byteArrayToInt(intValue)
            dataInputStream.read(intValue)
            wavHeader.mByteRate = byteArrayToInt(intValue)
            dataInputStream.read(shortValue)
            wavHeader.mBlockAlign = byteArrayToShort(shortValue)
            dataInputStream.read(shortValue)
            wavHeader.mBitsPerSample = byteArrayToShort(shortValue)
            wavHeader.mSubChunk2ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            ) + Char(dataInputStream.readByte().toUShort()) + Char(
                dataInputStream.readByte().toUShort()
            )
            dataInputStream.read(intValue)
            wavHeader.mSubChunk2Size = byteArrayToInt(intValue)
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    private fun byteArrayToShort(b: ByteArray): Short {
        return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).short
    }

    private fun byteArrayToInt(b: ByteArray): Int {
        return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int
    }

    /**
     * WAV文件头
     * */
    class WavHeader {
        var mChunkID = "RIFF"
        var mChunkSize = 0
        var mFormat = "WAVE"
        var mSubChunk1ID = "fmt "
        var mSubChunk1Size = 16
        var mAudioFormat: Short = 1
        var mNumChannel: Short = 1
        var mSampleRate = 8000
        var mByteRate = 0
        var mBlockAlign: Short = 0
        var mBitsPerSample: Short = 8
        var mSubChunk2ID = "data"
        var mSubChunk2Size = 0

        constructor() {}
        constructor(chunkSize: Int, sampleRateInHz: Int, channels: Int, bitsPerSample: Int) {
            mChunkSize = chunkSize
            mSampleRate = sampleRateInHz
            mBitsPerSample = bitsPerSample.toShort()
            mNumChannel = channels.toShort()
            mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8
            mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()
        }

        override fun toString(): String {
            return "WavFileHeader{" +
                    "mChunkID='" + mChunkID + '\'' +
                    ", mChunkSize=" + mChunkSize +
                    ", mFormat='" + mFormat + '\'' +
                    ", mSubChunk1ID='" + mSubChunk1ID + '\'' +
                    ", mSubChunk1Size=" + mSubChunk1Size +
                    ", mAudioFormat=" + mAudioFormat +
                    ", mNumChannel=" + mNumChannel +
                    ", mSampleRate=" + mSampleRate +
                    ", mByteRate=" + mByteRate +
                    ", mBlockAlign=" + mBlockAlign +
                    ", mBitsPerSample=" + mBitsPerSample +
                    ", mSubChunk2ID='" + mSubChunk2ID + '\'' +
                    ", mSubChunk2Size=" + mSubChunk2Size +
                    '}'
        }
    }
}

3、wav文件播放

使用audiotrack播放wav一般有3个步骤:

  1. 下载wav文件
  2. 初始化audiotrack
private void initAudioTracker(){
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            AudioFormat audioFormat = new AudioFormat.Builder()
                    .setEncoding(getEncoding())
                    .setSampleRate(mWav.getWavHeader().getMSampleRate())
                    .build();
            mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
                    , AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);

        }
  1. audiotrack.write播放音频

使用单独的Downloader线程对wav文件进行下载,加快缓冲速度,避免播放出现卡顿杂音现象。

使用RealPlayer线程对wav文件进行播放。

其中Downloader线程对应生产者,RealPlayer对应消费者。mSoundData则是生产者消费者之间的缓冲区。

package com.macoli.wav_player;

import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingQueue;

public class WavPlayer {
    public volatile boolean isPlaying = false ;
    private final LinkedBlockingQueue<byte[]> mSoundData = new LinkedBlockingQueue<>() ;
    private volatile Wav mWav ;
    private volatile int mDownloadComplete = -1 ;
    private final byte[] mWavReady = new byte[1] ;
    public WavPlayer() {

    }
    public void play(String urlStr , boolean local) {
        isPlaying = true ;
        mSoundData.clear();
        mDownloadComplete = -1 ;
        mWav = null ;
        new Thread(new Downloader(urlStr , local)).start();
        new Thread(new RealPlayer()).start();
    }

    private int getChannel() {
        return mWav.getWavHeader().getMNumChannel() == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
    }

    private int getEncoding() {
        int ENCODING = AudioFormat.ENCODING_DEFAULT;
        if (mWav.getWavHeader().getMBitsPerSample() == 8) {
            ENCODING = AudioFormat.ENCODING_PCM_8BIT;
        } else if (mWav.getWavHeader().getMBitsPerSample() == 16) {
            ENCODING = AudioFormat.ENCODING_PCM_16BIT;
        } else if (mWav.getWavHeader().getMBitsPerSample() == 32) {
            ENCODING = AudioFormat.ENCODING_PCM_FLOAT;
        }
        return ENCODING ;
    }

    private int getMiniBufferSize() {
        return AudioTrack.getMinBufferSize(
                mWav.getWavHeader().getMSampleRate(), getChannel(), getEncoding());
    }

    private WavOnCompletionListener onCompletionListener ;
    public void setOnCompletionListener(WavOnCompletionListener onCompletionListener) {
        this.onCompletionListener = onCompletionListener ;
    }

    public interface WavOnCompletionListener{
        void onCompletion(int status) ;
    }

    private class Downloader implements Runnable {

        private final String mUrlStr ;
        private final boolean isLocal ;
        private Downloader(String urlStr , boolean local) {
            mUrlStr = urlStr ;
            isLocal = local ;
        }
        @Override
        public void run() {
            mDownloadComplete = -1 ;
            InputStream in = null ;
            try {
                if (!isLocal) {
                    URL url = new URL(mUrlStr);
                    URLConnection urlConnection = url.openConnection() ;
                    in = new BufferedInputStream(urlConnection.getInputStream()) ;
                } else {
                    in = new BufferedInputStream(new FileInputStream(mUrlStr)) ;
                }

                if (in == null) {
                    mDownloadComplete = -2 ;
                    isPlaying = false ;
                    onCompletionListener.onCompletion(-2);
                    synchronized (mWavReady) {
                        mWavReady.notifyAll();
                    }

                    return ;
                }
                synchronized (mWavReady) {
                    mWav = new Wav(in) ;
                    mWavReady.notifyAll();
                }
            } catch (Exception e) {

                mDownloadComplete = -2 ;
                isPlaying = false ;
                onCompletionListener.onCompletion(-2);
                synchronized (mWavReady) {
                    mWavReady.notifyAll();
                }
                return ;

            }
            int iniBufferSize = getMiniBufferSize() ;
            byte[] buffer = new byte[iniBufferSize] ;
            int read = 0 ;
            long startTime = System.currentTimeMillis() ;

            try {
                int bufferFilledCount = 0 ;
                while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1) {
                    bufferFilledCount += read ;
                    if (bufferFilledCount >= iniBufferSize) {
                        byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
                        mSoundData.put(newBuffer) ;
                        read = 0 ;
                        bufferFilledCount = 0 ;
                    }
                }

                mDownloadComplete = 1 ;
            } catch (IOException | InterruptedException e) {
                mDownloadComplete = -2 ;
                isPlaying = false ;
                onCompletionListener.onCompletion(-2);
            } finally {
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    private class RealPlayer implements Runnable{
        private AudioTrack mAudioTrack;
        private void initAudioTracker(){
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_MEDIA)
                    .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                    .build();
            AudioFormat audioFormat = new AudioFormat.Builder()
                    .setEncoding(getEncoding())
                    .setSampleRate(mWav.getWavHeader().getMSampleRate())
                    .build();
            mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
                    , AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);

        }
        public void play() {
            mAudioTrack.play() ;
            byte[] buffer ;
            try {
                while(true) {
                    buffer = mSoundData.take();
                    if (mWav.getWavHeader().getMBitsPerSample() == 8) {
                        try {
                            mAudioTrack.write(buffer, 0, buffer.length, AudioTrack.WRITE_BLOCKING);
                        } catch (Exception e) {
                        }
                    } else if (mWav.getWavHeader().getMBitsPerSample() == 16) {
                        try {
                            ShortBuffer sb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
                            short[] out = new short[sb.capacity()];
                            sb.get(out);
                            mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
                        } catch (Exception e) {

                        }
                    } else if (mWav.getWavHeader().getMBitsPerSample() == 32) {
                        try {
                            FloatBuffer fb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer();
                            float[] out = new float[fb.capacity()];
                            fb.get(out);
                            mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
//                        mAudioTrack.write(mBuffer, 0, read ,  AudioTrack.WRITE_BLOCKING);
                        } catch (Exception e) {

                        }
                    }
                    if ((1 == mDownloadComplete && mSoundData.isEmpty()) || -2 == mDownloadComplete) {
                        break ;
                    }
                }
            } catch (Exception e) {
                isPlaying = false ;
                onCompletionListener.onCompletion(-2);
                return ;
            } finally {
                mAudioTrack.stop();
                mAudioTrack.release();
                mAudioTrack = null;
                isPlaying = false ;
            }
            onCompletionListener.onCompletion(1);
        }

        @Override
        public void run() {
            synchronized (mWavReady) {
                if (mWav == null) {
                    try {
                        mWavReady.wait();
                        if (mWav == null) {
                            return ;
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            initAudioTracker() ;
            play();
        }
    }
}

播放wav:

wavplayer.play(url , 是否是本地wav文件)

 val wavPlayer = WavPlayer()
 wavPlayer.play("/sdcard/Music/3.wav" , true)

QA:

Q:1、播放wav第一帧有爆音。

A: 由于wav文件有44字节的文件头,在读取文件的时候需要跳过wav文件头再向AudioTrack.write中进行写入。

Q:2、播放网络wav有杂音。

A:由于网络读取wav文件每次读取的字节数会远远小于我们设置的minbuffer,所以每次读取网络流的时候我们都要等待minbuffer填充满的时候再使用AudioTrack.write进行写入。

int bufferFilledCount = 0 ;
                while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1) {
                    bufferFilledCount += read ;
                    if (bufferFilledCount >= iniBufferSize) {
                        byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
                        mSoundData.put(newBuffer) ;
                        read = 0 ;
                        bufferFilledCount = 0 ;
                    }
                }

Q:3、播放wav失败,全部都是杂音。

A:查看wav文件头,看看wav的采样精度,如果采样精度是32的话,必须使用write(float[]),否则肯定播放失败。

public int write(@NonNull float[] audioData, int offsetInFloats, int sizeInFloats,
            @WriteMode int writeMode)

完整源码已上传:https://gitee.com/gggl/wav-player

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

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

相关文章

php 进程池设计与实现,phper必学!

php 进程池设计与实现phper 为什么要学习进程池池的概念为什么要有进程池?动态创建进程缺点进程池的优点选择子进程为新任务服务的方式进程池模型服务端客户端结语phper 为什么要学习进程池 在php开发过程中经常使用的 php-fpm 使用的进程模型就是进程池&#xff0c;学习进程…

如何基于FSM有限状态机实现Enemies AI

文章目录&#x1f35f; Preface&#x1f355; 巡逻状态&#x1f37f; 寻路状态&#x1f32d; 攻击状态&#x1f357; 完整代码&#x1f35f; Preface 本文简单介绍如何基于FSM有限状态机实现Enemies AI&#xff0c;首先定义敌人的AI逻辑&#xff1a;默认状态下Enemy为巡逻状态…

刷爆力扣之等价多米诺骨牌对的数量

刷爆力扣之等价多米诺骨牌对的数量 HELLO&#xff0c;各位看官大大好&#xff0c;我是阿呆 &#x1f648;&#x1f648;&#x1f648; 今天阿呆继续记录下力扣刷题过程&#xff0c;收录在专栏算法中 &#x1f61c;&#x1f61c;&#x1f61c; 该专栏按照不同类别标签进行刷题&…

使用 nlohmann 解析 json 文件

使用 nlohmann 解析 json 文件nlohmann/json的配置json基本数据结构json文件的读取、构造与输出C对象与nlohmann::json对象的转换C对象转换成nlohmann::json对象nlohmann::json对象转换成C对象序列化反序列化序列化nlohmann 是德国工程师&#xff0c;以其名字为工程名的 nlohm…

springboot项目的打包发布部署,jar和war的区别

简介&#xff1a; 1.Spring Boot使用了内嵌容器&#xff0c;因此它的部署方式也变得非常简单灵活&#xff0c;可以将Spring Boot项目打包成JAR包来独立运行&#xff0c;也可以打包成WAR包部署到Tomcat容器中运行&#xff0c;如果涉及大规模的部署&#xff0c;Jenkins成为最佳选…

【HCIP-Datacom】 IS-IS基础 ISIS动态路由协议配置(ISIS思维导图在底部)

目录 ISIS配置方法&#xff1a; 路由计算&#xff1a; ATT置位条件&#xff1a; 路由渗透&#xff1a; ISIS的认证&#xff1a; ISIS配置命令&#xff1a; ISIS的开销类型&#xff1a; ISIS配置方法&#xff1a; 进入ISIS进程 isis 1 //创建isis进程 设置实体名 network-entit…

.NET 升级发布后,IIS出现了System.IO.DirectoryNotFoundException

最近计划升级项目到.NET6, 在使用Release发布后发现IIS不能发现wwwroot目录,什么错误? 📢欢迎点赞 :👍 收藏 ⭐留言 📝 如有错误敬请指正,赐人玫瑰,手留余香!📢本文作者:由webmote 原创📢作者格言:无尽的折腾后,终于又回到了起点,工控,我来了 !1 发布的一…

《统计学习方法》 第十六章 主成分分析PCA

主成分分析(PCA) 假设xxx为mmm 维随机变量&#xff0c;其均值为μ\muμ&#xff0c;协方差矩阵为Σ\SigmaΣ 考虑由mmm维随机变量xxx到mmm维随机变量yyy的线性变换 yiαiTx∑k1mαkixk,i1,2,⋯,my _ { i } \alpha _ { i } ^ { T } x \sum _ { k 1 } ^ { m } \alpha _ { k …

计算点在线上的投影坐标

如题 计算点到线上的垂点&#xff0c;首先明确&#xff1a; 该线段必须给出确切的起始点和终点&#xff0c; 而不是一个向量&#xff0c;因为一个向量并不能代表一个线段。 所以参数列表如下&#xff1a; Vector3 VerticalPoint(Vector3 point, Vector3 lStart, Vector3 lEnd…

【论文翻译】增强复制状态机的两阶段提交协议

Enhancing Two Phase-Commit Protocol for Replicated State Machines Halit Uyanık and Tolga Ovatman Department of Computer Engineering Istanbul Technical University 34469 Istanbul, Turkey Email目录1 介绍2 设计和实现2.1 事件类型2.2 在状态机上执行事件2.3 具有优…

8、常用基本命令(重要)

文章目录8、常用基本命令&#xff08;重要&#xff09;8.1 帮助命令8.1.1 man 获得帮助信息8.1.2 help 获得 shell 内置命令的帮助信息8.1.3 常用快捷键8.2 文件目录类8.2.1 pwd 显示当前工作目录的绝对路径8.2.2 ls 列出目录的内容8.2.3 cd 切换目录8.2.4 mkdir 创建一个新的目…

Linux驱动入门

一、驱动简介 Linux的驱动在本质上就是一种软件程序&#xff0c;上层软件可以在不了解硬件特性的情况下&#xff0c;通过驱动提供的接口&#xff0c;和计算机硬件进行通信。 系统调用是内核和应用程序之间的接口&#xff0c;而驱动程序是内核和硬件之间的接口。它为应用程序屏蔽…

缓存穿透、缓存击穿、缓存雪崩及其解决方案

缓存&#xff08;cache&#xff09;&#xff0c;大家都非常熟悉&#xff0c;几乎每个系统乃至整个计算机体系中都会用到。在分布式系统架构中&#xff0c;主要用于减轻数据库的压力&#xff0c;提高系统的响应速度和并发吞吐&#xff0c;即空间(内存)换时间。当大量的读、写请求…

【模型推理加速系列】06: 基于resnet18加速方案评测

简介 花雪随风不厌看&#xff0c;更多还肯失林峦。愁人正在书窗下&#xff0c;一片飞来一片寒。小伙伴们好&#xff0c;我是微信公众号小窗幽记机器学习的首席称重师&#xff1a;卖麻辣烫的小男孩。今天这篇文章以resnet18模型为例&#xff0c;对比Pytorch、ONNX、TorchScript…

cmdline(二):uboot cmdline怎么传?cmdline kernel怎么用?

前面我们知道了cmdline是什么&#xff0c;已经在哪里添加cmdline&#xff1f;现在我们来看看在哪里传输cmdline&#xff0c;以及传输收到后怎么用&#xff1f; 参考内容来自前辈&#xff0c;感激&#xff1a; https://blog.csdn.net/weixin_42031299/article/details/12123950…

Spring Boot JPA EntityManager实体管理器示例

在本教程中&#xff0c;您将了解如何在 Spring Boot 示例中使用 JPA EntityManager&#xff08;使用 CRUD 操作和查询方法&#xff09;。我将向您展示&#xff1a; 在 Spring 引导中访问 JPA 实体管理器的方法如何使用实体管理器方法&#xff1a;执行SQL查询使用和CRUD操作cre…

【Android App】实现在线语音合成功能(使用云知声平台和WebSocket 超详细 附源码)

需要源码和Jar包请点赞关注收藏后评论区留下QQ~~~ 一、在线语音合成 虽然国产智能机大多集成了中文语音引擎&#xff0c;但是系统自带的语音工具无法满足商用要求&#xff0c;功能单一&#xff0c;所以势必引入第三方的语音引擎&#xff0c;依靠第三方提供的开发包统一支撑语音…

【新知实验室】——腾讯云音视频TRTC体验

腾讯实时音视频 TRTC 是什么&#xff1f; 腾讯实时音视频&#xff08;Tencent Real-Time Communication&#xff0c;TRTC&#xff09;将腾讯21年来在网络与音视频技术上的深度积累&#xff0c;以多人音视频通话和低延时互动直播两大场景化方案&#xff0c;通过腾讯云服务向开发…

clickHouse基础语法

clichouse数据类型 整形 lnt8 8bit,1字节 &#xff08;-128-127&#xff09;lnt16 16bitlnt32 32bitlnt64 64bit 无符号整型 相比于上面&#xff0c;就是把负数部分挪到正数部分 Ulnt8 &#xff08;0-255&#xff09;Ulnt16Ulnt32Ulnt64 浮点型 Float32 也就是floatFloa…

Instant Neural Graphics Primitives with a Multiresolution Hash Encoding以及源码浅析

背景 现存的一些新视图合成的训练过程和渲染速度都比较慢&#xff0c;其原因是因为query point需要使用MLP编码&#xff0c;而且在一个采样空间中&#xff0c;存在很多无效的query point也要计算其density和color&#xff0c;从而出现很多冗余计算。 作者针对这个问题&#x…