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.
This commit is contained in:
2026-08-01 00:04:18 +08:00
parent f09e5af698
commit e6fa681339
+129 -1
View File
@@ -2,7 +2,7 @@
-- ============================================================================ -- ============================================================================
-- 1. Configuration & Constants -- 1. Configuration & Constants
-- ============================================================================ -- ============================================================================
local APP_VERSION_TEXT = "v1.0.1" local APP_VERSION_TEXT = "v1.0.2"
local CONFIG = { local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group", CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -665,6 +665,117 @@ local function decodePcmS8Chunk(chunk)
return samples return samples
end 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) local function areHqSpeakersBusy(speakers)
for _, sp in ipairs(speakers) do for _, sp in ipairs(speakers) do
local playing = false local playing = false
@@ -971,12 +1082,29 @@ function Audio.playWavStreamItem(item)
local chunk = source() local chunk = source()
if chunk == nil then break end if chunk == nil then break end
if type(chunk) == "string" and #chunk > 0 then if type(chunk) == "string" and #chunk > 0 then
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 if not pushWavChunkToSpeakers(chunk, playSeq) then
Audio.stop("current") Audio.stop("current")
return return
end end
end end
end end
end
while areHqSpeakersBusy(Peripherals.speakers) do while areHqSpeakersBusy(Peripherals.speakers) do
if shouldStop(playSeq) then if shouldStop(playSeq) then