支持LRC歌词的终端播放器

Published on Oct 09, 2014

最终效果可以看这里:

来自asciinema的终端录像

缘起

自从降频之后,电脑特别卡……

降频是因为总是自动关机

自动关机据几个月观察是因为cpu过热

cpu过热猜测是因为散热有问题了

散热有问题猜测是该清灰换硅胶了

但三星……非常麻烦,自从去年毕业自己把本拆了一遍后就再懒得拆机了。

于是,想听歌时懒得打开vlc界面,就用命令行版本的cvlc来播放

可是……没有歌词?

某天想干脆自己做个支持lrc的终端播放器算了。

作为一个脚本小子,我只习惯python……

说干就干

谷歌搜寻了下:

  • python如何播放mp3音乐
  • lrc文件的格式,考虑下如何在终端打印歌词
  • vlc有没有python绑定

于是,有了以下版本,仅仅100行的多彩歌词显示终端播放器(使用=pygame=来播放音乐):

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-

    from pygame import mixer
    from mutagen.mp3 import MP3
    import re
    import time
    import sys
    import random

    f_mp3 = sys.argv[1]
    f_lrc = sys.argv[2]


    def lrc2dict(lrc):
        lrc_dict = {}
        remove = lambda x: x.strip('[|]')
        for line in lrc.split('\n'):
            time_stamps = re.findall(r'\[[^\]]+\]', line)
            if time_stamps:
                # 截取歌词
                lyric = line
                for tplus in time_stamps:
                    lyric = lyric.replace(tplus, '')
                # 解析时间
                # tplus: [02:31.79]
                # t 02:31.79
                for tplus in time_stamps:
                    t = remove(tplus)
                    tag_flag = t.split(':')[0]
                    # 跳过: [ar: 逃跑计划]
                    if not tag_flag.isdigit():
                        continue
                    time_lrc = int(tag_flag) * 60000
                    time_lrc += int(t.split(':')[1].split('.')[0]) * 1000
                    # ms也许没有
                    try:
                        time_lrc += int(t.split('.')[1])
                    except:
                        pass
                    # 截取到0.1s精度,降低cpu占用
                    time_lrc = time_lrc / 100 * 100
                    lrc_dict[time_lrc] = lyric
        return lrc_dict


    def print_lrc(mixer, lrc_d, color, wholetime):
        mixer.music.play()
        # 防止开头歌词来不及播放
        mixer.music.pause()
        time.sleep(0.001)
        mixer.music.play()
        while 1:
            # 截取到0.1s精度,降低cpu占用
            t = mixer.music.get_pos() / 100 * 100
            sys.stdout.write('[' +
                             time.strftime("%M:%S", time.localtime(t / 1000)) +
                             '/' +
                             time.strftime("%M:%S",
                                           time.localtime(wholetime)) +
                             '] ')
            if t in lrc_d:
                sys.stdout.write(color + lrc_d[t] + '\033[0m')
                sys.stdout.flush()
                # 向后清除
                sys.stdout.write("\033[K")
            else:
                sys.stdout.flush()
                # 向后清除
                # sys.stdout.write("\033[K")
            sys.stdout.write('\r')
            # 播放停止时退出
            if t < 0:
                sys.exit(0)
            # 0.05s循环
            time.sleep(0.05)

    with open(f_lrc) as f:
        lrc = f.read()

    lrc_d = lrc2dict(lrc)

    mixer.init()
    mixer.music.load(f_mp3)

    try:
        wholetime = MP3(f_mp3).info.length
    except:
        wholetime = 0

    colors = [
        '\x1B[31m',  # 红色
        '\x1B[32m',  # 绿色
        '\x1B[33m',  # 黄色
        '\x1B[34m',  # 蓝色
        '\x1B[35m',  # 紫色
        '\x1B[36m',  # 青色
        '\x1B[37m'  # 灰白
    ]
    color = random.choice(colors)
    print_lrc(mixer, lrc_d, color, wholetime)

那个不断循环查看播放进度打印对应歌词的实现真够丧心病狂的……精确到0.1s算了。

    Be aware that MP3 support is limited. On some systems an unsupported format can crash the program, e.g. Debian Linux. Consider using OGG instead.

开始在pygame的文档中看到这句话还没在意后来,播放某个叫=in my life=的mp3时发现完全不对劲……而且没有获取音频长度的功能1

后来想想,还是用vlc的py绑定吧。

vlc的py绑定相当赞:

完整覆盖libvlc功能,纯python,支持各个vlc版本和完整的文档。2

但开始我以为pip可以直接下载……后来发现pip搜索到的是macos独有的vlc python wrapper……和这个绑定是两码事……真给跪了……

自行下载vlc.py到当前目录或者扔到python搜索路径。

    #!/usr/bin/env python
    # -*- coding:utf-8 -*-

    import vlc
    import re
    import time
    import sys
    import random

    f_mp3 = sys.argv[1]
    f_lrc = sys.argv[2]


    def lrc2dict(lrc):
        lrc_dict = {}
        remove = lambda x: x.strip('[|]')
        for line in lrc.split('\n'):
            time_stamps = re.findall(r'\[[^\]]+\]', line)
            if time_stamps:
                # 截取歌词
                lyric = line
                for tplus in time_stamps:
                    lyric = lyric.replace(tplus, '')
                # 解析时间
                # tplus: [02:31.79]
                # t 02:31.79
                for tplus in time_stamps:
                    t = remove(tplus)
                    tag_flag = t.split(':')[0]
                    # 跳过: [ar: 逃跑计划]
                    if not tag_flag.isdigit():
                        continue
                    time_lrc = int(tag_flag) * 60000
                    time_lrc += int(t.split(':')[1].split('.')[0]) * 1000
                    # ms也许没有
                    try:
                        time_lrc += int(t.split('.')[1])
                    except:
                        pass
                    # 截取到0.1s精度,降低cpu占用
                    time_lrc = time_lrc / 1000 * 1000
                    lrc_dict[time_lrc] = lyric
        return lrc_dict


    def print_lrc(player, lrc_d, color):
        player.play()
        player.pause()
        # 防止无法获取整个音频时长
        # 音频时长必须加载才能读取
        time.sleep(0.1)
        player.play()
        wholetime = mediaObject.get_duration() / 1000 * 1000
        while 1:
            # 截取到0.1s精度,降低cpu占用, notwork fine for vlc
            # FIXME: get_time NOT WORK WELL, 只能精确到0.3s
            # t = player.get_position() * wholetime
            t = player.get_time() / 1000 * 1000
            # sys.stdout.write(str(t) + '\r')
            sys.stdout.write('[' +
                             time.strftime("%M:%S", time.localtime(t / 1000)) +
                             '/' +
                             time.strftime("%M:%S",
                                           time.localtime(wholetime / 1000)) +
                             '] ')
            if t not in lrc_d:
                sys.stdout.flush()
                # 向后清除
                # sys.stdout.write("\033[K")
            else:
                sys.stdout.write(color + lrc_d[t] + '\033[0m')
                sys.stdout.flush()
                # 向后清除
                sys.stdout.write("\033[K")
            sys.stdout.write('\r')
            # 播放停止时退出
            if t == wholetime:
                sys.exit(0)
            # 0.05s循环
            time.sleep(0.05)

    with open(f_lrc) as f:
        lrc = f.read()

    lrc_d = lrc2dict(lrc)

    vlcInstance = vlc.Instance()
    player = vlcInstance.media_player_new()
    mediaObject = vlcInstance.media_new(f_mp3)
    player.set_media(mediaObject)

    colors = [
        '\x1B[31m',  # 红色
        '\x1B[32m',  # 绿色
        '\x1B[33m',  # 黄色
        '\x1B[34m',  # 蓝色
        '\x1B[35m',  # 紫色
        '\x1B[36m',  # 青色
        '\x1B[37m'  # 灰白
    ]
    color = random.choice(colors)
    print_lrc(player, lrc_d, color)

奇葩的还是那个丧心病狂的循环,查询播放进度最短间隔只有0.3s。只好把lrc歌词显示精度降到1s了

反正对我是能用了。

也许有空可以让它支持播放列表的,用vlc完全不是问题。

嗯,完工。我擦嘞我竟然一天写了俩!

FootNotes