538 lines
18 KiB
JavaScript
538 lines
18 KiB
JavaScript
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
import { useSelector } from 'react-redux';
|
|
import { selectInSession } from '../store/features/activeSessionSlice';
|
|
import {
|
|
selectMetronomeSettings,
|
|
selectNoAudioUsers,
|
|
selectClientsWithAudioOverride
|
|
} from '../store/features/mixersSlice';
|
|
import { throttle } from 'lodash'; // Add lodash for throttling
|
|
//import { useJamClient } from '../context/JamClientContext';
|
|
import { MIX_MODES } from '../helpers/globals';
|
|
import { putTrackSyncChange } from '../helpers/rest';
|
|
import useVuHelpers from './useVuHelpers';
|
|
import { useJamServerContext } from '../context/JamServerContext';
|
|
import { useMixersContext } from '../context/MixersContext';
|
|
import { vuStore } from '../stores/vuStore';
|
|
|
|
|
|
|
|
/**
|
|
* Custom hook for managing mixer store state and actions.
|
|
* Converted from the original CoffeeScript Reflux store to modern React.
|
|
*/
|
|
export default function useMixerStore() {
|
|
//const jamClient = useJamClient();
|
|
// Phase 4: Replace CurrentSessionContext with Redux
|
|
const inSession = useSelector(selectInSession);
|
|
|
|
// Phase 5: Replace duplicate state with Redux selectors
|
|
const metro = useSelector(selectMetronomeSettings);
|
|
const noAudioUsers = useSelector(selectNoAudioUsers);
|
|
const clientsWithAudioOverride = useSelector(selectClientsWithAudioOverride);
|
|
|
|
const {
|
|
jamClient,
|
|
} = useJamServerContext();
|
|
const mixerHelper = useMixersContext();
|
|
const mixerHelperRef = useRef(null);
|
|
|
|
const logger = console; // Replace with your logging mechanism if needed
|
|
//const { updateVU } = useVuHelpers();
|
|
|
|
// Local state (not duplicated in Redux)
|
|
const [checkingMissingPeers, setCheckingMissingPeers] = useState({});
|
|
const [missingMixerPeers, setMissingMixerPeers] = useState({});
|
|
const [vuMeterUpdatePrefMap, setVuMeterUpdatePrefMap] = useState({ count: 0 });
|
|
const [session, setSession] = useState(null);
|
|
//const [masterMixers, setMasterMixers] = useState(null);
|
|
//const [personalMixers, setPersonalMixers] = useState(null);
|
|
//const [mixers, setMixers] = useState(null); //holds MixerHelper instance
|
|
|
|
// Refs for persistent values
|
|
const appRef = useRef(null);
|
|
const gearUtilsRef = useRef(null);
|
|
const sessionUtilsRef = useRef(null);
|
|
const recheckTimeoutRef = useRef(null);
|
|
|
|
// Constants
|
|
const METRO_SOUND_LOOKUP = {
|
|
0: "BuiltIn",
|
|
1: "SineWave",
|
|
2: "Beep",
|
|
3: "Click",
|
|
4: "Kick",
|
|
5: "Snare",
|
|
6: "MetroFile"
|
|
};
|
|
|
|
// Keep mixerHelperRef in sync with current mixerHelper
|
|
useEffect(() => {
|
|
mixerHelperRef.current = mixerHelper;
|
|
}, [mixerHelper]);
|
|
|
|
// Initialize global callbacks
|
|
useEffect(() => {
|
|
if (!window.JK) window.JK = {};
|
|
console.debug("MixerStore: setting up global callbacks", mixerHelper.mixers.current);
|
|
//window.JK.HandleVolumeChangeCallback2 = handleVolumeChangeCallback;
|
|
//window.JK.HandleMetronomeCallback2 = handleMetronomeCallback;
|
|
window.JK.HandleBridgeCallback2 = handleBridgeCallback;
|
|
//window.JK.HandleBackingTrackSelectedCallback2 = handleBackingTrackSelectedCallback;
|
|
|
|
return () => {
|
|
// Cleanup
|
|
if (window.JK) {
|
|
//delete window.JK.HandleVolumeChangeCallback2;
|
|
//delete window.JK.HandleMetronomeCallback2;
|
|
delete window.JK.HandleBridgeCallback2;
|
|
//delete window.JK.HandleBackingTrackSelectedCallback2;
|
|
}
|
|
if (recheckTimeoutRef.current) {
|
|
clearTimeout(recheckTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Initialize on app ready
|
|
const initialize = useCallback(async (app) => {
|
|
// appRef.current = app;
|
|
// gearUtilsRef.current = window.JK?.GearUtilsInstance;
|
|
// sessionUtilsRef.current = window.JK?.SessionUtils;
|
|
|
|
// if (jamClient) {
|
|
// await jamClient.SetVURefreshRate(150);
|
|
// await jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback2");
|
|
// await jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback2");
|
|
// }
|
|
}, [jamClient]);
|
|
|
|
// // Callback handlers
|
|
// const handleVolumeChangeCallback = useCallback((mixerId, isLeft, value, isMuted) => {
|
|
// // TODO: Visually update mixer
|
|
// // Original had TODO comments about updating fader values
|
|
// console.debug("volume change");
|
|
// }, []);
|
|
|
|
// const handleMetronomeCallback = useCallback((args) => {
|
|
// console.debug("MetronomeCallback: ", args);
|
|
// setMetro(prev => ({
|
|
// ...prev,
|
|
// tempo: args.bpm,
|
|
// cricket: args.cricket,
|
|
// sound: METRO_SOUND_LOOKUP[args.sound]
|
|
// }));
|
|
|
|
// // Update mixers with new metro data
|
|
// setMixers(prevMixers => {
|
|
// if (!prevMixers) return null;
|
|
// return new window.MixerHelper(session, masterMixers, personalMixers, metro, noAudioUsers, clientsWithAudioOverride, prevMixers.mixMode || MIX_MODES.PERSONAL);
|
|
// });
|
|
|
|
// issueChange();
|
|
// }, [session, masterMixers, personalMixers, noAudioUsers, clientsWithAudioOverride, issueChange]);
|
|
|
|
|
|
const handleBridgeCallback = useCallback((vuData) => {
|
|
let eventName = null;
|
|
let mixerId = null;
|
|
const value = null;
|
|
let vuInfo = null;
|
|
|
|
// const vuMeterUpdateRate = localStorage.getItem('vuMeterUpdateRate') || 'fast';
|
|
// this.vuMeterUpdatePrefMap['count'] = (this.vuMeterUpdatePrefMap['count'] || 0) + 1;
|
|
|
|
// if ((vuMeterUpdateRate === 'medium') && ((this.vuMeterUpdatePrefMap['count'] % 3) !== 0)) {
|
|
// // skip this update
|
|
// return;
|
|
// }
|
|
// if ((vuMeterUpdateRate === 'slow') && ((this.vuMeterUpdatePrefMap['count'] % 9) !== 0)) {
|
|
// // skip this update
|
|
// return;
|
|
// }
|
|
//console.log('vuMeterUpdateRate', vuMeterUpdateRate, @vuMeterUpdatePrefMap['count'])
|
|
//this.vuMeterUpdatePrefMap['count'] = 0;
|
|
|
|
//console.log("_DEBUG_ XXX: ", vuData);
|
|
|
|
|
|
//const result = [];
|
|
for (vuInfo of vuData) {
|
|
eventName = vuInfo[0];
|
|
const vuVal = 0.0;
|
|
if (eventName === "vu") {
|
|
mixerId = vuInfo[1];
|
|
const mode = vuInfo[2];
|
|
const leftValue = vuInfo[3];
|
|
const leftClipping = vuInfo[4];
|
|
const rightValue = vuInfo[5];
|
|
const rightClipping = vuInfo[6];
|
|
// TODO - no guarantee range will be -80 to 20. Get from the
|
|
// GetControlState for this mixer which returns min/max
|
|
// value is a DB value from -80 to 20. Convert to float from 0.0-1.0
|
|
|
|
// Convert dB to 0.0-1.0 range (existing logic)
|
|
const normalizedLevel = (leftValue + 80) / 80;
|
|
|
|
// Create qualified ID matching existing pattern
|
|
const fqId = (mode ? 'M' : 'P') + mixerId;
|
|
|
|
// Route to external store instead of mixerHelper
|
|
vuStore.updateLevel(fqId, normalizedLevel, leftClipping);
|
|
}
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
// const handleBackingTrackSelectedCallback = useCallback(() => {
|
|
// console.debug("backing track selected");
|
|
// }, []);
|
|
|
|
// Session management
|
|
const sessionEnded = useCallback(() => {
|
|
// Phase 5: Local state only - Redux handles noAudioUsers cleanup
|
|
setCheckingMissingPeers({});
|
|
setMissingMixerPeers({});
|
|
if (recheckTimeoutRef.current) {
|
|
clearTimeout(recheckTimeoutRef.current);
|
|
recheckTimeoutRef.current = null;
|
|
}
|
|
}, []);
|
|
|
|
const onSessionChange = useCallback(async (newSession) => {
|
|
console.log("[MixerStore] onSessionChange START:", {
|
|
sessionId: newSession.id(),
|
|
timestamp: Date.now()
|
|
});
|
|
console.debug("MixerStore: onSessionChange", newSession);
|
|
|
|
if (!newSession) return;
|
|
|
|
if (!inSession) {
|
|
sessionEnded();
|
|
}
|
|
|
|
setSession(newSession);
|
|
|
|
if (jamClient) {
|
|
console.log("[MixerStore] Fetching mixer control state...");
|
|
const newMasterMixers = await jamClient.SessionGetAllControlState(true);
|
|
const newPersonalMixers = await jamClient.SessionGetAllControlState(false);
|
|
|
|
console.log("[MixerStore] Fetched mixers:", {
|
|
masterCount: newMasterMixers.length,
|
|
personalCount: newPersonalMixers.length
|
|
});
|
|
|
|
// Initialize the mixerHelper with the constructor parameters
|
|
console.debug("MixerStore: onSessionChange: initializing mixerHelper", { newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride });
|
|
|
|
mixerHelper.updateMixerData(newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, MIX_MODES.PERSONAL);
|
|
|
|
// const m = useMixerHelper(newSession, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, mixers?.mixMode || MIX_MODES.PERSONAL);
|
|
// console.debug("MixerStore: onSessionChange: ", m);
|
|
// setMixers(m);
|
|
}
|
|
console.log("[MixerStore] onSessionChange END");
|
|
}, [jamClient, mixerHelper, metro, noAudioUsers, clientsWithAudioOverride, inSession, sessionEnded]);
|
|
|
|
// // Mixer actions
|
|
// const onMute = useCallback(async (mixersToMute, muting) => {
|
|
// const mixersArray = Array.isArray(mixersToMute) ? mixersToMute : [mixersToMute];
|
|
|
|
// for (const mixer of mixersArray) {
|
|
// if (mixers) {
|
|
// await mixers.mute(mixer.id, mixer.mode, muting);
|
|
// }
|
|
// }
|
|
|
|
// issueChange();
|
|
// }, [mixers, issueChange]);
|
|
|
|
// const onFaderChanged = useCallback(async (data, mixersList, gainType, controlGroup) => {
|
|
// if (mixers) {
|
|
// await mixers.faderChanged(data, mixersList, gainType, controlGroup);
|
|
// }
|
|
// issueChange();
|
|
// }, [mixers, issueChange]);
|
|
|
|
// const onPanChanged = useCallback((data, mixersList, groupId) => {
|
|
// if (mixers) {
|
|
// mixers.panChanged(data, mixersList, groupId);
|
|
// }
|
|
// issueChange();
|
|
// }, [mixers, issueChange]);
|
|
|
|
// const onLoopChanged = useCallback(async (mixer, shouldLoop) => {
|
|
// if (mixers) {
|
|
// await mixers.loopChanged(mixer, shouldLoop);
|
|
// }
|
|
// }, [mixers]);
|
|
|
|
// const onOpenMetronome = useCallback(async () => {
|
|
// if (jamClient && mixers) {
|
|
// await jamClient.SessionStopPlay();
|
|
// await jamClient.SessionOpenMetronome(mixers.metro.tempo, mixers.metro.sound, 1, 0);
|
|
// }
|
|
// }, [jamClient, mixers]);
|
|
|
|
// const onMetronomeChanged = useCallback(async (tempo, sound) => {
|
|
// console.debug("onMetronomeChanged", tempo, sound);
|
|
|
|
// setMetro(prev => ({ ...prev, tempo, sound }));
|
|
|
|
// if (jamClient) {
|
|
// await jamClient.SessionSetMetronome(tempo, sound, 1, 0);
|
|
// }
|
|
|
|
// setMixers(prevMixers => {
|
|
// if (!prevMixers) return null;
|
|
// return new window.MixerHelper(session, masterMixers, personalMixers, { ...metro, tempo, sound }, noAudioUsers, clientsWithAudioOverride, prevMixers.mixMode || MIX_MODES.PERSONAL);
|
|
// });
|
|
|
|
// issueChange();
|
|
// }, [jamClient, session, masterMixers, personalMixers, noAudioUsers, clientsWithAudioOverride, issueChange]);
|
|
|
|
// // Media close handlers
|
|
// const onCloseMedia = useCallback((codeInitiated) => {
|
|
// console.debug("MixerStore: onCloseMedia", codeInitiated);
|
|
// if (session?.recordedTracks()) {
|
|
// closeRecording(codeInitiated);
|
|
// } else if (session?.jamTracks() || session?.downloadingJamTrack) {
|
|
// closeJamTrack(codeInitiated);
|
|
// } else if (session?.backingTrack()?.path) {
|
|
// closeBackingTrack(codeInitiated);
|
|
// } else if (session?.isMetronomeOpen()) {
|
|
// closeMetronomeTrack(codeInitiated);
|
|
// } else {
|
|
// console.error("don't know how to close open media", session);
|
|
// }
|
|
// }, [session]);
|
|
|
|
// const closeRecording = useCallback((codeInitiated) => {
|
|
// console.debug("closing recording");
|
|
|
|
// if (!mixers?.mediaSummary.isOpener) {
|
|
// console.debug("not recording as not opener");
|
|
// return;
|
|
// }
|
|
|
|
// // TODO: Implement SessionActions.closeMedia
|
|
// // SessionActions.closeMedia(codeInitiated);
|
|
// }, [mixers]);
|
|
|
|
// const closeMetronomeTrack = useCallback((codeInitiated) => {
|
|
// console.debug("closing metronome");
|
|
|
|
// // TODO: Implement SessionActions.closeMedia
|
|
// // SessionActions.closeMedia(codeInitiated);
|
|
// }, []);
|
|
|
|
// const closeBackingTrack = useCallback((codeInitiated) => {
|
|
// console.debug("closing backing track");
|
|
|
|
// if (!mixers?.mediaSummary.isOpener) {
|
|
// console.debug("not closing backing as not opener");
|
|
// return;
|
|
// }
|
|
|
|
// // TODO: Implement SessionActions.closeMedia
|
|
// // SessionActions.closeMedia(codeInitiated);
|
|
// }, [mixers]);
|
|
|
|
// const closeJamTrack = useCallback((codeInitiated) => {
|
|
// console.debug("closing jam track");
|
|
|
|
// if (!mixers?.mediaSummary.isOpener) {
|
|
// console.debug("not closing jamtrack as not opener");
|
|
// return;
|
|
// }
|
|
|
|
// // TODO: Implement SessionActions.closeMedia
|
|
// // SessionActions.closeMedia(codeInitiated);
|
|
// }, [mixers]);
|
|
|
|
// // Peer management
|
|
// const onDeadUserRemove = useCallback((clientId) => {
|
|
// if (!session?.inSession()) return;
|
|
|
|
// const participant = session.participantsEverSeen?.[clientId];
|
|
// if (participant) {
|
|
// console.debug("todo: notify dead user");
|
|
// // TODO: trigger some notification store
|
|
// }
|
|
|
|
// setNoAudioUsers(prev => ({ ...prev, [clientId]: true }));
|
|
// issueChange();
|
|
// }, [session, issueChange]);
|
|
|
|
// const onClientsWithAudio = useCallback((clients) => {
|
|
// setClientsWithAudioOverride(clients);
|
|
// }, []);
|
|
|
|
// const onMissingPeerMixer = useCallback((clientId) => {
|
|
// const missingPeerAttempts = missingMixerPeers[clientId];
|
|
|
|
// if (!missingPeerAttempts || missingPeerAttempts < 5) {
|
|
// setCheckingMissingPeers(prev => ({ ...prev, [clientId]: true }));
|
|
// if (!recheckTimeoutRef.current) {
|
|
// recheckTimeoutRef.current = setTimeout(recheckForMixers, 2000);
|
|
// }
|
|
// } else {
|
|
// console.debug("ignoring missing peer recheck. missingPeerAttempts:", missingPeerAttempts);
|
|
// }
|
|
// }, [missingMixerPeers]);
|
|
|
|
// const recheckForMixers = useCallback(async () => {
|
|
// setCheckingMissingPeers(prev => {
|
|
// const newChecking = { ...prev };
|
|
// for (const clientId in newChecking) {
|
|
// setMissingMixerPeers(prevMissing => {
|
|
// const attempts = (prevMissing[clientId] || 0) + 1;
|
|
// return { ...prevMissing, [clientId]: attempts };
|
|
// });
|
|
// }
|
|
// return {};
|
|
// });
|
|
|
|
// recheckTimeoutRef.current = null;
|
|
|
|
// if (jamClient) {
|
|
// const newMasterMixers = await jamClient.SessionGetAllControlState(true);
|
|
// const newPersonalMixers = await jamClient.SessionGetAllControlState(false);
|
|
|
|
// setMasterMixers(newMasterMixers);
|
|
// setPersonalMixers(newPersonalMixers);
|
|
|
|
// console.debug("MixerStore: recheckForMixers");
|
|
// setMixers(prevMixers => {
|
|
// if (!prevMixers) return null;
|
|
// return new window.MixerHelper(session, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, prevMixers.mixMode || MIX_MODES.PERSONAL);
|
|
// });
|
|
// }
|
|
|
|
// issueChange();
|
|
// }, [jamClient, session, metro, noAudioUsers, clientsWithAudioOverride, issueChange]);
|
|
|
|
// // Other handlers
|
|
// const onInitGain = useCallback((mixer) => {
|
|
// if (mixers) {
|
|
// mixers.initGain(mixer);
|
|
// }
|
|
// }, [mixers]);
|
|
|
|
// const onInitPan = useCallback((mixer) => {
|
|
// if (mixers) {
|
|
// mixers.initPan(mixer);
|
|
// }
|
|
// }, [mixers]);
|
|
|
|
// const onMixersChanged = useCallback(async (type, text) => {
|
|
// if (jamClient) {
|
|
// const newMasterMixers = await jamClient.SessionGetAllControlState(true);
|
|
// const newPersonalMixers = await jamClient.SessionGetAllControlState(false);
|
|
|
|
// setMasterMixers(newMasterMixers);
|
|
// setPersonalMixers(newPersonalMixers);
|
|
|
|
// console.debug("MixerStore: onMixersChanged");
|
|
|
|
// setMixers(prevMixers => {
|
|
// if (!prevMixers) return null;
|
|
// return new window.MixerHelper(session, newMasterMixers, newPersonalMixers, metro, noAudioUsers, clientsWithAudioOverride, prevMixers.mixMode || MIX_MODES.PERSONAL);
|
|
// });
|
|
|
|
// // TODO: Implement SessionActions.mixersChanged.trigger
|
|
// // SessionActions.mixersChanged.trigger(type, text, mixers.getTrackInfo());
|
|
// }
|
|
|
|
// issueChange();
|
|
// }, [jamClient, session, metro, noAudioUsers, clientsWithAudioOverride, issueChange]);
|
|
|
|
// const onMixerModeChanged = useCallback((mode) => {
|
|
// if (mode === MIX_MODES.MASTER && appRef.current?.layout) {
|
|
// appRef.current.layout.showDialog('session-master-mix-dialog');
|
|
// } else if (appRef.current?.layout) {
|
|
// appRef.current.layout.closeDialog('session-master-mix-dialog');
|
|
// }
|
|
// }, []);
|
|
|
|
// const onSyncTracks = useCallback(async () => {
|
|
// console.debug("MixerStore: onSyncTracks");
|
|
// if (!session?.inSession()) {
|
|
// console.debug("dropping sync tracks because no longer in session");
|
|
// return;
|
|
// }
|
|
|
|
// if (mixers) {
|
|
// const allTracks = await mixers.getTrackInfo();
|
|
// console.log('allTracks', allTracks);
|
|
// const inputTracks = allTracks.userTracks;
|
|
// const backingTracks = allTracks.backingTracks;
|
|
// const metronomeTracks = allTracks.metronomeTracks;
|
|
|
|
// // create a trackSync request based on backend data
|
|
// const syncTrackRequest = {
|
|
// client_id: appRef.current?.clientId,
|
|
// tracks: inputTracks,
|
|
// backing_tracks: backingTracks,
|
|
// metronome_open: metronomeTracks.length > 0,
|
|
// id: session.id()
|
|
// };
|
|
|
|
// try {
|
|
// await putTrackSyncChange(syncTrackRequest);
|
|
// } catch (error) {
|
|
// if (error.status !== 404) {
|
|
// appRef.current?.notify({
|
|
// "title": "Can't Sync Local Tracks",
|
|
// "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
|
|
// "icon_url": "/assets/content/icon_alert_big.png"
|
|
// });
|
|
// } else {
|
|
// console.debug("Unable to sync local tracks because session is gone.");
|
|
// }
|
|
// }
|
|
// }
|
|
// }, [session, mixers]);
|
|
|
|
// Return the state and handlers
|
|
return {
|
|
// State
|
|
// session,
|
|
// metro,
|
|
// noAudioUsers,
|
|
// clientsWithAudioOverride,
|
|
|
|
// Initialization
|
|
initialize,
|
|
|
|
// Handlers
|
|
onSessionChange,
|
|
// onMute,
|
|
// onFaderChanged,
|
|
// onPanChanged,
|
|
// onLoopChanged,
|
|
// onOpenMetronome,
|
|
// onMetronomeChanged,
|
|
// onCloseMedia,
|
|
// onDeadUserRemove,
|
|
// onClientsWithAudio,
|
|
// onMissingPeerMixer,
|
|
// onInitGain,
|
|
// onInitPan,
|
|
// onMixersChanged,
|
|
// onMixerModeChanged,
|
|
// onSyncTracks,
|
|
|
|
// // Callbacks
|
|
// handleVolumeChangeCallback,
|
|
// handleMetronomeCallback,
|
|
// handleBridgeCallback,
|
|
// handleBackingTrackSelectedCallback,
|
|
};
|
|
}
|