Android低仿iOS Messages录音波形效果
一、目标
分析iOS Messages的录音波形效果,为神马笔记添加录音功能做准备。
二、功能分析
1. iOS Messages的波形效果
iOS Messages有2个波形效果
- 录音波形
- 播放波形
2. 录音波形
截图 | 说明 |
---|---|
波形从右向左移动,在左侧逐渐收敛。 | |
左侧的收敛过程非常漂亮。 高度、宽度、位移同时进行收敛,形成一个非常漂亮的动态过程。 |
三、实现效果
实现录音波形效果最大的难度在于实现波形的收敛过程。
尝试了同时收敛高度、宽度和位移的几种方案,效果都不是很理想。
达不到iOS Messages的收敛效果,最后只是简单收敛了高度,宽度和位移保持不变。
四、实现过程
1. 录制音频
音频录制采用MediaRecorder
方式,实现简单并且符合神马笔记的使用场景。
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()
获取。
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——单个波形
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;
}
}
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()
方法获取数据振幅数据,再转化为分贝时,很容易接近最大分贝,即使是轻轻的吹起,也会接近最大分贝。
这样一来导致波形非常不好看,为了美观,但分贝值接近最大分贝时,减去随机数使波形发生变化,不会那么呆板。
this.wave = parent.transform(amplitude);
if (Math.abs(wave - parent.maxWave) < 15) {
wave -= (5 + Math.random() * 10);
}
五、开发过程回顾
从录音到采集波形数据,再到绘制波形,实现过程比较简单。
需要注意的是波形是为了呈现了声音的变化过程,而不是声音分贝的准确值。
因此没有必要追求声音分贝的准确值。
六、接下来
实现录音播放功能,并绘制播放波形。
七、Finally
以无我无人无众生无寿者。
修一切善法。即得阿耨多罗三藐三菩提。