From e6fa6813393c54edaf6d32d98101f37fed5ee760 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Sat, 1 Aug 2026 00:04:18 +0800 Subject: [PATCH] fix(station): decode wav chunks to speaker samples Bump the station build to v1.0.2 and decode streamed WAV chunks into PCM samples on the client before feeding playAudioAll/playAudio, while keeping the HQ WAV path as a fallback for decode failures. --- station_broadcast.lua | 136 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 132 insertions(+), 4 deletions(-) diff --git a/station_broadcast.lua b/station_broadcast.lua index b71c849..6d82f80 100644 --- a/station_broadcast.lua +++ b/station_broadcast.lua @@ -2,7 +2,7 @@ -- ============================================================================ -- 1. Configuration & Constants -- ============================================================================ -local APP_VERSION_TEXT = "v1.0.1" +local APP_VERSION_TEXT = "v1.0.2" local CONFIG = { CLOUD_URL = "ws://stabadmin.fse-media.group", @@ -665,6 +665,117 @@ local function decodePcmS8Chunk(chunk) return samples end +local function readU16LE(data, offset) + local a, b = string.byte(data, offset, offset + 1) + if not a or not b then return nil end + return a + b * 256 +end + +local function readS16LE(data, offset) + local val = readU16LE(data, offset) + if not val then return nil end + if val >= 32768 then + val = val - 65536 + end + return val +end + +local function readU32LE(data, offset) + local a, b, c, d = string.byte(data, offset, offset + 3) + if not a or not b or not c or not d then return nil end + return a + b * 256 + c * 65536 + d * 16777216 +end + +local function clampSample8(val) + if val > 127 then return 127 end + if val < -128 then return -128 end + return val +end + +local function decodeWavChunkToSamples(chunk) + if type(chunk) ~= "string" or #chunk < 44 then + return nil, "wav chunk too short" + end + if chunk:sub(1, 4) ~= "RIFF" or chunk:sub(9, 12) ~= "WAVE" then + return nil, "invalid wav header" + end + + local offset = 13 + local audioFormat, channels, sampleRate, bitsPerSample + local dataOffset, dataSize + + while offset + 7 <= #chunk do + local chunkId = chunk:sub(offset, offset + 3) + local chunkSize = readU32LE(chunk, offset + 4) + if not chunkSize then break end + local payloadOffset = offset + 8 + if chunkId == "fmt " and chunkSize >= 16 then + audioFormat = readU16LE(chunk, payloadOffset) + channels = readU16LE(chunk, payloadOffset + 2) + sampleRate = readU32LE(chunk, payloadOffset + 4) + bitsPerSample = readU16LE(chunk, payloadOffset + 14) + elseif chunkId == "data" then + dataOffset = payloadOffset + dataSize = chunkSize + break + end + offset = payloadOffset + chunkSize + if chunkSize % 2 == 1 then + offset = offset + 1 + end + end + + if audioFormat ~= 1 then + return nil, "unsupported wav format " .. tostring(audioFormat) + end + if channels ~= 1 and channels ~= 2 then + return nil, "unsupported channels " .. tostring(channels) + end + if bitsPerSample ~= 8 and bitsPerSample ~= 16 then + return nil, "unsupported bits " .. tostring(bitsPerSample) + end + if not dataOffset or not dataSize or dataOffset + dataSize - 1 > #chunk then + return nil, "invalid wav data chunk" + end + + local frameSize = channels * (bitsPerSample / 8) + if frameSize <= 0 then + return nil, "invalid frame size" + end + + local samples = {} + local sampleIndex = 1 + local endOffset = dataOffset + dataSize - frameSize + + for pos = dataOffset, endOffset, frameSize do + local mixed = 0 + for ch = 0, channels - 1 do + local sampleOffset = pos + ch * (bitsPerSample / 8) + local sampleValue + if bitsPerSample == 8 then + local raw = string.byte(chunk, sampleOffset) + sampleValue = (raw or 128) - 128 + else + sampleValue = readS16LE(chunk, sampleOffset) or 0 + if sampleValue >= 0 then + sampleValue = math.floor(sampleValue / 256) + else + sampleValue = math.ceil(sampleValue / 256) + end + end + mixed = mixed + sampleValue + end + samples[sampleIndex] = clampSample8(math.floor(mixed / channels)) + sampleIndex = sampleIndex + 1 + end + + return samples, nil, { + channels = channels, + bitsPerSample = bitsPerSample, + sampleRate = sampleRate or 0 + } +end + local function areHqSpeakersBusy(speakers) for _, sp in ipairs(speakers) do local playing = false @@ -971,9 +1082,26 @@ function Audio.playWavStreamItem(item) local chunk = source() if chunk == nil then break end if type(chunk) == "string" and #chunk > 0 then - if not pushWavChunkToSpeakers(chunk, playSeq) then - Audio.stop("current") - return + local samples, err, meta = decodeWavChunkToSamples(chunk) + if samples and #samples > 0 then + Utils.log("INFO", string.format( + "Decoded WAV chunk: bytes=%d samples=%d rate=%d ch=%d bits=%d", + #chunk, + #samples, + tonumber(meta and meta.sampleRate) or 0, + tonumber(meta and meta.channels) or 0, + tonumber(meta and meta.bitsPerSample) or 0 + )) + if not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then + Audio.stop("current") + return + end + else + Utils.log("WARN", "WAV chunk decode failed, falling back to HQ path: " .. tostring(err)) + if not pushWavChunkToSpeakers(chunk, playSeq) then + Audio.stop("current") + return + end end end end