mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 09:31:20 -04:00
audio
This commit is contained in:
@@ -7,6 +7,7 @@ const logger = require('../globals/logger').child('audioForwardService');
|
||||
const { loadConfig } = require('../helpers/configLoader');
|
||||
const roverManager = require('./roverManager');
|
||||
const { isAdmin } = require('./roleService');
|
||||
const { getAudioLevels, audioLevelsEvents } = require('./audioLevelsService');
|
||||
|
||||
const audioForwardEvents = new EventEmitter();
|
||||
const config = loadConfig();
|
||||
@@ -200,6 +201,7 @@ function buildSilenceWriterArgs() {
|
||||
}
|
||||
|
||||
function buildClipWriterArgs(filePath) {
|
||||
const forwardGain = Math.max(0, Number(getAudioLevels()?.forwardGain) || 1);
|
||||
return [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
@@ -209,7 +211,7 @@ function buildClipWriterArgs(filePath) {
|
||||
filePath,
|
||||
'-vn',
|
||||
'-af',
|
||||
'aresample=16000,volume=12dB',
|
||||
`aresample=16000,volume=${forwardGain}`,
|
||||
'-f',
|
||||
's16le',
|
||||
'-ac',
|
||||
@@ -394,6 +396,15 @@ roverManager.managerEvents.on('rover', ({ roverId, action } = {}) => {
|
||||
}
|
||||
});
|
||||
|
||||
audioLevelsEvents.on('change', () => {
|
||||
workers.forEach((worker, roverId) => {
|
||||
if (worker?.contentKind === 'clip') {
|
||||
// Restart clip writer so forward gain changes are immediately reflected.
|
||||
startClipWriter(roverId, testAudioPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('audio:testPlay', ({ roverId } = {}, cb = () => {}) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const EventEmitter = require('events');
|
||||
const io = require('../globals/io');
|
||||
const logger = require('../globals/logger').child('audioLevelsService');
|
||||
const { loadConfig } = require('../helpers/configLoader');
|
||||
const { isAdmin } = require('./roleService');
|
||||
const roverManager = require('./roverManager');
|
||||
const { issueCommand } = require('./commandService');
|
||||
|
||||
const audioLevelsEvents = new EventEmitter();
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', 'data');
|
||||
const STORE_PATH = path.join(DATA_DIR, 'audio-levels.json');
|
||||
const config = loadConfig();
|
||||
const configuredDefaults = config.audioLevels || {};
|
||||
|
||||
const DEFAULTS = {
|
||||
hornGain: clampGain(configuredDefaults.hornGain, 1),
|
||||
ttsGain: clampGain(configuredDefaults.ttsGain, 1),
|
||||
forwardGain: clampGain(configuredDefaults.forwardGain, 1),
|
||||
};
|
||||
|
||||
function clampGain(value, fallback = 1) {
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return fallback;
|
||||
return Math.max(0, Math.min(4, num));
|
||||
}
|
||||
|
||||
function normalizeStore(raw = {}) {
|
||||
return {
|
||||
hornGain: clampGain(raw.hornGain, DEFAULTS.hornGain),
|
||||
ttsGain: clampGain(raw.ttsGain, DEFAULTS.ttsGain),
|
||||
forwardGain: clampGain(raw.forwardGain, DEFAULTS.forwardGain),
|
||||
updatedAt: Number.isFinite(raw.updatedAt) ? raw.updatedAt : null,
|
||||
updatedBy: typeof raw.updatedBy === 'string' ? raw.updatedBy : null,
|
||||
};
|
||||
}
|
||||
|
||||
let state = null;
|
||||
|
||||
function loadState() {
|
||||
if (state) return state;
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
|
||||
state = normalizeStore(raw);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
logger.warn('Failed to load audio levels store', err.message);
|
||||
}
|
||||
state = normalizeStore({});
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function persistState(next) {
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const normalized = normalizeStore(next);
|
||||
const tempPath = `${STORE_PATH}.${process.pid}.${Date.now()}.tmp`;
|
||||
fs.writeFileSync(tempPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
|
||||
fs.renameSync(tempPath, STORE_PATH);
|
||||
state = normalized;
|
||||
return state;
|
||||
}
|
||||
|
||||
function getAudioLevels() {
|
||||
const current = loadState();
|
||||
return {
|
||||
hornGain: current.hornGain,
|
||||
ttsGain: current.ttsGain,
|
||||
forwardGain: current.forwardGain,
|
||||
updatedAt: current.updatedAt,
|
||||
updatedBy: current.updatedBy,
|
||||
};
|
||||
}
|
||||
|
||||
function emitChange(reason = 'update') {
|
||||
audioLevelsEvents.emit('change', {
|
||||
reason,
|
||||
levels: getAudioLevels(),
|
||||
});
|
||||
}
|
||||
|
||||
function pushLevelsToRover(roverId) {
|
||||
if (!roverId) return;
|
||||
const record = roverManager.rovers.get(roverId);
|
||||
if (!record || !record.ws) return;
|
||||
try {
|
||||
issueCommand(roverId, {
|
||||
type: 'audioLevels',
|
||||
audioLevels: getAudioLevels(),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn('Failed to push audio levels to rover', roverId, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function pushLevelsToAllRovers() {
|
||||
roverManager.rovers.forEach((record, roverId) => {
|
||||
if (record?.ws) {
|
||||
pushLevelsToRover(roverId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setAudioLevels(input = {}, actor = null) {
|
||||
const current = loadState();
|
||||
const next = {
|
||||
...current,
|
||||
hornGain: clampGain(input.hornGain, current.hornGain),
|
||||
ttsGain: clampGain(input.ttsGain, current.ttsGain),
|
||||
forwardGain: clampGain(input.forwardGain, current.forwardGain),
|
||||
updatedAt: Date.now(),
|
||||
updatedBy: actor,
|
||||
};
|
||||
persistState(next);
|
||||
pushLevelsToAllRovers();
|
||||
emitChange('set');
|
||||
return getAudioLevels();
|
||||
}
|
||||
|
||||
roverManager.managerEvents.on('rover', ({ roverId, action } = {}) => {
|
||||
if (action === 'upsert' && roverId) {
|
||||
pushLevelsToRover(roverId);
|
||||
}
|
||||
});
|
||||
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('audioLevels:get', (_, cb = () => {}) => {
|
||||
cb({ success: true, levels: getAudioLevels() });
|
||||
});
|
||||
|
||||
socket.on('audioLevels:set', (payload = {}, cb = () => {}) => {
|
||||
try {
|
||||
if (!isAdmin(socket)) {
|
||||
throw new Error('Not authorized');
|
||||
}
|
||||
const actor = socket?.data?.user?.username || null;
|
||||
const levels = setAudioLevels(payload || {}, actor);
|
||||
cb({ success: true, levels });
|
||||
} catch (err) {
|
||||
cb({ error: err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
loadState();
|
||||
|
||||
module.exports = {
|
||||
getAudioLevels,
|
||||
setAudioLevels,
|
||||
pushLevelsToRover,
|
||||
audioLevelsEvents,
|
||||
};
|
||||
@@ -67,6 +67,9 @@ io.on('connection', (socket) => {
|
||||
if (!type) {
|
||||
throw new Error('type required');
|
||||
}
|
||||
if (type === 'audioLevels') {
|
||||
throw new Error('audioLevels command is service-managed');
|
||||
}
|
||||
const payload = data ? { ...data } : {};
|
||||
const isRebootCommand = type === 'reboot';
|
||||
const isSongCommand = type === 'song' || (type === 'raw' && isSongRawPayload(payload));
|
||||
|
||||
@@ -23,6 +23,7 @@ const { getAdminReason } = require('./adminReasonService');
|
||||
const { subscribe } = require('./eventBus');
|
||||
const { getSocketIp, isLocalNetwork } = require('../helpers/ipResolver');
|
||||
const { getAudioForwardState, audioForwardEvents } = require('./audioForwardService');
|
||||
const { getAudioLevels, audioLevelsEvents } = require('./audioLevelsService');
|
||||
|
||||
const config = loadConfig();
|
||||
const discordInvite = config.discord?.invite || null;
|
||||
@@ -93,6 +94,7 @@ function buildSession(socket) {
|
||||
verification: getVerificationStateForSocket(socket),
|
||||
isVerified: Boolean(socket?.data?.isVerified),
|
||||
audioForward: getAudioForwardState(),
|
||||
audioLevels: getAudioLevels(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -264,6 +266,10 @@ audioForwardEvents.on('change', () => {
|
||||
syncAll();
|
||||
});
|
||||
|
||||
audioLevelsEvents.on('change', () => {
|
||||
syncAll();
|
||||
});
|
||||
|
||||
// sync all sockets 20 seconds
|
||||
setInterval(() => {
|
||||
logger.info('Periodic session sync for all clients');
|
||||
|
||||
Reference in New Issue
Block a user