forked from mybigday/react-native-audio-pcm-stream
-
Notifications
You must be signed in to change notification settings - Fork 3
/
frequency.histogram.view.js
443 lines (390 loc) · 14.9 KB
/
frequency.histogram.view.js
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
// ref to https://github.com/xiangyuecn/Recorder/blob/1.2.23070100/src/extensions/frequency.histogram.view.js
/*
录音 Recorder扩展,频率直方图显示
使用本扩展需要引入lib.fft.js支持,直方图特意优化主要显示0-5khz语音部分(线性),其他高频显示区域较小,不适合用来展示音乐频谱,可自行修改源码恢复成完整的线性频谱,或修改成倍频程频谱(伯德图、对数频谱);本可视化插件可以移植到其他语言环境,如需定制可联系作者
https://github.com/xiangyuecn/Recorder
本扩展核心算法主要参考了Java开源库jmp123 版本0.3 的代码:
https://www.iteye.com/topic/851459
https://sourceforge.net/projects/jmp123/files/
*/
import {Platform} from 'react-native';
import LibFFT from './lib.fft';
var FrequencyHistogramView = function (set) {
return new fn(set);
};
var ViewTxt = 'FrequencyHistogramView';
var fn = function (set) {
var This = this;
var o = {
canvas, // e.g. https://github.com/flyskywhy/react-native-gcanvas
ctx,
/*
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小
//或者配置显示大小,手动把frequencyObj.elem显示到别的地方
,width:0 //显示宽度
,height:0 //显示高度
以上配置二选一
*/
scale: 1, // 缩放系数,使用 scale 倍宽高进行绘制,用于避免移动端绘制模糊,不过直方图都是直线,不存在模糊问题,所以设为 1 最方便
asyncFftAtFps: true, // 是否在每个 fps 的点上才计算 fft 而非每来一个 pcm 就计算一次 fft
fps: 20, // 绘制帧率,不可过高。影响整体对新 pcm 数据的响应速度,需要 asyncFftAtFps 为 true 才起作用
fpsH: 20, // 影响柱子的下降速度,越大下降越快,需要 asyncFftAtFps 为 false 才起作用
lineCount: 30, //直方图柱子数量,数量的多少对性能影响不大,密集运算集中在FFT算法中
widthRatio: 0.6, //柱子线条宽度占比,为所有柱子占用整个视图宽度的比例,剩下的空白区域均匀插入柱子中间;默认值也基本相当于一根柱子占0.6,一根空白占0.4;设为1不留空白,当视图不足容下所有柱子时也不留空白
spaceWidth: 0, //柱子间空白固定基础宽度,柱子宽度自适应,当不为0时widthRatio无效,当视图不足容下所有柱子时将不会留空白,允许为负数,让柱子发生重叠
minHeight: 0, //柱子保留基础高度,position不为±1时应该保留点高度
position: -1, //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比
mirrorEnable: false, //是否启用镜像,如果启用,视图宽度会分成左右两块,右边这块进行绘制,左边这块进行镜像(以中间这根柱子的中心进行镜像)
stripeEnable: true, //是否启用柱子顶上的峰值小横条,position不是-1时应当关闭,否则会很丑
stripeHeight: 3, //峰值小横条基础高度
stripeMargin: 6, //峰值小横条和柱子保持的基础距离
fallDuration: 1000, //柱子从最顶上下降到最底部最长时间ms
stripeFallDuration: 3500, //峰值小横条从最顶上下降到底部最长时间ms
//柱子颜色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间
linear: [
0,
'rgba(0,187,17,1)',
0.5,
'rgba(255,215,0,1)',
1,
'rgba(255,102,0,1)',
],
//峰值小横条渐变颜色配置,取值格式和linear一致,留空为柱子的渐变颜色
stripeLinear: null,
shadowBlur: 0, //柱子阴影基础大小,设为0不显示阴影,如果柱子数量太多时请勿开启,非常影响性能
shadowColor: '#bbb', //柱子阴影颜色
stripeShadowBlur: -1, //峰值小横条阴影基础大小,设为0不显示阴影,-1为柱子的大小,如果柱子数量太多时请勿开启,非常影响性能
stripeShadowColor: '', //峰值小横条阴影颜色,留空为柱子的阴影颜色
//当发生绘制时会回调此方法,参数为当前绘制的频率数据和采样率,可实现多个直方图同时绘制,只消耗一个input输入和计算时间
onDraw: function (frequencyData, sampleRate) {},
};
for (var k in set) {
o[k] = set[k];
}
This.set = set = o;
if (Platform.os === 'web' && set.canvas === undefined) {
var elem = set.elem;
if (elem) {
if (typeof elem == 'string') {
elem = document.querySelector(elem);
} else if (elem.length) {
elem = elem[0];
}
}
if (elem) {
set.width = elem.offsetWidth;
set.height = elem.offsetHeight;
}
var scale = set.scale;
var width = set.width * scale;
var height = set.height * scale;
if (!width || !height) {
throw new Error(ViewTxt + '无宽高');
}
var thisElem = (This.elem = document.createElement('div'));
var lowerCss = [
'',
'transform-origin:0 0;',
'transform:scale(' + 1 / scale + ');',
];
thisElem.innerHTML =
'<div style="width:' +
set.width +
'px;height:' +
set.height +
'px;overflow:hidden"><div style="width:' +
width +
'px;height:' +
height +
'px;' +
lowerCss.join('-webkit-') +
lowerCss.join('-ms-') +
lowerCss.join('-moz-') +
lowerCss.join('') +
'"><canvas/></div></div>';
var canvas = (This.canvas = thisElem.querySelector('canvas'));
var ctx = (This.ctx = canvas.getContext('2d'));
canvas.width = width;
canvas.height = height;
if (elem) {
elem.innerHTML = '';
elem.appendChild(thisElem);
}
} else {
if (set.canvas) {
This.canvas = set.canvas;
This.ctx = set.ctx ? set.ctx : set.canvas.getContext('2d');
set.width = set.width || set.canvas.width;
set.height = set.height || set.canvas.height;
} else {
if (!set.width || !set.height) {
throw new Error(ViewTxt + '无 canvas 也无宽高');
}
}
}
This.fft = LibFFT(1024);
//柱子所在高度
This.lastH = [];
//峰值小横条所在高度
This.stripesH = [];
};
fn.prototype = FrequencyHistogramView.prototype = {
genLinear: function (ctx, colors, from, to) {
var rtv = ctx.createLinearGradient(0, from, 0, to);
for (var i = 0; i < colors.length; ) {
rtv.addColorStop(colors[i++], colors[i++]);
}
return rtv;
},
input: function (pcmData, powerLevel, sampleRate) {
var This = this;
This.sampleRate = sampleRate;
This.pcmData = pcmData;
This.pcmPos = 0;
This.inputTime = Date.now();
return This.schedule();
},
schedule: function () {
var This = this,
set = This.set;
if (set.asyncFftAtFps) {
var interval = Math.floor(1000 / set.fps);
if (!This.timer) {
This.timer = setInterval(function () {
This.schedule();
}, interval);
}
var now = Date.now();
var drawTime = This.drawTime || 0;
if (now - This.inputTime > set.stripeFallDuration * 1.3) {
//超时没有输入,顶部横条已全部落下,干掉定时器
clearInterval(This.timer);
This.timer = 0;
return;
}
if (now - drawTime < interval) {
//没到间隔时间,不绘制
return;
}
This.drawTime = now;
}
//调用FFT计算频率数据
var bufferSize = This.fft.bufferSize;
var pcm = This.pcmData;
var pos = This.pcmPos;
var arr = new Int16Array(bufferSize);
for (var i = 0; i < bufferSize && pos < pcm.length; i++, pos++) {
arr[i] = pcm[pos];
}
This.pcmPos = pos;
var frequencyData = This.fft.transform(arr);
if (set.asyncFftAtFps) {
//推入绘制
This.draw(frequencyData, This.sampleRate);
} else {
return frequencyData;
}
},
frequencyData2H: function ({frequencyData, sampleRate}) {
var This = this,
set = This.set;
var scale = set.scale;
var width = set.width * scale;
var height = set.height * scale;
var lineCount = set.lineCount;
var bufferSize = This.fft.bufferSize;
//计算高度位置
var position = set.position;
var posAbs = Math.abs(set.position);
var originY = position === 1 ? 0 : height; //y轴原点
var heightY = height; //最高的一边高度
if (posAbs < 1) {
heightY = heightY / 2;
originY = heightY;
heightY = Math.floor(heightY * (1 + posAbs));
originY = Math.floor(
position > 0 ? originY * (1 - posAbs) : originY * (1 + posAbs),
);
}
var lastH = This.lastH;
var stripesH = This.stripesH;
var duration = 1000 / (set.asyncFftAtFps ? set.fps : set.fpsH);
var speed = Math.ceil(heightY / (set.fallDuration / duration));
var stripeSpeed = Math.ceil(heightY / (set.stripeFallDuration / duration));
var stripeMargin = set.stripeMargin * scale;
var Y0 = 1 << (Math.round(Math.log(bufferSize) / Math.log(2) + 3) << 1);
var logY0 = Math.log(Y0) / Math.log(10);
var dBmax = (20 * Math.log(0x7fff)) / Math.log(10);
var fftSize = bufferSize / 2;
var fftSize5k = Math.min(
fftSize,
Math.floor((fftSize * 5000) / (sampleRate / 2)),
); //5khz所在位置,8000采样率及以下最高只有4khz
var fftSize5kIsAll = fftSize5k === fftSize;
var line80 = fftSize5kIsAll ? lineCount : Math.round(lineCount * 0.8); //80%的柱子位置
var fftSizeStep1 = fftSize5k / line80;
var fftSizeStep2 = fftSize5kIsAll
? 0
: (fftSize - fftSize5k) / (lineCount - line80);
var fftIdx = 0;
for (var i = 0; i < lineCount; i++) {
//不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
var start = Math.ceil(fftIdx);
if (i < line80) {
//5khz以下
fftIdx += fftSizeStep1;
} else {
//5khz以上
fftIdx += fftSizeStep2;
}
var end = Math.min(Math.ceil(fftIdx), fftSize);
//参考AudioGUI.java .drawHistogram方法
//查找当前频段的最大"幅值"
var maxAmp = 0;
for (var j = start; j < end; j++) {
maxAmp = Math.max(maxAmp, Math.abs(frequencyData[j]));
}
//计算音量
var dB =
maxAmp > Y0
? Math.floor((Math.log(maxAmp) / Math.log(10) - logY0) * 17)
: 0;
var h = heightY * Math.min(dB / dBmax, 1);
//使柱子匀速下降
lastH[i] = (lastH[i] || 0) - speed;
if (h < lastH[i]) {
h = lastH[i];
}
if (h < 0) {
h = 0;
}
lastH[i] = h;
var shi = stripesH[i] || 0;
if (h && h + stripeMargin > shi) {
stripesH[i] = h + stripeMargin;
} else {
//使峰值小横条匀速度下落
var sh = shi - stripeSpeed;
if (sh < 0) {
sh = 0;
}
stripesH[i] = sh;
}
}
return {lastH, stripesH, originY, heightY};
},
draw: function (frequencyData, sampleRate) {
var This = this,
set = This.set;
var scale = set.scale;
var width = set.width * scale;
var height = set.height * scale;
var lineCount = set.lineCount;
const {lastH, stripesH, originY, heightY} = This.frequencyData2H({
frequencyData,
sampleRate,
});
//开始绘制图形
var ctx = This.ctx;
ctx.clearRect(0, 0, width, height);
var linear1 = This.genLinear(ctx, set.linear, originY, originY - heightY); //上半部分的填充
var stripeLinear1 =
(set.stripeLinear &&
This.genLinear(ctx, set.stripeLinear, originY, originY - heightY)) ||
linear1; //上半部分的峰值小横条填充
var linear2 = This.genLinear(ctx, set.linear, originY, originY + heightY); //下半部分的填充
var stripeLinear2 =
(set.stripeLinear &&
This.genLinear(ctx, set.stripeLinear, originY, originY + heightY)) ||
linear2; //上半部分的峰值小横条填充
//计算柱子间距
ctx.shadowBlur = set.shadowBlur * scale;
ctx.shadowColor = set.shadowColor;
var mirrorEnable = set.mirrorEnable;
var mirrorCount = mirrorEnable ? lineCount * 2 - 1 : lineCount; //镜像柱子数量翻一倍-1根
var widthRatio = set.widthRatio;
var spaceWidth = set.spaceWidth * scale;
if (spaceWidth !== 0) {
widthRatio = (width - spaceWidth * (mirrorCount + 1)) / width;
}
var lineWidth = Math.max(
1 * scale,
Math.floor((width * widthRatio) / mirrorCount),
); //柱子宽度至少1个单位
var spaceFloat = (width - mirrorCount * lineWidth) / (mirrorCount + 1); //均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
//绘制柱子
var minHeight = set.minHeight * scale;
var mirrorSubX = spaceFloat + lineWidth / 2;
var XFloat = mirrorEnable ? width / 2 - mirrorSubX : 0; //镜像时,中间柱子位于正中心
for (var i = 0, xFloat = XFloat, x, y, h; i < lineCount; i++) {
xFloat += spaceFloat;
x = Math.floor(xFloat);
h = Math.max(lastH[i], minHeight);
//绘制上半部分
if (originY !== 0) {
y = originY - h;
ctx.fillStyle = linear1;
ctx.fillRect(x, y, lineWidth, h);
}
//绘制下半部分
if (originY !== height) {
ctx.fillStyle = linear2;
ctx.fillRect(x, originY, lineWidth, h);
}
xFloat += lineWidth;
}
//绘制柱子顶上峰值小横条
if (set.stripeEnable) {
var stripeShadowBlur = set.stripeShadowBlur;
ctx.shadowBlur =
(stripeShadowBlur === -1 ? set.shadowBlur : stripeShadowBlur) * scale;
ctx.shadowColor = set.stripeShadowColor || set.shadowColor;
var stripeHeight = set.stripeHeight * scale;
for (var i = 0, xFloat = XFloat, x, y, h; i < lineCount; i++) {
xFloat += spaceFloat;
x = Math.floor(xFloat);
h = stripesH[i];
//绘制上半部分
if (originY !== 0) {
y = originY - h - stripeHeight;
if (y < 0) {
y = 0;
}
ctx.fillStyle = stripeLinear1;
ctx.fillRect(x, y, lineWidth, stripeHeight);
}
//绘制下半部分
if (originY !== height) {
y = originY + h;
if (y + stripeHeight > height) {
y = height - stripeHeight;
}
ctx.fillStyle = stripeLinear2;
ctx.fillRect(x, y, lineWidth, stripeHeight);
}
xFloat += lineWidth;
}
}
//镜像,从中间直接镜像即可
if (mirrorEnable) {
var srcW = Math.floor(width / 2);
ctx.save();
ctx.scale(-1, 1);
ctx.drawImage(
This.canvas,
Math.ceil(width / 2),
0,
srcW,
height,
-srcW,
0,
srcW,
height,
);
ctx.restore();
}
set.onDraw(frequencyData, sampleRate);
},
};
export default FrequencyHistogramView;