Android-音频相关开发

问题

  1. 如何实现录音,并获取pcm数据
  2. 如何上传(要求用二进制流的content-type上传pcm数据):Content-Type: application/octet-stream
  3. 获取到服务器的音频信息之后如何播放(音频可能有两种,一种url,一种pcm数据流)

实现录音,存成pcm数据

有两个选择:

  • medioRecord
  • audioRecord

区别:

  • medioRecord相对audioRecord更加上层一点,底层使用的是audioRecord
  • medioRecord会把对音频数据做一些编码相关的处理,可以通过它获取到可以直接播放的音频文件,具体格式参见tip1,而audioRecord获取到的是pcm格式数据,该格式数据不能直接播放
  • audioRecord具有实时性

结论:

​ 因为录制的音频要上传给服务端,而且服务端要求pcm流数据,所有选择audioRecord实现。

注意

客户端跟服务端的pcm数据格式一定要一致(采样率,声道等)(坑了我好久。。。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package com.example.mylibrary.audio;

import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Environment;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
* pcm录音utils
* @anthor kb_jay
* create at 2018/8/21 上午10:37
*/
public class AudioRecordUtil {
private static AudioRecordUtil mInstance;
private AudioRecord recorder;
//声音源
private static int audioSource = MediaRecorder.AudioSource.MIC;
//录音的采样频率
private static int audioRate = 16000;
//录音的声道,单声道
private static int audioChannel = AudioFormat.CHANNEL_IN_DEFAULT;
//量化的精度
private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
//缓存的大小
private static int bufferSize = AudioRecord.getMinBufferSize(audioRate, audioChannel, audioFormat);
//记录播放状态
private boolean isRecording = false;
//数字信号数组
private byte[] noteArray;
//PCM文件
private File pcmFile;
//wav文件
private File wavFile;
//文件输出流
private OutputStream os;
//文件根目录
private String basePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/xiaoIceStory/";

//pcm文件目录
private String inFileName = basePath + "/encode.pcm";

public String getPcmFilePath() {
return inFileName;
}

private AudioRecordUtil() {
//创建文件
createFile();
}

//创建文件夹,首先创建目录,然后创建对应的文件
private void createFile() {
File baseFile = new File(basePath);
if (!baseFile.exists()) {
baseFile.mkdirs();
}
pcmFile = new File(basePath + "/encode.pcm");

if (pcmFile.exists()) {
pcmFile.delete();
}
try {
pcmFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}

public static AudioRecordUtil getInstance() {
if (mInstance == null) {
synchronized (AudioRecordUtil.class){
if(mInstance==null){
mInstance = new AudioRecordUtil();
}
}
}
return mInstance;
}

//读取录音数字数据线程
class WriteThread implements Runnable {
@Override
public void run() {
writeData();
}
}

//录音线程执行体
private void writeData() {
noteArray = new byte[bufferSize];
//建立文件输出流
try {
os = new BufferedOutputStream(new FileOutputStream(pcmFile));
} catch (FileNotFoundException e) {
e.printStackTrace();
}

while (isRecording) {
int recordSize = recorder.read(noteArray, 0, bufferSize);
if (recordSize > 0) {
try {
os.write(noteArray);
} catch (IOException e) {
e.printStackTrace();
}
}
}

if (os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

//开始录音
public void startRecord() {
isRecording = true;
recorder = new AudioRecord(audioSource, audioRate,
audioChannel, audioFormat, bufferSize);
recorder.startRecording();
}

//记录数据
public void recordData() {
new Thread(new WriteThread()).start();
}

//停止录音
public void stopRecord() {
if (recorder != null) {
isRecording = false;
recorder.stop();
recorder.release();

}
}
}

参考:

https://developer.android.com/reference/android/media/MediaRecorder

https://developer.android.com/guide/topics/media/mediarecorder

https://www.jianshu.com/p/90c4071c7768

http://www.cnblogs.com/renhui/p/7463287.html

实现上传音频pcm数据流

  • content-type用来指定数据格式,比如multipart/form-data,application/json,application/octet-stream(二进制流),application/text等。详细参见http://www.runoob.com/http/http-content-type.html
  • okhttp中提供了设置content-type的方法:
1
2
3
4
/** Returns a new request body that transmits {@code content}. */
public static RequestBody create(final @Nullable MediaType contentType, final byte[] content) {
return create(contentType, content, 0, content.length);
}

因为服务端对requestBody格式有要求:

1
2
3
4
5
6
7
8
9
10
11
12
--this-is-a-boundary
Content-Disposition: form-data; name="metadata"
Content-Type: application/json; charset=UTF-8

{xxx这是一段json格式的数据xxx}

--this-is-a-boundary
Content-Disposition: form-data; name="audio"
Content-Type: application/octet-stream

pcm数据
--this-is-a-boundary--

所以采用如下方式:

1
2
@POST()
Flowable<ResponseBody> uploadPcm(@Url String url, @Body RequestBody body, @Header("xxx") String appId);

其中的body如下:

1
2
requestBody = RequestBody.create(MediaType.parse("*/*;charset=utf-8"), contentBytes, 0, contentBytes.length);
//contentBytes为服务端要求的requestBody对应的byte数组。

播放pcm数据使用AudioTrack

注意pcm数据格式需要跟服务端的一致:声道,采样率等。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.ms.xiaoicestorydemo.util;

import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;

import java.io.IOException;
import java.io.RandomAccessFile;

/**
* pcm数据播放utils
*
* @anthor kb_jay
* create at 2018/8/21 上午10:38
*/
public class AudioPlayUtils {

private final int BUFFER_SIZE = 1024 * 2;
private byte[] mBuffer = new byte[BUFFER_SIZE];
private AudioPlayUtils() {
}

private static AudioPlayUtils instance;

public static AudioPlayUtils getInstance() {
if (instance == null) {
synchronized (AudioPlayUtils.class) {
if (instance == null) {
instance = new AudioPlayUtils();
}
}
}
return instance;
}

//播放pcm文件音频
public void playPcmFileAudio(RandomAccessFile raf) {
int streamType = AudioManager.STREAM_MUSIC;
int simpleRate = 16000;
int channelConfig = AudioFormat.CHANNEL_IN_DEFAULT;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
int mode = AudioTrack.MODE_STREAM;

int minBufferSize = AudioTrack.getMinBufferSize(simpleRate, channelConfig, audioFormat);
AudioTrack audioTrack = new AudioTrack(streamType, simpleRate, channelConfig, audioFormat,
Math.max(minBufferSize, BUFFER_SIZE), mode);
audioTrack.play();
try {
int read;
while ((read = raf.read(mBuffer)) > 0) {
audioTrack.write(mBuffer, 0, read);
}
} catch (RuntimeException | IOException e) {
e.printStackTrace();
}
}
}

播放在线音频

使用exoMedia:git地址:https://github.com/brianwernick/ExoMedia

其中使用的是exoPlayer:git地址:https://github.com/google/ExoPlayer

tip1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    public static final int DEFAULT = 0;
/** 3GPP media file format*/
public static final int THREE_GPP = 1;
/** MPEG4 media file format*/
public static final int MPEG_4 = 2;
/** The following formats are audio only .aac or .amr formats */
/**
* AMR NB file format
* @deprecated Deprecated in favor of MediaRecorder.OutputFormat.AMR_NB
*/
public static final int RAW_AMR = 3;
/** AMR NB file format */
public static final int AMR_NB = 3;
/** AMR WB file format */
public static final int AMR_WB = 4;
/** @hide AAC ADIF file format */
public static final int AAC_ADIF = 5;
/** AAC ADTS file format */
public static final int AAC_ADTS = 6;
/** @hide Stream over a socket, limited to a single stream */
public static final int OUTPUT_FORMAT_RTP_AVP = 7;
/** H.264/AAC data encapsulated in MPEG2/TS */
public static final int MPEG_2_TS = 8;
/** VP8/VORBIS data in a WEBM container */
public static final int WEBM = 9;
};