Android多媒體之SoundPool+pcm流的音訊操作

張風捷特烈發表於2019-01-07

零、前言

今天比較簡單,先理一下錄製和播放的四位大將
再說一下SoundPool的使用和pcm轉wav
講一下C++檔案如何在Android中使用,也就是傳說中的JNI
最後講一下變速播放和變調播放


一、AudioRecord和MediaRecorder,AudioTrack和MediaPlayer

0.到現在接觸了四個類:

第一天:AudioRecord(錄音)AudioTrack(音訊播放)
第二天:MediaPlayer(媒體播放器--音訊部分)
第三天:MediaRecorder(媒體播放器--錄音部分)

四類.png


1.AudioRecord(基於位元組流錄音)
優點:
對音訊的實時處理,適合流媒體和語音電話

缺點:
輸出的是PCM的語音資料,需要自己處理位元組資料
如果儲存成音訊檔案不能被播放器播放
PCM採集的資料需要AudioTrack播放,AudioTrack也可以將PCM的資料轉換成其他格式
複製程式碼

1.1:音訊來源:int audioSource

音訊來源.png


1.2:聲道資訊:int channelConfig

錄音的聲道資訊是加IN的

聲道資訊.png


1.3:資料輸出格式:audioFormat

編碼格式.png


2.MediaRecorder(基於檔案錄音)
優點:
MediaRecorder錄製的音訊檔案是經過壓縮後的
已整合了錄音,編碼,壓縮等,支援一些的音訊格式檔案(.arm,.mp3,.3gp,.aac,.mp4,.webm)
操作簡單,不須自己處理位元組流,傳入檔案即可 

缺點:
無法實現實時處理音訊,輸出的音訊格式少。
複製程式碼

2.1:音訊來源:int audio_source

和AudioRecord的基本一致

音訊來源.png


2.2:輸出格式:int output_format

輸出格式.png


2.3:音訊編碼方式:int video_encoder

音訊編碼方式.png


3.AudioTrack
AudioTrack只能播放已經解碼的PCM流(wav音訊格式檔案)
複製程式碼

3.1:流型別:int streamType

流型別.png


3.2:模式:int mode
MODE_STREAM:適合大檔案
通過write一次次把音訊資料寫到AudioTrack中。
使用者提供的Buffer資料-->AudioTrack內部的Buffer,這在一定程度上會使引入延時。

MODE_STATIC:適合小檔案
所有資料通過一次write呼叫傳遞到AudioTrack中的內部緩衝區。
這種模式適用於像鈴聲這種記憶體佔用量較小,延時要求較高的檔案。
複製程式碼

模式.png


3.3:播放聲道:int channelConfig

錄音的聲道資訊是加OUT的

播放聲道.png


3.4:資料輸出格式:int audioFormat

這個和AudioRecord一樣

編碼方式.png


4.MediaPlayer
MediaPlayer可以播放多種格式的聲音檔案(mp3,w4a,aac)
MediaPlayer在framework層也例項化了AudioTrack,
其實質是MediaPlayer在framework層進行解碼後,生成PCM流,然後代理委託給AudioTrack,
最後AudioTrack傳遞給AudioFlinger進行混音,然後才傳遞給硬體播放
複製程式碼

二、SoundPool的使用

話說殺雞焉用牛刀,對於經常播放比較短小的音效,用SoundPool更好
SoundPool原始碼就616行,小巧很多,看到pool肯定是池啦

資原始檔.png


1.初始化

做一個兩個音效每次點選依次播放一個的效果

private SoundPool mSp;
private HashMap<String, Integer> mSoundMap = new HashMap<>();
private boolean isOne;

private void initSound() {
    SoundPool.Builder spb = new SoundPool.Builder();
    //設定可以同時播放的同步流的最大數量
    spb.setMaxStreams(10);
    //建立SoundPool物件
    mSp = spb.build();
    mSoundMap.put("effect1", mSp.load(this, R.raw.fall, 1));
    mSoundMap.put("effect2", mSp.load(this, R.raw.luozi, 1));
}
複製程式碼

2.播放

注意:資源載入完成會稍遲一些,如果載入和播放在上下行執行會無效
你可以初始時載入,稍後有動作再播放,也可以進行加完成載監聽

public void onViewClicked() {
    //資源Id,左音量,右音量,優先順序,迴圈次數,速率
    int id = mSoundMap.get(isOne ? "effect1" : "effect2");
    mSp.play(id, 1.0f, 1.0f, 1, 2, 1.0f);
    isOne = !isOne;
}
複製程式碼

3.載入完成監聽

三個引數:soundPool,第幾個,狀態(0==success)

mSp.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
   
});
複製程式碼

三、pcm與wav

兩者區別:pcm是無法被播放器播放的,wav可以被播放器播放
但它們的實質幾乎一樣,wav相當於披了件衣服(檔案頭),讓播放器認識它
pcm轉為wav並不複雜,就加個頭就行了,網上有很多,這裡參見

符合 RIFF(Resource Interchange FileFormat)規範。
所有的WAV都有一個檔案頭,這個檔案頭音訊流的編碼引數。
資料塊的記錄方式是little-endian位元組順序,標誌符並不是字串而是單獨的符號
複製程式碼

1.程式碼實現:PcmToWavUtil
public class PcmToWavUtil {

    /**
     * 快取的音訊大小
     */
    private int mBufferSize;
    /**
     * 取樣率
     */
    private int mSampleRate;
    /**
     * 聲道數
     */
    private int mChannel;


    /**
     * @param sampleRate sample rate、取樣率
     * @param channel channel、聲道
     * @param encoding Audio data format、音訊格式
     */
    public PcmToWavUtil(int sampleRate, int channel, int encoding) {
        this.mSampleRate = sampleRate;
        this.mChannel = channel;
        this.mBufferSize = AudioRecord.getMinBufferSize(mSampleRate, mChannel, encoding);
    }


    /**
     * pcm檔案轉wav檔案
     *
     * @param inFilename 原始檔路徑
     * @param outFilename 目標檔案路徑
     */
    public void pcmToWav(String inFilename, String outFilename) {
        FileInputStream in;
        FileOutputStream out;
        long totalAudioLen;
        long totalDataLen;
        long longSampleRate = mSampleRate;
        int channels = mChannel == AudioFormat.CHANNEL_IN_MONO ? 1 : 2;
        long byteRate = 16 * mSampleRate * channels / 8;
        byte[] data = new byte[mBufferSize];
        try {
            in = new FileInputStream(inFilename);
            out = new FileOutputStream(outFilename);
            totalAudioLen = in.getChannel().size();
            totalDataLen = totalAudioLen + 36;

            writeWaveFileHeader(out, totalAudioLen, totalDataLen,
                    longSampleRate, channels, byteRate);
            while (in.read(data) != -1) {
                out.write(data);
            }
            in.close();
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 加入wav檔案頭
     */
    private void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
                                     long totalDataLen, long longSampleRate, int channels, long byteRate)
            throws IOException {
        byte[] header = new byte[44];
        // RIFF/WAVE header
        header[0] = 'R';
        header[1] = 'I';
        header[2] = 'F';
        header[3] = 'F';
        header[4] = (byte) (totalDataLen & 0xff);
        header[5] = (byte) ((totalDataLen >> 8) & 0xff);
        header[6] = (byte) ((totalDataLen >> 16) & 0xff);
        header[7] = (byte) ((totalDataLen >> 24) & 0xff);
        //WAVE
        header[8] = 'W';
        header[9] = 'A';
        header[10] = 'V';
        header[11] = 'E';
        // 'fmt ' chunk
        header[12] = 'f';
        header[13] = 'm';
        header[14] = 't';
        header[15] = ' ';
        // 4 bytes: size of 'fmt ' chunk
        header[16] = 16;
        header[17] = 0;
        header[18] = 0;
        header[19] = 0;
        // format = 1
        header[20] = 1;
        header[21] = 0;
        header[22] = (byte) channels;
        header[23] = 0;
        header[24] = (byte) (longSampleRate & 0xff);
        header[25] = (byte) ((longSampleRate >> 8) & 0xff);
        header[26] = (byte) ((longSampleRate >> 16) & 0xff);
        header[27] = (byte) ((longSampleRate >> 24) & 0xff);
        header[28] = (byte) (byteRate & 0xff);
        header[29] = (byte) ((byteRate >> 8) & 0xff);
        header[30] = (byte) ((byteRate >> 16) & 0xff);
        header[31] = (byte) ((byteRate >> 24) & 0xff);
        // block align
        header[32] = (byte) (2 * 16 / 8);
        header[33] = 0;
        // bits per sample
        header[34] = 16;
        header[35] = 0;
        //data
        header[36] = 'd';
        header[37] = 'a';
        header[38] = 't';
        header[39] = 'a';
        header[40] = (byte) (totalAudioLen & 0xff);
        header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
        header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
        header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
        out.write(header, 0, 44);
    }
}
複製程式碼

2.使用:
private static final int DEFAULT_SAMPLE_RATE = 44100;//取樣頻率
private static final int DEFAULT_CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO;//單聲道
private static final int DEFAULT_AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT;//輸出格式:16位pcm

String inPath = "/sdcard/pcm錄音/keke.pcm";
String outPath = "/sdcard/pcm錄音/keke.wav";
PcmToWavUtil pcmToWavUtil = new PcmToWavUtil(DEFAULT_SAMPLE_RATE,DEFAULT_CHANNEL_CONFIG,DEFAULT_AUDIO_FORMAT);
pcmToWavUtil.pcmToWav(inPath,outPath);
複製程式碼

pcm轉wav.png


四、變速播放

0.回顧一下第一天對聲音的介紹:聲音三要素
[1] 音量 :(響度)聲波震動幅度---A--分貝
[2] 音調 : 聲音訊率(高音--頻率快--聲音尖 低音--頻率慢--聲音沉)----f--Hz
[3] 音色 :(音品)與材質有關 本質是諧波
複製程式碼

模擬訊號.png

變速的實現:

播放時取樣頻率進行倍速,使得週期發生變化。  
如兩倍速時,取樣頻率*2,波的週期減半,本來2s的波,1s就能放完   
由於聲音訊率變化,聲音的效果也隨之變化  
如2倍速時:頻率快,高音,聲音尖,0.5倍速時:頻率慢,低音,聲音沉
2倍速是就像一些短視訊的倍速變聲配音,0.5倍速時就像怪獸的吼聲...
複製程式碼

1.程式碼實現

第一天已經實現了播放pcm流的程式碼,基於此修改一下
AudioTrack在讀pcm時可以設定取樣頻率,抽成變數傳進去就行了

/**
 * 啟動播放
 *
 * @param path 檔案了路徑
 */
public void startPlay(String path, int rate) {
    try {
        isStart = true;
        setPath(path);//設定路徑--生成流dis
        mMinBufferSize = AudioTrack.getMinBufferSize(
                rate, DEFAULT_CHANNEL_CONFIG, AudioFormat.ENCODING_PCM_16BIT);
        //例項化AudioTrack
        audioTrack = new AudioTrack(
                DEFAULT_STREAM_TYPE, rate, DEFAULT_CHANNEL_CONFIG,
                DEFAULT_AUDIO_FORMAT, mMinBufferSize * 2, DEFAULT_PLAY_MODE);
        mExecutorService.execute(new PlayRunnable());//啟動播放執行緒
    } catch (Exception e) {
        e.printStackTrace();
    }
}
複製程式碼

2.Activity中使用

佈局挺簡單的,不廢話了

佈局介面.png

private float rate = 1;

//SeekBar的滑動監聽
mIdSb.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        rate = progress / 100.f;
        setInfo();
    }
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
    }
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
    }
});

//點選播放
mIvStartPlay.setOnClickListener(e -> {
    PCMAudioPlayerWithRate.getInstance().startPlay("/sdcard/pcm錄音/20190107075814.pcm", (int) (44100 * rate));
});
複製程式碼

五、JNI的一些簡單認識

1.新建一個支援C++的Android專案,看一下有哪裡不同

新建.png


2.app的gradle裡:

gradle裡多了.png


3.CMakeLists.txt何許人也

CMakeLists.png


4.依葫蘆畫瓢

C++檔案下載地址-----具體演算法解析地址

依葫蘆畫瓢.png


5.建立native函式

jni函式.png

自動生成.png


五、音調的變化

本段參考慕課網免費教程詳見

1.Java類

兩個臨時的float陣列是為了和C++的函式對應,用來處理資料流的

/**
 * 作者:張風捷特烈<br/>
 * 時間:2019/1/7 0007:9:50<br/>
 * 郵箱:1981462002@qq.com<br/>
 * 說明:處理音調的變化
 */
public class AudioEffect {
    private  int mBufferSize;
    private  byte[] mOutBuffer;
    private  float[] mTempInBuffer;
    private  float[] mTempOutBuffer;

    static {
        //載入so庫
        System.loadLibrary("audio-effect");
    }

    public AudioEffect(int bufferSize) {
        mBufferSize = bufferSize;
        mOutBuffer = new byte[mBufferSize];

        mTempInBuffer = new float[mBufferSize/2];
        mTempOutBuffer = new float[mBufferSize/2];
    }

    /**
     * 資料處理
     * @param rate 變換引數
     * @param in 資料
     * @param simpleRate 取樣頻率
     * @return 處理後的資料流
     */
    public synchronized byte[] process(float rate,byte[] in,int simpleRate) {
        native_process(rate,in,mOutBuffer,mBufferSize,simpleRate,mTempInBuffer,mTempOutBuffer);
        return mOutBuffer;
    }

    private static native void native_process(float rate, byte[] in, byte[] out, int size, int simpleRate,float[] tempIn, float[] tempOut);
}
複製程式碼

2.資料的處理:smbPitchShift.cpp
#include <jni.h>

extern "C"
JNIEXPORT void JNICALL
Java_top_toly_sound_audio_effect_AudioEffect_native_1process(JNIEnv *env, jclass type, jfloat rate,
                                                             jbyteArray in_, jbyteArray out_,
                                                             jint size, jint simpleRate,
                                                             jfloatArray tempIn_,
                                                             jfloatArray tempOut_) {
    //array轉化為指標
    jbyte *in = env->GetByteArrayElements(in_, NULL);
    jbyte *out = env->GetByteArrayElements(out_, NULL);
    jfloat *tempIn = env->GetFloatArrayElements(tempIn_, NULL);
    jfloat *tempOut = env->GetFloatArrayElements(tempOut_, NULL);

    // 輸入:byte[]轉為float[]
    for (int i = 0; i < size; i += 2) {
        int lo = in[i] & 0x000000FF;//取低位
        int hi = in[i + 1] & 0x000000FF;//取高位
        int frame = (hi << 8) + lo;//高位左移8位+低位
        tempIn[i >> 1] = (signed short) frame;//
    }

    smbPitchShift(rate, 1024, 1024, 4, simpleRate, tempIn, tempOut);

    //float[]輸出轉為byte
    for (int i = 0; i < size; i += 2) {
        int frame = (int) tempOut[i >> 1];
        out[i] = (jbyte) (frame & 0x000000FF);//取第一個位元組
        out[i + 1] = (jbyte) (frame >> 8);//右移8位,取第二個位元組
    }

    //釋放指標
    env->ReleaseByteArrayElements(in_, in, 0);
    env->ReleaseByteArrayElements(out_, out, 0);
    env->ReleaseFloatArrayElements(tempIn_, tempIn, 0);
    env->ReleaseFloatArrayElements(tempOut_, tempOut, 0);
}
複製程式碼

3.播放對流操作:PCMAudioPlayerWithRat中
//private float rate = 1;//音調分率
 public void setRate(float rate) {
        this.rate = rate;
    }

//開始是初始化startPlay中-----
 if (mAudioEffect == null) {
     L.d(mMinBufferSize + L.l());//7072
     mAudioEffect = new AudioEffect(2048);
 }
 
//PlayRunnable中,讀流時對流進行處理
 //對讀到的流進行處理
tempBuffer = rate == 1 ? tempBuffer :
         mAudioEffect.process(rate, tempBuffer, DEFAULT_SAMPLE_RATE);
複製程式碼

4.Activity中播放

佈局基本一樣,在拖拽時設定變聲的分率,點選也就播放而已

佈局2.png


5.小插曲

有個問題,也就是吱吱的聲音,經過測試,發現是bufferSize的鍋
如果讀取時的緩衝大小和AudioEffect緩衝大小一樣,會吱吱地響
經過一點點的調參,發現mMinBufferSize/3.388598效果還行,有一點點吱吱
最後列印一下mMinBufferSize = 7072 ,7072*/3.388598=2086.99
然後靈機一動,不就是2048嗎?------然後完美解決...費了我一個多小時...心塞
ok,就這樣,我可以很認真的說...到這裡剛摸到Android多媒體的門(也就是入門都沒有)


後記:捷文規範

1.本文成長記錄及勘誤表
專案原始碼 日期 備註
V0.1-github 2018-1-7 Android多媒體之SoundPool+pcm流的音訊操作
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
我的github 我的簡書 我的掘金 個人網站
3.宣告

1----本文由張風捷特烈原創,轉載請註明
2----歡迎廣大程式設計愛好者共同交流
3----個人能力有限,如有不正之處歡迎大家批評指證,必定虛心改正
4----看到這裡,我在此感謝你的喜歡與支援


icon_wx_200.png

相關文章