目录
开始播放wav的时候使用了系统的播放器mediaplayer进行播放,但是无奈mediaplayer支持的实在不好。
好些年前自己做过pcm播放使用的是audiotrack,参考:Android 利用AudioTrack播放 PCM 格式音频_mldxs的博客-CSDN博客
其实WAV和PCM两者之间只差了一个wav文件头而已,所以实现了一套audiotrack播放wav的功能。同时支持本地文件播放和网络文件播放
参考了:wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式

其中对我们比较重要的字段:
其余字段解释,详见wav文件格式解析_全职编码的博客-CSDN博客_wav文件格式
wav文件头共44个字节,文件头后紧跟着的就是pcm数据,也就是真正的播放数据了。
- 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 +
- '}'
- }
- }
- }
使用audiotrack播放wav一般有3个步骤:
- 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);
-
- }
下面是真正播放wav的代码了,代码很简单,不做过多介绍了:
使用单独的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();
- }
- }
- }
调用wavplayer播放wav:
wavplayer.play(url , 是否是本地wav文件)
- val wavPlayer = WavPlayer()
- wavPlayer.play("/sdcard/Music/3.wav" , true)
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)