parse and display lyrics

pull/190/head
xufuji456 4 years ago
parent 8006b12fe9
commit 05872ae26d
  1. 57
      app/src/main/java/com/frank/ffmpeg/activity/AudioPlayActivity.kt
  2. 8
      app/src/main/java/com/frank/ffmpeg/listener/OnLrcListener.kt
  3. 18
      app/src/main/java/com/frank/ffmpeg/model/LrcLine.kt
  4. 88
      app/src/main/java/com/frank/ffmpeg/tool/LrcLineTool.kt
  5. 258
      app/src/main/java/com/frank/ffmpeg/view/LrcView.kt
  6. 6
      app/src/main/res/layout/activity_audio_play.xml

@ -12,7 +12,15 @@ import android.widget.ImageView
import android.widget.SeekBar
import android.widget.TextView
import com.frank.ffmpeg.R
import com.frank.ffmpeg.handler.FFmpegHandler
import com.frank.ffmpeg.handler.FFmpegHandler.MSG_FINISH
import com.frank.ffmpeg.listener.OnLrcListener
import com.frank.ffmpeg.model.MediaBean
import com.frank.ffmpeg.util.FFmpegUtil
import com.frank.ffmpeg.util.TimeUtil
import com.frank.ffmpeg.model.LrcLine
import com.frank.ffmpeg.tool.LrcLineTool
import com.frank.ffmpeg.view.LrcView
class AudioPlayActivity : AppCompatActivity() {
@ -23,11 +31,14 @@ class AudioPlayActivity : AppCompatActivity() {
private const val MSG_DURATION = 234
}
private var path: String? = null
private var txtTitle: TextView? = null
private var txtArtist: TextView? = null
private var txtTime: TextView? = null
private var txtDuration: TextView? = null
private var audioBar: SeekBar? = null
private var lrcView: LrcView? = null
private lateinit var audioPlayer:MediaPlayer
@ -40,12 +51,29 @@ class AudioPlayActivity : AppCompatActivity() {
audioBar?.progress = audioPlayer.currentPosition
txtTime?.text = TimeUtil.getVideoTime(audioPlayer.currentPosition.toLong())
sendEmptyMessageDelayed(MSG_TIME, 1000)
lrcView?.seekToTime(audioPlayer.currentPosition.toLong())
}
MSG_DURATION -> {
val duration = msg.obj as Int
txtDuration?.text = TimeUtil.getVideoTime(duration.toLong())
audioBar?.max = duration
}
MSG_FINISH -> {
val result = msg.obj as MediaBean
txtTitle?.text = result.audioBean?.title
txtArtist?.text = result.audioBean?.artist
val lyrics = result.audioBean?.lyrics
if (lyrics != null) {
val lrcList = arrayListOf<LrcLine>()
for (i in lyrics.indices) {
Log.e(TAG, "lyrics=_=" + lyrics[i])
val line = LrcLineTool.getLrcLine(lyrics[i])
if (line != null) lrcList.addAll(line)
}
LrcLineTool.sortLyrics(lrcList)
lrcView?.setLrc(lrcList)
}
}
}
}
}
@ -56,6 +84,7 @@ class AudioPlayActivity : AppCompatActivity() {
initView()
initAudioPlayer()
initLrc()
}
private fun initView() {
@ -63,6 +92,7 @@ class AudioPlayActivity : AppCompatActivity() {
txtArtist = findViewById(R.id.txt_artist)
txtTime = findViewById(R.id.txt_time)
txtDuration = findViewById(R.id.txt_duration)
lrcView = findViewById(R.id.list_lyrics)
val btnPlay: ImageView = findViewById(R.id.img_play)
btnPlay.setOnClickListener {
if (isPlaying()) {
@ -85,13 +115,21 @@ class AudioPlayActivity : AppCompatActivity() {
}
override fun onStopTrackingTouch(seekBar: SeekBar?) {
audioPlayer.seekTo(audioBar?.progress!!)
val progress = audioBar?.progress ?: 0
audioPlayer.seekTo(progress)
lrcView?.seekToTime(progress.toLong())
}
})
lrcView?.setListener(object :OnLrcListener {
override fun onLrcSeek(position: Int, lrcLine: LrcLine) {
audioPlayer.seekTo(lrcLine.startTime.toInt())
Log.e(TAG, "lrc position=$position--time=${lrcLine.startTime}")
}
})
}
private fun initAudioPlayer() {
val path = intent.data?.path
path = intent.data?.path
Log.e(TAG, "path=$path")
if (TextUtils.isEmpty(path)) return
audioPlayer = MediaPlayer()
@ -106,14 +144,25 @@ class AudioPlayActivity : AppCompatActivity() {
}
}
private fun initLrc() {
if (path.isNullOrEmpty()) return
val ffmpegHandler = FFmpegHandler(mHandler)
val commandLine = FFmpegUtil.probeFormat(path)
ffmpegHandler.executeFFprobeCmd(commandLine)
}
private fun isPlaying() :Boolean {
return audioPlayer.isPlaying
}
override fun onStop() {
super.onStop()
audioPlayer.stop()
audioPlayer.release()
try {
audioPlayer.stop()
audioPlayer.release()
} catch (e: Exception) {
Log.e(TAG, "release player err=$e")
}
}
}

@ -0,0 +1,8 @@
package com.frank.ffmpeg.listener
import com.frank.ffmpeg.model.LrcLine
interface OnLrcListener {
fun onLrcSeek(position: Int, lrcLine: LrcLine)
}

@ -0,0 +1,18 @@
package com.frank.ffmpeg.model
class LrcLine : Comparable<LrcLine> {
var timeString: String? = null
var startTime: Long = 0
var endTime: Long = 0
var content: String? = null
override fun compareTo(another: LrcLine): Int {
return (this.startTime - another.startTime).toInt()
}
}

@ -0,0 +1,88 @@
package com.frank.ffmpeg.tool
import com.frank.ffmpeg.model.LrcLine
import java.util.ArrayList
import java.util.Collections
object LrcLineTool {
private fun createLine(lrcLine: String?): List<LrcLine>? {
try {
if (lrcLine == null || lrcLine.isEmpty() ||
lrcLine.indexOf("[") != 0 || lrcLine.indexOf("]") != 9) {
return null
}
val lastIndexOfRightBracket = lrcLine.lastIndexOf("]")
val content = lrcLine.substring(lastIndexOfRightBracket + 1)
val times = lrcLine.substring(0, lastIndexOfRightBracket + 1).replace("[", "-")
.replace("]", "-")
if (times.isEmpty() || !Character.isDigit(times[1])) return null
val arrTimes = times.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val listTimes = ArrayList<LrcLine>()
for (temp in arrTimes) {
if (temp.trim { it <= ' ' }.isEmpty()) {
continue
}
val mLrcLine = LrcLine()
mLrcLine.content = content
mLrcLine.timeString = temp
val startTime = timeConvert(temp)
mLrcLine.startTime = startTime
listTimes.add(mLrcLine)
}
return listTimes
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* string time to milliseconds
*/
private fun timeConvert(timeStr: String): Long {
var timeString = timeStr
timeString = timeString.replace('.', ':')
val times = timeString.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
return (Integer.valueOf(times[0]) * 60 * 1000 +
Integer.valueOf(times[1]) * 1000 +
Integer.valueOf(times[2])).toLong()
}
fun getLrcLine(line: String?): List<LrcLine>? {
if (line == null || line.isEmpty()) {
return null
}
val rows = ArrayList<LrcLine>()
try {
val lrcLines = createLine(line)
if (lrcLines != null && lrcLines.isNotEmpty()) {
rows.addAll(lrcLines)
}
} catch (e: Exception) {
return null
}
return rows
}
fun sortLyrics(lrcList: List<LrcLine>): List<LrcLine> {
Collections.sort(lrcList)
if (lrcList.isNotEmpty()) {
val size = lrcList.size
for (i in 0 until size) {
val lrcLine = lrcList[i]
if (i < size - 1) {
lrcLine.endTime = lrcList[i + 1].startTime
} else {
lrcLine.endTime = lrcLine.startTime + 10 * 1000
}
}
}
return lrcList
}
}

@ -0,0 +1,258 @@
package com.frank.ffmpeg.view
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Paint.Align
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import com.frank.ffmpeg.listener.OnLrcListener
import com.frank.ffmpeg.model.LrcLine
import kotlin.math.abs
/**
* LrcView: display lyrics with playing time, seek and sync
*/
class LrcView(context: Context, attr: AttributeSet) : View(context, attr) {
private val mPadding = 10
private val mLrcFontSize = 35
private var mHighLightRow = 0
private var mLastMotionY = 0f
private var currentMillis: Long = 0
private var mLrcLines: List<LrcLine>? = null
private var mLrcViewListener: OnLrcListener? = null
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val mNormalRowColor = Color.BLACK
private val mHighLightRowColor = Color.BLUE
private var mDisplayMode = DISPLAY_MODE_NORMAL
private val mode = MODE_HIGH_LIGHT_KARAOKE
init {
mPaint.textSize = mLrcFontSize.toFloat()
}
override fun onDraw(canvas: Canvas) {
val height = height
val width = width
if (mLrcLines == null || mLrcLines!!.isEmpty()) {
mPaint.color = mHighLightRowColor
mPaint.textSize = mLrcFontSize.toFloat()
mPaint.textAlign = Align.CENTER
canvas.drawText(noLrcTip, (width / 2).toFloat(), (height / 2 - mLrcFontSize).toFloat(), mPaint)
return
}
var rowY: Int
val rowX = width / 2
var rowNum: Int = mHighLightRow - 1
// 1: current highlight lyrics
val highlightRowY = height / 2 - mLrcFontSize
if (mode == MODE_HIGH_LIGHT_KARAOKE) {
// highlight one by one
drawKaraokeHighLightLrcRow(canvas, width, rowX, highlightRowY)
} else {
drawHighLrcRow(canvas, height, rowX, highlightRowY)
}
if (mDisplayMode == DISPLAY_MODE_SEEK) {
mPaint.color = mSeekLineColor
val mSeekLinePaddingX = 0
canvas.drawLine(mSeekLinePaddingX.toFloat(), (highlightRowY + mPadding).toFloat(),
(width - mSeekLinePaddingX).toFloat(), (highlightRowY + mPadding).toFloat(), mPaint)
mPaint.color = mSeekLineTextColor
mPaint.textSize = mSeekLineTextSize.toFloat()
mPaint.textAlign = Align.LEFT
canvas.drawText(mLrcLines!![mHighLightRow].timeString!!, 0f, highlightRowY.toFloat(), mPaint)
}
// lyrics above the highlight one
mPaint.color = mNormalRowColor
mPaint.textSize = mLrcFontSize.toFloat()
mPaint.textAlign = Align.CENTER
rowY = highlightRowY - mPadding - mLrcFontSize
while (rowY > -mLrcFontSize && rowNum >= 0) {
val text = mLrcLines!![rowNum].content
canvas.drawText(text!!, rowX.toFloat(), rowY.toFloat(), mPaint)
rowY -= mPadding + mLrcFontSize
rowNum--
}
// lyrics below the highlight one
rowNum = mHighLightRow + 1
rowY = highlightRowY + mPadding + mLrcFontSize
while (rowY < height && rowNum < mLrcLines!!.size) {
val text = mLrcLines!![rowNum].content
canvas.drawText(text!!, rowX.toFloat(), rowY.toFloat(), mPaint)
rowY += mPadding + mLrcFontSize
rowNum++
}
}
private fun drawKaraokeHighLightLrcRow(canvas: Canvas, width: Int, rowX: Int, highlightRowY: Int) {
val highLrcLine = mLrcLines!![mHighLightRow]
val highlightText = highLrcLine.content
mPaint.color = mNormalRowColor
mPaint.textSize = mLrcFontSize.toFloat()
mPaint.textAlign = Align.CENTER
canvas.drawText(highlightText!!, rowX.toFloat(), highlightRowY.toFloat(), mPaint)
val highLineWidth = mPaint.measureText(highlightText).toInt()
val leftOffset = (width - highLineWidth) / 2
val start = highLrcLine.startTime
val end = highLrcLine.endTime
val highWidth = ((currentMillis - start) * 1.0f / (end - start) * highLineWidth).toInt()
if (highWidth > 0) {
mPaint.color = mHighLightRowColor
val textBitmap = Bitmap.createBitmap(highWidth, highlightRowY + mPadding, Bitmap.Config.ARGB_8888)
val textCanvas = Canvas(textBitmap)
textCanvas.drawText(highlightText, (highLineWidth / 2).toFloat(), highlightRowY.toFloat(), mPaint)
canvas.drawBitmap(textBitmap, leftOffset.toFloat(), 0f, mPaint)
}
}
private fun drawHighLrcRow(canvas: Canvas, height: Int, rowX: Int, highlightRowY: Int) {
val highlightText = mLrcLines!![mHighLightRow].content
mPaint.color = mHighLightRowColor
mPaint.textSize = mLrcFontSize.toFloat()
mPaint.textAlign = Align.CENTER
canvas.drawText(highlightText!!, rowX.toFloat(), highlightRowY.toFloat(), mPaint)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
if (mLrcLines == null || mLrcLines!!.isEmpty()) {
return super.onTouchEvent(event)
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mLastMotionY = event.y
invalidate()
}
MotionEvent.ACTION_MOVE -> doSeek(event)
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
if (mDisplayMode == DISPLAY_MODE_SEEK) {
seekLrc(mHighLightRow, true)
}
mDisplayMode = DISPLAY_MODE_NORMAL
invalidate()
}
}
return true
}
/**
* Moving lyrics, when touch and move the screen
*/
private fun doSeek(event: MotionEvent) {
val y = event.y
val offsetY = y - mLastMotionY
if (abs(offsetY) < mMinSeekFiredOffset) {
return
}
mDisplayMode = DISPLAY_MODE_SEEK
val rowOffset = abs(offsetY.toInt() / mLrcFontSize)
if (offsetY < 0) {
mHighLightRow += rowOffset
} else if (offsetY > 0) {
mHighLightRow -= rowOffset
}
mHighLightRow = 0.coerceAtLeast(mHighLightRow)
mHighLightRow = mHighLightRow.coerceAtMost(mLrcLines!!.size - 1)
if (rowOffset > 0) {
mLastMotionY = y
invalidate()
}
}
fun setListener(listener: OnLrcListener) {
mLrcViewListener = listener
}
fun setLrc(lrcLines: List<LrcLine>) {
mLrcLines = lrcLines
invalidate()
}
private fun seekLrc(position: Int, cb: Boolean) {
if (mLrcLines == null || position < 0 || position > mLrcLines!!.size) {
return
}
val lrcLine = mLrcLines!![position]
mHighLightRow = position
invalidate()
if (mLrcViewListener != null && cb) {
mLrcViewListener!!.onLrcSeek(position, lrcLine)
}
}
fun seekToTime(time: Long) {
if (mLrcLines == null || mLrcLines!!.isEmpty()) {
return
}
if (mDisplayMode != DISPLAY_MODE_NORMAL) {
return
}
currentMillis = time
for (i in mLrcLines!!.indices) {
val current = mLrcLines!![i]
val next = if (i + 1 == mLrcLines!!.size) null else mLrcLines!![i + 1]
if (time >= current.startTime && next != null && time < next.startTime || time > current.startTime && next == null) {
seekLrc(i, false)
return
}
}
}
companion object {
private const val mSeekLineTextSize = 15
private const val mSeekLineColor = Color.RED
private const val mSeekLineTextColor = Color.BLUE
/**
* Mode normal
*/
const val DISPLAY_MODE_NORMAL = 0
/**
* Mode seek
*/
const val DISPLAY_MODE_SEEK = 1
/**
* Minimum seeking distance
*/
private const val mMinSeekFiredOffset = 10
private const val noLrcTip = "No lyrics..."
private const val MODE_HIGH_LIGHT_NORMAL = 0
private const val MODE_HIGH_LIGHT_KARAOKE = 1
}
}

@ -22,11 +22,13 @@
android:textColor="@color/colorPrimary"
android:textSize="18sp"/>
<androidx.recyclerview.widget.RecyclerView
<com.frank.ffmpeg.view.LrcView
android:id="@+id/list_lyrics"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
android:layout_centerInParent="true"
android:layout_marginTop="160dp"
android:layout_marginBottom="160dp"/>
<RelativeLayout
android:id="@+id/layout_control"

Loading…
Cancel
Save