今天比较简单,先理一下录制和播放的四位大将
再说一下SoundPool的使用和pcm转wav
讲一下C++文件如何在Android中使用,也就是传说中的JNI
最后讲一下变速播放和变调播放html
第一天:
AudioRecord(录音)
、AudioTrack(音频播放)
次日:MediaPlayer(媒体播放器--音频部分)
第三天:MediaRecorder(媒体播放器--录音部分)
git
优势:
对音频的实时处理,适合流媒体和语音电话
缺点:
输出的是PCM的语音数据,须要本身处理字节数据
若是保存成音频文件不能被播放器播放
PCM采集的数据须要AudioTrack播放,AudioTrack也能够将PCM的数据转换成其余格式
复制代码
int audioSource
int channelConfig
录音的声道信息是加IN的github
audioFormat
优势:
MediaRecorder录制的音频文件是通过压缩后的
已集成了录音,编码,压缩等,支持一些的音频格式文件(.arm,.mp3,.3gp,.aac,.mp4,.webm)
操做简单,不须本身处理字节流,传入文件便可
缺点:
没法实现实时处理音频,输出的音频格式少。
复制代码
int audio_source
和AudioRecord的基本一致web
int output_format
int video_encoder
AudioTrack只能播放已经解码的PCM流(wav音频格式文件)
复制代码
int streamType
int mode
MODE_STREAM:适合大文件
经过write一次次把音频数据写到AudioTrack中。
用户提供的Buffer数据-->AudioTrack内部的Buffer,这在必定程度上会使引入延时。
MODE_STATIC:适合小文件
全部数据经过一次write调用传递到AudioTrack中的内部缓冲区。
这种模式适用于像铃声这种内存占用量较小,延时要求较高的文件。
复制代码
int channelConfig
录音的声道信息是加OUT的算法
int audioFormat
这个和AudioRecord同样编程
MediaPlayer能够播放多种格式的声音文件(mp3,w4a,aac)
MediaPlayer在framework层也实例化了AudioTrack,
其实质是MediaPlayer在framework层进行解码后,生成PCM流,而后代理委托给AudioTrack,
最后AudioTrack传递给AudioFlinger进行混音,而后才传递给硬件播放
复制代码
话说
杀鸡焉用牛刀
,对于常常播放比较短小的音效,用SoundPool更好
SoundPool源码就616行,小巧不少,看到pool确定是池啦数组
作一个两个音效每次点击依次播放一个的效果缓存
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));
}
复制代码
注意:资源加载完成会稍迟一些,若是加载和播放在上下行执行会无效
你能够初始时加载,稍后有动做再播放,也能够进行加完成载监听bash
public void onViewClicked() {
//资源Id,左音量,右音量,优先级,循环次数,速率
int id = mSoundMap.get(isOne ? "effect1" : "effect2");
mSp.play(id, 1.0f, 1.0f, 1, 2, 1.0f);
isOne = !isOne;
}
复制代码
三个参数:soundPool,第几个,状态(0==success)微信
mSp.setOnLoadCompleteListener((soundPool, sampleId, status) -> {
});
复制代码
二者区别:pcm是没法被播放器播放的,wav能够被播放器播放
但它们的实质几乎同样,wav至关于披了件衣服(文件头),让播放器认识它
pcm转为wav并不复杂,就加个头就好了,网上有不少,这里参见
符合 RIFF(Resource Interchange FileFormat)规范。
全部的WAV都有一个文件头,这个文件头音频流的编码参数。
数据块的记录方式是little-endian字节顺序,标志符并非字符串而是单独的符号
复制代码
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);
}
}
复制代码
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);
复制代码
[1] 音量 :(响度)声波震动幅度---A--分贝
[2] 音调 : 声音频率(高音--频率快--声音尖 低音--频率慢--声音沉)----f--Hz
[3] 音色 :(音品)与材质有关 本质是谐波
复制代码
变速的实现:
播放时采样频率进行倍速,使得周期发生变化。
如两倍速时,采样频率*2,波的周期减半,原本2s的波,1s就能放完
因为声音频率变化,声音的效果也随之变化
如2倍速时:频率快,高音,声音尖,0.5倍速时:频率慢,低音,声音沉
2倍速是就像一些短视频的倍速变声配音,0.5倍速时就像怪兽的吼声...
复制代码
第一天已经实现了播放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();
}
}
复制代码
布局挺简单的,不废话了
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));
});
复制代码
本段参考
慕课网免费教程
:详见
两个临时的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);
}
复制代码
#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);
}
复制代码
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);
复制代码
布局基本同样,在拖拽时设置变声的分率,点击也就播放而已
有个问题,也就是吱吱的声音,通过测试,发现是bufferSize的锅
若是读取时的缓冲大小和AudioEffect缓冲大小同样,会吱吱地响
通过一点点的调参,发现mMinBufferSize/3.388598效果还行,有一点点吱吱
最后打印一下mMinBufferSize = 7072 ,7072*/3.388598=2086.99
而后灵机一动,不就是2048吗?------而后完美解决...费了我一个多小时...心塞
ok,就这样,我能够很认真的说...到这里刚摸到Android多媒体的门(也就是入门都没有)
项目源码 | 日期 | 备注 |
---|---|---|
V0.1-github | 2018-1-7 | Android多媒体之SoundPool+pcm流的音频操做 |
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
个人github | 个人简书 | 个人掘金 | 我的网站 |
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----我的能力有限,若有不正之处欢迎你们批评指证,一定虚心改正
4----看到这里,我在此感谢你的喜欢与支持