-
-
Save youthlin/a3b3fc033586bede6046086f3d889322 to your computer and use it in GitHub Desktop.
| --[[-- | |
| show lyrics on visualization 在可视化界面显示歌词 | |
| url 项目地址: https://gist.github.com/youthlin/a3b3fc033586bede6046086f3d889322 | |
| author 作者: youthlin | |
| author url 作者博客: https://youthlin.com | |
| How to install: | |
| 1. put this file on lua/intf/ | |
| 2. enable extra interface: luaintf (Settings[Show all] -> Interface -> Main Interfaces -> extract modules='luaintf'[make the 'Lua interpreter' checked]) | |
| (important: extraintf=luaintf not lua) | |
| 3. set lua interface to this file name: All Settings -> Interface -> Main Interface -> Lua -> Lua interface = lyrics | |
| 4. Settings - subtitle/osd - make "enable osd" checked. | |
| 如何启用: | |
| 1、将本文件放在 lua/intf/ 文件夹下 | |
| 2、设置(显示所有)-界面-主界面-扩展界面,填入 luaintf,并勾选 Lua interpreter(勾选会自动填为 lua, 需要手动改成 luaintf) | |
| 3、设置(显示所有)-界面-主界面-Lua,在 Lua 界面 字段填入本文件名: lyrics | |
| 4、设置-字幕/OSD-确保 "启用 OSD" 勾选,下方可以设置字体 | |
| INSTALLATION directory (\lua\intf\): 安装文件夹在哪里: | |
| * Windows (all users): %ProgramFiles%\VideoLAN\VLC\lua\intf\ | |
| * Windows (current user): %APPDATA%\VLC\lua\intf\ | |
| * Linux (all users): /usr/lib/vlc/lua/intf/ | |
| * Linux (current user): ~/.local/share/vlc/lua/intf/ | |
| * Mac OS X (all users): /Applications/VLC.app/Contents/MacOS/share/lua/intf/ | |
| * Mac OS X (current user): /Users/%your_name%/Library/Application Support/org.videolan.vlc/lua/intf/ | |
| Create directory if it does not exist! | |
| links: | |
| lrcview extension: http://eadmaster.altervista.org/pub/prj/lrcview.lua | |
| Times v3.2: https://addons.videolan.org/p/1154032/ | |
| vlc lua document: https://github.com/videolan/vlc/tree/master/share/lua | |
| document: https://github.com/verghost/vlc-lua-docs/blob/master/index.md | |
| VLC forum / Scripting VLC in lua: https://forum.videolan.org/viewforum.php?f=29 | |
| --]] -- | |
| -- [[ entrance ]] -- | |
| (function() -- 立即执行的函数: 入口 | |
| local lrc_config = { -- 配置项 | |
| supports = { "mp3", "wav", "flac", "ape", "aif", "m4a", "ogg" }, -- 后缀名 file extension | |
| pre = { -- 上一行歌词 | |
| show = false, | |
| prefix = "", | |
| suffix = " ♪\n" | |
| }, | |
| current = { -- 当前歌词 | |
| show = true, | |
| prefix = "", | |
| suffix = " ♪\n" | |
| }, | |
| next = { -- 下一行歌词 | |
| show = true, | |
| prefix = "", | |
| suffix = " ♪\n" | |
| }, | |
| lyrics_not_found = "未找到歌词", -- 未找到歌词时显示的文本 | |
| position = "top-right", -- 显示位置 | |
| meta_key = { "Lyrics", "LYRICS" }, -- 歌词元数据键名 | |
| } | |
| if vlc == nil then | |
| vlc = {} -- 消除警告 | |
| end | |
| local function info(lm) -- Info | |
| vlc.msg.info("[lyrics] " .. lm) | |
| end | |
| local function logerr(lm) -- Error | |
| vlc.msg.err("[lyrics] " .. lm) | |
| end | |
| local function debug(lm) -- debug | |
| vlc.msg.dbg("[lyrics] " .. lm) | |
| end | |
| info("lyrics started! 歌词插件已启动") | |
| local function sleep(st) -- 休眠的秒数 seconds | |
| vlc.misc.mwait(vlc.misc.mdate() + st * 1000000) | |
| end | |
| local function dump(name, object, level) -- 显示变量 | |
| if level == nil then | |
| level = 0 | |
| end | |
| local prefix = string.rep(" ", level * 2) | |
| if type(object) ~= "table" then -- 不是 table | |
| info(prefix .. "> " .. name .. " type=" .. type(object) .. " tostring = " .. tostring(object)) | |
| return false | |
| end | |
| info(prefix .. "> " .. name .. " tostring = " .. tostring(object)) | |
| for k, v in pairs(object) do | |
| k = name .. "." .. k | |
| info(prefix .. "> " .. k .. " => " .. type(v) .. " = " .. tostring(v)) | |
| if type(v) == "table" then | |
| dump(k, v, level + 1) | |
| end | |
| end | |
| return true | |
| end | |
| dump("vlc", vlc) | |
| dump("lrc_config", lrc_config) | |
| local function is_win() | |
| return vlc.win ~= nil | |
| end | |
| info("is windows == " .. tostring(is_win())) | |
| local curi = nil -- current uri | |
| local is_audio = false | |
| local seconds_to_lrc = nil -- seconds to lyrics | |
| local VLC_tc = 1 -- time corrector | |
| if tonumber(string.sub(vlc.misc.version(), 1, 1)) > 2 then | |
| VLC_tc = 1000000 | |
| end -- VLC3 | |
| debug("VLC_tc=" .. tostring(VLC_tc)) | |
| local function reset() -- 停止播放时重置变量 | |
| info("reset") | |
| curi = nil | |
| is_audio = false | |
| seconds_to_lrc = nil | |
| end | |
| local function get_lrc() -- 从 lrc 文件获取歌词 | |
| if curi == nil or curi == "" then | |
| return "" | |
| end | |
| local file = curi | |
| if is_win() then | |
| file = string.gsub(file, "file:///", "") -- file:///c:/xxx -> c:/xxx | |
| else | |
| file = string.gsub(file, "file://", "") -- file:///Users/xxx -> /Users/xxx | |
| end | |
| local dotIndex = -1 | |
| local dot = string.byte(".") | |
| for i = #file, 0, -1 do | |
| local ch = string.sub(file, i, i) | |
| local ch_byte = string.byte(ch) | |
| if ch_byte < 128 then | |
| if ch_byte == dot then | |
| dotIndex = i | |
| break | |
| end | |
| else | |
| break | |
| end | |
| end | |
| file = string.sub(file, 1, dotIndex) .. "lrc" | |
| if string.match(file, "^http") then | |
| info("从网络 lrc 文件读取歌词: " .. file .. " dotIndex=" .. dotIndex) | |
| local fd = vlc.stream(file) | |
| if not fd then | |
| logerr("未获取到网络歌词") | |
| return "" | |
| end | |
| dump("fd", fd) | |
| local result = fd:read(65653) | |
| fd = nil | |
| info('网络读取歌词 ok') | |
| return result | |
| end | |
| file = vlc.strings.decode_uri(file) | |
| info("从本地 lrc 文件读取歌词: " .. file .. " dotIndex=" .. dotIndex) | |
| -- 使用 vlc.io.open 而不是原生 io.open 可以兼容 Windows 下无法打开中文路径的问题 | |
| -- https://github.com/verghost/vlc-lua-docs/blob/master/m/io/index.md | |
| local f = vlc.io.open(file, "r") | |
| if f == nil then | |
| logerr("找不到 lrc 歌词文件: " .. file) | |
| return "" | |
| end | |
| local result = f:read("*all") | |
| f:close() | |
| info('本地 lrc 文件读取歌词 ok') | |
| return result | |
| end | |
| local function get_lyrics() -- 获取当前歌曲的歌词 | |
| debug("get_lyrics") | |
| local item = vlc.input.item() | |
| if item == nil then | |
| return "" | |
| end | |
| local metas = item:metas() | |
| dump("metas", metas) | |
| for _, key in ipairs(lrc_config.meta_key) do | |
| if metas[key] then | |
| info("从歌曲 Tag 中获取歌词: " .. key) | |
| return metas[key] | |
| end | |
| end | |
| return get_lrc() | |
| end | |
| local function extract_time(line) -- 获取一行歌词的开始时间 seconds | |
| if (line == nil) then | |
| return (-1) | |
| end | |
| local min, sec, mil = line:match('%[(%d%d):(%d%d)[.:](%d%d?%d?)%]') | |
| if (min == nil or sec == nil or mil == nil) then | |
| return (-1) | |
| end | |
| return (tonumber(min) * 60 + tonumber(sec) + tonumber("0." .. mil)) | |
| end | |
| local function build_lrc_table() -- 构造歌词表 | |
| debug("build_lrc_table") | |
| local lrc = {} | |
| local lyrics = get_lyrics() | |
| local i = 1 | |
| for line in lyrics:gmatch("[^\n]+") do | |
| line = string.gsub(line, "\n", "") -- remove newlines | |
| line = string.gsub(line, "\r", "") -- remove newlines | |
| local time = extract_time(line) | |
| if time >= 0 then | |
| lrc[i] = { | |
| time = time, | |
| lrc = string.gsub(line, "^%[.-%]", "") -- 去掉 [xxx] 时间 | |
| } | |
| i = i + 1 | |
| end | |
| end | |
| return lrc | |
| end | |
| local function uri_changed(uri) -- 读取当前歌曲的歌词 | |
| info("uri_changed to " .. uri) | |
| curi = uri | |
| local support = false | |
| for _, value in pairs(lrc_config.supports) do | |
| if string.match(uri, value .. "$") then | |
| support = true | |
| break | |
| end | |
| end | |
| is_audio = support | |
| info("is_audio=" .. tostring(support)) | |
| if not support then | |
| return | |
| end | |
| seconds_to_lrc = build_lrc_table() | |
| dump("seconds_to_lrc", seconds_to_lrc) | |
| if #seconds_to_lrc == 0 then | |
| info("没有歌词") | |
| end | |
| local input = vlc.object.input() | |
| dump("input", input) | |
| end | |
| local function get_show_lyrics() -- 获取要显示的歌词 | |
| local total = #seconds_to_lrc | |
| -- 没有歌词 | |
| if #seconds_to_lrc == 0 then | |
| -- debug("没有歌词") | |
| return lrc_config.lyrics_not_found | |
| end | |
| -- 有歌词 | |
| local input = vlc.object.input() | |
| local current_second = vlc.var.get(input, "time") / VLC_tc | |
| local prefix, current, next = "", "", "" | |
| debug("get_show_lyrics current_second = " .. current_second) | |
| local index = 1 | |
| for i = 1, total do | |
| local item = seconds_to_lrc[i] | |
| local second, value = item.time, item.lrc | |
| if second <= current_second then | |
| current = value | |
| index = i | |
| end | |
| end | |
| if index > 1 then | |
| prefix = seconds_to_lrc[index - 1].lrc | |
| end | |
| if index < total then | |
| next = seconds_to_lrc[index + 1].lrc | |
| end | |
| info("get_show_lyrics = " .. prefix .. " / " .. current .. " / " .. next) | |
| local result = "" | |
| if lrc_config.pre.show then | |
| result = result .. lrc_config.pre.prefix .. prefix .. lrc_config.pre.suffix | |
| end | |
| if lrc_config.current.show then | |
| result = result .. lrc_config.current.prefix .. current .. lrc_config.current.suffix | |
| end | |
| if lrc_config.next.show then | |
| result = result .. lrc_config.next.prefix .. next .. lrc_config.next.suffix | |
| end | |
| return result | |
| end | |
| local function do_task() -- 播放时持续调用的函数(每0.1s) | |
| --[[ | |
| local vout = vlc.object.vout() | |
| if vout == nil then | |
| -- info("可视化未开启") | |
| return | |
| end | |
| --]] | |
| if not is_audio then | |
| return | |
| end | |
| vlc.osd.message(get_show_lyrics(), nil, lrc_config.position) | |
| end | |
| -- 主循环 | |
| while true do | |
| if vlc.volume.get() == -256 then -- 当进程被杀时 | |
| break | |
| end -- inspired by syncplay.lua; kills vlc.exe process in Task Manager | |
| if vlc.playlist.status() == "stopped" then -- no input or stopped input | |
| if curi then -- input stopped | |
| info("stopped") | |
| reset() | |
| end | |
| sleep(1) | |
| else -- playing, paused | |
| local uri = nil | |
| if vlc.input.item() then | |
| uri = vlc.input.item():uri() | |
| end | |
| if not uri then --- WTF (VLC 2.1+): status playing with nil input? Stopping? O.K. in VLC 2.0.x | |
| info("WTF??? " .. vlc.playlist.status()) | |
| sleep(0.1) | |
| elseif not curi or curi ~= uri then -- new input (first input or changed input) | |
| uri_changed(uri) | |
| else -- current input | |
| do_task() | |
| if vlc.playlist.status() == "playing" then | |
| -- info("playing") | |
| elseif vlc.playlist.status() == "paused" then | |
| -- info("paused") | |
| sleep(0.3) | |
| else -- ? | |
| info("unknown play status") | |
| sleep(1) | |
| end | |
| sleep(0.1) -- 每 0.1 秒调用一次 task | |
| end | |
| end | |
| end | |
| end)() |
按照介绍的方法:
1、将本文件放在 lua/intf/ 文件夹下
2、设置(显示所有)-界面-主界面-扩展界面,填入 luaintf,并勾选 Lua interpreter(勾选会自动填为 lua, 需要手动改成 luaintf)
3、设置(显示所有)-界面-主界面-Lua,在 Lua 界面 字段填入本文件名: lyrics
4、设置-字幕/OSD-确保 "启用 OSD" 勾选,下方可以设置字体
在flac文件标签里的歌词还是不能显示,选可视化的时候,提示未找到歌词,不知道自己哪里弄错了,来请教
按照介绍的方法: 1、将本文件放在 lua/intf/ 文件夹下 2、设置(显示所有)-界面-主界面-扩展界面,填入 luaintf,并勾选 Lua interpreter(勾选会自动填为 lua, 需要手动改成 luaintf) 3、设置(显示所有)-界面-主界面-Lua,在 Lua 界面 字段填入本文件名: lyrics 4、设置-字幕/OSD-确保 "启用 OSD" 勾选,下方可以设置字体 在flac文件标签里的歌词还是不能显示,选可视化的时候,提示未找到歌词,不知道自己哪里弄错了,来请教
音乐标签:安卓版、Windows 版本,小众软件介绍。
另一个 Web 版: xhongc/music-tag-web
@Bigxxi 你好,可以把日志级别调整为debug看下。也可加微信交流:Youth_Lin
问题:在 Windows 环境中,lrc 路径包含中文(Non-Ascii字符),使用 lua 原生的 io.open 会报错 invalid argument
解决:使用 vlc.io.open 即可,vlc 内部封装的这个函数入参为 UTF-8 字符串,支持中文
脚本已更新
- 通过本文确认 io.open 在 Windows 上无法打开中文路径:LUA 编码 CODEPAGE CP936 UNICODE UTF8 及 相关库LIBRARY 整理
- 通过本项目搜到 vlc 内部有 io 库:VLC Lua Docs, IO Module
vlc 内部如何支持非 ASCII 字符的:
// vlc.io.open 的 C 实现【1】/modules/lua/libs/io.c
static int vlclua_io_open( lua_State *L )
// vlclua_io_open 调用 vlc_fopen【2】/src/text/filesystem.c
FILE *vlc_fopen (const char *filename, const char *mode)
// vlc_fopen 调用 vlc_open【3】/src/win32/filesystem.c 【4】
int vlc_open (const char *filename, int flags, ...)
// 调用同文件 widen_path 函数【5】/src/win32/filesystem.c
static wchar_t *widen_path (const char *path)
// 【6】include/vlc_charset.h
VLC_USED static inline wchar_t *ToWide (const char *utf8)
// 【7】是 windows 提供的 api
MultiByteToWideChar
顺便补充一下,如果有朋友使用外挂lrc文件在windows上无法显示中文歌词,需要将lrc文件改为utf-8编码,并且要在设置-字幕/OSD-字体改为【Microsoft YaHei UI】,部分中文字体也会导致歌词失效
注:必须开启音乐可视化后能显示歌词
音频->可视化->频谱(随便选一个其他的也行)
VLC 3.0.21版本会自动把luaintf改回lua,不知道是不是版本问题。一年前是没问题的,现在在另外个电脑上出现了这个情况
2025-10-10 更新:
将
'%[(%d%d):(%d%d)%.(%d%d)%]'改为
'%[(%d%d):(%d%d)[.:](%d%d?%d?)%]'以便能够匹配
- 毫秒前是冒号的
- 毫秒是 1-3 位数字的










How do I upload my extension to http://addons.videolan.org