零、前言
今天比较简单,先理一下录制和播放的四位大将
再说一下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
int audioSource
1.2:声道信息:
int channelConfig
int channelConfig
录音的声道信息是加IN的
1.3:数据输出格式:
audioFormat
audioFormat
2.MediaRecorder(基于文件录音)
优点:
MediaRecorder录制的音频文件是经过压缩后的
已集成了录音,编码,压缩等,支持一些的音频格式文件(.arm,.mp3,.3gp,.aac,.mp4,.webm)
操作简单,不须自己处理字节流,传入文件即可
缺点:
无法实现实时处理音频,输出的音频格式少。
复制代码
2.1:音频来源:
int audio_source
int audio_source
和AudioRecord的基本一致
2.2:输出格式:
int output_format
int output_format
2.3:音频编码方式:
int video_encoder
int video_encoder
3.AudioTrack
AudioTrack只能播放已经解码的PCM流(wav音频格式文件)
复制代码
3.1:流类型:
int streamType
int streamType
3.2:模式:
int mode
int mode
MODE_STREAM:适合大文件
通过write一次次把音频数据写到AudioTrack中。
用户提供的Buffer数据-->AudioTrack内部的Buffer,这在一定程度上会使引入延时。
MODE_STATIC:适合小文件
所有数据通过一次write调用传递到AudioTrack中的内部缓冲区。
这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。
复制代码
3.3:播放声道:
int channelConfig
int channelConfig
录音的声道信息是加OUT的
3.4:数据输出格式:
int audioFormat
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<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);
复制代码
四、变速播放
0.回顾一下第一天对声音的介绍:声音三要素
[1] 音量 :(响度)声波震动幅度---A--分贝
[2] 音调 : 声音频率(高音--频率快--声音尖 低音--频率慢--声音沉)----f--Hz
[3] 音色 :(音品)与材质有关 本质是谐波
复制代码
变速的实现:
播放时采样频率进行倍速,使得周期发生变化。
如两倍速时,采样频率*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中使用
布局挺简单的,不废话了
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++的函数对应,用来处理数据流的
/**
* 作者:张风捷特烈<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中
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.本文成长记录及勘误表
项目源码 |
日期 | 备注 |
---|---|---|
V0.1-github |
2018-1-7 |
Android多媒体之SoundPool+pcm流的音频操作 |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github |
我的简书 |
我的掘金 |
个人网站 |
3.声明
1—-本文由张风捷特烈原创,转载请注明
2—-欢迎广大编程爱好者共同交流
3—-个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4—-看到这里,我在此感谢你的喜欢与支持