每天一点点音视频_AudioTrack播放动态生成的声波_3

异常顺利, 下面的代码播放中央C的声音:

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

val sampleRate = 44100
val channel = AudioFormat.CHANNEL_OUT_STEREO
val encodingBit = AudioFormat.ENCODING_PCM_FLOAT
val sampleRateForCenterC = 261

val waveGenerator = WaveGenerator(1f, sampleRateForCenterC)

val minBuffSize = AudioTrack.getMinBufferSize(
sampleRate,
channel,
encodingBit
)
val player = AudioTrack.Builder()
.setAudioAttributes(
AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
)
.setAudioFormat(
AudioFormat.Builder()
.setEncoding(encodingBit)
.setSampleRate(sampleRate)
.setChannelMask(channel)
.build()
)
.setBufferSizeInBytes(minBuffSize)
.build()
player.play()
Thread {
val audioData = FloatArray(minBuffSize)
var startTime = 0f
while (!isDestroyed) {
val duration = waveGenerator.generateWave(sampleRate, startTime, audioData)
player.write(audioData, 0, minBuffSize, AudioTrack.WRITE_BLOCKING)
startTime += duration
}
player.stop()
}.start()

让我很意外的是,竟然很好听,纯正的中央C。在我的认识了,乐器的声音都不是单独的正弦波。可能那种复杂决定了音色吧。

还有问题, 播放一段时间后,会增加额外的杂音, 猜测可能是取整这种问题。

还有很多可以搞的, 可以调节频率, 可以显示波形线, 可以对不同频率的音进行叠加等等, 是个大坑。

我的每天一点点,还是专注于音视频的整个领域的知识点,先把面铺的广一点,至于这个坑,会填的。

明天: 每天一点点音视频_MediaCodec


仓库在这 WavePlayer

每天一点点音视频_AudioTrack播放动态生成的声波_2

之前,我们实现了一个获取某个时间的振幅的正弦波的函数, 今天要实现一个方法来获取一段时间的正弦波的数据。以便于提供给 AudioTrack 播放

代码如下:

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

@Test
fun `生成正弦波数据存入 buffer`() {
generator = WaveGenerator(1f, RATE_ONE)
val buffer = FloatArray(2)
generator.generateWave(1, 0f, buffer)
assertThat(buffer).usingTolerance(0.0001).containsExactly(0f, 0f)

generator.generateWave(2, 0f, buffer)
assertThat(buffer).usingTolerance(0.0001).containsExactly(0f, 0f)

generator.generateWave(4, 0f, buffer)
assertThat(buffer).usingTolerance(0.0001).containsExactly(0f, 1f)

generator.generateWave(1, 0.25f, buffer)
assertThat(buffer).usingTolerance(0.0001).containsExactly(1f, 1f)

generator.generateWave(2, 0.75f, buffer)
assertThat(buffer).usingTolerance(0.0001).containsExactly(-1f, 1f)
}

class WaveGenerator(val maxAmplitude: Float, val rate: Int) {

fun getAmplitude(time: Float): Float {
val cycle = PI.toFloat() * 2
val radiansPerSecond = rate * cycle
return sin(time * radiansPerSecond) * maxAmplitude
}

fun generateWave(sampleRate: Int, startTime: Float, buffer: FloatArray) {
val sampleInterDuration = 1f / sampleRate
buffer.forEachIndexed { index, _ ->
buffer[index] = getAmplitude(startTime + sampleInterDuration * index)
}
}
}

很快就写出来了,爽

不过,之前我想了好久的,之前也单元测试驱动过,写的很差,可能的确会子潜意识里酝酿吧。不过我还是总结以下思路吧。

  1. 首先准备好相关的数据(这让我想起来初中的时候,有个数学老师,一个老头,好像还教过我们生物,还教过我们鼓号队,它当时就说做数学题的时候,先把一致的条件都找出来。我又想起来,好像有段时间做数学题都有格式的(条件: 解:))

  2. 找到一种解法,简单的模型

  3. 将已知数据与模型对接,一般就是将某些数据转化为输入, 将输出转化成所需要的数据。

这好像就是函数是编程,或者解数学体的思路。

OK, 数据准备好了,下一步,只需要稍微转化一些,送入 AudioTrack 这个模型了。

明天见!!

每天一点点音视频_AudioTrack播放动态生成的声波_1

本来想来写播放动态生成的声波,昨天晚上失眠,想了想如何生成声波,于是,更失眠了。

这篇文章我们来写一个生成声波的函数, 所谓函数,是这个样子的:

振幅 = f(时间)

简化问题,我们生成正弦波, 先把正弦波的参数列一下:

  1. 时间
  2. 振幅
  3. 最大振幅
  4. 周期(频率)

以上的参数,也就是数据

剩下的是算法, 既然是正弦波, 肯定与 sin() 有关

最终的结果如下:

1
2
3
4
5
6
7
8

class WaveGenerator(val maxAmplitude: Float, val rate: Int) {
fun getAmplitude(time: Float): Float {
val cycle = PI.toFloat() * 2
val radiansPerSecond = rate * cycle
return sin(time * radiansPerSecond) * maxAmplitude
}
}

我使用了单元测试,爽!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
fun `计算某个时间点的振幅`() {
generator = WaveGenerator(1f, RATE_ONE)

assertThat(generator.getAmplitude(0f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.25f)).isWithin(0.001f).of(1f)
assertThat(generator.getAmplitude(0.5f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.75f)).isWithin(0.001f).of(-1f)
assertThat(generator.getAmplitude(1f)).isWithin(0.001f).of(0f)

generator = WaveGenerator(1f, RATE_TWO)

assertThat(generator.getAmplitude(0f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.25f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.5f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.75f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(1f)).isWithin(0.001f).of(0f)
assertThat(generator.getAmplitude(0.125f)).isWithin(0.001f).of(1f)
assertThat(generator.getAmplitude(0.375f)).isWithin(0.001f).of(-1f)

}

到点了,明天见

每天一点点音视频_AudioTrack的两种播放模式

AudioTrack 有两种模式, 一种是以流的方式,打开后每次写入一部分数据,另一种是以静态方式,一次写入整个音频数据。

流的方式适合大的音频文件或者动态接受或者生成的音频。后一种比较有意思,说的是音频数据动态生成的,因为播放的是 PCM, 我们时可以动态生成一个标准的正弦波的。

静态的方式适合小的文件,比如游戏中的音效。

另外, 两种模式的使用方式也不同

之前提到过, 流的方式播放音频时,使用方式如下:

  1. 创建
  2. 开始
  3. 写入数据, 可以多次, 这一步是同步的,阻塞的
  4. 停止

第三步的写入操作是阻塞的,也就是说,写入的数据,要完全写入底层的buffer才会返回, 而要写入底层的buffer,需要等到底层把之前的数据播放输出,空出buffer。

而在静态的方式播放音频时, 使用方式如下:

  1. 创建
  2. 一次性写入所有音频数据
  3. 播放

这里的写入是在播放之前, 而且这里的写入是非阻塞的,所以这就适合在 UI 使用,来实现音效播放。

明天: 每天一点点音视频_AudioTrack播放动态生成的声波


参考 例子 这个网站很棒呀

每天一点点音视频_AudioTrack和AudioRecord

AudioRecord 和 AudioTrack 这两个真的是好兄弟, 非常有对称美。

兄弟连 功能 数据 创建
AudioRecord 从音频输入设备读取数据 PCM 需要参数缓冲区大小, 采样率, 声道数, 数据位宽
AudioTrack 向音频输出设备写入数据 PCM 需要参数缓冲区大小, 采样率, 声道数, 数据位宽

以上表格就是想说明,它们两个很像,一个负责读,一个负责写,真正实现无缝对接。

AudioTrack 的使用也是与 AudioRecord相同的:

  1. 创建
  2. 开始
  3. 写数据
  4. 停止

但是, AudioTrack 的创建,还需要多一些东西。

明天: 每天一点点音视频_AudioTrack的两种播放模式


参考AudioTrack文档

每天一点点音视频_AudioRecord_实际应用总结

这里的实际应用总结是我转每去 github 搜的星最多的几个项目的使用的总结。我平时用的时候感觉使用的太草率了。没有去学习一些人家的。以星最多的项目来看并不是什么好的方式,因为它的星的多少根某一块小功能可能几乎没有关系,不过有个标准总是好一些的(我觉得)

ScreenRecorder

源码: app/src/main/java/net/yrom/screenrecorder/MicRecorder.java

在创建 AudioRecord 后,要检查它的状态是不是初始化好的,如果没有初始化好,说明传给它的参数有问题。

一个东西的使用还是很容易掌握的,关键是如歌让它适应在某个环境中,比如多线程的环境, 比如如何与其他的模块协同工作。

continuous-audiorecorder

源码: recorder/src/main/java/com/github/lassana/recorder/AudioRecorder.java

这个项目实现了一个 AudioRecorder, 增强版 AudioRecord, 添加了暂停继续功能。使用的 MediaRecorder。原理很简单,暂停继续的时候重新录制一个视频,然后和之前的视频合并,使用到了 mp4parser(跑题了)

Android-AudioRecorder-App

源码: app/src/main/java/in/arjsna/audiorecorder/recordingservice/AudioRecorder.java

这个代码让我眼前一亮,果断点星。使用了 RxJava 来包装 AudioRecord, 从而实现线程的调度,和与其他模块的数据传递。

另外, 实现了计时功能, 就是单独开个计时器, 我之前也这么做的,并不能很好的做到与实际录制的同步启动和停止。我想,可以使用之前说道的标记回调。

就这样吧,关键是这个流程更重要

录制了音频,就绪要播放, 先形成最小闭环,然后在让这个环更大

明天: 每天一点点音视频_AudioTrack

每天一点点音视频_AudioRecord_补充

之前的文章说过,AudioRecord 很简单,它的使用的确很简单,不过还有一些其他问题

构造的时候需要一个 audioSource

它代表了一个音频输入设备和特定的配置, 设备比如麦克风, 打电话的听筒声音等, 配置是根据特定的配置对设备的细分,比如适配语音识别的麦克风(这个没用过,不太理解)

setRecordPositionUpdateListener 是何用

与之有关的有 setNotificationMarkerPosition 和 setPositionNotificationPeriod, 这两个分别设置到达某个录制位置或者周期性的间隔多久调用一次回调。

注意参数为 frame 数量。

这可能用来结束? 或者周期性取样?

既然这么简单,不如一次性搞明白它, 明天来看看实际的应用: 每天一点点音视频_AudioRecord_实际应用总结

每天一点点音视频_AudioRecord

AudioRecord 可以说简单的不能再简单了, 作用是从音频输入硬件读取音频数据, 而这个数据就是之前说的 PCM 编码的音频数据。

这个类的使用:

  1. 创建
  2. 开始录制
  3. 读取数据
  4. 停止录制

这里面唯一比较复杂的就是创建的时候, 因为之前我们说道,在编码成PCM信号的时候,有三个参数,采样率,采样位宽,声道。这三个参数决定了1s内数据的大小。

在创建的时候, AudioRecord 还需要一个 Buffer, 而这 buffer 的大小就是由上面的的参数决定的。

为什么需要一个 Buffer?(忘记启动问题模式了)

因为声音数据是不断采集的,而我们的读取数据的操作可能会间隔一定的时间,假如我们10s读取一次,缓存就应该缓存10s的数据,但是这就导致我们播放时声音延迟了10s,这样说来如果要实现低延迟,甚至是实时的耳反效果,就需要Buffer尽量小,甚至没有, 但是,因为采样率是很高的,一帧的时间很短的,如果我们1帧1帧的传递,花在传递上的时间也会很多,导致达不到希望的采样率。

所以, AuidoRecord 提供了一个获取最小Buffer的大小的方法,由底层决定它。(之前想实现一个耳反,做不到,总会有延迟,据说安卓上用ndk实现会好一些,改天试试)

还有什么高级点的吗

我发现一个规律, 很多时候,一个类的初始化比较复杂,而操作比较简单,这其实也好厉害,做大量的准备,采取小的行动,产生大的效果。

依赖注入的方式就很能体现这这种方式, 一个类的时候,将需要的某些功能的实现类在构造的时候传入, 之后执行某个行为,就会对各种类做各种操作。(.. 跑题了)

明天: 每天一点点音视频_AudioRecord_补充

每天一点点音视频_PCM

WAV 是一种音频文件格式, 类似 MP4,使用中文件传输中的格式, 也就是存在于网络上, 存储器上的样子。

PCM(Pulse Code Modulation, 脉码调制录音) 是编码(一种东西转变成里一种东西), 是将物理的声波,通过采样,量化后变成的数字信号。这里提及了过程,进而可以提到几个概念

  1. 采样率, 每秒记录多少次声波的振幅, 越大,就越接近声波的原始的样子
  2. 位宽, 对每次的采样量化时,每次的数据的位数, 位数越多, 表示的数越多,也就是表示的不同的振幅越多,越细化
  3. 声道, 一般是左右声道,比如有两个麦克风,分别采样,采样后放一块作为一帧

有了这三个参数,就可以算出1s钟的 pcm 数据的大小了。

一秒钟采样多少次

每次双声道

每个声道的一次采集的数据为多宽。

明天: 每天一点点音视频_AudioRecord

每天一点点音视频_音频文件格式概览

最近在反思, 看大的项目的代码,看不下去, 没有反馈, 看书看过就忘, 通过看书, 我总结为,目的性不够, 没有带着问题去看。

我觉得带着问题去读书,去看代码,也符合《Atomic Habits》里的四个步骤:

  1. 当提出一个问题时, 就是一个信号
  2. 人对问题总有种想给个答案的冲动
  3. 就去看了
  4. 得到答案,满足了

甚至可以将带着问题去行动发散到整个生活中。

那今天的问题是啥呢

1. 音频文件格式是什么

音频文件是一个文件, 以二进制存储, 二进制有特定的结构, 这种特定的结构就是文件的格式。

2. 音频文件到底是什么

音频文件里存储着音频数据, 这些数据可能压缩了,可能没压缩,没压缩就是 PCM(Pulse Code Modulation), 脉冲编码调制。

压缩,就是使用特定的编码器,对数据进行编码,编码的目的是减小空间占用。

明天: 每天一点点音视频_PCM