目录

1、wav文件格式

2、wav文件解析

3、wav文件播放

QA:


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

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

其实WAV和PCM两者之间只差了一个wav文件头而已,所以实现了一套audiotrack播放wav的功能。同时支持本地文件播放和网络文件播放

1、wav文件格式

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

Android 打开WAV wav格式怎么用手机打开_android

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

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

其余字段解释,详见wav文件格式解析_全职编码的博客_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(初始化audiotrack依赖刚刚解析wav文件头的信息)
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播放音频

下面是真正播放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)

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