fix(audio): 修复非高音质环境下 PCM 音频分片播放时的爆音问题
1. 移除 server.js 生成的每个音频分片携带的 44 字节 WAV 头,防止将其作为音频样本解码导致每 0.25 秒出现点击声 ('哒')。 2. 优化 playAudio 队列满时的轮询机制,从原先阻塞等待 speaker_audio_empty(会导致扬声器饥饿)改为使用 0.05 秒的定时器定期尝试,以保持缓冲区处于满载状态。 3. 同步修复了 station_broadcast-warning.lua 中潜在的相同轮询饥饿问题。 4. 版本号递增至 v1.0.62。
This commit is contained in:
@@ -1,3 +1,11 @@
|
|||||||
|
--[[
|
||||||
|
Author: HenryDu8133 813367384@qq.com
|
||||||
|
Date: 2026-05-31 00:12:30
|
||||||
|
LastEditors: HenryDu8133 813367384@qq.com
|
||||||
|
LastEditTime: 2026-08-16 11:28:02
|
||||||
|
FilePath: \StationBroadcastDisplayCN\install_station_broadcast.lua
|
||||||
|
Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
|
||||||
|
--]]
|
||||||
local STATION_BROADCAST_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/station_broadcast.lua"
|
local STATION_BROADCAST_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/station_broadcast.lua"
|
||||||
local UPDATE_SCRIPT_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/update_station_broadcast.lua"
|
local UPDATE_SCRIPT_URL = "https://gitea.fse-media.group/Henry_Du/CC-StationBroadcast/raw/branch/main/update_station_broadcast.lua"
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
-- Random Audio Player
|
-- Random Audio Player
|
||||||
-- Simplified version that only plays random audio files
|
-- Simplified version that only plays random audio files
|
||||||
|
|
||||||
@@ -45,7 +46,15 @@ local function playDfpwmFile(path, volume)
|
|||||||
|
|
||||||
if controller and type(controller.playAudioAll) == "function" then
|
if controller and type(controller.playAudioAll) == "function" then
|
||||||
while not controller.playAudioAll(samples, volume) do
|
while not controller.playAudioAll(samples, volume) do
|
||||||
os.pullEvent("speaker_audio_empty")
|
local timerId = os.startTimer(0.05)
|
||||||
|
while true do
|
||||||
|
local ev = { os.pullEvent() }
|
||||||
|
if ev[1] == "timer" and ev[2] == timerId then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "speaker_audio_empty" then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
local pending = {}
|
local pending = {}
|
||||||
@@ -59,7 +68,15 @@ local function playDfpwmFile(path, volume)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if next(pending) then
|
if next(pending) then
|
||||||
os.pullEvent("speaker_audio_empty")
|
local timerId = os.startTimer(0.05)
|
||||||
|
while true do
|
||||||
|
local ev = { os.pullEvent() }
|
||||||
|
if ev[1] == "timer" and ev[2] == timerId then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "speaker_audio_empty" then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+126
-7
@@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
-- 1. Configuration & Constants
|
-- 1. Configuration & Constants
|
||||||
local APP_VERSION_TEXT = "v1.0.61"
|
local APP_VERSION_TEXT = "v1.0.62"
|
||||||
|
|
||||||
local CONFIG = {
|
local CONFIG = {
|
||||||
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
CLOUD_URL = "ws://stabadmin.fse-media.group",
|
||||||
@@ -1213,9 +1213,16 @@ local function queueSamplesSynchronized(samples, volume, playSeq)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if next(pending) then
|
if next(pending) then
|
||||||
|
local timerId = os.startTimer(0.05)
|
||||||
|
while true do
|
||||||
local ev = { os.pullEvent() }
|
local ev = { os.pullEvent() }
|
||||||
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
||||||
return false
|
return false
|
||||||
|
elseif ev[1] == "timer" and ev[2] == timerId then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "speaker_audio_empty" then
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1249,9 +1256,16 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if next(pending) then
|
if next(pending) then
|
||||||
|
local timerId = os.startTimer(0.05)
|
||||||
|
while true do
|
||||||
local ev = { os.pullEvent() }
|
local ev = { os.pullEvent() }
|
||||||
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
||||||
return false
|
return false
|
||||||
|
elseif ev[1] == "timer" and ev[2] == timerId then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "speaker_audio_empty" then
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1269,9 +1283,16 @@ local function playChunkOnSpeakers(samples, volume, playSeq, waitForDrain)
|
|||||||
ok = controller.playAudioAll(samples, volume)
|
ok = controller.playAudioAll(samples, volume)
|
||||||
end)
|
end)
|
||||||
if ok then break end
|
if ok then break end
|
||||||
|
local timerId = os.startTimer(0.05)
|
||||||
|
while true do
|
||||||
local ev = { os.pullEvent() }
|
local ev = { os.pullEvent() }
|
||||||
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
if ev[1] == "audio_interrupt" and tonumber(ev[2]) and tonumber(ev[2]) >= playSeq then
|
||||||
return false
|
return false
|
||||||
|
elseif ev[1] == "timer" and ev[2] == timerId then
|
||||||
|
break
|
||||||
|
elseif ev[1] == "speaker_audio_empty" then
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -1285,23 +1306,67 @@ end
|
|||||||
|
|
||||||
local function decodePcmS8Chunk(chunk)
|
local function decodePcmS8Chunk(chunk)
|
||||||
if type(chunk) ~= "string" or #chunk == 0 then return nil end
|
if type(chunk) ~= "string" or #chunk == 0 then return nil end
|
||||||
|
local startIdx = 1
|
||||||
|
if #chunk >= 44 and string.sub(chunk, 1, 4) == "RIFF" and string.sub(chunk, 9, 12) == "WAVE" then
|
||||||
|
local pos = 13
|
||||||
|
while pos + 7 <= #chunk do
|
||||||
|
local chunkId = string.sub(chunk, pos, pos + 3)
|
||||||
|
local a, b, c, d = string.byte(chunk, pos + 4, pos + 7)
|
||||||
|
local chunkSize = 0
|
||||||
|
if a and b and c and d then
|
||||||
|
chunkSize = a + b * 256 + c * 65536 + d * 16777216
|
||||||
|
end
|
||||||
|
if chunkId == "data" then
|
||||||
|
startIdx = pos + 8
|
||||||
|
break
|
||||||
|
end
|
||||||
|
pos = pos + 8 + chunkSize
|
||||||
|
end
|
||||||
|
if startIdx == 1 then
|
||||||
|
startIdx = 45 -- fallback
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local samples = {}
|
local samples = {}
|
||||||
for i = 1, #chunk do
|
local idx = 1
|
||||||
|
for i = startIdx, #chunk do
|
||||||
local b = string.byte(chunk, i)
|
local b = string.byte(chunk, i)
|
||||||
if b >= 128 then
|
if b >= 128 then
|
||||||
b = b - 256
|
b = b - 256
|
||||||
end
|
end
|
||||||
samples[i] = b
|
samples[idx] = b
|
||||||
|
idx = idx + 1
|
||||||
end
|
end
|
||||||
return samples
|
return samples
|
||||||
end
|
end
|
||||||
|
|
||||||
local function decodePcmS16LEChunk(chunk)
|
local function decodePcmS16LEChunk(chunk)
|
||||||
if type(chunk) ~= "string" or #chunk < 2 then return nil end
|
if type(chunk) ~= "string" or #chunk < 2 then return nil end
|
||||||
|
local startIdx = 1
|
||||||
|
if #chunk >= 44 and string.sub(chunk, 1, 4) == "RIFF" and string.sub(chunk, 9, 12) == "WAVE" then
|
||||||
|
local pos = 13
|
||||||
|
while pos + 7 <= #chunk do
|
||||||
|
local chunkId = string.sub(chunk, pos, pos + 3)
|
||||||
|
local a, b, c, d = string.byte(chunk, pos + 4, pos + 7)
|
||||||
|
local chunkSize = 0
|
||||||
|
if a and b and c and d then
|
||||||
|
chunkSize = a + b * 256 + c * 65536 + d * 16777216
|
||||||
|
end
|
||||||
|
if chunkId == "data" then
|
||||||
|
startIdx = pos + 8
|
||||||
|
break
|
||||||
|
end
|
||||||
|
pos = pos + 8 + chunkSize
|
||||||
|
end
|
||||||
|
if startIdx == 1 then
|
||||||
|
startIdx = 45 -- fallback to 44 bytes header + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local samples = {}
|
local samples = {}
|
||||||
local idx = 1
|
local idx = 1
|
||||||
for i = 2, #chunk, 2 do
|
for i = startIdx, #chunk - 1, 2 do
|
||||||
local b = string.byte(chunk, i)
|
local b = string.byte(chunk, i + 1)
|
||||||
if b >= 128 then
|
if b >= 128 then
|
||||||
b = b - 256
|
b = b - 256
|
||||||
end
|
end
|
||||||
@@ -1671,9 +1736,16 @@ function Audio.playPcmItem(item)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local playSeq = State.interruptSeq
|
local playSeq = State.interruptSeq
|
||||||
|
|
||||||
|
if not waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local source = item.audioData
|
local source = item.audioData
|
||||||
|
local streamObj = nil
|
||||||
|
|
||||||
if type(source) == "table" and source.read then
|
if type(source) == "table" and source.read then
|
||||||
|
streamObj = source
|
||||||
source = source.read
|
source = source.read
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1686,13 +1758,33 @@ function Audio.playPcmItem(item)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local prebufferChunks = math.max(0, tonumber(CONFIG.AUDIO_STREAM_PREROLL_CHUNKS) or 0)
|
||||||
|
if streamObj and prebufferChunks > 0 then
|
||||||
|
Utils.log("INFO", "Prebuffering live PCM8 stream: target chunks=" .. tostring(prebufferChunks))
|
||||||
|
while streamObj.count() < prebufferChunks and not streamObj.isFinished() do
|
||||||
|
if shouldStop(playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not waitForInterruptibleDelay(playSeq, 0.02) then
|
||||||
|
Audio.stop("current")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Utils.log("INFO", "Live PCM8 prebuffer ready: chunks=" .. tostring(streamObj.count()))
|
||||||
|
end
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
if shouldStop(playSeq) then break end
|
if shouldStop(playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
|
break
|
||||||
|
end
|
||||||
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 = decodePcmS8Chunk(chunk)
|
local samples = decodePcmS8Chunk(chunk)
|
||||||
if samples and not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then
|
if samples and not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -1706,9 +1798,16 @@ function Audio.playPcmS16LEItem(item)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local playSeq = State.interruptSeq
|
local playSeq = State.interruptSeq
|
||||||
|
|
||||||
|
if not waitForNativeSpeakersEmpty(Peripherals.speakers, playSeq) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local source = item.audioData
|
local source = item.audioData
|
||||||
|
local streamObj = nil
|
||||||
|
|
||||||
if type(source) == "table" and source.read then
|
if type(source) == "table" and source.read then
|
||||||
|
streamObj = source
|
||||||
source = source.read
|
source = source.read
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1721,13 +1820,33 @@ function Audio.playPcmS16LEItem(item)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local prebufferChunks = math.max(0, tonumber(CONFIG.AUDIO_STREAM_PREROLL_CHUNKS) or 0)
|
||||||
|
if streamObj and prebufferChunks > 0 then
|
||||||
|
Utils.log("INFO", "Prebuffering live PCM16 stream: target chunks=" .. tostring(prebufferChunks))
|
||||||
|
while streamObj.count() < prebufferChunks and not streamObj.isFinished() do
|
||||||
|
if shouldStop(playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not waitForInterruptibleDelay(playSeq, 0.02) then
|
||||||
|
Audio.stop("current")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Utils.log("INFO", "Live PCM16 prebuffer ready: chunks=" .. tostring(streamObj.count()))
|
||||||
|
end
|
||||||
|
|
||||||
while true do
|
while true do
|
||||||
if shouldStop(playSeq) then break end
|
if shouldStop(playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
|
break
|
||||||
|
end
|
||||||
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 = decodePcmS16LEChunk(chunk)
|
local samples = decodePcmS16LEChunk(chunk)
|
||||||
if samples and not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then
|
if samples and not playChunkOnSpeakers(samples, CONFIG.AUDIO_VOLUME, playSeq) then
|
||||||
|
Audio.stop("current")
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user