-- 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 mypath = "/"..fs.getDir(shell.getRunningProgram()) -- 扬声器配置 local speakerlist = { main = {}, left = {}, right = {} } local function printlog(...) if _G.Playprint then print(...) end end local function loadSpeakerConfig() local speaker_groups = fs.open(mypath.."/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