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:
+132
-4
@@ -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,9 +1082,26 @@ 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
|
||||||
if not pushWavChunkToSpeakers(chunk, playSeq) then
|
local samples, err, meta = decodeWavChunkToSamples(chunk)
|
||||||
Audio.stop("current")
|
if samples and #samples > 0 then
|
||||||
return
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user