From c8681417a87806c74f1360844bc7d9a115376eb5 Mon Sep 17 00:00:00 2001 From: HenryDu8133 <813367384@qq.com> Date: Mon, 3 Aug 2026 09:51:33 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E9=9F=B3=E9=A2=91=E6=A8=A1=E5=9D=97):?= =?UTF-8?q?=20=E9=87=8D=E6=9E=84wav=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE?= =?UTF-8?q?=E4=B8=8E=E8=A7=A3=E7=A0=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除冗余的流式wav解码工具函数,复用现有decodeWavChunkToSamples 简化playChunkOnSpeakers实现,移除固定帧拆分逻辑与无用常量 优化音频日志与错误处理,解码失败时回退至HQ播放路径 调整应用版本号从v1.0.50回退至v1.0.47 --- station_broadcast.lua | 282 +++++++----------------------------------- 1 file changed, 45 insertions(+), 237 deletions(-) diff --git a/station_broadcast.lua b/station_broadcast.lua index 5d0495b..05a3de7 100644 --- a/station_broadcast.lua +++ b/station_broadcast.lua @@ -1,6 +1,6 @@ -- 1. Configuration & Constants -local APP_VERSION_TEXT = "v1.0.50" +local APP_VERSION_TEXT = "v1.0.47" local CONFIG = { CLOUD_URL = "ws://stabadmin.fse-media.group", @@ -32,8 +32,6 @@ local COLORS = { SUCCESS = colors.green } -local NATIVE_AUDIO_FRAME_SAMPLES = 128 - -- 2. State Management @@ -1172,70 +1170,46 @@ end local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain) if not Peripherals.speakers or #Peripherals.speakers == 0 then return false end if waitForDrain == nil then waitForDrain = true end - local totalSamples = #samples - if totalSamples == 0 then return true end - - local speakerMap = {} - local nativeSpeakerCount = 0 + local pending = {} for _, sp in ipairs(Peripherals.speakers) do local name = peripheral.getName(sp) - if name then - speakerMap[name] = sp - if type(sp.playAudio) == "function" then - nativeSpeakerCount = nativeSpeakerCount + 1 + if name then pending[name] = sp end + end + + 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 - local controller = Peripherals.speakers[1] - local usedNativePath = false - - for frameStart = 1, totalSamples, NATIVE_AUDIO_FRAME_SAMPLES do - local frameEnd = math.min(frameStart + NATIVE_AUDIO_FRAME_SAMPLES - 1, totalSamples) - local frame = {} - 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 + if usedNativePath then + Utils.log("INFO", string.format("Audio samples queued via native speaker.playAudio: samples=%d", #samples)) + else + local controller = Peripherals.speakers[1] + if controller and type(controller.playAudioAll) == "function" then + Utils.log("WARN", "Falling back to controller.playAudioAll for decoded samples") while true do if shouldStop(playSeq) then return false end local ok = false pcall(function() - ok = controller.playAudioAll(frame, volume) + ok = controller.playAudioAll(samples, volume) end) if ok then break end local ev = { os.pullEvent() } @@ -1243,13 +1217,11 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain) return false end end + else + return false 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 return waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) end @@ -1378,152 +1350,6 @@ local function decodeWavChunkToSamples(chunk) } 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) for _, sp in ipairs(speakers) do local playing = false @@ -1689,18 +1515,6 @@ local function pushWavChunkToSpeakers(payload, playSeq) 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 = {} for _, sp in ipairs(Peripherals.speakers or {}) do local name = peripheral.getName(sp) @@ -1856,8 +1670,6 @@ function Audio.playWavStreamItem(item) Utils.log("INFO", "Live WAV prebuffer ready: chunks=" .. tostring(streamObj.count())) end - local wavDecoder = createWavStreamDecoder() - while true do if shouldStop(playSeq) then Audio.stop("current") @@ -1867,28 +1679,24 @@ function Audio.playWavStreamItem(item) local chunk = source() if chunk == nil then break end 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 meta and not wavDecoder.logged then - wavDecoder.logged = true - Utils.log("INFO", string.format( - "WAV stream header: rate=%d ch=%d bits=%d", - tonumber(meta and meta.sampleRate) or 0, - tonumber(meta and meta.channels) or 0, - tonumber(meta and meta.bitsPerSample) or 0 - )) - end + 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 + )) local playOk = playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq, false) if not playOk then Audio.stop("current") return end - elseif err and err ~= "need header" then - Utils.log("ERROR", "WAV stream decode failed: " .. tostring(err)) - Audio.stop("current") - return - elseif err == "need header" then - if not waitForInterruptibleDelay(playSeq, 0.01) then + 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