零、前言
今天比较简单,先理一下录制和播放的四位大将
再说一下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源码就616行,小巧很多,看到pool肯定是池啦杀鸡焉用牛刀
,对于经常播放比较短小的音效,用SoundPool更好
1.初始化
做一个两个音效每次点击依次播放一个的效果
private SoundPool mSp;private HashMapmSoundMap = 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
#includeextern "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.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流 3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正 4----看到这里,我在此感谢你的喜欢与支持