Android使用AudioTrack播放WAV音频文件
创始人
2024-02-20 19:10:09

目录

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_playerimport 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 = 0var mFormat = "WAVE"var mSubChunk1ID = "fmt "var mSubChunk1Size = 16var mAudioFormat: Short = 1var mNumChannel: Short = 1var mSampleRate = 8000var mByteRate = 0var mBlockAlign: Short = 0var mBitsPerSample: Short = 8var mSubChunk2ID = "data"var mSubChunk2Size = 0constructor() {}constructor(chunkSize: Int, sampleRateInHz: Int, channels: Int, bitsPerSample: Int) {mChunkSize = chunkSizemSampleRate = sampleRateInHzmBitsPerSample = bitsPerSample.toShort()mNumChannel = channels.toShort()mByteRate = mSampleRate * mNumChannel * mBitsPerSample / 8mBlockAlign = (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 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 ;}@Overridepublic 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);}@Overridepublic 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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...