歌词相关实现

歌词相关

  1. 歌词数据模型
// 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
}
  1. 歌词解析
// 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')
    // 包含每个字的持续时间
}
  1. 歌词显示视图
// LyricListView.swift
class LyricListView: BaseRelativeLayout {
    var data: Lyric?
    var tableView: UITableView!
    var datum: [Any] = []
    
    /// 当前显示的歌词行号
    var lyricLineNumber: Int = 0
    
    /// 歌词上下填充的占位行数
    var lyricPlaceholderSize = 0
    
    func 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 = lyricCurrentWordIndex
                    cell.lineView.wordPlayedTime = wordPlayedTime
                    cell.lineView.setNeedsDisplay()
                }
            }
        }
    }
}
  1. 歌词行视图
// LyricLineView.swift
class LyricLineView: UIView {
    var data: LyricLine?
    var accurate: Bool = false
    var lineSelected = false
    
    override 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] = lyricSelectedTextColor
                    wordStringNSString.draw(at: point, withAttributes: attributes)
                }
            } else {
                // 普通歌词绘制
                if lineSelected {
                    attributes[.foregroundColor] = lyricSelectedTextColor
                }
                wordStringNSString.draw(at: point, withAttributes: attributes)
            }
        }
    }
}
  1. 时间计算工具
// 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 + value
            if newTime < startTime {
                return index
            }
        }
        return -1
    }
}
  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?.lrc
            if let url = URL(string: urlString ?? "") {
                // 下载并解析歌词
            }
        }
    }
    
    // 播放进度更新时调用
    func updateProgress(_ progress: Float) {
        // 更新歌词显示
        lyricView?.setProgress(progress)
    }
}

这个实现的主要特点:

  1. 支持多种格式

    • LRC:简单的时间戳+歌词格式
    • KSC:支持精确到字的歌词显示
  2. 精确的时间控制

    • 毫秒级的时间计算
    • 支持精确到字的歌词显示
    • 平滑的滚动效果
  3. 良好的用户体验

    • 歌词居中显示
    • 支持拖拽交互
    • 显示拖拽位置的时间
    • 点击可以跳转到对应位置
  4. 性能优化

    • 使用占位行实现居中效果
    • 按需更新显示
    • 避免不必要的重绘

歌词同步机制:

  1. 时间同步机制
// 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
        }
    }
}
  1. 时间计算
// 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
}
  1. 滚动实现
// 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)
    }
}
  1. 播放器集成
// MusicPlayerManager.swift
class MusicPlayerManager {
    // 播放进度更新时调用
    func updateProgress(_ progress: Float) {
        // 更新歌词显示
        lyricView?.setProgress(progress)
    }
}

同步流程:

  1. 准备阶段

    • 解析歌词文件,获取每行歌词的开始时间
    • 将歌词数据存储在 parsedLyric
  2. 播放阶段

    • 播放器实时提供播放进度(秒)
    • 调用 setProgress 方法更新歌词显示
  3. 同步计算

    • 将播放时间转换为毫秒
    • 遍历歌词行,找到当前时间对应的行
    • 如果行号变化,滚动到新位置
  4. 显示更新

    • 使用动画滚动到当前歌词行
    • 保持当前行在屏幕中央
    • 高亮显示当前行

关键点:

  1. 使用毫秒级的时间计算,保证同步精度
  2. 倒序遍历歌词行,提高查找效率
  3. 使用动画滚动,提供流畅的视觉效果
  4. 保持当前行居中显示,提升用户体验
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值