歌詞相關
- 歌詞數據模型:
// Lyric.swift
class Lyric: BaseModel {/// 是否是精確到字的歌詞var isAccurate:Bool = false/// 所有的歌詞var datum:Array<LyricLine>!
}// LyricLine.swift
class LyricLine: BaseModel {/// 整行歌詞var data:String!/// 開始時間(毫秒)var startTime:Int!/// 每個字(KSC格式)var words:Array<String>!/// 每個字的持續時間(KSC格式)var wordDurations:Array<Int>!/// 結束時間var endTime:Int = 0
}
- 歌詞解析:
// LRCLyricParser.swift - LRC格式解析
static func parse(_ data:String) -> Lyric {let result = Lyric()result.isAccurate = false // LRC格式不精確到字// 按行分割let strings = data.components(separatedBy: "\n")for line in strings {if line.starts(with: "[0") {// 解析時間戳和歌詞內容// 例如:[00:00.300]愛的代價let lyricLine = LyricLine()// 解析時間戳lyricLine.startTime = DateUtil.parseToInt(commands[0])// 解析歌詞內容lyricLine.data = commands[1]result.datum.append(lyricLine)}}return result
}// KSCLyricParser.swift - KSC格式解析
static func parse(_ data:String) -> Lyric {let result = Lyric()result.isAccurate = true // KSC格式精確到字// 解析每行歌詞// 例如:karaoke.add('00:27.487', '00:32.068', '一時失志不免怨嘆', '347,373,1077,320,344,386,638,1096')// 包含每個字的持續時間
}
- 歌詞顯示視圖:
// LyricListView.swift
class LyricListView: BaseRelativeLayout {var data: Lyric?var tableView: UITableView!var datum: [Any] = []/// 當前顯示的歌詞行號var lyricLineNumber: Int = 0/// 歌詞上下填充的占位行數var lyricPlaceholderSize = 0func setProgress(_ progress: Float) {// 1. 計算當前應該顯示哪一行let newLineNumber = LyricUtil.getLineNumber(data!, progress) + lyricPlaceholderSize// 2. 如果行號變化,滾動到新位置if newLineNumber != lyricLineNumber {scrollPosition(newLineNumber)lyricLineNumber = newLineNumber}// 3. 如果是精確到字的歌詞,更新當前字的位置if data!.isAccurate {if let object = datum[lyricLineNumber] as? LyricLine {// 計算當前是第幾個字let lyricCurrentWordIndex = LyricUtil.getWordIndex(object, progress)// 計算當前字已經播放的時間let wordPlayedTime = LyricUtil.getWordPlayedTime(object, progress)// 更新顯示if let cell = getCell(lyricLineNumber) {cell.lineView.lyricCurrentWordIndex = lyricCurrentWordIndexcell.lineView.wordPlayedTime = wordPlayedTimecell.lineView.setNeedsDisplay()}}}}
}
- 歌詞行視圖:
// LyricLineView.swift
class LyricLineView: UIView {var data: LyricLine?var accurate: Bool = falsevar lineSelected = falseoverride func draw(_ rect: CGRect) {if let data = self.data {if accurate {// 精確到字的歌詞繪制// 1. 繪制整行歌詞(灰色)wordStringNSString.draw(at: point, withAttributes: attributes)if lineSelected {// 2. 計算高亮部分的寬度let lineLyricPlayedWidth = calculatePlayedWidth()// 3. 繪制高亮部分(紅色)let selectedRect = CGRect(x: point.x, y: point.y, width: lineLyricPlayedWidth, height: size.height)context.clip(to: selectedRect)attributes[.foregroundColor] = lyricSelectedTextColorwordStringNSString.draw(at: point, withAttributes: attributes)}} else {// 普通歌詞繪制if lineSelected {attributes[.foregroundColor] = lyricSelectedTextColor}wordStringNSString.draw(at: point, withAttributes: attributes)}}}
}
- 時間計算工具:
// LyricUtil.swift
class LyricUtil {/// 計算當前時間對應的歌詞行static func getLineNumber(_ lyric: Lyric, _ progress: Float) -> Int {let progress = progress * 1000 // 轉為毫秒// 倒序遍歷找到第一個開始時間小于等于當前時間的行for (index, value) in lyric.datum.enumerated().reversed() {if progress >= Float(value.startTime) {return index}}return 0}/// 計算當前時間對應的字(KSC格式)static func getWordIndex(_ line: LyricLine, _ progress: Float) -> Int {let newTime = Int(progress * 1000)var startTime = line.startTime!// 累加每個字的持續時間,找到當前字for (index, value) in line.wordDurations!.enumerated() {startTime = startTime + valueif newTime < startTime {return index}}return -1}
}
- 播放器集成:
// MusicPlayerManager.swift
class MusicPlayerManager {func prepareLyric() {// 1. 檢查是否有歌詞if data!.parsedLyric != nil {onLyricReady()} else if SuperStringUtil.isNotBlank(data!.lyric) {// 2. 解析本地歌詞parseLyric()} else {// 3. 從網絡獲取歌詞let urlString = data?.lrcif let url = URL(string: urlString ?? "") {// 下載并解析歌詞}}}// 播放進度更新時調用func updateProgress(_ progress: Float) {// 更新歌詞顯示lyricView?.setProgress(progress)}
}
這個實現的主要特點:
-
支持多種格式:
- LRC:簡單的時間戳+歌詞格式
- KSC:支持精確到字的歌詞顯示
-
精確的時間控制:
- 毫秒級的時間計算
- 支持精確到字的歌詞顯示
- 平滑的滾動效果
-
良好的用戶體驗:
- 歌詞居中顯示
- 支持拖拽交互
- 顯示拖拽位置的時間
- 點擊可以跳轉到對應位置
-
性能優化:
- 使用占位行實現居中效果
- 按需更新顯示
- 避免不必要的重繪
歌詞同步機制:
- 時間同步機制:
// LyricListView.swift
func setProgress(_ progress: Float) {if datum.count > 0 {// 1. 根據當前播放時間,計算應該顯示哪一行歌詞let newLineNumber = LyricUtil.getLineNumber(data!, progress) + lyricPlaceholderSize//所以為什么不二分// 2. 如果行號發生變化,滾動到新位置if newLineNumber != lyricLineNumber {scrollPosition(newLineNumber)lyricLineNumber = newLineNumber}}
}
- 時間計算:
// LyricUtil.swift
static func getLineNumber(_ lyric: Lyric, _ progress: Float) -> Int {// 將播放時間轉換為毫秒let progress = progress * 1000// 倒序遍歷歌詞行,找到第一個開始時間小于等于當前時間的行for (index, value) in lyric.datum.enumerated().reversed() {if progress >= Float(value.startTime) {return index}}return 0
}
- 滾動實現:
// LyricListView.swift
func scrollPosition(_ lineNumber: Int) {let indexPaht = IndexPath(item: lineNumber, section: 0)if tableView.visibleCells.count > 0 {// 使用動畫滾動到當前行,并保持居中tableView.selectRow(at: indexPaht, animated: true, scrollPosition: .middle)}
}
- 播放器集成:
// MusicPlayerManager.swift
class MusicPlayerManager {// 播放進度更新時調用func updateProgress(_ progress: Float) {// 更新歌詞顯示lyricView?.setProgress(progress)}
}
同步流程:
-
準備階段:
- 解析歌詞文件,獲取每行歌詞的開始時間
- 將歌詞數據存儲在
parsedLyric
中
-
播放階段:
- 播放器實時提供播放進度(秒)
- 調用
setProgress
方法更新歌詞顯示
-
同步計算:
- 將播放時間轉換為毫秒
- 遍歷歌詞行,找到當前時間對應的行
- 如果行號變化,滾動到新位置
-
顯示更新:
- 使用動畫滾動到當前歌詞行
- 保持當前行在屏幕中央
- 高亮顯示當前行
關鍵點:
- 使用毫秒級的時間計算,保證同步精度
- 倒序遍歷歌詞行,提高查找效率
- 使用動畫滾動,提供流暢的視覺效果
- 保持當前行居中顯示,提升用戶體驗