Android使用AudioTrack播放WAV音频文件

Posted mldxs

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android使用AudioTrack播放WAV音频文件相关的知识,希望对你有一定的参考价值。

目录

1、wav文件格式

2、wav文件解析

3、wav文件播放

QA:


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

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

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

1、wav文件格式

参考了:wav文件格式解析_全职编码的博客-CSDN博客_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_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

Android音频系统AudioTrack使用方法详解

今天,简单讲讲AudioTrack的使用方法。 1、Android AudioTrack简介 在android中播放声音可以用MediaPlayer和AudioTrack两种方案的,但是两种方案是有很大区别的,MediaPlayer可以播放多种格式的声音文件,例如MP3,AAC,WAV,OGG,MI

以上是关于Android使用AudioTrack播放WAV音频文件的主要内容,如果未能解决你的问题,请参考以下文章

Android音视频之AudioTrack播放音频

Android音频系统AudioTrack使用方法详解

在android中播放音轨中的音频

Android AudioTrack 在声音开始和结束时点击

MediaPlayer 和 AudioTrack 不输出相同的声音

安卓Android开发:使用AudioRecord录音将录音保存为wav文件使用AudioTrack保存录音