支持LRC歌词的终端播放器

一个简明python实现

最终效果可以看这里:

来自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