refactor(音频模块): 重构wav音频播放与解码逻辑
移除冗余的流式wav解码工具函数,复用现有decodeWavChunkToSamples 简化playChunkOnSpeakers实现,移除固定帧拆分逻辑与无用常量 优化音频日志与错误处理,解码失败时回退至HQ播放路径 调整应用版本号从v1.0.50回退至v1.0.47
This commit is contained in:
+45
-237
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user