服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - Android - Android音频编辑之音频转换PCM与WAV

Android音频编辑之音频转换PCM与WAV

2022-09-16 17:05Ihesong Android

这篇文章主要为大家详细介绍了Android音频编辑之音频转换PCM与WAV,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

前言

本篇开始讲解在Android平台上进行的音频编辑开发,首先需要对音频相关概念有基础的认识。所以本篇要讲解以下内容:

1. 常用音频格式简介
2. WAV和PCM的区别和联系
3. WAV文件头信息
4. 采样率简介
5. 声道数和采样位数下的PCM编码
6. 音频文件解码
7. PCM文件转WAV文件

现在先给出音频编辑的效果图,看看能不能提高大家的积极性~,哈哈

Android音频编辑之音频转换PCM与WAV

Android音频编辑之音频转换PCM与WAV

Android音频编辑之音频转换PCM与WAV

常用音频格式简介

在Android平台上进行音频开发,首先需要对常用的音频格式有个大致的了解。在Android平台上,常用的音频格式有:

  • WAV

WAV格式是微软公司开发的一种声音文件格式,也叫波形声音文件,是最早的数字音频格式,被Windows平台及其应用程序广泛支持。

WAV格式支持许多压缩算法,支持多种音频位数、采样频率和声道,采用44.1kHz的采样频率,16位量化位数,因此WAV的音质与CD相差无几,但WAV格式对存储空间需求太大不便于交流和传播。

补充:无损格式,缺点:体积十分大!

  • MP3

MP3的全称是Moving Picture Experts Group Audio Layer III。简单的说,MP3就是一种音频压缩技术,由于这种压缩方式的全称叫MPEG Audio Layer3,所以人们把它简称为MP3。
MP3是利用 MPEG Audio Layer 3 的技术,将音乐以1:10 甚至 1:12 的压缩率,压缩成容量较小的file,换句话说,能够在音质丢失很小的情况下把文件压缩到更小的程度。而且还非常好的保持了原来的音质。

正是因为MP3体积小,音质高的特点使得MP3格式几乎成为网上音乐的代名词。每分钟音乐的MP3格式只有1MB左右大小,这样每首歌的大小只有3-4MB。使用MP3播放器对MP3文件进行实时的解压缩(解码),这样,高品质的MP3音乐就播放出来了。

补充:最高比特率320K,高频部分一刀切是他的缺点。音质不高!

  • AMR

全称Adaptive Multi-Rate 和 Adaptive Multi-Rate Wideband,主要用于移动设备的音频,压缩比比较大,但相对其他的压缩格式质量比较差,多用于人声,通话,效果还是很不错的。

  • Ogg

Ogg全称应该是OGG Vobis(ogg Vorbis) 是一种新的音频压缩格式,类似于MP3等现有的音乐格式。
但有一点不同的是,它是完全免费、开放和没有专利限制的。OGG Vobis有一个很出众的特点,就是支持多声道,随着它的流行,以后用随身听来听DTS编码的多声道作品将不会是梦想。

Vorbis 是这种音频压缩机制的名字,而Ogg则是一个计划的名字,该计划意图设计一个完全开放性的多媒体系统。目前该计划只实现了OggVorbis这一部分。

Ogg Vorbis文件的扩展名是.OGG。这种文件的设计格式是非常先进的。现在创建的OGG文件可以在未来的任何播放器上播放,因此,这种文件格式可以不断地进行大小和音质的改良,而不影响旧有的编码器或播放器。
补充:目前最好的有损格式之一,MP3部分支持,智能手机装软件部分可以支持,最高比特率500kbps。

  • AAC

AAC(Advanced Audio Coding),中文称为“高级音频编码”,出现于1997年,基于 MPEG-2的音频编码技术。

优点:相对于mp3,AAC格式的音质更佳,文件更小。

不足:AAC属于有损压缩的格式,与时下流行的APE、FLAC等无损格式相比音质存在“本质上”的差距。加之,目前传输速度更快的USB3.0和16G以上大容量MP3正在加速普及,也使得AAC头上“小巧”的光环不复存在了。

前景:以发展的眼光来看,正如“高清”正在被越来越多的人所接受一样,“无损”必定是未来音乐格式的绝对主流。AAC这种“有损”格式的前景不容乐观

  • FLAC

FLAC即是Free Lossless Audio Codec的缩写,中文可解为无损音频压缩编码。

FLAC是一套著名的自由音频压缩编码,其特点是无损压缩。不同于其他有损压缩编码如MP3 及 AAC,它不会破任何原有的音频资讯,所以可以还原音乐光盘音质。现在它已被很多软件及硬件音频产品所支持。简而言之,FLAC与MP3相仿,但是是无损压缩的,也就是说音频以FLAC方式压缩不会丢失任何信息。这种压缩与Zip的方式类似,但是FLAC将给你更大的压缩比率,因为FLAC是专门针对音频的特点设计的压缩方式,并且你可以使用播放器播放FLAC压缩的文件,就象通常播放你的MP3文件一样。

补充:为无损格式,较ape而言,他体积大点,但是兼容性好,编码速度快,播放器支持更广。

WAV和PCM的区别和联系

在Android平台上要进行音频编辑操作(比如裁剪,插入,合成等),通常都是需要将音频文件解码为WAV格式的音频文件或者PCM文件。那么WAV和PCM之间有什么关系,这里有必要了解一下。

PCM(Pulse Code Modulation—-脉码调制录音)。所谓PCM录音就是将声音等模拟信号变成符号化的脉冲列,再予以记录。PCM信号是由[1]、[0]等符号构成的数字信号,而未经过任何编码和压缩处理。与模拟信号比,它不易受传送系统的杂波及失真的影响。动态范围宽,可得到音质相当好的影响效果。也就是说,PCM就是没有压缩的编码方式,PCM文件就是采用PCM这种没有压缩的编码方式编码的音频数据文件。

WAV是由微软开发的一种音频格式。WAV符合 PIFF Resource Interchange File Format规范。所有的WAV都有一个文件头,这个文件头音频流的编码参数。WAV对音频流的编码没有硬性规定,除了PCM之外,还有几乎所有支持ACM规范的编码都可以为WAV的音频流进行编码。WAV也可以使用多种音频编码来压缩其音频流,不过我们常见的都是音频流被PCM编码处理的WAV,但这不表示WAV只能使用PCM编码,MP3编码同样也可以运用在WAV中,和AVI一样,只要安装好了相应的Decode,就可以欣赏这些WAV了。

在Windows平台下,基于PCM编码的WAV是被支持得最好的音频格式,所有音频软件都能完美支持,由于本身可以达到较高的音质的要求,因此,WAV也是音乐编辑创作的首选格式,适合保存音乐素材。因此,基于PCM编码的WAV被作为了一种中介的格式,常常使用在其他编码的相互转换之中,例如MP3转换成WMA。

如上引用的描述,也就是说我们对音频进行编辑操作,其实就是音频解码后的PCM音频采样数据进行操作,因为PCM记录的就是采样后的音频信息,而我们常说的WAV文件是在PCM数据的基础上添加一组头信息,用于描述这个WAV文件的采样率,声道数,采样位数,音频数据大小等信息,这样这个WAV就可以被音频播放器正确读取并播放,而单纯的PCM文件因为只有编码的音频数据,没有其他描述信息,所以无法被音频播放器识别播放。

WAV文件头信息

接下来有必要了解一下WAV文件头信息是什么样的格式信息。

WAV文件头信息由大小44个字节的数据组成:

4字节数据,内容为“RIFF”,表示资源交换文件标识
4字节数据,内容为一个整数,表示从下个地址开始到文件尾的总字节数
4字节数据,内容为“WAVE”,表示WAV文件标识
4字节数据,内容为“fmt ”,表示波形格式标识(fmt ),最后一位空格。
4字节数据,内容为一个整数,表示PCMWAVEFORMAT的长度
2字节数据,内容为一个短整数,表示格式种类(值为1时,表示数据为线性PCM编码)
2字节数据,内容为一个短整数,表示通道数,单声道为1,双声道为2
4字节数据,内容为一个整数,表示采样率,比如44100
4字节数据,内容为一个整数,表示波形数据传输速率(每秒平均字节数),大小为 采样率 * 通道数 * 采样位数
2字节数据,内容为一个短整数,表示DATA数据块长度,大小为 通道数 * 采样位数
2字节数据,内容为一个短整数,表示采样位数,即PCM位宽,通常为8位或16位
4字节数据,内容为“data”,表示数据标记符
4字节数据,内容为一个整数,表示接下来声音数据的总大小

由以上信息可知,对于一个PCM文件来说,只要知道它的大小,采样率,声道数,采样位数,就可以通过添加一个WAV文件头得到一个WAV文件了。

采样率简介

那么采样率是什么意思,我们来了解下。

音频采样率是指录音设备在一秒钟内对声音信号的采样次数,采样频率越高声音的还原就越真实越自然。在当今的主流采集卡上,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级,22.05KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则更加精确一些。

在数字音频领域,常用的采样率有:

8,000 Hz - 电话所用采样率, 对于人的说话已经足够
11,025 Hz

22,050 Hz - 无线电广播所用采样率

32,000 Hz - miniDV 数码视频 camcorder、DAT (LP mode)所用采样率

44,100 Hz - 音频 CD, 也常用于 MPEG-1 音频(VCD, SVCD, MP3)所用采样率

47,250 Hz - 商用 PCM 录音机所用采样率

48,000 Hz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率

50,000 Hz - 商用数字录音机所用采样率

96,000 或者 192,000 Hz - DVD-Audio、一些 LPCM DVD 音轨、BD-ROM(蓝光盘)音轨、和 HD-DVD (高清晰度 DVD)音轨所用所用采样率

2.8224 MHz - Direct Stream Digital 的 1 位 sigma-delta modulation 过程所用采样率。

通常歌曲的采样率是44100,而Android平台的人声录音支持8000,16000,32000三种采样率。

声道数和采样位数下的PCM编码
接下来再了解下声道数和采样位数代表什么意思,在PCM编码中是如何应用的。

声道通常可以分为单声道和双声道,双声道又分为左声道和右声道。

采样位数表示一个采样数据用多少位来表示,通常为8位和16位,对于8位表示一个字节来表示一个采样数据,16位表示用两个字节表示一个采样数据,两个字节为低位字节和高位字节,通常低位字节在前,高位字节在后。

因此结合声道和采样字节数(采样位数),可以组成下图的PCM数据格式:

 

可以看到8位单声道的PCM数据,只需要一个字节就能表示一个采样数据,而16位双声道(立体声)的PCM数据,需要4个字节来表示一个采样数据。那么计算一个PCM大小的方法就很简单了。

对于8位单声道,采样率为8000,1分钟的PCM音频来说,大小是

?
1
2
//采样率 * 通道数 * 采样位数/8 * 秒数
8000 * 1 * 8/8 * 60 = 480000,大约480k

对于16位双声道,采样率为44100,1分钟的PCM音频来说,大小是

?
1
2
//采样率 * 通道数 * 采样位数/8 * 秒数
44100 * 2 * 16/8 * 60 = 10584000,大约10M

而WAV文件的大小就是比PCM多出44个字节数。

音频文件解码

有了以上音频相关知识的了解之后,现在可以来对android上常用音频文件进行解码和信息提取了。这里涉及了三个音频相关的类:
- MediaExtractor 媒体文件数据提取器,负责媒体文件数据的提取操作。
- MediaFormat 媒体文件格式信息,负责读取媒体文件的格式(如采样率,时长,声道数等)信息。
- MediaCodec 媒体文件编解码类,负责媒体文件数据的编解码操作。

解码器支持解码常用的音频格式,如mp3, wav, 3gpp, 3gp, amr, aac, m4a, ogg, flac等,解码后的数据是PCM编码的数据。下面用代码实现下如何用上述类实现音频文件的解码操作,得到一个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
/**
* 将音乐文件解码
*
* @param musicFileUrl 源文件路径
* @param decodeFileUrl 解码文件路径
* @param startMicroseconds 开始时间 微秒
* @param endMicroseconds 结束时间 微秒
* @param decodeOperateInterface 解码过程回调
*/
private boolean decodeMusicFile(String musicFileUrl, String decodeFileUrl,
 long startMicroseconds, long endMicroseconds, DecodeOperateInterface decodeOperateInterface) {
 
//采样率,声道数,时长,音频文件类型
int sampleRate = 0;
int channelCount = 0;
long duration = 0;
String mime = null;
 
//MediaExtractor, MediaFormat, MediaCodec
MediaExtractor mediaExtractor = new MediaExtractor();
MediaFormat mediaFormat = null;
MediaCodec mediaCodec = null;
 
//给媒体信息提取器设置源音频文件路径
try {
 mediaExtractor.setDataSource(musicFileUrl);
}catch (Exception ex){
 ex.printStackTrace();
 try {
 mediaExtractor.setDataSource(new FileInputStream(musicFileUrl).getFD());
 } catch (Exception e) {
 e.printStackTrace();
 LogUtil.e("设置解码音频文件路径错误");
 }
}
 
//获取音频格式轨信息
mediaFormat = mediaExtractor.getTrackFormat(0);
 
//从音频格式轨信息中读取 采样率,声道数,时长,音频文件类型
sampleRate = mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ? mediaFormat.getInteger(
 MediaFormat.KEY_SAMPLE_RATE) : 44100;
channelCount = mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ? mediaFormat.getInteger(
 MediaFormat.KEY_CHANNEL_COUNT) : 1;
duration = mediaFormat.containsKey(MediaFormat.KEY_DURATION) ? mediaFormat.getLong(
 MediaFormat.KEY_DURATION) : 0;
mime = mediaFormat.containsKey(MediaFormat.KEY_MIME) ? mediaFormat.getString(MediaFormat.KEY_MIME)
  : "";
 
LogUtil.i("歌曲信息Track info: mime:"
 + mime
 + " 采样率sampleRate:"
 + sampleRate
 + " channels:"
 + channelCount
 + " duration:"
 + duration);
 
if (TextUtils.isEmpty(mime) || !mime.startsWith("audio/")) {
 LogUtil.e("解码文件不是音频文件mime:" + mime);
 return false;
}
 
if (mime.equals("audio/ffmpeg")) {
 mime = "audio/mpeg";
 mediaFormat.setString(MediaFormat.KEY_MIME, mime);
}
 
if (duration <= 0) {
 LogUtil.e("音频文件duration为" + duration);
 return false;
}
 
//解码的开始时间和结束时间
startMicroseconds = Math.max(startMicroseconds, 0);
endMicroseconds = endMicroseconds < 0 ? duration : endMicroseconds;
endMicroseconds = Math.min(endMicroseconds, duration);
 
if (startMicroseconds >= endMicroseconds) {
 return false;
}
 
//创建一个解码器
try {
 mediaCodec = MediaCodec.createDecoderByType(mime);
 
 mediaCodec.configure(mediaFormat, null, null, 0);
} catch (Exception e) {
 LogUtil.e("解码器configure出错");
 return false;
}
 
//得到输出PCM文件的路径
decodeFileUrl = decodeFileUrl.substring(0, decodeFileUrl.lastIndexOf("."));
String pcmFilePath = decodeFileUrl + ".pcm";
 
//后续解码操作
getDecodeData(mediaExtractor, mediaCodec, pcmFilePath, sampleRate, channelCount,
 startMicroseconds, endMicroseconds, decodeOperateInterface);
 
return true;
}

以上操作创建了MediaExtractor,获取MediaFormat用于读取音频文件的相关信息如采样率,文件类型,声道数等。然后创建了MediaCodec用于后续和MediaExtractor一起进行音频的解码操作。接下来看看具体的解码过程:

?
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* 解码数据
*/
private void getDecodeData(MediaExtractor mediaExtractor, MediaCodec mediaCodec,
 String decodeFileUrl, int sampleRate, int channelCount, final long startMicroseconds,
 final long endMicroseconds, final DecodeOperateInterface decodeOperateInterface) {
 
//初始化解码状态,未解析完成
boolean decodeInputEnd = false;
boolean decodeOutputEnd = false;
 
//当前读取采样数据的大小
int sampleDataSize;
//当前输入数据的ByteBuffer序号,当前输出数据的ByteBuffer序号
int inputBufferIndex;
int outputBufferIndex;
//音频文件的采样位数字节数,= 采样位数/8
int byteNumber;
 
//上一次的解码操作时间,当前解码操作时间,用于通知回调接口
long decodeNoticeTime = System.currentTimeMillis();
long decodeTime;
 
//当前采样的音频时间,比如在当前音频的第40秒的时候
long presentationTimeUs = 0;
 
//定义编解码的超时时间
final long timeOutUs = 100;
 
//存储输入数据的ByteBuffer数组,输出数据的ByteBuffer数组
ByteBuffer[] inputBuffers;
ByteBuffer[] outputBuffers;
 
//当前编解码器操作的 输入数据ByteBuffer 和 输出数据ByteBuffer,可以从targetBuffer中获取解码后的PCM数据
ByteBuffer sourceBuffer;
ByteBuffer targetBuffer;
 
//获取输出音频的媒体格式信息
MediaFormat outputFormat = mediaCodec.getOutputFormat();
 
MediaCodec.BufferInfo bufferInfo;
 
byteNumber = (outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") : 0) / 8;
 
//开始解码操作
mediaCodec.start();
 
//获取存储输入数据的ByteBuffer数组,输出数据的ByteBuffer数组
inputBuffers = mediaCodec.getInputBuffers();
outputBuffers = mediaCodec.getOutputBuffers();
 
mediaExtractor.selectTrack(0);
 
//当前解码的缓存信息,里面的有效数据在offset和offset+size之间
bufferInfo = new MediaCodec.BufferInfo();
 
//获取解码后文件的输出流
BufferedOutputStream bufferedOutputStream =
 FileFunction.getBufferedOutputStreamFromFile(decodeFileUrl);
 
//开始进入循环解码操作,判断读入源音频数据是否完成,输出解码音频数据是否完成
while (!decodeOutputEnd) {
 if (decodeInputEnd) {
 return;
 }
 
 decodeTime = System.currentTimeMillis();
 
 //间隔1秒通知解码进度
 if (decodeTime - decodeNoticeTime > Constant.OneSecond) {
 final int decodeProgress =
  (int) ((presentationTimeUs - startMicroseconds) * Constant.NormalMaxProgress
   / endMicroseconds);
 
 if (decodeProgress > 0) {
  notifyProgress(decodeOperateInterface, decodeProgress);
 }
 
 decodeNoticeTime = decodeTime;
 }
 
 try {
 
 //操作解码输入数据
 
 //从队列中获取当前解码器处理输入数据的ByteBuffer序号
 inputBufferIndex = mediaCodec.dequeueInputBuffer(timeOutUs);
 
 if (inputBufferIndex >= 0) {
  //取得当前解码器处理输入数据的ByteBuffer
  sourceBuffer = inputBuffers[inputBufferIndex];
  //获取当前ByteBuffer,编解码器读取了多少采样数据
  sampleDataSize = mediaExtractor.readSampleData(sourceBuffer, 0);
 
  //如果当前读取的采样数据<0,说明已经完成了读取操作
  if (sampleDataSize < 0) {
  decodeInputEnd = true;
  sampleDataSize = 0;
  } else {
  presentationTimeUs = mediaExtractor.getSampleTime();
  }
 
  //然后将当前ByteBuffer重新加入到队列中交给编解码器做下一步读取操作
  mediaCodec.queueInputBuffer(inputBufferIndex, 0, sampleDataSize, presentationTimeUs,
   decodeInputEnd ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
 
  //前进到下一段采样数据
  if (!decodeInputEnd) {
  mediaExtractor.advance();
  }
 
 } else {
  //LogUtil.e("inputBufferIndex" + inputBufferIndex);
 }
 
 //操作解码输出数据
 
 //从队列中获取当前解码器处理输出数据的ByteBuffer序号
 outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, timeOutUs);
 
 if (outputBufferIndex < 0) {
  //输出ByteBuffer序号<0,可能是输出缓存变化了,输出格式信息变化了
  switch (outputBufferIndex) {
  case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
   outputBuffers = mediaCodec.getOutputBuffers();
   LogUtil.e(
    "MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED [AudioDecoder]output buffers have changed.");
   break;
  case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
   outputFormat = mediaCodec.getOutputFormat();
 
   sampleRate =
    outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE) ? outputFormat.getInteger(
     MediaFormat.KEY_SAMPLE_RATE) : sampleRate;
   channelCount =
    outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT) ? outputFormat.getInteger(
     MediaFormat.KEY_CHANNEL_COUNT) : channelCount;
   byteNumber =
    (outputFormat.containsKey("bit-width") ? outputFormat.getInteger("bit-width") : 0)
     / 8;
 
   LogUtil.e(
    "MediaCodec.INFO_OUTPUT_FORMAT_CHANGED [AudioDecoder]output format has changed to "
     + mediaCodec.getOutputFormat());
   break;
  default:
   //LogUtil.e("error [AudioDecoder] dequeueOutputBuffer returned " + outputBufferIndex);
   break;
  }
  continue;
 }
 
 //取得当前解码器处理输出数据的ByteBuffer
 targetBuffer = outputBuffers[outputBufferIndex];
 
 byte[] sourceByteArray = new byte[bufferInfo.size];
 
 //将解码后的targetBuffer中的数据复制到sourceByteArray中
 targetBuffer.get(sourceByteArray);
 targetBuffer.clear();
 
 //释放当前的输出缓存
 mediaCodec.releaseOutputBuffer(outputBufferIndex, false);
 
 //判断当前是否解码数据全部结束了
 if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
  decodeOutputEnd = true;
 }
 
 //sourceByteArray就是最终解码后的采样数据
 //接下来可以对这些数据进行采样位数,声道的转换,但这是可选的,默认是和源音频一样的声道和采样位数
 if (sourceByteArray.length > 0 && bufferedOutputStream != null) {
  if (presentationTimeUs < startMicroseconds) {
  continue;
  }
 
  //采样位数转换,按自己需要是否实现
  byte[] convertByteNumberByteArray =
   convertByteNumber(byteNumber, Constant.ExportByteNumber, sourceByteArray);
 
  //声道转换,按自己需要是否实现
  byte[] resultByteArray = convertChannelNumber(channelCount, Constant.ExportChannelNumber,
   Constant.ExportByteNumber, convertByteNumberByteArray);
 
  //将解码后的PCM数据写入到PCM文件
  try {
  bufferedOutputStream.write(resultByteArray);
  } catch (Exception e) {
  LogUtil.e("输出解压音频数据异常" + e);
  }
 }
 
 if (presentationTimeUs > endMicroseconds) {
  break;
 }
 } catch (Exception e) {
 LogUtil.e("getDecodeData异常" + e);
 }
}
 
if (bufferedOutputStream != null) {
 try {
 bufferedOutputStream.close();
 } catch (IOException e) {
 LogUtil.e("关闭bufferedOutputStream异常" + e);
 }
}
 
//重置采样率,按自己需要是否实现
if (sampleRate != Constant.ExportSampleRate) {
 Resample(sampleRate, decodeFileUrl);
}
 
notifyProgress(decodeOperateInterface, 100);
 
//释放mediaCodec 和 mediaExtractor
if (mediaCodec != null) {
 mediaCodec.stop();
 mediaCodec.release();
}
 
if (mediaExtractor != null) {
 mediaExtractor.release();
}
}

以上操作是在一个循环中,不断取得源音频输入数据,加入到输入队列中,交给MediaCodec处理,然后再从解码后的输出队列中取得输出数据,写入到文件中,其中要判断源音频输入数据是否读取完毕,解码后的输出数据是否完成,来终止这个循环。后续的采样位数转换,声道数转换,以及采样率转换都是可选的,不是必须的,默认不实现的话,输出的PCM数据和源音频是一样的采样位数,声道数,和采样率。

PCM文件转WAV文件
现在我们得到了解码后的PCM文件,但是它是不可直接播放的,因为不带音频相关的格式信息,下面我们将PCM和指定的音频相关格式信息去转换得到一个可播放的WAV文件:

?
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
/**
* PCM文件转WAV文件
* @param inPcmFilePath 输入PCM文件路径
* @param outWavFilePath 输出WAV文件路径
* @param sampleRate 采样率,例如44100
* @param channels 声道数 单声道:1或双声道:2
* @param bitNum 采样位数,8或16
*/
public static void convertPcm2Wav(String inPcmFilePath, String outWavFilePath, int sampleRate,
 int channels, int bitNum) {
 
FileInputStream in = null;
FileOutputStream out = null;
byte[] data = new byte[1024];
 
try {
 //采样字节byte率
 long byteRate = sampleRate * channels * bitNum / 8;
 
 in = new FileInputStream(inPcmFilePath);
 out = new FileOutputStream(outWavFilePath);
 
 //PCM文件大小
 long totalAudioLen = in.getChannel().size();
 
 //总大小,由于不包括RIFF和WAV,所以是44 - 8 = 36,在加上PCM文件大小
 long totalDataLen = totalAudioLen + 36;
 
 writeWaveFileHeader(out, totalAudioLen, totalDataLen, sampleRate, channels, byteRate);
 
 int length = 0;
 while ((length = in.read(data)) > 0) {
 out.write(data, 0, length);
 }
} catch (Exception e) {
 e.printStackTrace();
} finally {
 if (in != null) {
 try {
  in.close();
 } catch (IOException e) {
  e.printStackTrace();
 }
 }
 if (out != null) {
 try {
  out.close();
 } catch (IOException e) {
  e.printStackTrace();
 }
 }
}
}
 
 
/**
* 输出WAV文件
* @param out WAV输出文件流
* @param totalAudioLen 整个音频PCM数据大小
* @param totalDataLen 整个数据大小
* @param sampleRate 采样率
* @param channels 声道数
* @param byteRate 采样字节byte率
* @throws IOException
*/
private static void writeWaveFileHeader(FileOutputStream out, long totalAudioLen,
 long totalDataLen, int sampleRate, int channels, long byteRate) throws IOException {
byte[] header = new byte[44];
header[0] = 'R'; // RIFF
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);
header[8] = 'W';//WAVE
header[9] = 'A';
header[10] = 'V';
header[11] = 'E';
//FMT Chunk
header[12] = 'f'; // 'fmt '
header[13] = 'm';
header[14] = 't';
header[15] = ' ';//过渡字节
//数据大小
header[16] = 16; // 4 bytes: size of 'fmt ' chunk
header[17] = 0;
header[18] = 0;
header[19] = 0;
//编码方式 10H为PCM编码格式
header[20] = 1; // format = 1
header[21] = 0;
//通道数
header[22] = (byte) channels;
header[23] = 0;
//采样率,每个通道的播放速度
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);
//音频数据传送速率,采样率*通道数*采样深度/8
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);
// 确定系统一次要处理多少个这样字节的数据,确定缓冲区,通道数*采样位数
header[32] = (byte) (channels * 16 / 8);
header[33] = 0;
//每个样本的数据位数
header[34] = 16;
header[35] = 0;
//Data chunk
header[36] = 'd';//data
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);
}

上面操作其实也很简单,只要你知道了WAV文件头信息的格式,将采样率,声道数,采样位数,PCM音频数据大小等信息填充进去,然后将这个44个字节数据拼接到PCM文件的开头,就得到了一个可播放的WAV文件了。

总结

上文讲解了常用音频文件的格式,采样率,声道,采样位数概念,以及PCM数据是如何构成等内容。然后是如何从音频文件解码为PCM数据文件,以及得到PCM编码的WAV文件,有了以上的理解后,后续进行音频文件的裁剪,插入,合成等编辑操作就更容易理解了。请继续关注后续的音频编辑操作处理。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/hesong1120/article/details/79043482

延伸 · 阅读

精彩推荐