diff --git a/client/src/Admin.jsx b/client/src/Admin.jsx index 82f275b..49c8c7a 100644 --- a/client/src/Admin.jsx +++ b/client/src/Admin.jsx @@ -1,6 +1,5 @@ import { useState, useEffect, useRef } from 'react'; import './Admin.css'; -import AudioRoutingMatrix from './components/AudioRoutingMatrix'; const API_URL = import.meta.env.VITE_API_URL || '/api'; @@ -409,9 +408,6 @@ function Admin() { -

- Le routing audio se configure dans l'onglet "Audio" via la matrice de routing. -

- {currentDevice && currentDevice.inputDeviceId && (
diff --git a/client/src/components/AudioRoutingMatrix.css b/client/src/components/AudioRoutingMatrix.css deleted file mode 100644 index aa844ce..0000000 --- a/client/src/components/AudioRoutingMatrix.css +++ /dev/null @@ -1,245 +0,0 @@ -.routing-matrix-container { - background: var(--color-surface); - border: 1px solid var(--color-border); - border-radius: 12px; - padding: var(--spacing-xl); - margin-top: var(--spacing-lg); -} - -.routing-actions { - margin-top: var(--spacing-xl); - display: flex; - justify-content: flex-start; - gap: var(--spacing-sm); -} - -.routing-matrix-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: var(--spacing-xl); -} - -.routing-matrix-header h3 { - margin: 0; - font-size: 1.25rem; - font-weight: 600; -} - -.ws-status { - font-size: 0.8rem; - font-weight: 600; - padding: 4px 10px; - border-radius: 12px; - white-space: nowrap; -} - -.ws-status.connected { - color: #44ff44; - background: rgba(68, 255, 68, 0.1); -} - -.ws-status.disconnected { - color: #888; - background: rgba(136, 136, 136, 0.1); -} - -.routing-section { - margin-bottom: var(--spacing-xl); -} - -.routing-section:last-child { - margin-bottom: 0; -} - -.routing-section h4 { - margin: 0 0 var(--spacing-sm) 0; - font-size: 1rem; - font-weight: 600; - color: var(--color-text); -} - -.routing-description { - margin: 0 0 var(--spacing-lg) 0; - color: var(--color-text-secondary); - font-size: 0.9rem; -} - -.routing-matrix { - display: inline-grid; - gap: 2px; - background: var(--color-border); - border: 1px solid var(--color-border); - border-radius: 8px; - overflow-x: auto; - max-width: 100%; -} - -.matrix-corner { - background: var(--color-surface-hover); - min-height: 50px; -} - -.matrix-header-cell { - background: var(--color-surface-hover); - padding: var(--spacing-sm); - font-size: 0.85rem; - font-weight: 600; - color: var(--color-text-secondary); - text-align: center; - display: flex; - align-items: center; - justify-content: center; - min-height: 50px; - word-break: break-word; - hyphens: auto; -} - -.matrix-label-cell { - background: var(--color-surface-hover); - padding: var(--spacing-sm); - font-size: 0.85rem; - font-weight: 600; - color: var(--color-text-secondary); - display: flex; - align-items: center; - min-width: 120px; - word-break: break-word; -} - -.label-content { - display: flex; - flex-direction: column; - gap: 4px; - width: 100%; -} - -.label-text { - flex: 1; -} - -.header-content { - display: flex; - flex-direction: column; - gap: 4px; - width: 100%; - align-items: center; -} - -.header-text { - text-align: center; -} - -.matrix-cell { - background: var(--color-bg); - padding: var(--spacing-sm); - min-height: 60px; - min-width: 80px; - transition: all 0.2s; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: var(--spacing-xs); - position: relative; -} - -.cell-checkbox { - width: 100%; - min-height: 30px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; -} - -.matrix-cell:hover { - background: var(--color-surface-hover); - border: 1px solid var(--color-primary); -} - -.matrix-cell.active { - background: var(--color-primary); - color: white; -} - -.matrix-cell.active:hover { - background: var(--color-primary-hover); -} - -.checkmark { - font-size: 1.2rem; - font-weight: bold; -} - -.gain-select { - width: 100%; - padding: 4px 8px; - font-size: 0.75rem; - background: rgba(59, 130, 246, 0.2); - color: #ffffff; - border: 1px solid rgba(255, 255, 255, 1); - border-radius: 4px; - cursor: pointer; - font-weight: 600; - text-align: center; -} - -.gain-select:focus { - outline: none; - background: rgba(59, 130, 246, 0.3); - border-color: rgba(255, 255, 255, 1); - box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2); -} - -@media (max-width: 1024px) { - .matrix-header-cell, - .matrix-label-cell { - font-size: 0.75rem; - padding: var(--spacing-xs); - min-width: 80px; - } - - .matrix-cell { - min-width: 70px; - min-height: 50px; - padding: var(--spacing-sm); - } - - .gain-select { - font-size: 0.7rem; - padding: 3px 6px; - } -} - -@media (max-width: 768px) { - .routing-matrix-container { - padding: var(--spacing-md); - } - - .routing-matrix-header { - flex-direction: column; - gap: var(--spacing-md); - align-items: stretch; - } - - .matrix-header-cell, - .matrix-label-cell { - font-size: 0.7rem; - min-width: 60px; - } - - .matrix-cell { - min-width: 65px; - min-height: 45px; - } - - .checkmark { - font-size: 1rem; - } - - .gain-select { - font-size: 0.65rem; - padding: 2px 4px; - } -} diff --git a/client/src/components/AudioRoutingMatrix.jsx b/client/src/components/AudioRoutingMatrix.jsx deleted file mode 100644 index 5662f1c..0000000 --- a/client/src/components/AudioRoutingMatrix.jsx +++ /dev/null @@ -1,349 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import './AudioRoutingMatrix.css'; -import VUMeter from './VUMeter.jsx'; -import { useAudioLevels } from '../hooks/useAudioLevels.js'; - -const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'; - -function AudioRoutingMatrix({ groups, channelNames }) { - const { levels, connected: wsConnected } = useAudioLevels(); - const [routing, setRouting] = useState({ inputToGroup: {}, groupToOutput: {}, gains: {} }); - const [loading, setLoading] = useState(true); - const [showOnlyNamedChannels, setShowOnlyNamedChannels] = useState(false); - const [audioDevice, setAudioDevice] = useState({ inputChannels: 8, outputChannels: 8 }); - - useEffect(() => { - loadRouting(); - loadAudioDevice(); - }, []); - - const loadRouting = async () => { - try { - const res = await fetch(`${API_URL}/admin/audio/routing`); - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - const data = await res.json(); - setRouting(data.routing || { inputToGroup: {}, groupToOutput: {}, gains: {} }); - } catch (error) { - console.error('Erreur chargement routing:', error); - } finally { - setLoading(false); - } - }; - - const loadAudioDevice = async () => { - try { - const res = await fetch(`${API_URL}/admin/audio/device`); - if (res.ok) { - const data = await res.json(); - setAudioDevice({ - inputChannels: data.device?.inputChannels || 8, - outputChannels: data.device?.outputChannels || 8 - }); - } - } catch (error) { - console.error('Erreur chargement audio device:', error); - } - }; - - const saveRouting = async () => { - try { - const res = await fetch(`${API_URL}/admin/audio/routing`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(routing) - }); - - if (res.ok) { - alert('Configuration de routing sauvegardée!'); - } else { - const errorText = await res.text(); - console.error('Erreur serveur:', errorText); - alert(`Erreur: ${res.status} - ${errorText}`); - } - } catch (error) { - console.error('Erreur sauvegarde routing:', error); - alert('Erreur lors de la sauvegarde'); - } - }; - - const toggleInputToGroup = (inputId, groupId) => { - setRouting(prev => { - const inputToGroup = { ...prev.inputToGroup }; - if (!inputToGroup[inputId]) { - inputToGroup[inputId] = []; - } - - const groupArray = [...inputToGroup[inputId]]; - const index = groupArray.indexOf(groupId); - - if (index > -1) { - groupArray.splice(index, 1); - } else { - groupArray.push(groupId); - } - - inputToGroup[inputId] = groupArray; - - return { ...prev, inputToGroup }; - }); - }; - - const toggleGroupToOutput = (groupId, outputId) => { - setRouting(prev => { - const groupToOutput = { ...prev.groupToOutput }; - if (!groupToOutput[groupId]) { - groupToOutput[groupId] = []; - } - - const outputArray = [...groupToOutput[groupId]]; - const index = outputArray.indexOf(outputId); - - if (index > -1) { - outputArray.splice(index, 1); - } else { - outputArray.push(outputId); - } - - groupToOutput[groupId] = outputArray; - - return { ...prev, groupToOutput }; - }); - }; - - const isInputRoutedToGroup = (inputId, groupId) => { - return routing.inputToGroup[inputId]?.includes(groupId) || false; - }; - - const isGroupRoutedToOutput = (groupId, outputId) => { - return routing.groupToOutput[groupId]?.includes(outputId) || false; - }; - - const getGainForInputToGroup = (inputId, groupId) => { - const key = `in_${inputId}_${groupId}`; - return routing.gains?.[key] || 0.0; - }; - - const getGainForGroupToOutput = (groupId, outputId) => { - const key = `${groupId}_out_${outputId}`; - return routing.gains?.[key] || 0.0; - }; - - const setGainForInputToGroup = (inputId, groupId, gainDb) => { - setRouting(prev => { - const gains = { ...prev.gains }; - const key = `in_${inputId}_${groupId}`; - gains[key] = parseFloat(gainDb); - return { ...prev, gains }; - }); - }; - - const setGainForGroupToOutput = (groupId, outputId, gainDb) => { - setRouting(prev => { - const gains = { ...prev.gains }; - const key = `${groupId}_out_${outputId}`; - gains[key] = parseFloat(gainDb); - return { ...prev, gains }; - }); - }; - - const formatGain = (gainDb) => { - if (gainDb === 0) return '0dB'; - return gainDb > 0 ? `+${gainDb}dB` : `${gainDb}dB`; - }; - - const getChannelName = (type, id) => { - const name = channelNames?.[type]?.[id]; - return name || `${type === 'inputs' ? 'Input' : 'Output'} ${id}`; - }; - - const hasCustomName = (type, id) => { - return channelNames?.[type]?.[id] !== undefined; - }; - - const getVisibleInputChannels = () => { - const allInputs = Array.from({length: audioDevice.inputChannels}, (_, i) => i); - if (showOnlyNamedChannels) { - return allInputs.filter(i => hasCustomName('inputs', i)); - } - return allInputs; - }; - - const getVisibleOutputChannels = () => { - const allOutputs = Array.from({length: audioDevice.outputChannels}, (_, i) => i); - if (showOnlyNamedChannels) { - return allOutputs.filter(i => hasCustomName('outputs', i)); - } - return allOutputs; - }; - - if (loading) { - return
Chargement...
; - } - - return ( -
-
-
-

Matrice de routing audio

- - {wsConnected ? '● Live' : '○ Offline'} - -
- -
- -
-

Inputs vers Groupes

-

- Sélectionnez quels inputs audio alimentent chaque groupe -

- -
-
- - {groups.map(group => ( -
- {group.name} -
- ))} - - {getVisibleInputChannels().map(i => ( - -
-
- {getChannelName('inputs', i)} - {wsConnected && levels.inputs[i] && ( - - )} -
-
- - {groups.map(group => { - const isRouted = isInputRoutedToGroup(String(i), group.id); - const gain = getGainForInputToGroup(String(i), group.id); - - return ( -
-
toggleInputToGroup(String(i), group.id)} - > - {isRouted && } -
- {isRouted && ( - - )} -
- ); - })} -
- ))} -
-
- -
-

Groupes vers Outputs

-

- Sélectionnez vers quels outputs chaque groupe envoie son audio -

- -
-
- - {getVisibleOutputChannels().map(i => ( -
-
- {getChannelName('outputs', i)} - {wsConnected && levels.outputs[i] && ( - - )} -
-
- ))} - - {groups.map(group => ( - -
-
- {group.name} - {wsConnected && levels.groups[group.id] && ( - - )} -
-
- - {getVisibleOutputChannels().map(i => { - const isRouted = isGroupRoutedToOutput(group.id, String(i)); - const gain = getGainForGroupToOutput(group.id, String(i)); - - return ( -
-
toggleGroupToOutput(group.id, String(i))} - > - {isRouted && } -
- {isRouted && ( - - )} -
- ); - })} -
- ))} -
-
- -
- -
-
- ); -} - -export default AudioRoutingMatrix; diff --git a/server/api/admin.js b/server/api/admin.js index 8f65f3e..b71a8c7 100644 --- a/server/api/admin.js +++ b/server/api/admin.js @@ -529,66 +529,6 @@ router.put('/audio/channels/names', (req, res) => { } }); -/** - * GET /admin/audio/routing - * Récupère la configuration de routing actuelle - * Format: { inputToGroup: { "0": ["production"], "1": ["technique"] }, groupToOutput: { "production": ["0", "1"] } } - */ -router.get('/audio/routing', (req, res) => { - try { - const config = configManager.get(); - const routing = config.audio?.routing || { inputToGroup: {}, groupToOutput: {}, gains: {} }; - - res.json({ - routing - }); - } catch (error) { - console.error('Erreur GET /admin/audio/routing:', error); - res.status(500).json({ error: 'Failed to load routing' }); - } -}); - -/** - * POST /audio/routing - * Sauvegarde la configuration de routing - * Body: { inputToGroup: {...}, groupToOutput: {...}, gains: {...} } - */ -router.post('/audio/routing', (req, res) => { - try { - const { inputToGroup, groupToOutput, gains } = req.body; - - const config = configManager.get(); - - if (!config.audio.routing) { - config.audio.routing = { inputToGroup: {}, groupToOutput: {}, gains: {} }; - } - - if (inputToGroup !== undefined) { - config.audio.routing.inputToGroup = inputToGroup; - } - - if (groupToOutput !== undefined) { - config.audio.routing.groupToOutput = groupToOutput; - } - - if (gains !== undefined) { - config.audio.routing.gains = gains; - } - - configManager.save(config); - - addLog('info', 'Audio routing updated'); - - res.json({ - message: 'Audio routing updated', - routing: config.audio.routing - }); - - } catch (error) { - console.error('Erreur POST /admin/audio/routing:', error); - res.status(500).json({ error: 'Failed to update routing' }); - } -}); /** * POST /admin/audio/device diff --git a/server/bridge/GroupAudioRouter.js b/server/bridge/GroupAudioRouter.js deleted file mode 100644 index 2b2e158..0000000 --- a/server/bridge/GroupAudioRouter.js +++ /dev/null @@ -1,417 +0,0 @@ -/** - * GroupAudioRouter.js - * Gestion du routing audio multi-canaux entre entrées physiques, groupes LiveKit et sorties physiques - * - * Architecture : - * - Mix de plusieurs canaux physiques vers un groupe (avec gains individuels) - * - Distribution d'un groupe vers plusieurs canaux physiques (avec gains individuels) - * - Support canaux partagés (mixage additif) - * - Gestion gains par route (-120dB à +6dB) - */ - -import { EventEmitter } from 'events'; -import { getLogger } from '../utils/Logger.js'; - -const logger = getLogger('Routing'); - -/** - * Représente une route audio avec gain - */ -class AudioRoute { - constructor(source, destination, gain = 0.0) { - this.source = source; // Numéro de canal ou nom de groupe - this.destination = destination; // Nom de groupe ou numéro de canal - this.gain = gain; // Gain en dB (-120 à +6) - this.linearGain = this._dbToLinear(gain); - } - - /** - * Met à jour le gain en dB - */ - setGain(gainDb) { - this.gain = Math.max(-120, Math.min(6, gainDb)); - this.linearGain = this._dbToLinear(this.gain); - } - - /** - * Convertit dB en gain linéaire - */ - _dbToLinear(db) { - if (db <= -120) return 0.0; - return Math.pow(10, db / 20); - } -} - -/** - * Router audio principal - */ -export class GroupAudioRouter extends EventEmitter { - constructor(config = {}) { - super(); - - this.config = { - sampleRate: config.sampleRate || 48000, - frameSize: config.frameSize || 960, // 20ms à 48kHz - maxInputChannels: config.maxInputChannels || 32, - maxOutputChannels: config.maxOutputChannels || 32, - groups: config.groups || [] - }; - - // Routes : input -> group - this.inputToGroupRoutes = new Map(); // Map - // Routes : group -> output - this.groupToOutputRoutes = new Map(); // Map - - // Buffers audio - this.inputBuffers = new Map(); // Map - this.groupBuffers = new Map(); // Map - this.outputBuffers = new Map(); // Map - - // Statistiques - this.stats = { - framesProcessed: 0, - clippingEvents: 0, - routesActive: 0 - }; - } - - /** - * Configure le routing depuis la config YAML - */ - configure(routingConfig) { - logger.info('Configuration du routing audio...'); - logger.debug(' Groupes disponibles:', this.config.groups.map(g => `${g.name || g} (id: ${g.id || g})`).join(', ')); - logger.debug(' inputToGroup:', JSON.stringify(routingConfig.inputToGroup || {})); - logger.debug(' groupToOutput:', JSON.stringify(routingConfig.groupToOutput || {})); - - // Réinitialise les routes - this.inputToGroupRoutes.clear(); - this.groupToOutputRoutes.clear(); - - // Configure input -> group - if (routingConfig.inputToGroup) { - Object.entries(routingConfig.inputToGroup).forEach(([channelId, groups]) => { - const channel = parseInt(channelId); - - groups.forEach(groupName => { - this.addInputToGroupRoute(channel, groupName, this._getGain(routingConfig.gains, `in_${channel}_${groupName}`)); - }); - }); - } - - // Configure group -> output - if (routingConfig.groupToOutput) { - Object.entries(routingConfig.groupToOutput).forEach(([groupName, channels]) => { - channels.forEach(channelId => { - const channel = parseInt(channelId); - this.addGroupToOutputRoute(groupName, channel, this._getGain(routingConfig.gains, `${groupName}_out_${channel}`)); - }); - }); - } - - this._updateStatsActiveRoutes(); - logger.success(`Routing configuré : ${this.stats.routesActive} routes actives`); - this.emit('configured', this.stats); - } - - /** - * Récupère le gain depuis la config - */ - _getGain(gainsConfig, routeKey) { - return gainsConfig && gainsConfig[routeKey] ? gainsConfig[routeKey] : 0.0; - } - - /** - * Ajoute une route input -> group - */ - addInputToGroupRoute(inputChannel, groupName, gainDb = 0.0) { - const key = `in_${inputChannel}`; - - if (!this.inputToGroupRoutes.has(key)) { - this.inputToGroupRoutes.set(key, []); - } - - const route = new AudioRoute(inputChannel, groupName, gainDb); - this.inputToGroupRoutes.get(key).push(route); - - logger.info(`Input ${inputChannel} → Group "${groupName}" (${gainDb}dB)`); - this._updateStatsActiveRoutes(); - } - - /** - * Ajoute une route group -> output - */ - addGroupToOutputRoute(groupName, outputChannel, gainDb = 0.0) { - const key = groupName; - - if (!this.groupToOutputRoutes.has(key)) { - this.groupToOutputRoutes.set(key, []); - } - - const route = new AudioRoute(groupName, outputChannel, gainDb); - this.groupToOutputRoutes.get(key).push(route); - - logger.info(`Group "${groupName}" → Output ${outputChannel} (${gainDb}dB)`); - this._updateStatsActiveRoutes(); - } - - /** - * Supprime toutes les routes d'une entrée - */ - removeInputRoutes(inputChannel) { - this.inputToGroupRoutes.delete(`in_${inputChannel}`); - this._updateStatsActiveRoutes(); - } - - /** - * Supprime toutes les routes d'un groupe vers les sorties - */ - removeGroupOutputRoutes(groupName) { - this.groupToOutputRoutes.delete(groupName); - this._updateStatsActiveRoutes(); - } - - /** - * Met à jour le gain d'une route spécifique - */ - setRouteGain(source, destination, gainDb) { - // Cherche dans input -> group - const inputKey = typeof source === 'number' ? `in_${source}` : null; - if (inputKey && this.inputToGroupRoutes.has(inputKey)) { - const routes = this.inputToGroupRoutes.get(inputKey); - const route = routes.find(r => r.destination === destination); - if (route) { - route.setGain(gainDb); - console.log(`Gain modifié : Input ${source} -> Group "${destination}" = ${gainDb}dB`); - return true; - } - } - - // Cherche dans group -> output - if (typeof source === 'string' && this.groupToOutputRoutes.has(source)) { - const routes = this.groupToOutputRoutes.get(source); - const route = routes.find(r => r.destination === destination); - if (route) { - route.setGain(gainDb); - console.log(`Gain modifié : Group "${source}" -> Output ${destination} = ${gainDb}dB`); - return true; - } - } - - return false; - } - - /** - * ÉTAPE 1 : Traite les entrées audio physiques vers les buffers de groupe - * Mixe plusieurs canaux d'entrée vers chaque groupe (avec gains individuels) - * - * @param {Map} inputChannelsData - Données PCM par canal d'entrée - */ - processInputsToGroups(inputChannelsData) { - // Réinitialise les buffers de groupe - this.groupBuffers.clear(); - this.config.groups.forEach(group => { - // Utiliser l'ID (slugifié) plutôt que le nom pour correspondre au routing - const groupId = group.id || group.name || group; - this.groupBuffers.set(groupId, new Float32Array(this.config.frameSize)); - }); - - // Compter le nombre de sources par groupe pour normalisation - const groupSourceCount = new Map(); - inputChannelsData.forEach((_, channelId) => { - const key = `in_${channelId}`; - const routes = this.inputToGroupRoutes.get(key); - if (routes) { - routes.forEach(route => { - groupSourceCount.set( - route.destination, - (groupSourceCount.get(route.destination) || 0) + 1 - ); - }); - } - }); - - // Pour chaque canal d'entrée - inputChannelsData.forEach((pcmData, channelId) => { - const key = `in_${channelId}`; - const routes = this.inputToGroupRoutes.get(key); - - if (!routes || routes.length === 0) return; - - // Stocke le buffer d'entrée - this.inputBuffers.set(channelId, pcmData); - - // Applique chaque route (mixage additif vers les groupes) - routes.forEach(route => { - const groupBuffer = this.groupBuffers.get(route.destination); - if (!groupBuffer) { - logger.warn(`Buffer groupe "${route.destination}" introuvable pour routing depuis Input ${channelId}`); - return; - } - - // Mixage avec gain + atténuation par nombre de sources - const sourceCount = groupSourceCount.get(route.destination) || 1; - const mixGain = route.linearGain / sourceCount; - - for (let i = 0; i < pcmData.length && i < groupBuffer.length; i++) { - groupBuffer[i] += pcmData[i] * mixGain; - } - }); - }); - - // Normalisation anti-clipping (soft limiter simple) - this.groupBuffers.forEach((buffer, groupName) => { - for (let i = 0; i < buffer.length; i++) { - if (Math.abs(buffer[i]) > 1.0) { - this.stats.clippingEvents++; - if (this.stats.clippingEvents % 1000 === 1) { - logger.warn(`Clipping détecté sur groupe "${groupName}" (${this.stats.clippingEvents} événements)`); - } - buffer[i] = Math.sign(buffer[i]) * 1.0; // Hard clipping - } - } - }); - - this.stats.framesProcessed++; - return this.groupBuffers; - } - - /** - * ÉTAPE 2 : Traite les buffers de groupe vers les sorties audio physiques - * Distribue chaque groupe vers plusieurs canaux de sortie (avec gains individuels) - * Support du mixage additif si plusieurs groupes vont vers la même sortie - * - * @param {Map} groupBuffersData - Données PCM par groupe (depuis LiveKit) - * @returns {Map} Buffers de sortie par canal physique - */ - processGroupsToOutputs(groupBuffersData) { - // Réinitialise les buffers de sortie - this.outputBuffers.clear(); - - // Pour chaque groupe - groupBuffersData.forEach((pcmData, groupName) => { - const routes = this.groupToOutputRoutes.get(groupName); - - if (!routes || routes.length === 0) { - return; - } - - // Applique chaque route vers les sorties - routes.forEach(route => { - const outputChannel = route.destination; - - // Crée le buffer de sortie si nécessaire - if (!this.outputBuffers.has(outputChannel)) { - this.outputBuffers.set(outputChannel, new Float32Array(this.config.frameSize)); - } - - const outputBuffer = this.outputBuffers.get(outputChannel); - - // Mixage avec gain (additif si canal partagé) - for (let i = 0; i < pcmData.length && i < outputBuffer.length; i++) { - outputBuffer[i] += pcmData[i] * route.linearGain; - } - }); - }); - - // Normalisation anti-clipping sur les sorties - this.outputBuffers.forEach((buffer, channelId) => { - for (let i = 0; i < buffer.length; i++) { - if (Math.abs(buffer[i]) > 1.0) { - this.stats.clippingEvents++; - buffer[i] = Math.sign(buffer[i]) * 1.0; // Hard clipping - } - } - }); - - return this.outputBuffers; - } - - /** - * Récupère le buffer d'un groupe spécifique - */ - getGroupBuffer(groupName) { - return this.groupBuffers.get(groupName) || null; - } - - /** - * Récupère le buffer d'une sortie spécifique - */ - getOutputBuffer(channelId) { - return this.outputBuffers.get(channelId) || null; - } - - /** - * Récupère toutes les routes configurées - */ - getRoutingConfig() { - const inputToGroup = {}; - const groupToOutput = {}; - const gains = {}; - - // Input -> Group - this.inputToGroupRoutes.forEach((routes, key) => { - const inputChannel = key.replace('in_', ''); - inputToGroup[inputChannel] = routes.map(r => r.destination); - - routes.forEach(route => { - if (route.gain !== 0.0) { - gains[`in_${inputChannel}_${route.destination}`] = route.gain; - } - }); - }); - - // Group -> Output - this.groupToOutputRoutes.forEach((routes, groupName) => { - groupToOutput[groupName] = routes.map(r => r.destination); - - routes.forEach(route => { - if (route.gain !== 0.0) { - gains[`${groupName}_out_${route.destination}`] = route.gain; - } - }); - }); - - return { inputToGroup, groupToOutput, gains }; - } - - /** - * Récupère les statistiques - */ - getStats() { - return { - framesProcessed: this.stats.framesProcessed, - clippingEvents: this.stats.clippingEvents, - routesActive: this.stats.routesActive, - inputToGroupRoutes: this.inputToGroupRoutes.size, - groupToOutputRoutes: this.groupToOutputRoutes.size, - activeGroups: this.groupBuffers.size, - activeOutputs: this.outputBuffers.size - }; - } - - /** - * Met à jour le compteur de routes actives - */ - _updateStatsActiveRoutes() { - let count = 0; - this.inputToGroupRoutes.forEach(routes => count += routes.length); - this.groupToOutputRoutes.forEach(routes => count += routes.length); - this.stats.routesActive = count; - } - - /** - * Détruit le router et libère les ressources - */ - destroy() { - this.inputToGroupRoutes.clear(); - this.groupToOutputRoutes.clear(); - this.inputBuffers.clear(); - this.groupBuffers.clear(); - this.outputBuffers.clear(); - this.removeAllListeners(); - logger.info('GroupAudioRouter détruit'); - } -} - -export default GroupAudioRouter;