feat: 重构音频系统,移除外部依赖并新增WAV格式支持
- 移除install和update脚本中的aukit/austream下载配置与目标,不再部署辅助脚本 - 替换station_broadcast.lua的aukit库为CC内置dfpwm模块,重写音频播放逻辑 - 添加原生扬声器API支持,支持WAV格式音频播放 - 重写云端服务器音频处理逻辑,支持直接传输WAV文件无需预转换 - 新增WAV文件时长解析、音频序列合成等辅助功能
This commit is contained in:
+164
-27
@@ -14,7 +14,7 @@ local CONFIG = {
|
||||
DEBUG = true
|
||||
}
|
||||
|
||||
local aukit = require "aukit"
|
||||
local dfpwm = require "cc.audio.dfpwm"
|
||||
|
||||
local COLORS = {
|
||||
INFO = colors.white,
|
||||
@@ -527,12 +527,14 @@ end
|
||||
|
||||
function Audio.enqueueDfpwm(dfpwmData)
|
||||
if type(dfpwmData) ~= "string" or #dfpwmData == 0 then return end
|
||||
Audio.enqueueItem({ dfpwmData = dfpwmData, priority = 1 })
|
||||
Audio.enqueueItem({ audioData = dfpwmData, audioFormat = "dfpwm", priority = 1 })
|
||||
end
|
||||
|
||||
function Audio.enqueueItem(item)
|
||||
if type(item) ~= "table" then return end
|
||||
if type(item.dfpwmData) ~= "string" and type(item.dfpwmData) ~= "table" then return end
|
||||
item.audioData = item.audioData or item.dfpwmData
|
||||
item.audioFormat = tostring(item.audioFormat or (item.dfpwmData and "dfpwm") or "dfpwm"):lower()
|
||||
if type(item.audioData) ~= "string" and type(item.audioData) ~= "table" then return end
|
||||
item.priority = tonumber(item.priority) or 0
|
||||
local idx = 1
|
||||
while idx <= #State.audioQueue do
|
||||
@@ -578,6 +580,19 @@ local function shouldStop(playSeq)
|
||||
return State.interruptSeq ~= playSeq
|
||||
end
|
||||
|
||||
local function waitForInterruptibleDelay(playSeq, seconds)
|
||||
local timer = os.startTimer(seconds)
|
||||
while true do
|
||||
local ev = { os.pullEvent() }
|
||||
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
||||
return false
|
||||
end
|
||||
if ev[1] == "timer" and ev[2] == timer then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function playChunkOnSpeakers(samples, volume, playSeq)
|
||||
if not Peripherals.speakers or #Peripherals.speakers == 0 then return false end
|
||||
local controller = Peripherals.speakers[1]
|
||||
@@ -639,6 +654,82 @@ local function playChunkOnSpeakers(samples, volume, playSeq)
|
||||
return true
|
||||
end
|
||||
|
||||
local function areHqSpeakersBusy(speakers)
|
||||
for _, sp in ipairs(speakers) do
|
||||
local playing = false
|
||||
local queued = 0
|
||||
pcall(function()
|
||||
if type(sp.speakIsPlaying) == "function" then
|
||||
playing = sp.speakIsPlaying() and true or false
|
||||
end
|
||||
if type(sp.speakQueueSize) == "function" then
|
||||
queued = tonumber(sp.speakQueueSize()) or 0
|
||||
end
|
||||
end)
|
||||
if playing or queued > 0 then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function playHqAudioItem(item)
|
||||
if not Peripherals.speakers or #Peripherals.speakers == 0 then
|
||||
Utils.log("WARN", "No speakers found; cannot play audio")
|
||||
return
|
||||
end
|
||||
|
||||
local playSeq = State.interruptSeq
|
||||
local payload = item.audioData
|
||||
if type(payload) ~= "string" or #payload == 0 then return end
|
||||
|
||||
local format = tostring(item.audioFormat or "file"):lower()
|
||||
local controller = Peripherals.speakers[1]
|
||||
local called = false
|
||||
|
||||
if controller then
|
||||
local ok, result = pcall(function()
|
||||
if format == "wav" and type(controller.speakWavAll) == "function" then
|
||||
return controller.speakWavAll(payload, CONFIG.AUDIO_VOLUME)
|
||||
elseif type(controller.speakFileAll) == "function" then
|
||||
return controller.speakFileAll(payload, CONFIG.AUDIO_VOLUME)
|
||||
end
|
||||
end)
|
||||
if ok and result ~= false then
|
||||
called = true
|
||||
end
|
||||
end
|
||||
|
||||
if not called then
|
||||
for _, sp in ipairs(Peripherals.speakers) do
|
||||
local ok, result = pcall(function()
|
||||
if format == "wav" and type(sp.speakWav) == "function" then
|
||||
return sp.speakWav(payload, CONFIG.AUDIO_VOLUME)
|
||||
elseif type(sp.speakFile) == "function" then
|
||||
return sp.speakFile(payload, CONFIG.AUDIO_VOLUME)
|
||||
end
|
||||
end)
|
||||
if ok and result ~= false then
|
||||
called = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not called then
|
||||
Utils.log("ERROR", "HQ speaker API not available for format " .. format)
|
||||
return
|
||||
end
|
||||
|
||||
while areHqSpeakersBusy(Peripherals.speakers) do
|
||||
if shouldStop(playSeq) then
|
||||
Audio.stop("current")
|
||||
return
|
||||
end
|
||||
if not waitForInterruptibleDelay(playSeq, 0.1) then
|
||||
Audio.stop("current")
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Audio.playDfpwmItem(item)
|
||||
if not Peripherals.speakers or #Peripherals.speakers == 0 then
|
||||
Utils.log("WARN", "No speakers found; cannot play audio")
|
||||
@@ -646,25 +737,44 @@ function Audio.playDfpwmItem(item)
|
||||
end
|
||||
|
||||
local playSeq = State.interruptSeq
|
||||
local source = item.dfpwmData
|
||||
local source = item.audioData
|
||||
|
||||
-- Support both static string data and streaming functions
|
||||
if type(source) == "table" and source.read then
|
||||
source = source.read
|
||||
end
|
||||
|
||||
local ok, iter = pcall(aukit.stream.dfpwm, source, 48000, 1, false)
|
||||
if not ok then
|
||||
Utils.log("ERROR", "Failed to create audio stream: " .. tostring(iter))
|
||||
return
|
||||
if type(source) ~= "function" then
|
||||
local data = source
|
||||
source = function()
|
||||
local chunk = data
|
||||
data = nil
|
||||
return chunk
|
||||
end
|
||||
end
|
||||
|
||||
for chunk in iter do
|
||||
local decoder = dfpwm.make_decoder()
|
||||
while true do
|
||||
if shouldStop(playSeq) then break end
|
||||
if not playChunkOnSpeakers(chunk[1], CONFIG.AUDIO_VOLUME, playSeq) then break end
|
||||
local chunk = source()
|
||||
if chunk == nil then break end
|
||||
if type(chunk) == "string" and #chunk > 0 then
|
||||
local samples = decoder(chunk)
|
||||
if samples and not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Audio.playItem(item)
|
||||
local format = tostring(item.audioFormat or "dfpwm"):lower()
|
||||
if format == "wav" or format == "file" then
|
||||
return playHqAudioItem(item)
|
||||
end
|
||||
return Audio.playDfpwmItem(item)
|
||||
end
|
||||
|
||||
function Audio.playerLoop()
|
||||
while true do
|
||||
if #State.audioQueue == 0 then
|
||||
@@ -674,7 +784,7 @@ function Audio.playerLoop()
|
||||
State.currentPlaying = item
|
||||
State.currentPriority = tonumber(item.priority) or 0
|
||||
|
||||
local ok, err = pcall(Audio.playDfpwmItem, item)
|
||||
local ok, err = pcall(Audio.playItem, item)
|
||||
if not ok then
|
||||
Utils.log("ERROR", "Audio Playback Crash: " .. tostring(err))
|
||||
end
|
||||
@@ -927,6 +1037,23 @@ function Network.syncFiles()
|
||||
end
|
||||
|
||||
function Network.messageLoop()
|
||||
local function finishAudioRx()
|
||||
if not State.audioRx then return end
|
||||
local rx = State.audioRx
|
||||
if rx.stream then
|
||||
rx.stream.finish()
|
||||
else
|
||||
Audio.enqueueItem({
|
||||
audioData = table.concat(rx.chunks or {}),
|
||||
audioFormat = rx.audioFormat or "wav",
|
||||
priority = rx.priority,
|
||||
source = rx.source,
|
||||
audioId = rx.audioId
|
||||
})
|
||||
end
|
||||
State.audioRx = nil
|
||||
end
|
||||
|
||||
while true do
|
||||
local event, url, msg = os.pullEvent()
|
||||
if event == "websocket_message" and url == CONFIG.CLOUD_URL then
|
||||
@@ -937,6 +1064,7 @@ function Network.messageLoop()
|
||||
pcall(State.audioRx.stream.finish)
|
||||
end
|
||||
local priority = tonumber(data.priority) or 0
|
||||
local audioFormat = tostring(data.audioFormat or "dfpwm"):lower()
|
||||
local interruptMode = tostring(data.interruptMode or "")
|
||||
if interruptMode == "" then
|
||||
if priority >= 2 then interruptMode = "all"
|
||||
@@ -950,27 +1078,31 @@ function Network.messageLoop()
|
||||
Audio.stop("current", priority)
|
||||
end
|
||||
|
||||
local stream = Audio.createStream()
|
||||
State.audioRx = {
|
||||
audioId = data.audioId,
|
||||
expectedBytes = tonumber(data.byteLength) or 0,
|
||||
receivedBytes = 0,
|
||||
stream = stream,
|
||||
priority = priority,
|
||||
source = data.source or data.category
|
||||
source = data.source or data.category,
|
||||
audioFormat = audioFormat
|
||||
}
|
||||
|
||||
-- Start playing immediately by enqueuing the stream
|
||||
Audio.enqueueItem({
|
||||
dfpwmData = stream,
|
||||
priority = priority,
|
||||
source = State.audioRx.source,
|
||||
audioId = data.audioId
|
||||
})
|
||||
if audioFormat == "dfpwm" then
|
||||
local stream = Audio.createStream()
|
||||
State.audioRx.stream = stream
|
||||
Audio.enqueueItem({
|
||||
audioData = stream,
|
||||
audioFormat = audioFormat,
|
||||
priority = priority,
|
||||
source = State.audioRx.source,
|
||||
audioId = data.audioId
|
||||
})
|
||||
else
|
||||
State.audioRx.chunks = {}
|
||||
end
|
||||
elseif data.type == "audio_end" then
|
||||
if State.audioRx and (not data.audioId or data.audioId == State.audioRx.audioId) then
|
||||
State.audioRx.stream.finish()
|
||||
State.audioRx = nil
|
||||
finishAudioRx()
|
||||
end
|
||||
elseif data.type == "audio_stop" then
|
||||
local scope = tostring(data.scope or "")
|
||||
@@ -1001,14 +1133,19 @@ function Network.messageLoop()
|
||||
-- Memory protection: limit to 10MB per audio item
|
||||
if State.audioRx.receivedBytes + #msg > 10 * 1024 * 1024 then
|
||||
Utils.log("ERROR", "Audio item too large, discarding")
|
||||
State.audioRx.stream.finish()
|
||||
if State.audioRx.stream then
|
||||
State.audioRx.stream.finish()
|
||||
end
|
||||
State.audioRx = nil
|
||||
else
|
||||
State.audioRx.stream.push(msg)
|
||||
if State.audioRx.stream then
|
||||
State.audioRx.stream.push(msg)
|
||||
else
|
||||
table.insert(State.audioRx.chunks, msg)
|
||||
end
|
||||
State.audioRx.receivedBytes = State.audioRx.receivedBytes + #msg
|
||||
if State.audioRx.expectedBytes > 0 and State.audioRx.receivedBytes >= State.audioRx.expectedBytes then
|
||||
State.audioRx.stream.finish()
|
||||
State.audioRx = nil
|
||||
finishAudioRx()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user