• Android使用AudioTrack播放WAV音频文件


    目录

    1、wav文件格式

    2、wav文件解析

    3、wav文件播放

    QA:


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

    好些年前自己做过pcm播放使用的是audiotrack,参考:Android 利用AudioTrack播放 PCM 格式音频_mldxs的博客-CSDN博客

    其实WAV和PCM两者之间只差了一个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文件解析

    1. package com.macoli.wav_player
    2. import java.io.DataInputStream
    3. import java.io.InputStream
    4. import java.nio.ByteBuffer
    5. import java.nio.ByteOrder
    6. class Wav(private val inputStream : InputStream) {
    7. val wavHeader : WavHeader = WavHeader()
    8. init {
    9. parseHeader()
    10. }
    11. private fun parseHeader() {
    12. val dataInputStream : DataInputStream = DataInputStream(inputStream)
    13. val intValue = ByteArray(4)
    14. val shortValue = ByteArray(2)
    15. try {
    16. wavHeader.mChunkID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
    17. dataInputStream.readByte().toUShort()
    18. ) + Char(dataInputStream.readByte().toUShort()) + Char(
    19. dataInputStream.readByte().toUShort()
    20. )
    21. dataInputStream.read(intValue)
    22. wavHeader.mChunkSize = byteArrayToInt(intValue)
    23. wavHeader.mFormat = "" + Char(dataInputStream.readByte().toUShort()) + Char(
    24. dataInputStream.readByte().toUShort()
    25. ) + Char(dataInputStream.readByte().toUShort()) + Char(
    26. dataInputStream.readByte().toUShort()
    27. )
    28. wavHeader.mSubChunk1ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
    29. dataInputStream.readByte().toUShort()
    30. ) + Char(dataInputStream.readByte().toUShort()) + Char(
    31. dataInputStream.readByte().toUShort()
    32. )
    33. dataInputStream.read(intValue)
    34. wavHeader.mSubChunk1Size = byteArrayToInt(intValue)
    35. dataInputStream.read(shortValue)
    36. wavHeader.mAudioFormat = byteArrayToShort(shortValue)
    37. dataInputStream.read(shortValue)
    38. wavHeader.mNumChannel = byteArrayToShort(shortValue)
    39. dataInputStream.read(intValue)
    40. wavHeader.mSampleRate = byteArrayToInt(intValue)
    41. dataInputStream.read(intValue)
    42. wavHeader.mByteRate = byteArrayToInt(intValue)
    43. dataInputStream.read(shortValue)
    44. wavHeader.mBlockAlign = byteArrayToShort(shortValue)
    45. dataInputStream.read(shortValue)
    46. wavHeader.mBitsPerSample = byteArrayToShort(shortValue)
    47. wavHeader.mSubChunk2ID = "" + Char(dataInputStream.readByte().toUShort()) + Char(
    48. dataInputStream.readByte().toUShort()
    49. ) + Char(dataInputStream.readByte().toUShort()) + Char(
    50. dataInputStream.readByte().toUShort()
    51. )
    52. dataInputStream.read(intValue)
    53. wavHeader.mSubChunk2Size = byteArrayToInt(intValue)
    54. } catch (e: Exception) {
    55. e.printStackTrace()
    56. }
    57. }
    58. private fun byteArrayToShort(b: ByteArray): Short {
    59. return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).short
    60. }
    61. private fun byteArrayToInt(b: ByteArray): Int {
    62. return ByteBuffer.wrap(b).order(ByteOrder.LITTLE_ENDIAN).int
    63. }
    64. /**
    65. * WAV文件头
    66. * */
    67. class WavHeader {
    68. var mChunkID = "RIFF"
    69. var mChunkSize = 0
    70. var mFormat = "WAVE"
    71. var mSubChunk1ID = "fmt "
    72. var mSubChunk1Size = 16
    73. var mAudioFormat: Short = 1
    74. var mNumChannel: Short = 1
    75. var mSampleRate = 8000
    76. var mByteRate = 0
    77. var mBlockAlign: Short = 0
    78. var mBitsPerSample: Short = 8
    79. var mSubChunk2ID = "data"
    80. var mSubChunk2Size = 0
    81. constructor() {}
    82. constructor(chunkSize: Int, sampleRateInHz: Int, channels: Int, bitsPerSample: Int) {
    83. mChunkSize = chunkSize
    84. mSampleRate = sampleRateInHz
    85. mBitsPerSample = bitsPerSample.toShort()
    86. mNumChannel = channels.toShort()
    87. mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8
    88. mBlockAlign = (mNumChannel * mBitsPerSample / 8).toShort()
    89. }
    90. override fun toString(): String {
    91. return "WavFileHeader{" +
    92. "mChunkID='" + mChunkID + '\'' +
    93. ", mChunkSize=" + mChunkSize +
    94. ", mFormat='" + mFormat + '\'' +
    95. ", mSubChunk1ID='" + mSubChunk1ID + '\'' +
    96. ", mSubChunk1Size=" + mSubChunk1Size +
    97. ", mAudioFormat=" + mAudioFormat +
    98. ", mNumChannel=" + mNumChannel +
    99. ", mSampleRate=" + mSampleRate +
    100. ", mByteRate=" + mByteRate +
    101. ", mBlockAlign=" + mBlockAlign +
    102. ", mBitsPerSample=" + mBitsPerSample +
    103. ", mSubChunk2ID='" + mSubChunk2ID + '\'' +
    104. ", mSubChunk2Size=" + mSubChunk2Size +
    105. '}'
    106. }
    107. }
    108. }

    3、wav文件播放

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

    1. 下载wav文件
    2. 初始化audiotrack(初始化audiotrack依赖刚刚解析wav文件头的信息)
    1. private void initAudioTracker(){
    2. AudioAttributes audioAttributes = new AudioAttributes.Builder()
    3. .setUsage(AudioAttributes.USAGE_MEDIA)
    4. .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
    5. .build();
    6. AudioFormat audioFormat = new AudioFormat.Builder()
    7. .setEncoding(getEncoding())
    8. .setSampleRate(mWav.getWavHeader().getMSampleRate())
    9. .build();
    10. mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
    11. , AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
    12. }
    1. audiotrack.write播放音频

    下面是真正播放wav的代码了,代码很简单,不做过多介绍了:

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

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

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

    1. package com.macoli.wav_player;
    2. import android.media.AudioAttributes;
    3. import android.media.AudioFormat;
    4. import android.media.AudioManager;
    5. import android.media.AudioTrack;
    6. import java.io.BufferedInputStream;
    7. import java.io.FileInputStream;
    8. import java.io.IOException;
    9. import java.io.InputStream;
    10. import java.net.URL;
    11. import java.net.URLConnection;
    12. import java.nio.ByteBuffer;
    13. import java.nio.ByteOrder;
    14. import java.nio.FloatBuffer;
    15. import java.nio.ShortBuffer;
    16. import java.util.Arrays;
    17. import java.util.concurrent.LinkedBlockingQueue;
    18. public class WavPlayer {
    19. public volatile boolean isPlaying = false ;
    20. private final LinkedBlockingQueue<byte[]> mSoundData = new LinkedBlockingQueue<>() ;
    21. private volatile Wav mWav ;
    22. private volatile int mDownloadComplete = -1 ;
    23. private final byte[] mWavReady = new byte[1] ;
    24. public WavPlayer() {
    25. }
    26. public void play(String urlStr , boolean local) {
    27. isPlaying = true ;
    28. mSoundData.clear();
    29. mDownloadComplete = -1 ;
    30. mWav = null ;
    31. new Thread(new Downloader(urlStr , local)).start();
    32. new Thread(new RealPlayer()).start();
    33. }
    34. private int getChannel() {
    35. return mWav.getWavHeader().getMNumChannel() == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
    36. }
    37. private int getEncoding() {
    38. int ENCODING = AudioFormat.ENCODING_DEFAULT;
    39. if (mWav.getWavHeader().getMBitsPerSample() == 8) {
    40. ENCODING = AudioFormat.ENCODING_PCM_8BIT;
    41. } else if (mWav.getWavHeader().getMBitsPerSample() == 16) {
    42. ENCODING = AudioFormat.ENCODING_PCM_16BIT;
    43. } else if (mWav.getWavHeader().getMBitsPerSample() == 32) {
    44. ENCODING = AudioFormat.ENCODING_PCM_FLOAT;
    45. }
    46. return ENCODING ;
    47. }
    48. private int getMiniBufferSize() {
    49. return AudioTrack.getMinBufferSize(
    50. mWav.getWavHeader().getMSampleRate(), getChannel(), getEncoding());
    51. }
    52. private WavOnCompletionListener onCompletionListener ;
    53. public void setOnCompletionListener(WavOnCompletionListener onCompletionListener) {
    54. this.onCompletionListener = onCompletionListener ;
    55. }
    56. public interface WavOnCompletionListener{
    57. void onCompletion(int status) ;
    58. }
    59. private class Downloader implements Runnable {
    60. private final String mUrlStr ;
    61. private final boolean isLocal ;
    62. private Downloader(String urlStr , boolean local) {
    63. mUrlStr = urlStr ;
    64. isLocal = local ;
    65. }
    66. @Override
    67. public void run() {
    68. mDownloadComplete = -1 ;
    69. InputStream in = null ;
    70. try {
    71. if (!isLocal) {
    72. URL url = new URL(mUrlStr);
    73. URLConnection urlConnection = url.openConnection() ;
    74. in = new BufferedInputStream(urlConnection.getInputStream()) ;
    75. } else {
    76. in = new BufferedInputStream(new FileInputStream(mUrlStr)) ;
    77. }
    78. if (in == null) {
    79. mDownloadComplete = -2 ;
    80. isPlaying = false ;
    81. onCompletionListener.onCompletion(-2);
    82. synchronized (mWavReady) {
    83. mWavReady.notifyAll();
    84. }
    85. return ;
    86. }
    87. synchronized (mWavReady) {
    88. mWav = new Wav(in) ;
    89. mWavReady.notifyAll();
    90. }
    91. } catch (Exception e) {
    92. mDownloadComplete = -2 ;
    93. isPlaying = false ;
    94. onCompletionListener.onCompletion(-2);
    95. synchronized (mWavReady) {
    96. mWavReady.notifyAll();
    97. }
    98. return ;
    99. }
    100. int iniBufferSize = getMiniBufferSize() ;
    101. byte[] buffer = new byte[iniBufferSize] ;
    102. int read = 0 ;
    103. long startTime = System.currentTimeMillis() ;
    104. try {
    105. int bufferFilledCount = 0 ;
    106. while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1) {
    107. bufferFilledCount += read ;
    108. if (bufferFilledCount >= iniBufferSize) {
    109. byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
    110. mSoundData.put(newBuffer) ;
    111. read = 0 ;
    112. bufferFilledCount = 0 ;
    113. }
    114. }
    115. mDownloadComplete = 1 ;
    116. } catch (IOException | InterruptedException e) {
    117. mDownloadComplete = -2 ;
    118. isPlaying = false ;
    119. onCompletionListener.onCompletion(-2);
    120. } finally {
    121. if (in != null) {
    122. try {
    123. in.close();
    124. } catch (IOException e) {
    125. e.printStackTrace();
    126. }
    127. }
    128. }
    129. }
    130. }
    131. private class RealPlayer implements Runnable{
    132. private AudioTrack mAudioTrack;
    133. private void initAudioTracker(){
    134. AudioAttributes audioAttributes = new AudioAttributes.Builder()
    135. .setUsage(AudioAttributes.USAGE_MEDIA)
    136. .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
    137. .build();
    138. AudioFormat audioFormat = new AudioFormat.Builder()
    139. .setEncoding(getEncoding())
    140. .setSampleRate(mWav.getWavHeader().getMSampleRate())
    141. .build();
    142. mAudioTrack = new AudioTrack(audioAttributes, audioFormat, getMiniBufferSize()
    143. , AudioTrack.MODE_STREAM, AudioManager.AUDIO_SESSION_ID_GENERATE);
    144. }
    145. public void play() {
    146. mAudioTrack.play() ;
    147. byte[] buffer ;
    148. try {
    149. while(true) {
    150. buffer = mSoundData.take();
    151. if (mWav.getWavHeader().getMBitsPerSample() == 8) {
    152. try {
    153. mAudioTrack.write(buffer, 0, buffer.length, AudioTrack.WRITE_BLOCKING);
    154. } catch (Exception e) {
    155. }
    156. } else if (mWav.getWavHeader().getMBitsPerSample() == 16) {
    157. try {
    158. ShortBuffer sb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
    159. short[] out = new short[sb.capacity()];
    160. sb.get(out);
    161. mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
    162. } catch (Exception e) {
    163. }
    164. } else if (mWav.getWavHeader().getMBitsPerSample() == 32) {
    165. try {
    166. FloatBuffer fb = ByteBuffer.wrap(buffer, 0, buffer.length).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer();
    167. float[] out = new float[fb.capacity()];
    168. fb.get(out);
    169. mAudioTrack.write(out, 0, out.length, AudioTrack.WRITE_BLOCKING);
    170. // mAudioTrack.write(mBuffer, 0, read , AudioTrack.WRITE_BLOCKING);
    171. } catch (Exception e) {
    172. }
    173. }
    174. if ((1 == mDownloadComplete && mSoundData.isEmpty()) || -2 == mDownloadComplete) {
    175. break ;
    176. }
    177. }
    178. } catch (Exception e) {
    179. isPlaying = false ;
    180. onCompletionListener.onCompletion(-2);
    181. return ;
    182. } finally {
    183. mAudioTrack.stop();
    184. mAudioTrack.release();
    185. mAudioTrack = null;
    186. isPlaying = false ;
    187. }
    188. onCompletionListener.onCompletion(1);
    189. }
    190. @Override
    191. public void run() {
    192. synchronized (mWavReady) {
    193. if (mWav == null) {
    194. try {
    195. mWavReady.wait();
    196. if (mWav == null) {
    197. return ;
    198. }
    199. } catch (InterruptedException e) {
    200. e.printStackTrace();
    201. }
    202. }
    203. }
    204. initAudioTracker() ;
    205. play();
    206. }
    207. }
    208. }

    调用wavplayer播放wav:

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

    1. val wavPlayer = WavPlayer()
    2. 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进行写入。

    1. int bufferFilledCount = 0 ;
    2. while ((read = in.read(buffer , bufferFilledCount , iniBufferSize - bufferFilledCount)) != -1) {
    3. bufferFilledCount += read ;
    4. if (bufferFilledCount >= iniBufferSize) {
    5. byte[] newBuffer = Arrays.copyOf(buffer , iniBufferSize) ;
    6. mSoundData.put(newBuffer) ;
    7. read = 0 ;
    8. bufferFilledCount = 0 ;
    9. }
    10. }

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

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

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

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

  • 相关阅读:
    spring框架
    全国各省市建设工程类专业职称评审要求总结(欢迎补充完善、沟通交流)
    04-二维数组的查找
    智慧风电:数字孪生 3D 风机智能设备运维
    「未来已来」三大办公领域巨头(Google、Microsoft、金山),开启AI办公新纪元
    2022年全国最新消防设施操作员(初级消防设施操作员)模拟题及答案
    MySQL:快照读和当前读
    DVWA 靶场之 Command Injection(命令执行)middle&high
    前端周刊第二十三期
    2024年6月 青少年python一级等级考试真题试卷
  • 原文地址:https://blog.csdn.net/mldxs/article/details/128062029