1
1
package io .agora .lrcview ;
2
2
3
3
import android .animation .ObjectAnimator ;
4
- import android .animation .ValueAnimator ;
5
4
import android .content .Context ;
6
5
import android .content .res .TypedArray ;
7
6
import android .graphics .Canvas ;
13
12
import android .os .Handler ;
14
13
import android .os .Looper ;
15
14
import android .util .AttributeSet ;
16
- import android .util .FloatProperty ;
17
- import android .util .Property ;
18
15
import android .util .TypedValue ;
19
16
import android .view .View ;
20
17
21
18
import androidx .annotation .Nullable ;
22
19
import androidx .annotation .RequiresApi ;
23
20
21
+ import java .util .ArrayList ;
24
22
import java .util .List ;
25
23
26
24
import io .agora .lrcview .bean .LrcData ;
@@ -46,7 +44,34 @@ public class PitchView extends View {
46
44
47
45
private int pitchMax = 0 ;//最大值
48
46
private int pitchMin = 100 ;//最小值
47
+ private int totalPitch = 0 ;
48
+
49
+ // 完成 PitchView.OnActionListener#onOriginalPitch的需求
50
+ // 当前 Pitch 所在的字的开始时间
51
+ private long currentPitchStartTime = 0 ;
52
+ // 当前 Pitch 所在的字的结束时间
53
+ private long currentPitchEndTime = 0 ;
54
+ // 当前 Pitch 所在的句的结束时间
55
+ private long currentEntryEndTime = 0 ;
56
+ // 当前在打分的所在句的结束时间
57
+ private long currentScoreEntryEndTime = 0 ;
58
+ // 当前在打分的所在句的结束时间
59
+ private long lrcEndTime = 0 ;
60
+
61
+ // 音调指示器的半径
49
62
private int indicatorRadius ;
63
+ // 每句最高分
64
+ private int scorePerSentence = 100 ;
65
+ // 初始分数
66
+ private float mInitialScore ;
67
+ // 每句歌词分数
68
+ public List <Double > sentenceScoreList = new ArrayList <>();
69
+ // 累计分数
70
+ public float cumulatedScore ;
71
+ // 歌曲总分数
72
+ public float totalScore ;
73
+ // 分数阈值 大于此值计分 小于不计分
74
+ public final float scoreCountLine = 0.4f ;
50
75
51
76
private final Paint mPaint = new Paint (Paint .ANTI_ALIAS_FLAG );
52
77
private int mNormalTextColor ;
@@ -56,14 +81,16 @@ public class PitchView extends View {
56
81
57
82
private float dotPointX = 0F ;//亮点坐标
58
83
84
+ // 音调及分数回调
85
+ public OnActionListener onActionListener ;
86
+
87
+ //<editor-fold desc="Init Related">
59
88
public PitchView (Context context ) {
60
- super (context );
61
- init (null );
89
+ this (context , null );
62
90
}
63
91
64
92
public PitchView (Context context , @ Nullable AttributeSet attrs ) {
65
- super (context , attrs );
66
- init (attrs );
93
+ this (context , attrs , 0 );
67
94
}
68
95
69
96
public PitchView (Context context , @ Nullable AttributeSet attrs , int defStyleAttr ) {
@@ -74,7 +101,6 @@ public PitchView(Context context, @Nullable AttributeSet attrs, int defStyleAttr
74
101
@ RequiresApi (api = Build .VERSION_CODES .LOLLIPOP )
75
102
public PitchView (Context context , @ Nullable AttributeSet attrs , int defStyleAttr , int defStyleRes ) {
76
103
super (context , attrs , defStyleAttr , defStyleRes );
77
-
78
104
init (attrs );
79
105
}
80
106
@@ -87,12 +113,12 @@ private void init(@Nullable AttributeSet attrs) {
87
113
TypedArray ta = getContext ().obtainStyledAttributes (attrs , R .styleable .PitchView );
88
114
mNormalTextColor = ta .getColor (R .styleable .PitchView_pitchNormalTextColor , getResources ().getColor (R .color .lrc_normal_text_color ));
89
115
mDoneTextColor = ta .getColor (R .styleable .PitchView_pitchDoneTextColor , getResources ().getColor (R .color .lrc_current_text_color ));
116
+ mInitialScore = ta .getFloat (R .styleable .PitchView_pitchInitialScore , 50f );
90
117
ta .recycle ();
91
118
92
119
int startColor = getResources ().getColor (R .color .pitch_start );
93
120
int endColor = getResources ().getColor (R .color .pitch_end );
94
121
linearGradient = new LinearGradient (dotPointX , 0 , 0 , 0 , startColor , endColor , Shader .TileMode .CLAMP );
95
-
96
122
}
97
123
98
124
@ Override
@@ -124,24 +150,24 @@ protected void onDraw(Canvas canvas) {
124
150
drawLocalPitch (canvas );
125
151
drawItems (canvas );
126
152
}
153
+ //</editor-fold>
127
154
128
155
private void drawLocalPitch (Canvas canvas ) {
129
156
mPaint .setShader (null );
130
157
mPaint .setColor (mNormalTextColor );
131
158
float value = getPitchHeight ();
132
- if (value >= 0 ){
159
+ if (value >= 0 ) {
133
160
canvas .drawCircle (dotPointX , value , indicatorRadius , mPaint );
134
161
}
135
162
}
136
163
137
164
private float getPitchHeight () {
138
165
float res = 0 ;
139
- if (mLocalPitch != 0 && pitchMax != 0 && pitchMin != 100 ){
166
+ if (mLocalPitch != 0 && pitchMax != 0 && pitchMin != 100 ) {
140
167
float realPitchMax = pitchMax + 5 ;
141
168
float realPitchMin = pitchMin - 5 ;
142
- res = (float ) (1 - ((mLocalPitch - pitchMin ) / (realPitchMax - realPitchMin )) ) * getHeight ();
143
- }
144
- else if (mLocalPitch == 0 ){
169
+ res = (1 - ((mLocalPitch - pitchMin ) / (realPitchMax - realPitchMin ))) * getHeight ();
170
+ } else if (mLocalPitch == 0 ) {
145
171
res = getHeight ();
146
172
}
147
173
return res ;
@@ -168,7 +194,7 @@ private void drawItems(Canvas canvas) {
168
194
float x = dotPointX * 1.3f - currentPX ;
169
195
float y = 0 ;
170
196
float widthTone = 0 ;
171
- float mItemHeight = getHeight () / (float ) ( realPitchMax - realPitchMin );//高度
197
+ float mItemHeight = getHeight () / (realPitchMax - realPitchMin );//高度
172
198
long preEndTIme = 0 ;
173
199
for (int i = 0 ; i < entrys .size (); i ++) {
174
200
LrcEntryData entry = lrcData .entrys .get (i );
@@ -213,14 +239,30 @@ private void drawItems(Canvas canvas) {
213
239
*
214
240
* @param data 歌词信息对象
215
241
*/
216
- public void setLrcData (LrcData data ) {
242
+ public void setLrcData (@ Nullable LrcData data ) {
217
243
lrcData = data ;
244
+ totalPitch = 0 ;
245
+
246
+ mCurrentTime = 0 ;
247
+ pitchMax = 0 ;
248
+ pitchMin = 100 ;
249
+
250
+ currentPitchStartTime = 0 ;
251
+ currentPitchEndTime = 0 ;
252
+ currentEntryEndTime = 0 ;
253
+ currentScoreEntryEndTime = 0 ;
254
+ sentenceScoreList .clear ();
218
255
219
256
if (lrcData != null && lrcData .entrys != null && !lrcData .entrys .isEmpty ()) {
257
+
258
+ lrcEndTime = lrcData .entrys .get (lrcData .entrys .size () - 1 ).getEndTime ();
259
+ totalScore = scorePerSentence * lrcData .entrys .size () + mInitialScore ;
260
+
220
261
for (LrcEntryData entry : lrcData .entrys ) {
221
262
for (LrcEntryData .Tone tone : entry .tones ) {
222
263
pitchMin = Math .min (pitchMin , tone .pitch );
223
264
pitchMax = Math .max (pitchMax , tone .pitch );
265
+ totalPitch ++;
224
266
}
225
267
}
226
268
}
@@ -233,49 +275,157 @@ public void setLrcData(LrcData data) {
233
275
234
276
private void setMLocalPitch (float mLocalPitch ) {
235
277
this .mLocalPitch = mLocalPitch ;
278
+ invalidate ();
236
279
}
280
+
237
281
/**
238
- * 更新音调
282
+ * 根据当前播放时间获取 Pitch
239
283
*
240
- * @param pitch 单位hz
284
+ * @return 当前时间歌词的 Pitch
241
285
*/
242
- public void updateLocalPitch (double pitch ) {
243
- mHandler .postDelayed (new Runnable () {
244
- @ Override
245
- public void run () {
246
- if (mLocalPitch == pitch ){
247
- mLocalPitch = 0 ;
286
+ private float findPitchByTime () {
287
+ if (lrcData == null ) return 0 ;
288
+
289
+ float resPitch = 0 ;
290
+ int entryCount = lrcData .entrys .size ();
291
+ for (int i = 0 ; i < entryCount ; i ++) {
292
+ LrcEntryData tempEntry = lrcData .entrys .get (i );
293
+ if (mCurrentTime >= tempEntry .getStartTime ()) { // 索引
294
+ int toneCount = tempEntry .tones .size ();
295
+ for (int j = 0 ; j < toneCount ; j ++) {
296
+ LrcEntryData .Tone tempTone = tempEntry .tones .get (j );
297
+ if (mCurrentTime <= tempTone .end ) {
298
+ resPitch = tempTone .pitch ;
299
+ currentPitchStartTime = tempTone .begin ;
300
+ currentPitchEndTime = tempTone .end ;
301
+
302
+ currentEntryEndTime = tempEntry .getEndTime ();
303
+ break ;
304
+ }
248
305
}
306
+ break ;
307
+ }
308
+ }
309
+ if (resPitch == 0 ) {
310
+ currentPitchStartTime = 0 ;
311
+ currentPitchEndTime = 0 ;
312
+ currentEntryEndTime = 0 ;
313
+ }
314
+ return resPitch ;
315
+ }
316
+
317
+ /**
318
+ * 更新音调,更新分数,执行圆点动画
319
+ *
320
+ * @param pitch 单位hz
321
+ */
322
+ public void updateLocalPitch (float pitch ) {
323
+ if (lrcData == null ) return ;
324
+ float desiredPitch = findPitchByTime ();
325
+ if (desiredPitch != 0 )
326
+ updateScore (pitchToTone (pitch ), pitchToTone (desiredPitch ));
327
+
328
+ mHandler .removeCallbacksAndMessages (null );
329
+ mHandler .postDelayed (() -> {
330
+ if (mLocalPitch == pitch ) {
331
+ mLocalPitch = 0 ;
249
332
}
250
333
}, 2000L );
251
- ObjectAnimator .ofFloat (this , "mLocalPitch" , this .mLocalPitch , (float ) pitch ).setDuration (50 ).start ();
252
- invalidate ();
334
+ ObjectAnimator .ofFloat (this , "mLocalPitch" , this .mLocalPitch , pitch ).setDuration (50 ).start ();
335
+ }
336
+
337
+ /**
338
+ * 更新当前分数
339
+ *
340
+ * @param currentTone 演唱值
341
+ * @param desiredTone 理想值
342
+ */
343
+ private void updateScore (double currentTone , double desiredTone ) {
344
+ double score = 1 - Math .abs (desiredTone - currentTone ) / desiredTone ;
345
+ score = score >= scoreCountLine ? score : 0f ;
346
+ score *= scorePerSentence ;
347
+
348
+ // 当前未在打分 <==> 定位打分句结束时间到当前句
349
+ if (sentenceScoreList .isEmpty ()) currentScoreEntryEndTime = currentEntryEndTime ;
350
+
351
+ // 打分句结束时间已过 或者 最后一句已经结束
352
+ if (mCurrentTime > currentScoreEntryEndTime || mCurrentTime > lrcEndTime ) { // 已经到下一句了
353
+ // 分数列表不为空
354
+ if (!sentenceScoreList .isEmpty ()) {
355
+
356
+ // 计算歌词当前句的分数 = 所有打分/分数个数
357
+ double tempScore = 0 ;
358
+ for (Double toneScore : sentenceScoreList )
359
+ tempScore += toneScore ;
360
+
361
+ // 统计到累计分数
362
+ cumulatedScore += tempScore / sentenceScoreList .size ();
363
+ // 回调到上层
364
+ dispatchScore (score );
365
+ // 清除打分
366
+ sentenceScoreList .clear ();
367
+ }
368
+ }
369
+
370
+ sentenceScoreList .add (score );
371
+ }
372
+
373
+ /**
374
+ * 根据当前歌曲时间决定是否回调{@link OnActionListener#onScore(double, double, double)}
375
+ *
376
+ * @param score 本次算法返回的分数
377
+ */
378
+ private void dispatchScore (double score ) {
379
+ if (onActionListener != null ) onActionListener .onScore (score , cumulatedScore , totalScore );
253
380
}
254
381
255
382
/**
256
383
* 更新进度,单位毫秒
384
+ * 根据当前时间,决定是否回调{@link OnActionListener#onOriginalPitch(float, int)}
385
+ * 与打分逻辑无关
257
386
*
258
387
* @param time 当前播放时间,毫秒
259
388
*/
260
389
public void updateTime (long time ) {
261
390
if (lrcData == null ) {
262
391
return ;
392
+ } else if (time < currentPitchStartTime || time > currentPitchEndTime ) {
393
+ onActionListener .onOriginalPitch (findPitchByTime (), totalPitch );
263
394
}
264
395
265
396
this .mCurrentTime = time ;
266
397
267
398
invalidate ();
268
399
}
269
400
270
- /**
271
- * 重置内部状态,清空已经加载的歌词
272
- */
273
- public void reset () {
274
- lrcData = null ;
275
- mCurrentTime = 0 ;
276
- pitchMax = 0 ;
277
- pitchMin = 100 ;
401
+ @ Override
402
+ protected void onDetachedFromWindow () {
403
+ super .onDetachedFromWindow ();
404
+ if (onActionListener != null ) onActionListener = null ;
405
+ }
278
406
279
- invalidate ();
407
+ public static double pitchToTone (double pitch ) {
408
+ double eps = 1e-6 ;
409
+ return (Math .max (0 , Math .log (pitch / 55 + eps ) / Math .log (2 ))) * 12 ;
410
+ }
411
+
412
+ public static interface OnActionListener {
413
+
414
+ /**
415
+ * 咪咕歌词原始参考pitch值回调, 用于开发者自行实现打分逻辑. 歌词每个tone回调一次
416
+ * pitch: 当前tone的pitch值
417
+ * totalCount: 整个xml的tone个数, 用于开发者方便自己在app层计算平均分.
418
+ */
419
+ void onOriginalPitch (float pitch , int totalCount );
420
+
421
+ /**
422
+ * paas组件内置的打分回调, 每句歌词结束的时候提供回调(句指xml中的sentence节点),
423
+ * 并提供totalScore参考值用于按照百分比方式显示分数
424
+ *
425
+ * @param score 这次回调的分数 0-10之间
426
+ * @param cumulativeScore 累计的分数 初始分累计到当前的分数
427
+ * @param totalScore 总分 = 初始分(默认值0分) + xml中sentence的个数 * 10
428
+ */
429
+ void onScore (double score , double cumulativeScore , double totalScore );
280
430
}
281
431
}
0 commit comments