Android低仿iOS Messages录音波形效果

Android低仿iOS Messages录音波形效果

一、目标

分析iOS Messages的录音波形效果,为神马笔记添加录音功能做准备。

二、功能分析

1. iOS Messages的波形效果

iOS Messages有2个波形效果

  1. 录音波形
  2. 播放波形

2. 录音波形

截图 说明
波形从右向左移动,在左侧逐渐收敛。
左侧的收敛过程非常漂亮。
高度、宽度、位移同时进行收敛,形成一个非常漂亮的动态过程。

三、实现效果

实现录音波形效果最大的难度在于实现波形的收敛过程。

尝试了同时收敛高度、宽度和位移的几种方案,效果都不是很理想。

达不到iOS Messages的收敛效果,最后只是简单收敛了高度,宽度和位移保持不变。

四、实现过程

1. 录制音频

音频录制采用MediaRecorder方式,实现简单并且符合神马笔记的使用场景。

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
public boolean start() {
boolean result = true;

{
sampler.clear();
waveform.clear();
}

{
mRecorder = new MediaRecorder();

mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

mRecorder.setAudioChannels(1);
mRecorder.setAudioSamplingRate(44100);
mRecorder.setAudioEncodingBitRate(192000);

mRecorder.setOutputFile(targetFile.getAbsolutePath());
}

try {
mRecorder.prepare();
mRecorder.start();

mStartingTimeMillis = System.currentTimeMillis();
mElapsedMillis = 0;

} catch (IOException e) {
e.printStackTrace();

mRecorder.stop();
mRecorder.release();
mRecorder = null;

result = false;
}

{
this.invalidate();
}

return result;
}

2. 波形采样

波形数据通过调用MediaRecorder#getMaxAmplitude()获取。

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
public static class Sampler {

int rate; // sample rate per second
int interval; // sample interval in milliseconds

int[] array;
int size;

public Sampler(int rate) {
this.rate = rate;
this.interval = 1000 / rate;

this.array = new int[1024];
this.clear();
}

public int getRate() {
return this.rate;
}

public int getInterval() {
return this.interval;
}

public int size() {
return this.size;
}

public int[] getArray() {
int[] tmp = new int[size];
System.arraycopy(this.array, 0, tmp, 0, size);

return tmp;
}

public int get(int index) {
return array[index];
}

void clear() {
this.size = 0;
Arrays.fill(array, 0);
}

void add(long time, int value) {
value = Math.abs(value);

int index = (int)(time / interval);
if (index >= array.length) {
this.expend(index);
}

if (index < size) { // if same sample, average it
array[index] += value;
array[index] /= 2;
} else {
array[index] = value;
}

this.size = (index < size)? size: (index + 1);
}

void expend(int min) {
int[] tmp = new int[min + 1024];
Arrays.fill(tmp, 0);

System.arraycopy(array, 0, tmp, 0, array.length);

this.array = tmp;
}
}

3. 绘制波形

  • Waveform——完整波形
  • Wave——单个波形
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
private static class Waveform {

ArrayList<Wave> list;
ArrayList<Wave> recycler;

int lastWave;

TapeView parent;

Waveform(TapeView parent) {
this.parent = parent;

this.list = new ArrayList<>();
this.recycler = new ArrayList<>();

int rate = parent.sampleRate;
float duration = parent.sampleDuration;
int count = (int)(rate * duration * 1.5f);
for (int i = 0; i < count; i++) {
recycler.add(new Wave());
}

this.lastWave = -1;
}

void add(int position, int amplitude) {
if (position <= this.lastWave) {
return;
}

{
Wave wave = this.fetch(position, amplitude);
list.add(wave);
}

this.lastWave = position;
}

int getLast() {
return this.lastWave;
}

void clear() {
this.lastWave = -1;

this.recycler.addAll(list);
this.list.clear();
}

void draw(Canvas canvas) {

for (Wave wave : list) {
wave.draw(canvas);
}

int min = -parent.getWaveWidth();
for (int i = list.size() - 1; i >= 0; i--) {
Wave wave = list.get(i);
int left = wave.left;
if (left <= min) {
recycler.add(list.remove(i));
}
}
}

Wave fetch(int positon, int amplitude) {

Wave wave;

int index = recycler.size() - 1;
if (index < 0) {
wave = new Wave();
} else {
wave = recycler.remove(index);
}

wave.init(parent, positon, amplitude);
return wave;
}
}
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
private static class Wave {

int position;
TapeView parent;

int wave;

int left;

Wave() {

}

void init(TapeView parent, int position, int amplitude) {
this.parent = parent;
this.position = position;

this.wave = parent.transform(amplitude);
if (Math.abs(wave - parent.maxWave) < 15) {
wave -= (5 + Math.random() * 10);
}
}

void draw(Canvas canvas) {
int x = getX();
this.left = x;

if (x <= -parent.getWaveWidth()) {
return;
}

int height = getHeight(x);
int y = (parent.getHeight() - height) / 2;

{
Drawable d = parent.drawable;
d.setBounds(x, y, x + parent.sampleSolid, y + height);
d.draw(canvas);
}
}

int getX() {
int x = parent.getWidth();

float t = this.getTime();
int speed = parent.getSpeed();

x -= (speed * t);
return x;
}

int getHeight(int x) {
int height = parent.getWaveHeight(this.wave);
x = Math.max(x, 0);

int d = parent.slowDownDistance;
if (x < d) {

float scale = (d - x) * 1.f / parent.getFrictionDistance();
scale = parent.interpolator.getInterpolation(scale);
scale = 1 - scale;

height *= scale;
height = (height < 2)? 2: height;
}

return height;
}

float getTime() {
long duration = parent.getDuration();
duration -= parent.getSampler().getInterval() * this.position;

float t = duration / 1000.f;
return t;
}

}

4. 处理吹气情况

用户朝麦克风吃气时,采用MediaRecorder#getMaxAmplitude()方法获取数据振幅数据,再转化为分贝时,很容易接近最大分贝,即使是轻轻的吹起,也会接近最大分贝。

这样一来导致波形非常不好看,为了美观,但分贝值接近最大分贝时,减去随机数使波形发生变化,不会那么呆板。

1
2
3
4
this.wave = parent.transform(amplitude);
if (Math.abs(wave - parent.maxWave) < 15) {
wave -= (5 + Math.random() * 10);
}

五、开发过程回顾

从录音到采集波形数据,再到绘制波形,实现过程比较简单。

需要注意的是波形是为了呈现了声音的变化过程,而不是声音分贝的准确值。

因此没有必要追求声音分贝的准确值。

六、接下来

实现录音播放功能,并绘制播放波形。

七、Finally

以无我无人无众生无寿者。 修一切善法。即得阿耨多罗三藐三菩提。