fix: 修复音乐和临时广播无法播放(AUDIO FREE)的问题

- 重写 waitForInterruptibleDelay/waitForNativeSpeakersEmpty 使用 os.clock()
  轮询,替换会抢占 websocket_message 事件的 os.pullEvent()
- 重写 queueSamplesSynchronized/playChunkOnSpeakers 重试循环使用
  os.sleep() 轮询,不再调用 pullEvent 吞掉音频分块事件
- 重写 Audio.createStream().read() 与 Audio.playerLoop 使用 sleep 轮询,
  不再使用 os.pullEvent('audio_chunk')/'audio_queue' 抢事件
- 修复 Audio.playItem 将 wav-stream/wav-stream-live 错误路由到
  playPcmS16LEItem,改为路由到 playWavStreamItem(服务端分块每块
  带完整 44 字节 WAV 头,需 decodeWavChunkToSamples)
- Network.messageLoop audio_start 分支将 wav-stream-live 加入流式
  接收判定,避免走 chunks 缓冲逻辑
- 版本号升级至 v1.0.97
This commit is contained in:
2026-08-17 14:30:54 +08:00
parent e9819d4894
commit 849b43af39
+48 -70
View File
@@ -1,6 +1,6 @@
-- 1. Configuration & Constants -- 1. Configuration & Constants
local APP_VERSION_TEXT = "v1.0.76" local APP_VERSION_TEXT = "v1.0.97"
local CONFIG = { local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group", CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -1053,18 +1053,16 @@ function Audio.createStream()
stream.push = function(chunk) stream.push = function(chunk)
if type(chunk) ~= "string" then return end if type(chunk) ~= "string" then return end
table.insert(chunks, chunk) table.insert(chunks, chunk)
os.queueEvent("audio_chunk")
end end
stream.finish = function() stream.finish = function()
finished = true finished = true
os.queueEvent("audio_chunk")
end end
stream.read = function() stream.read = function()
while #chunks == 0 do while #chunks == 0 do
if finished then return nil end if finished then return nil end
os.pullEvent("audio_chunk") os.sleep(0.02)
end end
return table.remove(chunks, 1) return table.remove(chunks, 1)
end end
@@ -1156,7 +1154,6 @@ function Audio.enqueueItem(item)
idx = idx + 1 idx = idx + 1
end end
table.insert(State.audioQueue, idx, item) table.insert(State.audioQueue, idx, item)
os.queueEvent("audio_queue")
end end
function Audio.stop(mode, flushBelow) function Audio.stop(mode, flushBelow)
@@ -1194,41 +1191,46 @@ local function shouldStop(playSeq)
end end
local function waitForInterruptibleDelay(playSeq, seconds) local function waitForInterruptibleDelay(playSeq, seconds)
local timer = os.startTimer(seconds) local endTime = os.clock() + seconds
while true do while os.clock() < endTime do
local ev = { os.pullEvent() } if shouldStop(playSeq) then
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
return false return false
end end
if ev[1] == "timer" and ev[2] == timer then local remaining = endTime - os.clock()
return true if remaining <= 0 then break end
end local sleepTime = math.min(remaining, 0.02)
os.sleep(sleepTime)
end end
return not shouldStop(playSeq)
end end
local function waitForNativeSpeakersEmpty(speakers, playSeq) local function waitForNativeSpeakersEmpty(speakers, playSeq)
local waiting = {} local maxWait = os.clock() + 2.0
for _, sp in ipairs(speakers or {}) do while os.clock() < maxWait do
local name = peripheral.getName(sp)
if name then waiting[name] = true end
end
local timerId = os.startTimer(2.0) -- Maximum 2 seconds wait for empty
while next(waiting) do
if shouldStop(playSeq) then return false end if shouldStop(playSeq) then return false end
local ev = { os.pullEvent() } local allEmpty = true
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then for _, sp in ipairs(speakers or {}) do
return false 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
allEmpty = false
break
end
end end
if ev[1] == "timer" and ev[2] == timerId then if allEmpty then
Utils.log("WARN", "Timeout waiting for native speakers to empty") return true
break
end
if ev[1] == "speaker_audio_empty" and ev[2] and waiting[ev[2]] then
waiting[ev[2]] = nil
end end
os.sleep(0.02)
end end
Utils.log("WARN", "Timeout waiting for native speakers to empty (polling)")
return true return true
end end
@@ -1246,6 +1248,7 @@ local function queueSamplesSynchronized(samples, volume, playSeq)
end end
end end
local maxAttempts = 400
while next(pending) do while next(pending) do
if shouldStop(playSeq) then return false end if shouldStop(playSeq) then return false end
for name, sp in pairs(pending) do for name, sp in pairs(pending) do
@@ -1258,17 +1261,12 @@ local function queueSamplesSynchronized(samples, volume, playSeq)
end end
end end
if next(pending) then if next(pending) then
local timerId = os.startTimer(0.05) maxAttempts = maxAttempts - 1
while true do if maxAttempts <= 0 then
local ev = { os.pullEvent() } Utils.log("WARN", "Speaker buffer full or failed, skipping chunk (sync)")
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then break
return false
elseif ev[1] == "timer" and ev[2] == timerId then
break
elseif ev[1] == "speaker_audio_empty" then
break
end
end end
os.sleep(0.05)
end end
end end
@@ -1286,7 +1284,7 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
end end
local usedNativePath = false local usedNativePath = false
local maxAttempts = 20 local maxAttempts = 400
while next(pending) do while next(pending) do
if shouldStop(playSeq) then return false end if shouldStop(playSeq) then return false end
for name, sp in pairs(pending) do for name, sp in pairs(pending) do
@@ -1307,17 +1305,7 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
Utils.log("WARN", "Speaker buffer full or failed, skipping chunk") Utils.log("WARN", "Speaker buffer full or failed, skipping chunk")
break break
end end
local timerId = os.startTimer(0.05) os.sleep(0.05)
while true do
local ev = { os.pullEvent() }
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
return false
elseif ev[1] == "timer" and ev[2] == timerId then
break
elseif ev[1] == "speaker_audio_empty" then
break
end
end
end end
end end
@@ -1327,7 +1315,7 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
local controller = Peripherals.speakers[1] local controller = Peripherals.speakers[1]
if controller and type(controller.playAudioAll) == "function" then if controller and type(controller.playAudioAll) == "function" then
Utils.log("WARN", "Falling back to controller.playAudioAll for decoded samples") Utils.log("WARN", "Falling back to controller.playAudioAll for decoded samples")
local maxAttempts = 20 local maxAttempts2 = 400
while true do while true do
if shouldStop(playSeq) then return false end if shouldStop(playSeq) then return false end
local ok = false local ok = false
@@ -1335,22 +1323,12 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
ok = controller.playAudioAll(samples, volume) ok = controller.playAudioAll(samples, volume)
end) end)
if ok then break end if ok then break end
maxAttempts = maxAttempts - 1 maxAttempts2 = maxAttempts2 - 1
if maxAttempts <= 0 then if maxAttempts2 <= 0 then
Utils.log("WARN", "Controller buffer full or failed, skipping chunk") Utils.log("WARN", "Controller buffer full or failed, skipping chunk")
break break
end end
local timerId = os.startTimer(0.05) os.sleep(0.05)
while true do
local ev = { os.pullEvent() }
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
return false
elseif ev[1] == "timer" and ev[2] == timerId then
break
elseif ev[1] == "speaker_audio_empty" then
break
end
end
end end
else else
return false return false
@@ -2027,7 +2005,7 @@ function Audio.playItem(item)
elseif format == "pcm" then elseif format == "pcm" then
return Audio.playPcmItem(item) return Audio.playPcmItem(item)
elseif format == "wav-stream" or format == "wav-stream-live" then elseif format == "wav-stream" or format == "wav-stream-live" then
return Audio.playPcmS16LEItem(item) return Audio.playWavStreamItem(item)
end end
return Audio.playDfpwmItem(item) return Audio.playDfpwmItem(item)
end end
@@ -2035,7 +2013,7 @@ end
function Audio.playerLoop() function Audio.playerLoop()
while true do while true do
if #State.audioQueue == 0 then if #State.audioQueue == 0 then
os.pullEvent("audio_queue") os.sleep(0.05)
else else
local item = table.remove(State.audioQueue, 1) local item = table.remove(State.audioQueue, 1)
State.currentPlaying = item State.currentPlaying = item
@@ -2403,12 +2381,12 @@ function Network.messageLoop()
audioId = data.audioId audioId = data.audioId
}) })
State.audioRx = nil State.audioRx = nil
elseif audioFormat == "dfpwm" or audioFormat == "pcm" or audioFormat == "wav-stream" then elseif audioFormat == "dfpwm" or audioFormat == "pcm" or audioFormat == "wav-stream" or audioFormat == "wav-stream-live" then
local stream = Audio.createStream() local stream = Audio.createStream()
State.audioRx.stream = stream State.audioRx.stream = stream
if audioFormat == "pcm" then if audioFormat == "pcm" then
Utils.log("INFO", "Receiving live PCM audio stream") Utils.log("INFO", "Receiving live PCM audio stream")
elseif audioFormat == "wav-stream" then elseif audioFormat == "wav-stream" or audioFormat == "wav-stream-live" then
Utils.log("INFO", "Receiving live WAV chunk stream") Utils.log("INFO", "Receiving live WAV chunk stream")
end end
Audio.enqueueItem({ Audio.enqueueItem({