Android多媒体之SoundPool+pcm流的音频操作

3,482 阅读9分钟

零、前言

今天比较简单,先理一下录制和播放的四位大将
再说一下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-github2018-1-7Android多媒体之SoundPool+pcm流的音频操作
2.更多关于我
笔名QQ微信爱好
张风捷特烈1981462002zdl1994328语言
我的github我的简书我的掘金个人网站
3.声明

1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


icon_wx_200.png