-
-
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)() |
VLC 3.0.21版本会自动把luaintf改回lua,不知道是不是版本问题。一年前是没问题的,现在在另外个电脑上出现了这个情况
2025-10-10 更新:
将
'%[(%d%d):(%d%d)%.(%d%d)%]'改为
'%[(%d%d):(%d%d)[.:](%d%d?%d?)%]'以便能够匹配
- 毫秒前是冒号的
- 毫秒是 1-3 位数字的
I have lyrics shown in the console verbose, but no lyrics are on display although the OSD option is enabled:

The plugin is working because the messages tab shows:
lua info: [lyrics] get_show_lyrics = Who wants to be right as rain? / It's better when something is wrong / You get excitement in your bones
However I must be doing something wrong because I cannot get to have them on display. This is my VLC version if you could help me.
[user@hostname ~]$ vlc --version
VLC media player 3.0.21 Vetinari (revision 3.0.21-0-gdd8bfdbabe8)
VLC version 3.0.21 Vetinari (3.0.21-0-gdd8bfdbabe8)
Compiled by mockbuild on b1e929b8a7d445d3806013e2b645e9b8 (Jun 25 2025 00:00:00)
Compiler: gcc version 14.3.1 20250523 (Red Hat 14.3.1-1) (GCC)
This program comes with NO WARRANTY, to the extent permitted by law.
You may redistribute it under the terms of the GNU General Public License;
see the file named COPYING for details.
Written by the VideoLAN team; see the AUTHORS file.
Nevermind, I went to Audio > Visualizations > Spectrum and now it's showing
VLC 3.0.21版本会自动把luaintf改回lua,不知道是不是版本问题。一年前是没问题的,现在在另外个电脑上出现了这个情况
我也同样不行 VLC 3.0.20
注:必须开启音乐可视化后能显示歌词
音频->可视化->频谱(随便选一个其他的也行)