feat(music): switch station music to live PCM streaming

Move music playback off the HLS/download-first path and stream 48k mono PCM chunks over WebSocket so stations can start playing immediately. Add station-side PCM chunk playback support and stream lifecycle logging to make no-audio issues easier to trace.
This commit is contained in:
2026-07-31 21:00:29 +08:00
parent 6d65b8216a
commit 709388ec3a
2 changed files with 208 additions and 18 deletions
+56 -2
View File
@@ -654,6 +654,19 @@ local function playChunkOnSpeakers(samples, volume, playSeq)
return true
end
local function decodePcmS8Chunk(chunk)
if type(chunk) ~= "string" or #chunk == 0 then return nil end
local samples = {}
for i = 1, #chunk do
local b = string.byte(chunk, i)
if b >= 128 then
b = b - 256
end
samples[i] = b
end
return samples
end
local function areHqSpeakersBusy(speakers)
for _, sp in ipairs(speakers) do
local playing = false
@@ -824,10 +837,47 @@ function Audio.playDfpwmItem(item)
end
end
function Audio.playPcmItem(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 source = item.audioData
if type(source) == "table" and source.read then
source = source.read
end
if type(source) ~= "function" then
local data = source
source = function()
local chunk = data
data = nil
return chunk
end
end
while true do
if shouldStop(playSeq) then break end
local chunk = source()
if chunk == nil then break end
if type(chunk) == "string" and #chunk > 0 then
local samples = decodePcmS8Chunk(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" or format == "stream" or format == "hls" then
return playHqAudioItem(item)
elseif format == "pcm" then
return Audio.playPcmItem(item)
end
return Audio.playDfpwmItem(item)
end
@@ -1160,9 +1210,12 @@ function Network.messageLoop()
audioId = data.audioId
})
State.audioRx = nil
elseif audioFormat == "dfpwm" then
elseif audioFormat == "dfpwm" or audioFormat == "pcm" then
local stream = Audio.createStream()
State.audioRx.stream = stream
if audioFormat == "pcm" then
Utils.log("INFO", "Receiving live PCM audio stream")
end
Audio.enqueueItem({
audioData = stream,
audioFormat = audioFormat,
@@ -1204,7 +1257,8 @@ function Network.messageLoop()
else
if State.audioRx then
-- Memory protection: limit to 10MB per audio item
if State.audioRx.receivedBytes + #msg > 10 * 1024 * 1024 then
local isLiveStream = State.audioRx.stream and (tonumber(State.audioRx.expectedBytes) or 0) <= 0
if (not isLiveStream) and State.audioRx.receivedBytes + #msg > 10 * 1024 * 1024 then
Utils.log("ERROR", "Audio item too large, discarding")
if State.audioRx.stream then
State.audioRx.stream.finish()