jam-cloud/jam-ui/src/store/features/mixersSlice.js

382 lines
12 KiB
JavaScript

import { createSlice, createSelector } from '@reduxjs/toolkit';
const initialState = {
// Core mixer collections
chatMixer: null,
broadcastMixer: null,
recordingMixer: null,
// Mixer arrays by type (populated by groupMixersByType)
recordingTrackMixers: [],
backingTrackMixers: [],
jamTrackMixers: [],
metronomeTrackMixers: [],
adhocTrackMixers: [],
// Master and personal mixer arrays (source data from jamClient)
masterMixers: [],
personalMixers: [],
// Lookup tables (computed by organizeMixers reducer)
allMixers: {}, // Format: { 'M123': mixer, 'P456': mixer }
mixersByResourceId: {}, // Format: { 'rid': { master: mixer, personal: mixer } }
mixersByTrackId: {}, // Format: { 'trackId': { master: mixer, personal: mixer } }
// Simulated/derived mixers for category controls
simulatedMusicCategoryMixers: {
PERSONAL: null,
MASTER: null
},
simulatedChatCategoryMixers: {
PERSONAL: null,
MASTER: null
},
// Metronome state
metronome: null, // Current metronome mixer object
metronomeSettings: {
tempo: 120,
sound: "Beep",
cricket: false
},
// Media summary (open/closed states for media types)
// This tracks what media is currently open in the session
mediaSummary: {
mediaOpen: false,
backingTrackOpen: false,
jamTrackOpen: false,
recordingOpen: false,
metronomeOpen: false,
isOpener: false,
userNeedsMediaControls: false,
jamTrack: null
},
// Peer state (for managing missing audio users)
noAudioUsers: {},
clientsWithAudioOverride: {},
missingMixerPeers: {},
checkingMissingPeers: {},
// Ready state (indicates mixers have been organized and are ready to use)
isReady: false,
// Loading/errors
loading: false,
error: null
};
export const mixersSlice = createSlice({
name: 'mixers',
initialState,
reducers: {
// Initialize from jamClient.SessionGetAllControlState
setMasterMixers: (state, action) => {
state.masterMixers = action.payload;
},
setPersonalMixers: (state, action) => {
state.personalMixers = action.payload;
},
// Organize mixers - builds lookup tables (matches useMixerHelper organizeMixers logic)
// This should be called after master/personal mixers are set
organizeMixers: (state) => {
const newAllMixers = {};
const newMixersByResourceId = {};
const newMixersByTrackId = {};
// Process master mixers
for (const masterMixer of state.masterMixers) {
newAllMixers['M' + masterMixer.id] = masterMixer;
const mixerPair = {};
newMixersByResourceId[masterMixer.rid] = mixerPair;
newMixersByTrackId[masterMixer.id] = mixerPair;
mixerPair.master = masterMixer;
}
// Process personal mixers
for (const personalMixer of state.personalMixers) {
newAllMixers['P' + personalMixer.id] = personalMixer;
let mixerPair = newMixersByResourceId[personalMixer.rid];
if (!mixerPair) {
// Create new pair if no master exists (e.g., MonitorGroup)
mixerPair = {};
newMixersByResourceId[personalMixer.rid] = mixerPair;
}
newMixersByTrackId[personalMixer.id] = mixerPair;
mixerPair.personal = personalMixer;
}
// Update state
state.allMixers = { ...state.allMixers, ...newAllMixers };
state.mixersByResourceId = { ...state.mixersByResourceId, ...newMixersByResourceId };
state.mixersByTrackId = { ...state.mixersByTrackId, ...newMixersByTrackId };
state.isReady = true;
},
// Group mixers by type - categorizes mixers into recording, backing, jam, metronome, adhoc
// This complex logic will be implemented in useMixerHelper Redux version
// For now, just accept the categorized arrays
setRecordingTrackMixers: (state, action) => {
state.recordingTrackMixers = action.payload;
},
setBackingTrackMixers: (state, action) => {
state.backingTrackMixers = action.payload;
},
setJamTrackMixers: (state, action) => {
state.jamTrackMixers = action.payload;
},
setMetronomeTrackMixers: (state, action) => {
state.metronomeTrackMixers = action.payload;
},
setAdhocTrackMixers: (state, action) => {
state.adhocTrackMixers = action.payload;
},
// Update individual mixer (for real-time updates from jamClient)
updateMixer: (state, action) => {
const { mixerId, mode, updates } = action.payload;
const key = (mode ? 'M' : 'P') + mixerId;
if (state.allMixers[key]) {
state.allMixers[key] = { ...state.allMixers[key], ...updates };
// Also update in master/personal arrays
if (mode) {
const index = state.masterMixers.findIndex(m => m.id === mixerId);
if (index !== -1) {
state.masterMixers[index] = { ...state.masterMixers[index], ...updates };
}
} else {
const index = state.personalMixers.findIndex(m => m.id === mixerId);
if (index !== -1) {
state.personalMixers[index] = { ...state.personalMixers[index], ...updates };
}
}
}
},
// Metronome
setMetronome: (state, action) => {
state.metronome = action.payload;
state.mediaSummary.metronomeOpen = !!action.payload;
},
setMetronomeSettings: (state, action) => {
state.metronomeSettings = { ...state.metronomeSettings, ...action.payload };
},
// Media summary updates
updateMediaSummary: (state, action) => {
state.mediaSummary = { ...state.mediaSummary, ...action.payload };
},
setMediaSummary: (state, action) => {
state.mediaSummary = action.payload;
},
// Chat/broadcast mixers
setChatMixer: (state, action) => {
state.chatMixer = action.payload;
},
setBroadcastMixer: (state, action) => {
state.broadcastMixer = action.payload;
},
setRecordingMixer: (state, action) => {
state.recordingMixer = action.payload;
},
// Simulated mixers (for category controls)
setSimulatedMusicCategoryMixers: (state, action) => {
state.simulatedMusicCategoryMixers = action.payload;
},
setSimulatedChatCategoryMixers: (state, action) => {
state.simulatedChatCategoryMixers = action.payload;
},
// Peer management
addNoAudioUser: (state, action) => {
state.noAudioUsers[action.payload] = true;
},
removeNoAudioUser: (state, action) => {
delete state.noAudioUsers[action.payload];
},
setNoAudioUsers: (state, action) => {
state.noAudioUsers = action.payload;
},
setClientsWithAudioOverride: (state, action) => {
state.clientsWithAudioOverride = action.payload;
},
setMissingMixerPeers: (state, action) => {
state.missingMixerPeers = action.payload;
},
setCheckingMissingPeers: (state, action) => {
state.checkingMissingPeers = action.payload;
},
// Clear on session end
clearMixers: (state) => {
return { ...initialState };
}
}
});
export const {
setMasterMixers,
setPersonalMixers,
organizeMixers,
setRecordingTrackMixers,
setBackingTrackMixers,
setJamTrackMixers,
setMetronomeTrackMixers,
setAdhocTrackMixers,
updateMixer,
setMetronome,
setMetronomeSettings,
updateMediaSummary,
setMediaSummary,
setChatMixer,
setBroadcastMixer,
setRecordingMixer,
setSimulatedMusicCategoryMixers,
setSimulatedChatCategoryMixers,
addNoAudioUser,
removeNoAudioUser,
setNoAudioUsers,
setClientsWithAudioOverride,
setMissingMixerPeers,
setCheckingMissingPeers,
clearMixers
} = mixersSlice.actions;
export default mixersSlice.reducer;
// Selectors
export const selectAllMixers = (state) => state.mixers.allMixers;
export const selectMasterMixers = (state) => state.mixers.masterMixers;
export const selectPersonalMixers = (state) => state.mixers.personalMixers;
export const selectMixersByResourceId = (state) => state.mixers.mixersByResourceId;
export const selectMixersByTrackId = (state) => state.mixers.mixersByTrackId;
export const selectChatMixer = (state) => state.mixers.chatMixer;
export const selectBroadcastMixer = (state) => state.mixers.broadcastMixer;
export const selectRecordingMixer = (state) => state.mixers.recordingMixer;
export const selectMetronome = (state) => state.mixers.metronome;
export const selectMetronomeSettings = (state) => state.mixers.metronomeSettings;
export const selectMediaSummary = (state) => state.mixers.mediaSummary;
export const selectMixersReady = (state) => state.mixers.isReady;
export const selectBackingTrackMixers = (state) => state.mixers.backingTrackMixers;
export const selectJamTrackMixers = (state) => state.mixers.jamTrackMixers;
export const selectRecordingTrackMixers = (state) => state.mixers.recordingTrackMixers;
export const selectMetronomeTrackMixers = (state) => state.mixers.metronomeTrackMixers;
export const selectAdhocTrackMixers = (state) => state.mixers.adhocTrackMixers;
export const selectSimulatedMusicCategoryMixers = (state) => state.mixers.simulatedMusicCategoryMixers;
export const selectSimulatedChatCategoryMixers = (state) => state.mixers.simulatedChatCategoryMixers;
export const selectNoAudioUsers = (state) => state.mixers.noAudioUsers;
export const selectClientsWithAudioOverride = (state) => state.mixers.clientsWithAudioOverride;
export const selectMissingMixerPeers = (state) => state.mixers.missingMixerPeers;
export const selectCheckingMissingPeers = (state) => state.mixers.checkingMissingPeers;
// Computed selector for getting a specific mixer by ID and mode
export const selectMixer = (mixerId, mode) => (state) => {
const key = (mode ? 'M' : 'P') + mixerId;
return state.mixers.allMixers[key];
};
// Computed selector for getting mixer pair by resource ID
export const selectMixerPairByResourceId = (resourceId) => (state) => {
return state.mixers.mixersByResourceId[resourceId];
};
// Computed selector for getting mixer pair by track ID
export const selectMixerPairByTrackId = (trackId) => (state) => {
return state.mixers.mixersByTrackId[trackId];
};
// Composed memoized selectors for useMixerHelper optimization
// These group related data that's typically used together
export const selectCoreMixerState = createSelector(
[selectChatMixer, selectBroadcastMixer, selectRecordingMixer],
(chatMixer, broadcastMixer, recordingMixer) => ({
chatMixer,
broadcastMixer,
recordingMixer
})
);
export const selectTrackMixerState = createSelector(
[
selectRecordingTrackMixers,
selectBackingTrackMixers,
selectJamTrackMixers,
selectMetronomeTrackMixers,
selectAdhocTrackMixers
],
(recordingTrackMixers, backingTrackMixers, jamTrackMixers, metronomeTrackMixers, adhocTrackMixers) => ({
recordingTrackMixers,
backingTrackMixers,
jamTrackMixers,
metronomeTrackMixers,
adhocTrackMixers
})
);
export const selectMixerLookupTables = createSelector(
[selectAllMixers, selectMixersByResourceId, selectMixersByTrackId],
(allMixers, mixersByResourceId, mixersByTrackId) => ({
allMixers,
mixersByResourceId,
mixersByTrackId
})
);
export const selectMasterPersonalMixers = createSelector(
[selectMasterMixers, selectPersonalMixers],
(masterMixers, personalMixers) => ({
masterMixers,
personalMixers
})
);
export const selectMixerMetadata = createSelector(
[selectMetronome, selectMetronomeSettings, selectMediaSummary, selectNoAudioUsers, selectClientsWithAudioOverride, selectMixersReady],
(metronome, metronomeSettings, mediaSummary, noAudioUsers, clientsWithAudioOverride, isReady) => ({
metronome,
metronomeSettings,
mediaSummary,
noAudioUsers,
clientsWithAudioOverride,
isReady
})
);
export const selectSimulatedCategoryMixers = createSelector(
[selectSimulatedMusicCategoryMixers, selectSimulatedChatCategoryMixers],
(simulatedMusicCategoryMixers, simulatedChatCategoryMixers) => ({
simulatedMusicCategoryMixers,
simulatedChatCategoryMixers
})
);