相关文章推荐
酒量小的柿子  ·  char* TCHAR* - CSDN文库·  昨天    · 
高大的灌汤包  ·  Ext ...·  1 年前    · 
冷冷的单杠  ·  MySQL - ...·  1 年前    · 

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

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

1、wav文件格式

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

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

  • NumChannels : 声道(一般1-8)
  • SampleRate:采样频率(常见的有8000,16000,44100,48000)
  • 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个步骤:

  • 下载wav文件
  • 初始化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);
    
  • 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)
    

    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)
    

    完整源码已上传:gitee.com/gggl/wav-pl…

    本文转自 blog.csdn.net/mldxs/artic…,如有侵权,请联系删除。

    分类:
    Android
    标签: