Compare commits

..
2 Commits
Author SHA1 Message Date
Henry_Du b140aafa2d fix(audio): prebuffer live wav stream startup
Buffer a couple of live wav chunks before beginning playback so startup jitter is reduced when the stream first begins feeding speakers.
2026-08-01 11:11:22 +08:00
Henry_Du eb0adfc2e0 feat: handle line.txt and train arrival/departure events 2026-08-01 11:10:17 +08:00
2 changed files with 88 additions and 2 deletions
+60 -1
View File
@@ -838,6 +838,10 @@ function resolveManagedPath(inputPath) {
return { logicalPath: 'list.txt', fullPath: listFilePath }; return { logicalPath: 'list.txt', fullPath: listFilePath };
} }
if (normalized === 'Audio/general/line.txt' || normalized === 'uploads/audio/general/line.txt') {
return { logicalPath: 'Audio/general/line.txt', fullPath: path.join(audioGeneralDir, 'line.txt') };
}
const prefixes = [ const prefixes = [
{ prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' }, { prefix: 'Audio/music/', baseDir: audioMusicDir, logicalPrefix: 'Audio/music/' },
{ prefix: 'Audio/voice/', baseDir: audioVoiceDir, logicalPrefix: 'Audio/voice/' }, { prefix: 'Audio/voice/', baseDir: audioVoiceDir, logicalPrefix: 'Audio/voice/' },
@@ -923,7 +927,7 @@ function getManagedFiles() {
files.push({ files.push({
name: file, name: file,
path: `Audio/general/${file}`, path: `Audio/general/${file}`,
type: 'beep', type: file === 'line.txt' ? 'config' : 'beep',
size: stats.size, size: stats.size,
mtime: stats.mtimeMs mtime: stats.mtimeMs
}); });
@@ -1587,7 +1591,11 @@ function handleMessage(ws, data) {
handleStationRegister(ws, data); handleStationRegister(ws, data);
break; break;
case 'train_arrival': case 'train_arrival':
handleTrainArrival(ws, data);
break;
case 'train_departure': case 'train_departure':
handleTrainDeparture(ws, data);
break;
case 'play_sequence': case 'play_sequence':
break; break;
case 'request_station_time': case 'request_station_time':
@@ -1752,6 +1760,57 @@ function findStationByFlexibleName(targetName) {
return null; return null;
} }
function handleTrainArrival(ws, data) {
const trainId = data.trainId;
const stationName = getCorrectStationName(data.stationName);
const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
const trainData = {
trainId,
stationName,
status: 'arrived',
lastUpdated: Date.now()
};
if (existingIndex >= 0) {
activeTrains[existingIndex] = trainData;
} else {
activeTrains.push(trainData);
}
log('INFO', `Train ${trainId} arrived at ${stationName}`);
saveData();
broadcastUpdate();
ws.send(JSON.stringify({
type: 'train_arrival_ack',
success: true,
message: 'Train arrival processed successfully',
trainId
}));
}
function handleTrainDeparture(ws, data) {
const trainId = data.trainId;
const stationName = getCorrectStationName(data.stationName);
const existingIndex = activeTrains.findIndex(t => t.trainId === trainId);
if (existingIndex >= 0) {
activeTrains.splice(existingIndex, 1);
}
log('INFO', `Train ${trainId} departed from ${stationName}`);
saveData();
broadcastUpdate();
ws.send(JSON.stringify({
type: 'train_departure_ack',
success: true,
message: 'Train departure processed successfully',
trainId
}));
}
function handleStationTimeRequest(ws, data) { function handleStationTimeRequest(ws, data) {
// Simple logic: default to 60s or lookup schedule // Simple logic: default to 60s or lookup schedule
const stationTime = 60; const stationTime = 60;
+28 -1
View File
@@ -2,7 +2,7 @@
-- ============================================================================ -- ============================================================================
-- 1. Configuration & Constants -- 1. Configuration & Constants
-- ============================================================================ -- ============================================================================
local APP_VERSION_TEXT = "v1.0.13" local APP_VERSION_TEXT = "v1.0.14"
local CONFIG = { local CONFIG = {
CLOUD_URL = "ws://stabadmin.fse-media.group", CLOUD_URL = "ws://stabadmin.fse-media.group",
@@ -14,6 +14,7 @@ local CONFIG = {
DISK_AUDIO_STATION = "disk/Audio/Station/", DISK_AUDIO_STATION = "disk/Audio/Station/",
STATION_IDENTITY_FILE = "station_identity.cfg", STATION_IDENTITY_FILE = "station_identity.cfg",
AUDIO_VOLUME = 3, AUDIO_VOLUME = 3,
AUDIO_STREAM_PREROLL_CHUNKS = 2,
DEBUG = true DEBUG = true
} }
@@ -740,6 +741,14 @@ function Audio.createStream()
return table.remove(chunks, 1) return table.remove(chunks, 1)
end end
stream.count = function()
return #chunks
end
stream.isFinished = function()
return finished
end
return stream return stream
end end
@@ -1393,8 +1402,10 @@ function Audio.playWavStreamItem(item)
local playSeq = State.interruptSeq local playSeq = State.interruptSeq
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
@@ -1407,6 +1418,22 @@ function Audio.playWavStreamItem(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 WAV 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 WAV prebuffer ready: chunks=" .. tostring(streamObj.count()))
end
while true do while true do
if shouldStop(playSeq) then if shouldStop(playSeq) then
Audio.stop("current") Audio.stop("current")