博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Android多媒体之SoundPool+pcm流的音频操作
阅读量:6800 次
发布时间:2019-06-26

本文共 11746 字,大约阅读时间需要 39 分钟。

零、前言

今天比较简单,先理一下录制和播放的四位大将

再说一下SoundPool的使用和pcm转wav
讲一下C++文件如何在Android中使用,也就是传说中的JNI
最后讲一下变速播放和变调播放


一、AudioRecord和MediaRecorder,AudioTrack和MediaPlayer

0.到现在接触了四个类:

第一天:AudioRecord(录音)AudioTrack(音频播放)

第二天:MediaPlayer(媒体播放器--音频部分)
第三天:MediaRecorder(媒体播放器--录音部分)


1.AudioRecord(基于字节流录音)
优点:对音频的实时处理,适合流媒体和语音电话缺点:输出的是PCM的语音数据,需要自己处理字节数据如果保存成音频文件不能被播放器播放PCM采集的数据需要AudioTrack播放,AudioTrack也可以将PCM的数据转换成其他格式复制代码

1.1:音频来源:int audioSource


1.2:声道信息:int channelConfig

录音的声道信息是加IN的


1.3:数据输出格式:audioFormat


2.MediaRecorder(基于文件录音)
优点:MediaRecorder录制的音频文件是经过压缩后的已集成了录音,编码,压缩等,支持一些的音频格式文件(.arm,.mp3,.3gp,.aac,.mp4,.webm)操作简单,不须自己处理字节流,传入文件即可 缺点:无法实现实时处理音频,输出的音频格式少。复制代码

2.1:音频来源:int audio_source

和AudioRecord的基本一致


2.2:输出格式:int output_format


2.3:音频编码方式:int video_encoder


3.AudioTrack
AudioTrack只能播放已经解码的PCM流(wav音频格式文件)复制代码

3.1:流类型:int streamType


3.2:模式:int mode
MODE_STREAM:适合大文件通过write一次次把音频数据写到AudioTrack中。用户提供的Buffer数据-->AudioTrack内部的Buffer,这在一定程度上会使引入延时。MODE_STATIC:适合小文件所有数据通过一次write调用传递到AudioTrack中的内部缓冲区。这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。复制代码


3.3:播放声道:int channelConfig

录音的声道信息是加OUT的


3.4:数据输出格式:int audioFormat

这个和AudioRecord一样


4.MediaPlayer
MediaPlayer可以播放多种格式的声音文件(mp3,w4a,aac)MediaPlayer在framework层也实例化了AudioTrack,其实质是MediaPlayer在framework层进行解码后,生成PCM流,然后代理委托给AudioTrack,最后AudioTrack传递给AudioFlinger进行混音,然后才传递给硬件播放复制代码

二、SoundPool的使用

话说杀鸡焉用牛刀,对于经常播放比较短小的音效,用SoundPool更好

SoundPool源码就616行,小巧很多,看到pool肯定是池啦


1.初始化

做一个两个音效每次点击依次播放一个的效果

private SoundPool mSp;private HashMap
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位pcmString 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);复制代码


四、变速播放

0.回顾一下第一天对声音的介绍:声音三要素
[1] 音量 :(响度)声波震动幅度---A--分贝[2] 音调 : 声音频率(高音--频率快--声音尖 低音--频率慢--声音沉)----f--Hz[3] 音色 :(音品)与材质有关 本质是谐波复制代码

变速的实现:

播放时采样频率进行倍速,使得周期发生变化。  如两倍速时,采样频率*2,波的周期减半,本来2s的波,1s就能放完   由于声音频率变化,声音的效果也随之变化  如2倍速时:频率快,高音,声音尖,0.5倍速时:频率慢,低音,声音沉2倍速是就像一些短视频的倍速变声配音,0.5倍速时就像怪兽的吼声...复制代码

1.代码实现

第一天已经实现了,基于此修改一下

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中使用

布局挺简单的,不废话了

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项目,看一下有哪里不同


2.app的gradle里:


3.CMakeLists.txt何许人也


4.依葫芦画瓢

-----


5.创建native函数


五、音调的变化

本段参考慕课网免费教程

1.Java类

两个临时的float数组是为了和C++的函数对应,用来处理数据流的

/** * 作者:张风捷特烈
* 时间:2019/1/7 0007:9:50
* 邮箱:1981462002@qq.com
* 说明:处理音调的变化 */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 
extern "C"JNIEXPORT void JNICALLJava_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中播放

布局基本一样,在拖拽时设置变声的分率,点击也就播放而已


5.小插曲

有个问题,也就是吱吱的声音,经过测试,发现是bufferSize的锅

如果读取时的缓冲大小和AudioEffect缓冲大小一样,会吱吱地响
经过一点点的调参,发现mMinBufferSize/3.388598效果还行,有一点点吱吱
最后打印一下mMinBufferSize = 7072 ,7072*/3.388598=2086.99
然后灵机一动,不就是2048吗?------然后完美解决...费了我一个多小时...心塞
ok,就这样,我可以很认真的说...到这里刚摸到Android多媒体的门(也就是入门都没有)


后记:捷文规范

1.本文成长记录及勘误表
日期 备注
2018-1-7
2.更多关于我
笔名 QQ 微信 爱好
张风捷特烈 1981462002 zdl1994328 语言
3.声明

1----本文由张风捷特烈原创,转载请注明

2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持


你可能感兴趣的文章
国家工业信息安全产业发展联盟成立
查看>>
雅虎修改公司章程 防止激进投资者进驻董事会
查看>>
卫翰思治下的爱立信:被华为全面超越 近一年股价跌40%
查看>>
超融合设备如何影响私有云部署
查看>>
鳄鱼还是木头?亚信安全提醒:APT攻击防范要当心“水坑”
查看>>
互联健康 共融共生
查看>>
传郭台铭已与夏普高层会晤 商讨收购事宜
查看>>
政府单位专享 国内“特供版”Windows 10真的来了
查看>>
中国百毫秒量子存储器 《自然》:非凡绝技
查看>>
小鱼易连打造基于互联网环境下的高清视频会议终端
查看>>
微型传感器探测屁的来源:可为医疗提供依据
查看>>
历史性时刻!亚马逊股价突破1000美元
查看>>
互联网如何打造智慧医疗:边界确定价值
查看>>
LinkedIn高管告诫年轻人:设定好目标 不要急于求成
查看>>
科林环保子公司签下6.3亿光伏合作协议
查看>>
黑客入侵凯特王妃妹妹账号盗数千照片 欲卖给媒体
查看>>
【人生苦短,我用Python】Python免费精品课连载(1)——Python入门
查看>>
用物联网新技术保市民平安
查看>>
IBM向认知转型 选择混合云路径
查看>>
智能安防市场的痛点到底在哪里?2016欧美消费者调查问卷解读!
查看>>