添加中文和歌词支持,添加多通道

This commit is contained in:
HKXluo
2025-11-12 15:16:22 +08:00
parent 93e163525e
commit 62b95e38b0
4 changed files with 1020 additions and 329 deletions

520
MusicLyrics.lua Normal file
View File

@@ -0,0 +1,520 @@
-- 歌词显示软件
local function loadRemoteFont(url)
local response = http.get(url)
if not response then
error("无法连接到字体服务器")
end
if response.getResponseCode() ~= 200 then
error("字体服务器返回错误: " .. response.getResponseCode())
end
local content = response.readAll()
response.close()
local sandbox = {}
local chunk, err = load(content, "=remoteFont", "t", sandbox)
if not chunk then
error("加载字体失败: " .. err)
end
local success, result = pcall(chunk)
if not success then
error("执行字体脚本失败: " .. result)
end
return sandbox.font or sandbox[1] or result
end
-- 显示单个字符的函数
local function displayChar(charMap, x, y, textColor, backgroundColor)
local origTextColor = term.getTextColor()
local origBackgroundColor = term.getBackgroundColor()
term.setTextColor(textColor)
term.setBackgroundColor(backgroundColor)
for row = 1, #charMap do
term.setCursorPos(x, y + row - 1)
local line = charMap[row]
for col = 1, #line do
local byte = string.byte(line, col)
if byte < 128 then
term.setTextColor(backgroundColor)
term.setBackgroundColor(textColor)
term.write(string.char(byte + 128))
else
term.setTextColor(textColor)
term.setBackgroundColor(backgroundColor)
term.write(string.char(byte))
end
end
end
term.setTextColor(origTextColor)
term.setBackgroundColor(origBackgroundColor)
end
-- 显示UTF-8字符串的函数
local function displayUtf8String(str, font, x, y, textColor, backgroundColor)
local function utf8codes(str)
local i = 1
return function()
if i > #str then return end
local b1 = string.byte(str, i)
i = i + 1
if b1 < 0x80 then
return b1
elseif b1 >= 0xC0 and b1 < 0xE0 then
local b2 = string.byte(str, i) or 0
i = i + 1
return (b1 - 0xC0) * 64 + (b2 - 0x80)
elseif b1 >= 0xE0 and b1 < 0xF0 then
local b2 = string.byte(str, i) or 0
i = i + 1
local b3 = string.byte(str, i) or 0
i = i + 1
return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80)
else
return 32
end
end
end
local cursorX = x
for code in utf8codes(str) do
local charMap = font[code]
if not charMap then
charMap = font[32] or {{"\x80"}}
end
displayChar(charMap, cursorX, y, textColor, backgroundColor)
cursorX = cursorX + #charMap[1]
end
end
-- 解析歌词时间
local function parseTime(timeStr)
local minutes, seconds, milliseconds = timeStr:match("(%d+):(%d+)%.(%d+)")
if minutes and seconds and milliseconds then
return tonumber(minutes) * 60 + tonumber(seconds) + tonumber(milliseconds) / 1000
end
return 0
end
-- 解析歌词内容
local function parseLyrics(lyricStr)
local lyrics = {}
if not lyricStr or lyricStr == "" then return lyrics end
for line in lyricStr:gmatch("[^\r\n]+") do
if line:match("%[.+%]") then
local timeStr = line:match("%[(.+)%]")
local text = line:match("%].*$")
if text then text = text:sub(2) else text = "" end
-- 过滤掉作词、作曲等元信息
if timeStr and not timeStr:match("by:") and not timeStr:match("offset:") and not text:match("作词") and not text:match("作曲") then
local time = parseTime(timeStr)
table.insert(lyrics, {time = time, text = text})
end
end
end
-- 按时间排序
table.sort(lyrics, function(a, b) return a.time < b.time end)
return lyrics
end
-- 计算字符串宽度
local function getStringWidth(str, font)
local width = 0
local function utf8codes(s)
local i = 1
return function()
if i > #s then return end
local b1 = string.byte(s, i)
i = i + 1
if b1 < 0x80 then
return b1
elseif b1 >= 0xC0 and b1 < 0xE0 then
local b2 = string.byte(s, i) or 0
i = i + 1
return (b1 - 0xC0) * 64 + (b2 - 0x80)
elseif b1 >= 0xE0 and b1 < 0xF0 then
local b2 = string.byte(s, i) or 0
i = i + 1
local b3 = string.byte(s, i) or 0
i = i + 1
return (b1 - 0xE0) * 4096 + (b2 - 0x80) * 64 + (b3 - 0x80)
else
return 32
end
end
end
for code in utf8codes(str) do
local charMap = font[code] or font[32]
width = width + #charMap[1]
end
return width
end
-- 获取当前应该显示的歌词索引
local function getCurrentLyricIndex(lyricPairs, currentTime)
local currentIndex = 1
for i = 1, #lyricPairs do
if lyricPairs[i].time <= currentTime then
currentIndex = i
else
break
end
end
return currentIndex
end
-- 解析颜色参数
local function parseColor(colorStr)
local colorsMap = {
white = colors.white,
orange = colors.orange,
magenta = colors.magenta,
lightBlue = colors.lightBlue,
yellow = colors.yellow,
lime = colors.lime,
pink = colors.pink,
gray = colors.gray,
lightGray = colors.lightGray,
cyan = colors.cyan,
purple = colors.purple,
blue = colors.blue,
brown = colors.brown,
green = colors.green,
red = colors.red,
black = colors.black
}
return colorsMap[colorStr] or colors.white
end
-- 主显示函数
local function displayLyrics(url, notzh, fontType, colorsConfig)
-- 选择字体
local fontUrl
if fontType == "12px" then
fontUrl = "https://alist.liulikeji.cn/d/HFS/fusion-pixel-12px-proportional-zh_hans.lua"
else
fontUrl = "https://alist.liulikeji.cn/d/HFS/fusion-pixel-8px-proportional-zh_hans.lua"
end
local font = loadRemoteFont(fontUrl)
-- 获取歌词数据
local response = http.get(url)
if not response then
print("无法连接到服务器")
return
end
if response.getResponseCode() ~= 200 then
print("服务器返回错误: " .. response.getResponseCode())
response.close()
return
end
local content = response.readAll()
response.close()
-- 解析JSON数据
local data = textutils.unserialiseJSON(content)
if not data or not data.lrc or not data.lrc.lyric then
print("无法解析歌词数据")
return
end
-- 解析歌词
local lyrics = parseLyrics(data.lrc.lyric)
local tlyrics = {}
if not notzh and data.tlyric and data.tlyric.lyric then
tlyrics = parseLyrics(data.tlyric.lyric)
end
if #lyrics == 0 and #tlyrics == 0 then
print("没有找到有效歌词")
return
end
-- 构建时间索引映射
local timeIndexMap = {}
for i, lyric in ipairs(lyrics) do
local timeKey = string.format("%.3f", lyric.time)
timeIndexMap[timeKey] = {original = i}
end
for i, lyric in ipairs(tlyrics) do
local timeKey = string.format("%.3f", lyric.time)
if timeIndexMap[timeKey] then
timeIndexMap[timeKey].translation = i
else
timeIndexMap[timeKey] = {translation = i}
end
end
-- 构建歌词对数组
local lyricPairs = {}
local allTimes = {}
for timeKey, _ in pairs(timeIndexMap) do
table.insert(allTimes, tonumber(timeKey))
end
table.sort(allTimes)
for _, time in ipairs(allTimes) do
local timeKey = string.format("%.3f", time)
local indices = timeIndexMap[timeKey]
local originalText = ""
local translationText = ""
if indices.original then
originalText = lyrics[indices.original].text or ""
end
if indices.translation then
translationText = tlyrics[indices.translation].text or ""
end
table.insert(lyricPairs, {
time = time,
original = originalText,
translation = translationText
})
end
if #lyricPairs == 0 then
print("没有找到匹配的歌词对")
return
end
-- 计算显示参数
local screenWidth, screenHeight = term.getSize()
local fontHeight = #font[32] -- 使用空格字符获取字体高度
-- 检查是否有翻译歌词
local hasTranslation = false
if not notzh then
for _, pair in ipairs(lyricPairs) do
if pair.translation and pair.translation ~= "" then
hasTranslation = true
break
end
end
end
-- 根据是否显示翻译来计算行高
local lyricPairHeight
if notzh or not hasTranslation then
lyricPairHeight = fontHeight + 1 -- 只有原文的高度
else
lyricPairHeight = fontHeight * 2 + 1 -- 原文+翻译的高度
end
local maxLyricPairs = math.floor(screenHeight / lyricPairHeight)
local visibleLyricPairs = math.max(1, maxLyricPairs) -- 确保至少显示1对
-- 显示循环
while true do
-- 获取当前播放时间
local currentTime = _G.getPlay - 1 or 0 -- 直接获取_G.getPlay的值
-- 获取当前歌词索引
local currentIndex = getCurrentLyricIndex(lyricPairs, currentTime)
term.setBackgroundColor(colorsConfig.background)
-- 清屏
term.clear()
-- 计算显示范围 - 真正的居中显示策略
local startPairIndex, endPairIndex
if visibleLyricPairs < 3 then
-- 当可显示歌词少于3个时优先显示当前和下一个歌词
startPairIndex = currentIndex
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
-- 如果显示空间还有剩余,尝试向前补充显示前面的歌词
if endPairIndex - startPairIndex + 1 < visibleLyricPairs then
local remainingSpace = visibleLyricPairs - (endPairIndex - startPairIndex + 1)
startPairIndex = math.max(1, startPairIndex - remainingSpace)
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
end
else
-- 当可显示歌词3个或更多时采用真正的居中显示策略
-- 计算当前歌词上方和下方应该显示的歌词数量
local aboveCount, belowCount
if visibleLyricPairs % 2 == 1 then
-- 奇数个显示位置:上下数量相等
local halfCount = math.floor(visibleLyricPairs / 2)
aboveCount = halfCount
belowCount = halfCount
else
-- 偶数个显示位置上面少一个下面多一个如你要求的4个和5个
aboveCount = math.floor(visibleLyricPairs / 2) - 1
belowCount = math.floor(visibleLyricPairs / 2)
end
-- 计算起始和结束索引
startPairIndex = math.max(1, currentIndex - aboveCount)
endPairIndex = math.min(#lyricPairs, currentIndex + belowCount)
-- 边界调整:如果前面不够显示,向后补充
if currentIndex - startPairIndex < aboveCount then
local needMore = aboveCount - (currentIndex - startPairIndex)
endPairIndex = math.min(#lyricPairs, endPairIndex + needMore)
end
-- 边界调整:如果后面不够显示,向前补充
if endPairIndex - currentIndex < belowCount then
local needMore = belowCount - (endPairIndex - currentIndex)
startPairIndex = math.max(1, startPairIndex - needMore)
end
-- 最终调整确保显示足够的行数
if endPairIndex - startPairIndex + 1 < visibleLyricPairs then
if startPairIndex > 1 then
startPairIndex = math.max(1, endPairIndex - visibleLyricPairs + 1)
else
endPairIndex = math.min(#lyricPairs, startPairIndex + visibleLyricPairs - 1)
end
end
end
local startY = 2
-- 显示歌词对
for i = startPairIndex, endPairIndex do
local lyricPair = lyricPairs[i]
-- 计算Y坐标 - 根据是否显示翻译动态计算
local pairY
if notzh or not hasTranslation then
-- 不显示翻译时,每行只占一个字体高度
pairY = startY + (i - startPairIndex) * (fontHeight + 1)
else
-- 显示翻译时,每对歌词占两个字体高度
pairY = startY + (i - startPairIndex) * (fontHeight * 2 + 1)
end
-- 设置颜色
local textColor = colorsConfig.normalMain
local translationColor = colorsConfig.normalSub
local bgColor = colorsConfig.background
if i == currentIndex then
textColor = colorsConfig.selectedMain
translationColor = colorsConfig.selectedSub
bgColor = colorsConfig.selectedBackground
end
-- 显示原文(第一行)
if lyricPair.original and lyricPair.original ~= "" then
local x = math.floor((screenWidth - getStringWidth(lyricPair.original, font)) / 2)
displayUtf8String(lyricPair.original, font, x, pairY, textColor, bgColor)
end
-- 显示翻译(第二行)- 只有当notzh为false且有翻译时才显示
if not notzh and hasTranslation and lyricPair.translation and lyricPair.translation ~= "" then
local x = math.floor((screenWidth - getStringWidth(lyricPair.translation, font)) / 2)
displayUtf8String(lyricPair.translation, font, x, pairY + fontHeight, translationColor, bgColor)
end
end
sleep(0.5)
end
term.clear()
term.setCursorPos(1, 1)
print("歌词显示已退出")
end
-- 解析命令行参数
local function parseArgs(args)
local url = nil
local notzh = false
local fontType = "8px" -- 默认8px
-- 默认颜色配置
local colorsConfig = {
background = colors.black, -- 背景颜色
normalMain = colors.gray, -- 歌词主颜色
normalSub = colors.gray, -- 歌词辅颜色
selectedMain = colors.white, -- 歌词选中主颜色
selectedSub = colors.lightGray, -- 选中辅颜色
selectedBackground = colors.black -- 选中背景色
}
for i = 1, #args do
local arg = args[i]
-- 处理颜色参数
if arg:match("--normabg=") then
local colorName = arg:sub(6)
colorsConfig.background = parseColor(colorName)
elseif arg:match("--normalmain=") then
local colorName = arg:sub(14)
colorsConfig.normalMain = parseColor(colorName)
elseif arg:match("--normalsub=") then
local colorName = arg:sub(13)
colorsConfig.normalSub = parseColor(colorName)
elseif arg:match("--selectedmain=") then
local colorName = arg:sub(16)
colorsConfig.selectedMain = parseColor(colorName)
elseif arg:match("--selectedsub=") then
local colorName = arg:sub(15)
colorsConfig.selectedSub = parseColor(colorName)
elseif arg:match("--selectedbg=") then
local colorName = arg:sub(14)
colorsConfig.selectedBackground = parseColor(colorName)
elseif arg == "--notzh" then
notzh = true
elseif arg == "--8px" then
fontType = "8px"
elseif arg == "--12px" then
fontType = "12px"
elseif not url and arg:sub(1, 4) == "http" then
url = arg
end
end
return url, notzh, fontType, colorsConfig
end
-- 主程序
local function main(...)
local args = {...}
local url, notzh, fontType, colorsConfig = parseArgs(args)
-- 如果没有通过参数提供URL则要求用户输入
if not url then
term.clear()
term.setCursorPos(1, 1)
write("请输入歌词URL: ")
url = read()
if not url or url == "" then
print("URL不能为空")
return
end
end
displayLyrics(url, notzh, fontType, colorsConfig)
end
-- 运行主程序
main(...)