Files
ComputerCraft-Music168-Player/speakerlib.lua
2025-11-12 15:43:29 +08:00

384 lines
12 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 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 <url>", 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 <url>")
printlog(programName .. " stop")
end