refactor(音频模块): 重构wav音频播放与解码逻辑

移除冗余的流式wav解码工具函数,复用现有decodeWavChunkToSamples
简化playChunkOnSpeakers实现,移除固定帧拆分逻辑与无用常量
优化音频日志与错误处理,解码失败时回退至HQ播放路径
调整应用版本号从v1.0.50回退至v1.0.47
This commit is contained in:
2026-08-03 09:51:33 +08:00
parent bf1895ebc5
commit c8681417a8
+45 -237
View File
@@ -1,6 +1,6 @@
-- 1. Configuration & Constants -- 1. Configuration & Constants
local APP_VERSION_TEXT = "v1.0.50" local APP_VERSION_TEXT = "v1.0.47"
local CONFIG = { local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group", CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -32,8 +32,6 @@ local COLORS = {
SUCCESS = colors.green SUCCESS = colors.green
} }
local NATIVE_AUDIO_FRAME_SAMPLES = 128
-- 2. State Management -- 2. State Management
@@ -1172,70 +1170,46 @@ end
local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain) local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
if not Peripherals.speakers or #Peripherals.speakers == 0 then return false end if not Peripherals.speakers or #Peripherals.speakers == 0 then return false end
if waitForDrain == nil then waitForDrain = true end if waitForDrain == nil then waitForDrain = true end
local totalSamples = #samples local pending = {}
if totalSamples == 0 then return true end
local speakerMap = {}
local nativeSpeakerCount = 0
for _, sp in ipairs(Peripherals.speakers) do for _, sp in ipairs(Peripherals.speakers) do
local name = peripheral.getName(sp) local name = peripheral.getName(sp)
if name then if name then pending[name] = sp end
speakerMap[name] = sp end
if type(sp.playAudio) == "function" then
nativeSpeakerCount = nativeSpeakerCount + 1 local usedNativePath = false
while next(pending) do
if shouldStop(playSeq) then return false end
for name, sp in pairs(pending) do
local ok = false
pcall(function()
if type(sp.playAudio) == "function" then
ok = sp.playAudio(samples, volume)
end
end)
if ok then
usedNativePath = true
pending[name] = nil
end
end
if next(pending) then
local ev = { os.pullEvent() }
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
return false
end end
end end
end end
local controller = Peripherals.speakers[1] if usedNativePath then
local usedNativePath = false Utils.log("INFO", string.format("Audio samples queued via native speaker.playAudio: samples=%d", #samples))
else
for frameStart = 1, totalSamples, NATIVE_AUDIO_FRAME_SAMPLES do local controller = Peripherals.speakers[1]
local frameEnd = math.min(frameStart + NATIVE_AUDIO_FRAME_SAMPLES - 1, totalSamples) if controller and type(controller.playAudioAll) == "function" then
local frame = {} Utils.log("WARN", "Falling back to controller.playAudioAll for decoded samples")
for i = frameStart, frameEnd do
frame[#frame + 1] = samples[i]
end
if nativeSpeakerCount > 0 then
local pending = {}
for name, sp in pairs(speakerMap) do
if type(sp.playAudio) == "function" then
pending[name] = sp
end
end
while next(pending) do
if shouldStop(playSeq) then return false end
for name, sp in pairs(pending) do
local ok = false
pcall(function()
ok = sp.playAudio(frame, volume)
end)
if ok then
usedNativePath = true
pending[name] = nil
end
end
if next(pending) then
local ev = { os.pullEvent() }
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
return false
end
end
end
else
if not controller or type(controller.playAudioAll) ~= "function" then
return false
end
if frameStart == 1 then
Utils.log("WARN", "Falling back to controller.playAudioAll for decoded samples")
end
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
pcall(function() pcall(function()
ok = controller.playAudioAll(frame, volume) ok = controller.playAudioAll(samples, volume)
end) end)
if ok then break end if ok then break end
local ev = { os.pullEvent() } local ev = { os.pullEvent() }
@@ -1243,13 +1217,11 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
return false return false
end end
end end
else
return false
end end
end end
if usedNativePath then
Utils.log("INFO", string.format("Audio samples queued via native speaker.playAudio: samples=%d frame=%d", totalSamples, NATIVE_AUDIO_FRAME_SAMPLES))
end
if not waitForDrain then return true end if not waitForDrain then return true end
return waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) return waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq)
end end
@@ -1378,152 +1350,6 @@ local function decodeWavChunkToSamples(chunk)
} }
end end
local function parseWavHeader(buf)
if type(buf) ~= "string" or #buf < 44 then
return nil, nil, "wav header too short"
end
if buf:sub(1, 4) ~= "RIFF" or buf:sub(9, 12) ~= "WAVE" then
return nil, nil, "invalid wav header"
end
local offset = 13
local audioFormat, channels, sampleRate, bitsPerSample
local dataOffset, dataSize
while offset + 7 <= #buf do
local chunkId = buf:sub(offset, offset + 3)
local chunkSize = readU32LE(buf, offset + 4)
if not chunkSize then break end
local payloadOffset = offset + 8
if chunkId == "fmt " and chunkSize >= 16 then
audioFormat = readU16LE(buf, payloadOffset)
channels = readU16LE(buf, payloadOffset + 2)
sampleRate = readU32LE(buf, payloadOffset + 4)
bitsPerSample = readU16LE(buf, 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, nil, "unsupported wav format " .. tostring(audioFormat)
end
if channels ~= 1 and channels ~= 2 then
return nil, nil, "unsupported channels " .. tostring(channels)
end
if bitsPerSample ~= 8 and bitsPerSample ~= 16 then
return nil, nil, "unsupported bits " .. tostring(bitsPerSample)
end
if not dataOffset or not dataSize then
return nil, nil, "wav missing data chunk"
end
return {
channels = channels,
bitsPerSample = bitsPerSample,
sampleRate = sampleRate or 0,
dataSize = dataSize
}, dataOffset, nil
end
local function decodePcmBytesToSamples(pcm, channels, bitsPerSample)
if type(pcm) ~= "string" or #pcm == 0 then return {} end
local frameSize = channels * (bitsPerSample / 8)
if frameSize <= 0 then return {} end
local usableLen = #pcm - (#pcm % frameSize)
if usableLen <= 0 then return {} end
local samples = {}
local sampleIndex = 1
local endOffset = usableLen - frameSize + 1
for pos = 1, 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(pcm, sampleOffset)
sampleValue = (raw or 128) - 128
else
sampleValue = readS16LE(pcm, 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
end
local function createWavStreamDecoder()
local state = {
header = nil,
headerParsed = false,
pending = "",
frameSize = 0,
logged = false
}
local function decode(chunk)
if type(chunk) ~= "string" or #chunk == 0 then return {}, nil, state.header end
local buf = state.pending .. chunk
state.pending = ""
if not state.headerParsed then
local header, dataOffset, err = parseWavHeader(buf)
if err then
state.pending = buf
if #state.pending > 128 * 1024 then
return nil, err, nil
end
return {}, "need header", nil
end
state.header = header
state.headerParsed = true
state.frameSize = (tonumber(header.channels) or 1) * ((tonumber(header.bitsPerSample) or 16) / 8)
buf = buf:sub(dataOffset)
end
local frameSize = tonumber(state.frameSize) or 0
if frameSize <= 0 then return nil, "invalid frame size", state.header end
local usableLen = #buf - (#buf % frameSize)
local usable = ""
if usableLen > 0 then
usable = buf:sub(1, usableLen)
end
local rest = buf:sub(usableLen + 1)
if rest and rest ~= "" then
state.pending = rest
end
local samples = decodePcmBytesToSamples(
usable,
tonumber(state.header and state.header.channels) or 1,
tonumber(state.header and state.header.bitsPerSample) or 16
)
return samples, nil, state.header
end
return { decode = decode }
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
@@ -1689,18 +1515,6 @@ local function pushWavChunkToSpeakers(payload, playSeq)
end end
end end
local hasFallback = false
for _, sp in ipairs(Peripherals.speakers or {}) do
if type(sp.speakWav) == "function" then
hasFallback = true
break
end
end
if not hasFallback then
Utils.log("ERROR", "No speaker supports speakWav/speakWavAll for WAV chunk fallback")
return false
end
local pending = {} local pending = {}
for _, sp in ipairs(Peripherals.speakers or {}) do for _, sp in ipairs(Peripherals.speakers or {}) do
local name = peripheral.getName(sp) local name = peripheral.getName(sp)
@@ -1856,8 +1670,6 @@ function Audio.playWavStreamItem(item)
Utils.log("INFO", "Live WAV prebuffer ready: chunks=" .. tostring(streamObj.count())) Utils.log("INFO", "Live WAV prebuffer ready: chunks=" .. tostring(streamObj.count()))
end end
local wavDecoder = createWavStreamDecoder()
while true do while true do
if shouldStop(playSeq) then if shouldStop(playSeq) then
Audio.stop("current") Audio.stop("current")
@@ -1867,28 +1679,24 @@ 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 = wavDecoder.decode(chunk) local samples, err, meta = decodeWavChunkToSamples(chunk)
if samples and #samples > 0 then if samples and #samples > 0 then
if meta and not wavDecoder.logged then Utils.log("INFO", string.format(
wavDecoder.logged = true "Decoded WAV chunk: bytes=%d samples=%d rate=%d ch=%d bits=%d",
Utils.log("INFO", string.format( #chunk,
"WAV stream header: rate=%d ch=%d bits=%d", #samples,
tonumber(meta and meta.sampleRate) or 0, tonumber(meta and meta.sampleRate) or 0,
tonumber(meta and meta.channels) or 0, tonumber(meta and meta.channels) or 0,
tonumber(meta and meta.bitsPerSample) or 0 tonumber(meta and meta.bitsPerSample) or 0
)) ))
end
local playOk = playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq, false) local playOk = playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq, false)
if not playOk then if not playOk then
Audio.stop("current") Audio.stop("current")
return return
end end
elseif err and err ~= "need header" then else
Utils.log("ERROR", "WAV stream decode failed: " .. tostring(err)) Utils.log("WARN", "WAV chunk decode failed, falling back to HQ path: " .. tostring(err))
Audio.stop("current") if not pushWavChunkToSpeakers(chunk, playSeq) then
return
elseif err == "need header" then
if not waitForInterruptibleDelay(playSeq, 0.01) then
Audio.stop("current") Audio.stop("current")
return return
end end