Android uses AudioTrack to play WAV audio files

Table of contents

1. wav file format

2. Analysis of wav files

3. Wav file playback

QA:


When I started playing wav, I used the system player mediaplayer to play it, but the support of mediaplayer is really not good.

I did pcm playback many years ago and used audiotrack. Reference: Android uses AudioTrack to play PCM format audio_mldxs' Blog - CSDN Blog

In fact, there is only one wav file header difference between WAV and PCM, so a set of audiotrack functions for playing wav has been realized. Support local file playback and network file playback at the same time

1. wav file format

Referenced: wav file format analysis_full-time coding blog-CSDN blog_wav file format

General structure of wav file

 Among the fields that are more important to us:

  1. NumChannels: channel (generally 1-8)
  2. SampleRate: Sampling frequency (8000, 16000, 44100, 48000 are common)
  3. BitsPerSample: Sampling precision (commonly 8, 16, 32, representing a sample occupies 1, 2, 4 bytes respectively)

For the explanation of the rest of the fields, see wav file format analysis for details_full-time coding blog-CSDN blog_wav file format

The wav file header has a total of 44 bytes, followed by the pcm data, which is the real playback data.

2. Analysis of wav files

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 file playback

There are generally 3 steps to play wav with audiotrack:

  1. download wav file
  2. Initialize audiotrack (initialize audiotrack depends on the information just parsed wav file header)
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 to play audio

The following is the code for actually playing wav. The code is very simple, so I won’t introduce too much:

Use a separate Downloader thread to download wav files to speed up buffering and avoid stuttering and noise during playback.

Use the RealPlayer thread to play the wav file.

Among them, the Downloader thread corresponds to the producer, and the RealPlayer corresponds to the consumer. mSoundData is a buffer between producers and consumers.

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();
        }
    }
}

Call wavplayer to play wav:

wavplayer.play(url, whether it is a local wav file)

 val wavPlayer = WavPlayer()
 wavPlayer.play("/sdcard/Music/3.wav" , true)

QA:

Q: 1. There is a popping sound in the first frame of playing wav.

A: Since the wav file has a 44-byte file header, it is necessary to skip the wav file header and write to AudioTrack.write when reading the file.

Q: 2. There is noise when playing network wav.

A: Since the number of bytes read by the network to read the wav file each time will be much smaller than the minbuffer we set, so every time we read the network stream, we have to wait for the minbuffer to be filled before using AudioTrack.write to write enter.

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. Failed to play wav, all are noises.

A: Check the wav file header to check the sampling precision of wav. If the sampling precision is 32, you must use write(float[]), otherwise the playback will definitely fail.

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

The complete source code has been uploaded: https://gitee.com/gggl/wav-player

Guess you like

Origin blog.csdn.net/mldxs/article/details/128062029