From 62b95e38b0546581eeb57084aa831d036462c110 Mon Sep 17 00:00:00 2001 From: HKXluo Date: Wed, 12 Nov 2025 15:16:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=B8=AD=E6=96=87=E5=92=8C?= =?UTF-8?q?=E6=AD=8C=E8=AF=8D=E6=94=AF=E6=8C=81=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=A4=9A=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MusicLyrics.lua | 520 ++++++++++++++++++++++++++++++++++++++++++++++++ music168.lua | 193 +++++++++++------- speaker.lua | 254 ----------------------- speakerlib.lua | 382 +++++++++++++++++++++++++++++++++++ 4 files changed, 1020 insertions(+), 329 deletions(-) create mode 100644 MusicLyrics.lua delete mode 100644 speaker.lua create mode 100644 speakerlib.lua diff --git a/MusicLyrics.lua b/MusicLyrics.lua new file mode 100644 index 0000000..53feecf --- /dev/null +++ b/MusicLyrics.lua @@ -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(...) + diff --git a/music168.lua b/music168.lua index 7858693..660c556 100644 --- a/music168.lua +++ b/music168.lua @@ -1,10 +1,9 @@ -----------------------------------------------------------------系统启动阶段------------------------------------------------------------------------------------------------- - --*获取程序所在目录 -local mypath = "/"..fs.getDir(shell.getRunningProgram()) +local mypath = "/"..fs.getDir(shell.getRunningProgram()) if not fs.exists(mypath.."/lib/basalt.lua") then shell.run("wget https://git.liulikeji.cn/GitHub/Basalt/releases/download/v1.6.3/basalt.lua lib/basalt.lua") end -if not fs.exists(mypath.."/speaker.lua") then shell.run("wget https://git.liulikeji.cn/xingluo/ComputerCraft-Music168-Player/releases/download/v1.0.0/speaker.lua") end +if not fs.exists(mypath.."/speakerlib.lua") then shell.run("wget https://git.liulikeji.cn/xingluo/ComputerCraft-Music168-Player/releases/download/v1.1.0/speakerlib.lua") end --*GUI库导入 basalt = require(mypath.."/lib/basalt") @@ -15,7 +14,6 @@ main = { } _G.Playprint = false _G.Playopen =false - --*GUI框架配置表 local sub = { ["UI"] = { @@ -36,7 +34,6 @@ local sub = { mainf:addFrame():setPosition(2, "parent.h + 1"):setSize("parent.w-2", 13):setBackground(colors.orange), } } - --创建动画 play_Gui_UP = mainf:addAnimation():setObject(sub["BF"][1]):move(1,1,0.3) play_Gui_DO = mainf:addAnimation():setObject(sub["BF"][1]):move(1,mainf:getHeight()+1,1) @@ -52,21 +49,21 @@ play_Gui = { sub["BF"][1]:addButton():setPosition(1,1):setSize(3, 1):setText("V"):onClick(function() play_Gui_DO:play() play_GUI_state=false main[1]:enable() end):setBackground(colors.red):setForeground(colors.white), sub["BF"][1]:addLabel():setText("NO Music"):setPosition(sub["BF"][1]:getWidth()/2 - #play_name/2,1):setBackground(colors.red):setForeground(colors.white), sub["BF"][1]:addLabel():setText("NO Music"):setPosition(sub["BF"][1]:getWidth()/2 - #play_id/2,2):setBackground(colors.red):setForeground(colors.white), - sub["BF"][1]:addLabel():setText(" "):setPosition(3,4):setSize("parent.w-4", "parent.h-10"):setBackground(colors.white):setForeground(colors.red), - sub["BF"][1]:addButton():setPosition(3,"parent.h-5"):setSize(1, 1):setText("\3"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition(8,"parent.h-5"):setSize(1, 1):setText("\25"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w/2","parent.h-5"):setSize(2, 1):setText("+-"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w-3","parent.h-5"):setSize(1, 1):setText("@"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w-8","parent.h-5"):setSize(1, 1):setText("E"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addProgressbar():setPosition(3, "parent.h - 4"):setSize("parent.w - 4", 1):setProgressBar(colors.red, "=", colors.white):setBackground(colors.red):setBackgroundSymbol("-"):setForeground(colors.white), - sub["BF"][1]:addLabel():setText("00:00"):setPosition("3", "parent.h - 3"):setSize(5, 1):setForeground(colors.white), - sub["BF"][1]:addLabel():setText("00:00"):setPosition("parent.w - 6", "parent.h - 3"):setSize(5, 1):setForeground(colors.white), - sub["BF"][1]:addButton():setPosition(3, "parent.h - 2"):setSize(3, 1):setText("=O="):onClick(function() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w /2 - 4","parent.h - 2"):setSize(2, 1):setText("|\17"):onClick(function() play_set_1() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w / 2 ", "parent.h - 2"):setSize(2, 1):setText("I>"):onClick(function() if play_data_table["play"] then _G.Playstop = true play_data_table["play"]=false else play_data_table["play"]=true end end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w / 2 +4", "parent.h - 2"):setSize(2, 1):setText("\16|"):onClick(function() play_set_0() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addButton():setPosition("parent.w - 4", "parent.h - 2"):setSize(3, 1):setText("=T="):onClick(function() play_table_Gui_UP:play() main[1]:disable() sub["BF"][1]:disable() end):setForeground(colors.white):setBackground(colors.red), - sub["BF"][1]:addSlider():setPosition(3, "parent.h - 4"):setSize("parent.w - 4", 1):setMaxValue(100):setBackground(colors.red):setForeground(colors.white),--:setBackgroundSymbol("\x8c"):setSymbol(" "), + sub["BF"][1]:addProgram():setPosition(2,2):setSize("parent.w-2", "parent.h-4"), + 1,--sub["BF"][1]:addButton():setPosition(3,"parent.h-5"):setSize(1, 1):setText("\3"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + 1,--sub["BF"][1]:addButton():setPosition(8,"parent.h-5"):setSize(1, 1):setText("\25"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + 1,--sub["BF"][1]:addButton():setPosition("parent.w/2","parent.h-5"):setSize(2, 1):setText("+-"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + 1,--sub["BF"][1]:addButton():setPosition("parent.w-3","parent.h-5"):setSize(1, 1):setText("@"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + 1,--sub["BF"][1]:addButton():setPosition("parent.w-8","parent.h-5"):setSize(1, 1):setText("E"):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addProgressbar():setPosition(3, "parent.h - 2"):setSize("parent.w - 4", 1):setProgressBar(colors.red, "=", colors.white):setBackground(colors.red):setBackgroundSymbol("-"):setForeground(colors.white), + sub["BF"][1]:addLabel():setText("00:00"):setPosition("3", "parent.h - 1"):setSize(5, 1):setForeground(colors.white), + sub["BF"][1]:addLabel():setText("00:00"):setPosition("parent.w - 6", "parent.h - 1"):setSize(5, 1):setForeground(colors.white), + sub["BF"][1]:addButton():setPosition(3, "parent.h - 0"):setSize(3, 1):setText("=O="):onClick(function() end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addButton():setPosition("parent.w /2 - 4","parent.h - 0"):setSize(2, 1):setText("|\17"):onClick(function() play_set_1() end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addButton():setPosition("parent.w / 2 ", "parent.h - 0"):setSize(2, 1):setText("I>"):onClick(function() if play_data_table["play"] then _G.Playstop = true play_data_table["play"]=false else play_data_table["play"]=true end end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addButton():setPosition("parent.w / 2 +4", "parent.h - 0"):setSize(2, 1):setText("\16|"):onClick(function() play_set_0() end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addButton():setPosition("parent.w - 4", "parent.h - 0"):setSize(3, 1):setText("=T="):onClick(function() play_table_Gui_UP:play() main[1]:disable() sub["BF"][1]:disable() end):setForeground(colors.white):setBackground(colors.red), + sub["BF"][1]:addSlider():setPosition(3, "parent.h - 2"):setSize("parent.w - 4", 1):setMaxValue(100):setBackground(colors.red):setForeground(colors.white),--:setBackgroundSymbol("\x8c"):setSymbol(" "), } --创建播放UI play_column_Gui = { @@ -75,13 +72,11 @@ play_column_Gui = { sub["BF"][2]:addButton():setPosition("parent.w-1", 1):setSize(1, 1):setText("T"):onClick(function() play_table_Gui_UP:play() main[1]:disable() end):setForeground(colors.white):setBackground(colors.lightGray), sub["BF"][2]:addButton():setPosition(1, 1):setSize("parent.w -5", 1):setText(""):onClick(function() play_Gui_UP:play() play_GUI_state=true main[1]:disable() end):setBackground(colors.lights), } - play_table_Gui = { sub["play_table"][1]:addButton():setPosition("parent.w-3",1):setSize(3, 1):setText("V"):onClick(function() if not play_GUI_state then main[1]:enable() end sub["BF"][1]:enable() play_table_Gui_DO:play() end):setBackground(colors.no):setForeground(colors.white), sub["play_table"][1]:addLabel():setText("PlyaTable"):setPosition(1,1):setForeground(colors.white), sub["play_table"][1]:addList():setPosition(2,3):setSize("parent.w-2", "parent.h-2"):setScrollable(true), } - --创建菜单栏 menuBut = { sub["menu"][1]:addButton():setPosition(3,1):setSize(3, 1):setText("{Q}"):onClick(function() for index, value in ipairs(menuBut) do value:setBackground(colors.lightGray) end menuBut[1]:setBackground(colors.red) for index, value in ipairs(sub["UI"]) do value:hide() end sub["UI"][1]:show() end):setForeground(colors.white):setBackground(colors.red), @@ -93,19 +88,14 @@ menuBut = { -----------------------------------------------------------------DATA--------------------------------------------------------------------------------------------------------- play_data_table = { ["music"] = {} , ["play"] = false ,["play_table"] = {}, ["play_table_index"] = 0, ["mode"] = "" , } _G.Playopen = false +_G.getPlay = 0 +_G.getPlaymax = 0 +_G.setPlay = nil -----------------------------------------------------------------模块--------------------------------------------------------------------------------------------------------- ---多线程 -thread_table = {} -function AddThread(funct) - thread1 = mainf:addThread() - thread1:start(funct) - table.insert(thread_table,thread1) - return #thread_table -end --音乐+ function play_set_1() - _G.music168_playopen = false + _G.music168_playopen = false os.queueEvent("music168_play_stop") _G.getPlay = 0 _G.Playopen = false _G.Playstop = false @@ -118,7 +108,7 @@ function play_set_1() end --音乐- function play_set_0() - _G.music168_playopen = false + _G.music168_playopen = false os.queueEvent("music168_play_stop") _G.getPlay = 0 _G.Playopen = false _G.Playstop = false @@ -129,8 +119,6 @@ function play_set_0() play_table_Gui[3]:selectItem(table_index+1) end end - - --获取URL function GetmusicUrl(music_id) while true do @@ -144,14 +132,14 @@ function GetmusicUrl(music_id) end end end - --dfpwm转码 - --播放 function playmusic(music_name,music_id,play_table,index) _G.getPlay = 0 + _G.getPlaymax = 0 _G.Playopen = false - _G.Playstop = false + _G.music168_playopen = false os.queueEvent("music168_play_stop") + play_Gui[2]:setText(music_name):setPosition(sub["BF"][1]:getWidth()/2 +1 - #music_name/2,1) play_Gui[3]:setText(music_id):setPosition(sub["BF"][1]:getWidth()/2 +1 - #tostring(music_id)/2,2) play_column_Gui[1]:setText(music_name.." | "..tostring(music_id)) @@ -159,24 +147,23 @@ function playmusic(music_name,music_id,play_table,index) play_data_table["play_table"] = play_table play_data_table["play_table_index"] = index play_data_table["play"] = true - play_table_Gui[3]:clear() for index, value in ipairs(play_table) do play_table_Gui[3]:addItem(value["name"].." | "..tostring(value["id"])) end + + play_table_Gui[3]:selectItem(index) _G.music168_music_id = music_id - + _G.music168_playopen = true --basalt.debug("true") --play_thread_id = AddThread(function () -- - - --end) - end +printUtf8 = load(http.get("https://alist.liulikeji.cn/d/HFS/utf8ptrint.lua").readAll())() --搜索 server_url = "http://music168.liulikeji.cn:15843/" function Search(input_str,GUI_in,api) @@ -194,7 +181,6 @@ function Search(input_str,GUI_in,api) table_get = textutils.unserialiseJSON(json_str) if table_get["code"] ~= 404 then kg_a=true end end - if http1 then if kg_a then if api=="search" then @@ -208,18 +194,24 @@ function Search(input_str,GUI_in,api) Search_table[index]=out_table end end - a=2 if play_lib_F then play_lib_F:remove() end play_lib_F = GUI_in[3]:addFrame():setPosition(1, 1):setSize("parent.w", "parent.h"):setBackground(colors.white):setScrollable() - for index, value in ipairs(Search_table) do - frame = play_lib_F:addFrame():setPosition(2, a):setSize("parent.w-2", 3):setBackground(colors.lightBlue):onClick(function() if play_data_table["play"] then shell.run(mypath.."/speaker.lua stop") if _G.Playopen then end _G.music168_playopen = false play_data_table["play"]=false end play_Gui_UP:play() play_GUI_state = true main[1]:disable() playmusic(value["name"],value["id"],Search_table,index) end) - frame:addLabel():setText(value["name"]):setPosition(1, 1) - frame:addLabel():setText("id: "..value["id"]):setPosition(1, 2) - frame:addLabel():setText("artists: "..value["artists_name"]):setPosition(1, 3) - a=a+4 + id = value["id"] + local frame = play_lib_F:addFrame():setPosition(2, a):setSize("parent.w-2", 4):setBackground(colors.lightBlue):onClick(function() if play_data_table["play"] then shell.run(mypath.."/speakerlib.lua stop") if _G.Playopen then end _G.music168_playopen = false os.queueEvent("music168_play_stop") play_data_table["play"]=false end play_Gui_UP:play() play_GUI_state = true main[1]:disable() _G.music168_playopen = false os.queueEvent("music168_play_stop") playmusic(value["name"],value["id"],Search_table,index) end) + local textf = frame:addFrame():setPosition(1, 1):setSize("parent.w", 3) + textf:addProgram():setPosition(1, 1):setSize("parent.w", 4):execute(function () + term.setBackgroundColor(colors.lightGray) + term.clear() + printUtf8(value["name"],colors.white,colors.lightGray) + end):injectEvent("char", false, "w"):disable() + --frame:addLabel():setText(value["name"]):setPosition(1, 1) + frame:addLabel():setText("name:"..value["name"].." id:"..value["id"].." artists:"..value["artists_name"]):setPosition(1, 4) + --frame:addLabel():setText("artists: "..value["artists_name"]):setPosition(1, 3) + a=a+5 end + break; else frame = GUI_in[3]:addFrame():setPosition(2, 2):setSize("parent.w-2", 3):setBackground(colors.lightBlue) @@ -231,6 +223,13 @@ function Search(input_str,GUI_in,api) end end +play_Gui[4]:onError(function(self, event, err) + +end) + +play_Gui[4]:onDone(function() + +end) -----------------------------------------------------------------渲染界面阶段------------------------------------------------------------------------------------------------- GUI = { @@ -246,18 +245,38 @@ GUI = { }, } -_G.getPlay = nil - function thread2() while true do - --basalt.debug(_G.music168_playopen) + --basalt.debug(_G.music168_playopen = false os.queueEvent("music168_play_stop")) local screenWidth, _ = term.getSize() - if play_Gui[18]:getIndex() ~=1 then _G.setPlay = (play_Gui[18]:getIndex() / (screenWidth-2)) * 100 play_Gui[18]:setIndex(1) end - sleep(0.1) - -- - if _G.getPlay ~= nil then - play_Gui[10]:setProgress(_G.getPlay*100) + + -- 处理用户拖动进度条设置播放位置 + + w,h = term.getSize() + if w >= 100 and h >= 30 then px = "--12px" else px = "--8px" end + + if play_Gui[18]:getIndex() ~=1 then + local sliderValue = play_Gui[18]:getValue() or 0 + _G.setPlay = _G.getPlaymax * (sliderValue / 100) + play_Gui[18]:setIndex(1) end + + sleep(0.1) + + -- 更新播放进度条 + if _G.getPlay ~= nil and _G.getPlaymax ~= nil and _G.getPlaymax > 0 then + play_Gui[10]:setProgress((_G.getPlay / _G.getPlaymax) * 100) + + -- 更新当前播放时间显示 + local current = _G.getPlay or 0 + local total = _G.getPlaymax or 0 + local currentTimeStr = string.format("%02d:%02d", math.floor(current / 60), current % 60) + local totalTimeStr = string.format("%02d:%02d", math.floor(total / 60), total % 60) + + play_Gui[11]:setText(currentTimeStr) -- 当前播放时间 + play_Gui[12]:setText(totalTimeStr) -- 总时间 + end + if play_data_table["play"]== true then _G.Playstop = false play_Gui[15]:setText("II") @@ -274,13 +293,13 @@ function thread2() index = play_table_Gui[3]:getItemIndex() if play_data_table["play"] then - shell.run(mypath.."/speaker.lua stop") + shell.run(mypath.."/speakerlib.lua stop") if _G.Playopen then end play_data_table["play"]=false end - _G.music168_playopen = false + _G.music168_playopen = false os.queueEvent("music168_play_stop") sleep(0.1) playmusic(play_data_table["play_table"][index]["name"],play_data_table["play_table"][index]["id"],play_data_table["play_table"],index) @@ -299,31 +318,55 @@ end function speakerp() function speaker_thread() + local startTime = os.clock() - - --play_time_thread_id = AddThread(function() local time_f = os.date("%M%S") while true do while _G.Playopen do time_f = os.date("%M%S") sleep(0.1) end time_F = os.date("%M%S") play_Gui[11]:setText(string.format("%02d", os.date("*t",time_F-time_f).min or 00)..":"..string.format("%02d", os.date("*t",time_F-time_f).sec or 00) or "00:00") sleep(1) end end) - --basalt.debug(mypath) - --shell.run(mypath.."/speaker play "..dfpwmURL.readAll()) if _G.music168_music_id then --basalt.debug(music168_music_id) - _G.Playopen = true - dfpwmURL = http.post("http://gmapi.liulikeji.cn:15842/dfpwm",textutils.serialiseJSON({ ["url"] = GetmusicUrl(_G.music168_music_id) } )) - shell.run(mypath.."/speaker play "..dfpwmURL.readAll()) - play_set_0() - end + + _G.Playopen = true + play_Gui[4]:stop() + play_Gui[4]:execute(function () + shell.run("MusicLyrics.lua http://music168.liulikeji.cn:15843/api/song/lyric?id=".._G.music168_music_id.." "..px) + end) + play_Gui[4]:injectEvent("char","w") + sleep(0.1) + + --dfpwmURL = http.post("http://gmapi.liulikeji.cn:15842/dfpwm",textutils.serialiseJSON({ ["url"] = GetmusicUrl(_G.music168_music_id) } )) + shell.run(mypath.."/speakerlib play "..GetmusicUrl(_G.music168_music_id)) + -- 检查是否播放完成自动跳转下一首 + if _G.music168_playopen then + play_set_0() + play_Gui[4]:stop() + + end + end + end + + function while_thread() + os.pullEvent("music168_play_stop") + _G.getPlay = 0 + _G.getPlaymax = 0 + play_Gui[4]:stop() end - function while_thread() while _G.music168_playopen do sleep(0.01) end end while true do - - if _G.music168_playopen then parallel.waitForAny(speaker_thread, while_thread) end + if _G.music168_playopen then + parallel.waitForAny(speaker_thread, while_thread) + sleep(0.1) + end sleep(0.01) end end -_G.music168_playopen = false +function gc() + while true do + play_Gui[4]:injectEvent(os.pullEvent()) + end + +end +_G.music168_playopen = false os.queueEvent("music168_play_stop") -----------------------------------------------------------------启动循环渲染器----------------------------------------------------------------------------------------------- -parallel.waitForAll(basalt.autoUpdate, thread2, paste, speakerp) ------------------------------------------------------------------以下结束----------------------------------------------------------------------------------------------------- \ No newline at end of file +parallel.waitForAll(basalt.autoUpdate, thread2, paste, speakerp,gc) +-----------------------------------------------------------------以下结束----------------------------------------------------------------------------------------------------- diff --git a/speaker.lua b/speaker.lua deleted file mode 100644 index 6789ac9..0000000 --- a/speaker.lua +++ /dev/null @@ -1,254 +0,0 @@ -if _G.Playprint == nil then _G.Playprint = true end -local function get_speakers(name) - if name then - local speaker = peripheral.wrap(name) - if speaker == nil then - error(("Speaker %q does not exist"):format(name), 0) - return - elseif not peripheral.hasType(name, "speaker") then - error(("%q is not a speaker"):format(name), 0) - end - - return { speaker } - else - local speakers = { peripheral.find("speaker") } - if #speakers == 0 then - error("No speakers attached", 0) - end - return speakers - end -end -function spekerStop() - local speakers = { peripheral.find("speaker") } - for _, speaker in pairs(speakers) do - speaker.stop() - end -end - -function speakerPlay(buffer) - local speakers = { peripheral.find("speaker") } - for _, speaker in pairs(speakers) do - a = speaker.playAudio(buffer) - end - return a -end - - -local function pcm_decoder(chunk) - local buffer = {} - for i = 1, #chunk do - buffer[i] = chunk:byte(i) - 128 - end - return buffer -end - - -function displayProgressBar(percent) - term.setCursorPos(1, 1) - local screenWidth, _ = term.getSize() -- 获取终端的宽度和高度 - local barLength = math.floor(screenWidth - 8) -- 进度条长度为屏幕宽度减去固定长度(用于百分比显示) - - local numBars = math.floor(percent / (100 / barLength)) - - -- 构建进度条字符串 - local progressBar = "[" - for i = 1, barLength do - if i <= numBars then - progressBar = progressBar .. "=" - else - progressBar = progressBar .. " " - end - end - progressBar = progressBar .. "] " .. math.floor(percent) .. "%" -- 百分比不显示小数点 - - -- 清空屏幕并输出进度条到屏幕顶部 - - term.setCursorPos(1, 3) - print(progressBar) -end - --- 测试函数 - - - - -local cmd = ... -if cmd == "stop" then - _G.Playopen = false - local speakers = { peripheral.find("speaker") } - for _, speaker in pairs(speakers) do - speaker.stop() - end - spekerStop() -elseif cmd == "play" then - local _, file, type = ... - - local handle, err - if http and file:match("^https?://") then - if type == "mp3" then - if _G.Playprint then print("mp3 > dfpwm.....") end - local json = textutils.serialiseJSON({ ["url"] = file } ) - handle, err = http.get{ url = http.post("http://gmapi.liulikeji.cn:15842/dfpwm",json).readAll(), binary = true } - else - handle, err = http.get{ url = file, binary = true } - end - else - handle, err = fs.open(file, "rb") - end - - if not handle then - error(err, 0) - end - - local start = handle.read(4) - local pcm = false - local size = 16 * 1024 - 4 - if start == "RIFF" then - handle.read(4) - if handle.read(8) ~= "WAVEfmt " then - handle.close() - error("Could not play audio: Unsupported WAV file", 0) - end - - local fmtsize = ("= 1 then break end - - if chunk == nil then break end - - if start then - chunk, start = start .. chunk, nil - size = size + 4 - end - - end - - local buffer = decoder(chunk) - if b1 then - speakerPlay(buffer) - b1=false - c1 = true - - else - - while setPlay==nil and _G.Playstop==false do - local speakers = { peripheral.find("speaker") } - for i,speaker in pairs(speakers) do - os.pullEvent("speaker_audio_empty") - end - speakerPlay(buffer) - break - end - - end - - end - _G.getPlay=nil - end - - - play1() - - - spekerStop() - - handle.close() -else - local programName = arg[0] or fs.getName(shell.getRunningProgram()) -end \ No newline at end of file diff --git a/speakerlib.lua b/speakerlib.lua new file mode 100644 index 0000000..43d8f25 --- /dev/null +++ b/speakerlib.lua @@ -0,0 +1,382 @@ +-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers +-- +-- SPDX-License-Identifier: MPL-2.0 + +-- 全局控制变量 +_G.getPlaymax = 0 -- 总时间(秒) +_G.getPlay = 0 -- 当前播放时间(秒) +_G.setPlay = 0 -- 设置播放进度(秒) +_G.Playopen = true -- 播放开关(false停止) +_G.Playstop = false -- 暂停控制(true暂停,false恢复) +_G.Playprint = false -- 信息输出开关(true开,false关) +_G.setVolume = 1 -- 音量控制(0-3) + +local API_URL = "http://newgmapi.liulikeji.cn/api/ffmpeg" + +-- 扬声器配置 +local speakerlist = { + main = {}, + left = {}, + right = {} +} + +local function printlog(...) + if _G.Playprint then + print(...) + end +end + +local function loadSpeakerConfig() + local speaker_groups = fs.open("speaker_groups.cfg","r") + if speaker_groups then + local content = speaker_groups.readAll() + speaker_groups.close() + if content then + local success, tableData = pcall(textutils.unserialise, content) + if success and type(tableData) == "table" then + speakerlist = { main = {}, left = {}, right = {} } + for group_name, speakers in pairs(tableData) do + if speakerlist[group_name] then + for _, speaker_name in ipairs(speakers) do + local speaker = peripheral.wrap(speaker_name) + if speaker and peripheral.hasType(speaker_name, "speaker") then + table.insert(speakerlist[group_name], speaker) + end + end + end + end + return + end + end + end + + -- 默认配置:所有扬声器都在main组 + speakerlist = { + main = { peripheral.find("speaker") }, + left = {}, + right = {} + } +end + +local function Get_dfpwm_url(INPUT_URL, args) + local requestData = { + input_url = INPUT_URL, + args = args, + output_format = "dfpwm" + } + + local response, err = http.post( + API_URL, + textutils.serializeJSON(requestData), + { ["Content-Type"] = "application/json" } + ) + + if not response then + error("HTTP Request Failure: "..(err or "Unknown error")) + end + + local responseData = textutils.unserializeJSON(response.readAll()) + response.close() + + if responseData.status ~= "success" then + error("Conversion failed: "..(responseData.error or "Unknown error")) + end + + return responseData.download_url +end + +local function get_total_duration(url) + if _G.Playprint then printlog("Calculating duration...") end + local handle, err = http.get(url) + if not handle then + error("Could not get duration: " .. (err or "Unknown error")) + end + + local data = handle.readAll() + handle.close() + + -- DFPWM: 每字节8个样本,48000采样率 + local total_length = (#data * 8) / 48000 + return total_length, #data +end + +local function play_audio_chunk(speakers, buffer) + if #speakers > 0 and buffer and #buffer > 0 then + for _, speaker in pairs(speakers) do + local success = false + while not success and _G.Playopen do + success = speaker.playAudio(buffer, _G.setVolume) + if not success then + os.pullEvent("speaker_audio_empty") + end + end + end + end +end + +local cmd = ... + +if cmd == "stop" then + local all_speakers = {} + for _, group in pairs(speakerlist) do + for _, speaker in pairs(group) do + table.insert(all_speakers, speaker) + end + end + + for _, speaker in pairs(all_speakers) do + speaker.stop() + end +elseif cmd == "play" then + local _, file = ... + if not file then + error("Usage: speaker play ", 0) + end + + if not http or not file:match("^https?://") then + error("Only HTTP/HTTPS URLs are supported", 0) + end + + -- 加载扬声器配置 + loadSpeakerConfig() + + -- 检查是否有扬声器 + local has_speakers = false + for _, group in pairs(speakerlist) do + if #group > 0 then + has_speakers = true + break + end + end + + if not has_speakers then + error("No speakers attached", 0) + end + + -- 获取DFPWM转换URL + local main_dfpwm_url, left_dfpwm_url, right_dfpwm_url + local main_httpfile, left_httpfile, right_httpfile + + if _G.Playprint then printlog("Converting audio...") end + + if #speakerlist.main > 0 then + main_dfpwm_url = Get_dfpwm_url(file, { "-vn", "-ar", "48000", "-ac", "1" }) + end + + if #speakerlist.left > 0 then + left_dfpwm_url = Get_dfpwm_url(file, { "-vn", "-ar", "48000", "-filter_complex", "pan=mono|c0=FL" }) + end + + if #speakerlist.right > 0 then + right_dfpwm_url = Get_dfpwm_url(file, { "-vn", "-ar", "48000", "-filter_complex", "pan=mono|c0=FR" }) + end + + -- 计算总时长(使用任意一个通道) + local total_length, total_size + if main_dfpwm_url then + total_length, total_size = get_total_duration(main_dfpwm_url) + elseif left_dfpwm_url then + total_length, total_size = get_total_duration(left_dfpwm_url) + elseif right_dfpwm_url then + total_length, total_size = get_total_duration(right_dfpwm_url) + else + error("No audio channels available", 0) + end + + -- 设置总时间 + _G.getPlaymax = total_length + _G.getPlay = 0 + + if _G.Playprint then + printlog("Playing " .. file .. " (" .. math.ceil(total_length) .. "s)") + end + + -- 创建HTTP连接 + if main_dfpwm_url then + main_httpfile = http.get(main_dfpwm_url) + if not main_httpfile then + error("Could not open main audio stream") + end + end + + if left_dfpwm_url then + left_httpfile = http.get(left_dfpwm_url) + if not left_httpfile then + error("Could not open left audio stream") + end + end + + if right_dfpwm_url then + right_httpfile = http.get(right_dfpwm_url) + if not right_httpfile then + error("Could not open right audio stream") + end + end + + -- 初始化DFPWM解码器 + local decoder = require "cc.audio.dfpwm".make_decoder() + local left_decoder = require "cc.audio.dfpwm".make_decoder() + local right_decoder = require "cc.audio.dfpwm".make_decoder() + + -- 每次读取的字节数(DFPWM: 每秒6000字节) + local chunk_size = 6000 + local bytes_read = 0 + + -- 初始化播放位置 + if _G.setPlay > 0 then + local skip_bytes = math.floor(_G.setPlay * 6000) + if skip_bytes < total_size then + -- 跳过指定字节数 + local skipped = 0 + while skipped < skip_bytes and _G.Playopen do + local to_skip = math.min(8192, skip_bytes - skipped) + if main_httpfile then main_httpfile.read(to_skip) end + if left_httpfile then left_httpfile.read(to_skip) end + if right_httpfile then right_httpfile.read(to_skip) end + skipped = skipped + to_skip + bytes_read = bytes_read + to_skip + end + _G.getPlay = _G.setPlay + _G.setPlay = 0 + end + end + + -- 主播放循环 + while bytes_read < total_size and _G.Playopen do + -- 检查是否需要设置播放位置 + if _G.setPlay > 0 then + -- 重新打开所有连接并跳转 + if main_httpfile then main_httpfile.close() end + if left_httpfile then left_httpfile.close() end + if right_httpfile then right_httpfile.close() end + + if main_dfpwm_url then + main_httpfile = http.get(main_dfpwm_url) + if not main_httpfile then error("Could not reopen main stream") end + end + + if left_dfpwm_url then + left_httpfile = http.get(left_dfpwm_url) + if not left_httpfile then error("Could not reopen left stream") end + end + + if right_dfpwm_url then + right_httpfile = http.get(right_dfpwm_url) + if not right_httpfile then error("Could not reopen right stream") end + end + + local skip_bytes = math.floor(_G.setPlay * 6000) + if skip_bytes < total_size then + local skipped = 0 + while skipped < skip_bytes and _G.Playopen do + local to_skip = math.min(8192, skip_bytes - skipped) + if main_httpfile then main_httpfile.read(to_skip) end + if left_httpfile then left_httpfile.read(to_skip) end + if right_httpfile then right_httpfile.read(to_skip) end + skipped = skipped + to_skip + bytes_read = skip_bytes + end + _G.getPlay = _G.setPlay + _G.setPlay = 0 + end + end + + -- 检查暂停状态 + while _G.Playstop and _G.Playopen do + os.sleep(0.1) + end + + -- 检查停止状态 + if not _G.Playopen then + break + end + + -- 读取音频数据 + local main_chunk, left_chunk, right_chunk + local main_buffer, left_buffer, right_buffer + + if main_httpfile then + main_chunk = main_httpfile.read(chunk_size) + end + + if left_httpfile then + left_chunk = left_httpfile.read(chunk_size) + end + + if right_httpfile then + right_chunk = right_httpfile.read(chunk_size) + end + + -- 检查是否所有通道都没有数据 + if (not main_chunk or #main_chunk == 0) and + (not left_chunk or #left_chunk == 0) and + (not right_chunk or #right_chunk == 0) then + break + end + + -- 解码音频数据 + if main_chunk and #main_chunk > 0 then + main_buffer = decoder(main_chunk) + end + + if left_chunk and #left_chunk > 0 then + left_buffer = left_decoder(left_chunk) + end + + if right_chunk and #right_chunk > 0 then + right_buffer = right_decoder(right_chunk) + end + + -- 并行播放所有通道 + parallel.waitForAll( + function() + if main_buffer and #main_buffer > 0 then + play_audio_chunk(speakerlist.main, main_buffer) + end + end, + function() + if right_buffer and #right_buffer > 0 then + play_audio_chunk(speakerlist.right, right_buffer) + end + end, + function() + if left_buffer and #left_buffer > 0 then + play_audio_chunk(speakerlist.left, left_buffer) + end + end + ) + + -- 更新进度 + local max_chunk_size = math.max( + main_chunk and #main_chunk or 0, + left_chunk and #left_chunk or 0, + right_chunk and #right_chunk or 0 + ) + bytes_read = bytes_read + max_chunk_size + _G.getPlay = bytes_read / 6000 + + if _G.Playprint then + term.setCursorPos(1, term.getCursorPos()) + printlog(("Playing: %ds / %ds"):format(math.floor(_G.getPlay), math.ceil(total_length))) + end + end + + -- 关闭HTTP连接 + if main_httpfile then main_httpfile.close() end + if left_httpfile then left_httpfile.close() end + if right_httpfile then right_httpfile.close() end + + if _G.Playprint and _G.Playopen then + printlog("Playback finished.") + end + + -- 重置播放状态 + _G.Playopen = true + _G.Playstop = false + _G.getPlay = 0 +else + local programName = arg[0] or fs.getName(shell.getRunningProgram()) + printlog("Usage:") + printlog(programName .. " play ") + printlog(programName .. " stop") +end