fix(ui): implement independent Audio Input and Session Mix fader controls
- Add mixType prop to JKSessionAudioInputs to select master/personal mixer - Store both masterMixers and personalMixers in myTracks for each track - Fix async state issue in fillTrackVolumeObject by returning volumeObj directly - Change SessionTrackGain to use useLayoutEffect for synchronous visual updates - Fix controlGroup to null for individual track controls (fixes persistence) - Prevent fader reset when mixer temporarily becomes undefined during re-renders Audio Inputs now uses master mixer, Session Mix uses personal mixer, matching the legacy app behavior. Both faders persist independently. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
aa731c96d0
commit
78cdafbb8a
|
|
@ -1,8 +1,8 @@
|
|||
import React from 'react';
|
||||
import JKSessionMyTrack from './JKSessionMyTrack.js';
|
||||
import { getInstrumentIcon45_inverted, convertClientInstrumentToServer } from '../../helpers/utils';
|
||||
import { getInstrumentIcon45, convertClientInstrumentToServer } from '../../helpers/utils';
|
||||
|
||||
const JKSessionAudioInputs = ({ myTracks, chat, mixerHelper, isRemote = false }) => {
|
||||
const JKSessionAudioInputs = ({ myTracks, chat, mixerHelper, isRemote = false, mixType = 'default' }) => {
|
||||
return (
|
||||
<div className='d-flex' style={{ gap: '0.5rem' }}>
|
||||
<div>
|
||||
|
|
@ -18,12 +18,24 @@ const JKSessionAudioInputs = ({ myTracks, chat, mixerHelper, isRemote = false })
|
|||
const serverInstrument = typeof instrumentId === 'number'
|
||||
? convertClientInstrumentToServer(instrumentId)
|
||||
: (instrumentId || track.track?.instrument);
|
||||
const instrumentIcon = getInstrumentIcon45_inverted(serverInstrument);
|
||||
const instrumentIcon = getInstrumentIcon45(serverInstrument);
|
||||
|
||||
// Select the appropriate mixer based on mixType:
|
||||
// - 'personal': Audio Mix view (what I hear) - uses personalMixers
|
||||
// - 'master': Session Mix view (what goes out) - uses masterMixers
|
||||
// - 'default': use the mixers based on current mixMode (legacy behavior)
|
||||
let selectedMixers = track.mixers;
|
||||
if (mixType === 'personal' && track.personalMixers) {
|
||||
selectedMixers = track.personalMixers;
|
||||
} else if (mixType === 'master' && track.masterMixers) {
|
||||
selectedMixers = track.masterMixers;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div key={track.track.client_track_id || index}>
|
||||
<JKSessionMyTrack
|
||||
key={track.track.client_track_id || index}
|
||||
{...track}
|
||||
mixers={selectedMixers}
|
||||
instrumentIcon={instrumentIcon}
|
||||
mode={mixerHelper.mixMode}
|
||||
isRemote={isRemote}
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ const JKSessionMyTrack = ({
|
|||
side="best"
|
||||
mixers={mixers}
|
||||
/>
|
||||
<SessionTrackGain mixers={mixers?.mixer} gainType="track" controlGroup="track" orientation="vertical" />
|
||||
<SessionTrackGain mixers={mixers?.mixer} gainType="track" controlGroup={null} orientation="vertical" />
|
||||
</div>
|
||||
<div className="track-buttons">
|
||||
<div className="track-menu-container">
|
||||
|
|
|
|||
|
|
@ -1342,6 +1342,7 @@ const JKSessionScreen = () => {
|
|||
chat={chat}
|
||||
mixerHelper={mixerHelper}
|
||||
isRemote={false}
|
||||
mixType="master"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1358,6 +1359,7 @@ const JKSessionScreen = () => {
|
|||
chat={chat}
|
||||
mixerHelper={mixerHelper}
|
||||
isRemote={true}
|
||||
mixType="personal"
|
||||
/>
|
||||
<JKSessionRemoteTracks
|
||||
mixerHelper={mixerHelper}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useRef, useEffect, useLayoutEffect, useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import './VolumeSlider.css'; // Keep the same CSS for now
|
||||
import { useMixersContext } from '../../context/MixersContext';
|
||||
import useFaderHelpers from '../../hooks/useFaderHelpers';
|
||||
|
||||
const SessionTrackGain = ({ mixers, gainType, controlGroup, sessionController, orientation = 'vertical' }) => {
|
||||
//console.debug("SessionTrackGain: Rendering for mixers", mixers, "gainType", gainType, "controlGroup", controlGroup);
|
||||
|
||||
const mixerHelper = useMixersContext();
|
||||
const faderHelpers = useFaderHelpers();
|
||||
|
|
@ -38,20 +37,14 @@ const SessionTrackGain = ({ mixers, gainType, controlGroup, sessionController, o
|
|||
const mixer = mixers;
|
||||
const currentVolume = mixer?.volume_left;
|
||||
|
||||
// Check if volume has actually changed
|
||||
if (currentVolume !== prevVolumeRef.current) {
|
||||
// Only update if we have valid volume data
|
||||
// Don't reset to fallback if mixer temporarily becomes undefined during re-renders
|
||||
if (mixer && currentVolume !== undefined && currentVolume !== prevVolumeRef.current) {
|
||||
prevVolumeRef.current = currentVolume;
|
||||
|
||||
if (mixer && currentVolume !== undefined) {
|
||||
const newValue = faderHelpers.convertAudioTaperToPercent(currentVolume);
|
||||
console.log('SessionTrackGain: initial value', newValue);
|
||||
setCurrentValue(newValue);
|
||||
setDisplayedVolume(currentVolume);
|
||||
mixerHelper.initGain(mixer);
|
||||
} else {
|
||||
setCurrentValue(50); // fallback
|
||||
setDisplayedVolume(0);
|
||||
}
|
||||
const newValue = faderHelpers.convertAudioTaperToPercent(currentVolume);
|
||||
setCurrentValue(newValue);
|
||||
setDisplayedVolume(currentVolume);
|
||||
mixerHelper.initGain(mixer);
|
||||
}
|
||||
}, [mixers, faderHelpers, mixerHelper]);
|
||||
|
||||
|
|
@ -89,7 +82,6 @@ const SessionTrackGain = ({ mixers, gainType, controlGroup, sessionController, o
|
|||
);
|
||||
}
|
||||
|
||||
console.log('SessionTrackGain: updateSlider', { clientPos, clampedPosition, percentage });
|
||||
|
||||
// Update local state for UI
|
||||
setCurrentValue(Math.round(percentage));
|
||||
|
|
@ -146,8 +138,11 @@ const SessionTrackGain = ({ mixers, gainType, controlGroup, sessionController, o
|
|||
};
|
||||
|
||||
// Update the slider position when value changes
|
||||
useEffect(() => {
|
||||
if (!sliderTrackRef.current) return;
|
||||
// Using useLayoutEffect to ensure visual update happens synchronously before paint
|
||||
useLayoutEffect(() => {
|
||||
if (!sliderTrackRef.current || !sliderThumbRef.current || !sliderFillRef.current) {
|
||||
return;
|
||||
}
|
||||
const percentage = currentValue / 100;
|
||||
if (orientation === 'vertical') {
|
||||
const trackHeight = sliderTrackRef.current.clientHeight;
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ const useMixerHelper = () => {
|
|||
const fillTrackVolumeObject = useCallback((mixerId, mode, currentAllMixers, broadcast = true) => {
|
||||
const mixer = getMixer(mixerId, mode, currentAllMixers);
|
||||
if (mixer == null) {
|
||||
console.error("MixerHelper: fillTrackVolumeObject: unable to find mixer with ID: #{mixerId}, mode: #{mode}");
|
||||
return null;
|
||||
}
|
||||
|
||||
setTrackVolumeObject({
|
||||
// Build volume object directly and return it (don't rely on async state update)
|
||||
const volumeObj = {
|
||||
id: mixer.id,
|
||||
_id: mixer._id,
|
||||
groupID: mixer.group_id,
|
||||
|
|
@ -158,7 +158,10 @@ const useMixerHelper = () => {
|
|||
isMetronome: mixer.is_metronome,
|
||||
volR: mixer.volume_left,
|
||||
loop: mixer.loop
|
||||
});
|
||||
};
|
||||
|
||||
// Also update React state for other consumers
|
||||
setTrackVolumeObject(volumeObj);
|
||||
|
||||
// Redux: Update mixer range in sessionUISlice
|
||||
dispatch(setCurrentMixerRange({
|
||||
|
|
@ -166,18 +169,22 @@ const useMixerHelper = () => {
|
|||
max: mixer.range_high
|
||||
}));
|
||||
|
||||
return mixer;
|
||||
// Return both mixer and the volume object
|
||||
return { mixer, volumeObj };
|
||||
}, [getMixer, setTrackVolumeObject, dispatch]);
|
||||
|
||||
const setMixerVolume = useCallback(async (mixer, volumePercent, relative, originalVolume, controlGroup) => {
|
||||
const setMixerVolume = useCallback(async (mixer, volumePercent, relative, originalVolume, controlGroup, baseVolumeObj) => {
|
||||
const newVolume = faderHelpers.convertPercentToAudioTaper(volumePercent);
|
||||
|
||||
// Use the passed volumeObj instead of relying on async state
|
||||
const sourceVolumeObj = baseVolumeObj || trackVolumeObject;
|
||||
|
||||
let updatedTrackVolumeObject;
|
||||
if (relative) {
|
||||
updatedTrackVolumeObject = {
|
||||
...trackVolumeObject,
|
||||
volL: trackVolumeObject.volL + (newVolume - originalVolume),
|
||||
volR: trackVolumeObject.volR + (newVolume - originalVolume),
|
||||
...sourceVolumeObj,
|
||||
volL: sourceVolumeObj.volL + (newVolume - originalVolume),
|
||||
volR: sourceVolumeObj.volR + (newVolume - originalVolume),
|
||||
};
|
||||
|
||||
// Apply clamping
|
||||
|
|
@ -187,7 +194,7 @@ const useMixerHelper = () => {
|
|||
if (updatedTrackVolumeObject.volR > 20) updatedTrackVolumeObject.volR = 20;
|
||||
} else {
|
||||
updatedTrackVolumeObject = {
|
||||
...trackVolumeObject,
|
||||
...sourceVolumeObj,
|
||||
volL: newVolume,
|
||||
volR: newVolume,
|
||||
};
|
||||
|
|
@ -202,7 +209,6 @@ const useMixerHelper = () => {
|
|||
console.log("setMixerVolume: setting session mixer category playout state for controlGroup", controlGroup, "controlGroupsArg", controlGroupsArg, "volume", updatedTrackVolumeObject.volL);
|
||||
await jamClient.setSessionMixerCategoryPlayoutState(controlGroup === 'music', controlGroupsArg, updatedTrackVolumeObject.volL);
|
||||
} else {
|
||||
console.log("setMixerVolume: setting session mixer volume for mixer", mixer.id, "mode", mixer.mode, "volume", updatedTrackVolumeObject.volL);
|
||||
await jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedTrackVolumeObject);
|
||||
}
|
||||
}, [trackVolumeObject, faderHelpers, jamClient, setTrackVolumeObject]);
|
||||
|
|
@ -465,11 +471,18 @@ const useMixerHelper = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Ensure mixer objects have the correct mode set for proper persistence
|
||||
const mixerWithMode = mixer ? { ...mixer, mode } : null;
|
||||
const oppositeMode = mode === MIX_MODES.MASTER ? MIX_MODES.PERSONAL : MIX_MODES.MASTER;
|
||||
const oppositeMixerWithMode = oppositeMixer ? { ...oppositeMixer, mode: oppositeMode } : null;
|
||||
const vuMixerWithMode = vuMixer ? { ...vuMixer, mode } : null;
|
||||
const muteMixerWithMode = muteMixer ? { ...muteMixer, mode: muteMixer === oppositeMixer ? oppositeMode : mode } : null;
|
||||
|
||||
return {
|
||||
mixer: mixer,
|
||||
oppositeMixer: oppositeMixer,
|
||||
vuMixer: vuMixer,
|
||||
muteMixer: muteMixer
|
||||
mixer: mixerWithMode,
|
||||
oppositeMixer: oppositeMixerWithMode,
|
||||
vuMixer: vuMixerWithMode,
|
||||
muteMixer: muteMixerWithMode
|
||||
};
|
||||
}, [getMixerByTrackId, groupedMixersForClientId, personalMixers, allMixers]);
|
||||
|
||||
|
|
@ -520,7 +533,10 @@ const useMixerHelper = () => {
|
|||
}
|
||||
|
||||
for (const track of participant.tracks || []) {
|
||||
const mixerData = findMixerForTrack(participant.client_id, track, true, mixMode);
|
||||
// Get mixers for BOTH modes to support independent Audio Mix (personal) and Session Mix (master) controls
|
||||
const masterMixerData = findMixerForTrack(participant.client_id, track, true, MIX_MODES.MASTER);
|
||||
const personalMixerData = findMixerForTrack(participant.client_id, track, true, MIX_MODES.PERSONAL);
|
||||
const mixerData = mixMode === MIX_MODES.MASTER ? masterMixerData : personalMixerData;
|
||||
const hasMixer = !!mixerData.mixer;
|
||||
|
||||
const instrumentIcon = getInstrumentIcon45(track.instrument_id || track.instrument);
|
||||
|
|
@ -538,6 +554,9 @@ const useMixerHelper = () => {
|
|||
},
|
||||
mixerFinder: [participant.client_id, track, true],
|
||||
mixers: mixerData,
|
||||
// Store both master and personal mixers for independent control
|
||||
masterMixers: masterMixerData,
|
||||
personalMixers: personalMixerData,
|
||||
hasMixer,
|
||||
name,
|
||||
trackName,
|
||||
|
|
@ -690,11 +709,12 @@ const useMixerHelper = () => {
|
|||
const mute = useCallback(async (mixerId, mode, muting) => {
|
||||
if (mode == null) { mode = mixMode; }
|
||||
|
||||
const mixer = fillTrackVolumeObject(mixerId, mode);
|
||||
if (!mixer) return;
|
||||
const result = fillTrackVolumeObject(mixerId, mode);
|
||||
if (!result) return;
|
||||
|
||||
context.trackVolumeObject.mute = muting;
|
||||
await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, context.trackVolumeObject);
|
||||
const { volumeObj } = result;
|
||||
const updatedVolumeObj = { ...volumeObj, mute: muting };
|
||||
await context.jamClient.SessionSetTrackVolumeData(mixerId, mode, updatedVolumeObj);
|
||||
|
||||
const updatedMixer = getMixer(mixerId, mode);
|
||||
updatedMixer.mute = muting;
|
||||
|
|
@ -723,7 +743,6 @@ const useMixerHelper = () => {
|
|||
}
|
||||
const originalVolume = getOriginalVolume(mixers, gainType);
|
||||
|
||||
console.log("MixerHelper: faderChanged called", { data, mixers, gainType, controlGroup, originalVolume });
|
||||
|
||||
// Handle multiple mixers (master + personal pairs like web version)
|
||||
const mixerIds = mixers.map(m => m.id);
|
||||
|
|
@ -741,23 +760,26 @@ const useMixerHelper = () => {
|
|||
mode = i === 0 ? MIX_MODES.MASTER : MIX_MODES.PERSONAL;
|
||||
}
|
||||
|
||||
const mixer = fillTrackVolumeObject(m.id, mode, allMixers, broadcast);
|
||||
if (mixer == null) {
|
||||
const result = fillTrackVolumeObject(m.id, mode, allMixers, broadcast);
|
||||
if (result == null) {
|
||||
console.error("MixerHelper: faderChanged: mixer is null, skipping", m, gainType, controlGroup);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { mixer, volumeObj } = result;
|
||||
|
||||
// Handle relative volume adjustments for music category (matches web version)
|
||||
const relative = gainType === 'music' && (mixer.name === CategoryGroupIds.UserMedia || mixer.name === CategoryGroupIds.MediaTrack);
|
||||
|
||||
await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, allMixers);
|
||||
await setMixerVolume(mixer, data.percentage, relative, originalVolume, controlGroup, volumeObj);
|
||||
|
||||
// Redux: Update local mixer state via dispatch
|
||||
const newVolume = faderHelpers.convertPercentToAudioTaper(data.percentage);
|
||||
dispatch(updateMixer({
|
||||
mixerId: mixer.id,
|
||||
mode: mixer.mode,
|
||||
updates: {
|
||||
volume_left: trackVolumeObject.volL
|
||||
volume_left: newVolume
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
|
@ -779,8 +801,10 @@ const useMixerHelper = () => {
|
|||
const result = [];
|
||||
for (const mixer of mixers) {
|
||||
const broadcast = !(data.dragging);
|
||||
const filledMixer = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast);
|
||||
const fillResult = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers, broadcast);
|
||||
if (!fillResult) continue;
|
||||
|
||||
const { mixer: filledMixer } = fillResult;
|
||||
await setMixerPan(filledMixer, data.percentage);
|
||||
|
||||
const updatedMixer = getMixer(filledMixer.id, filledMixer.mode, allMixers);
|
||||
|
|
@ -797,16 +821,17 @@ const useMixerHelper = () => {
|
|||
}, [faderHelpers, panHelpers]);
|
||||
|
||||
const loopChanged = useCallback(async (mixer, shouldLoop) => {
|
||||
fillTrackVolumeObject(mixer.id, mixer.mode, allMixers);
|
||||
//context.trackVolumeObject.loop = shouldLoop;
|
||||
setTrackVolumeObject(prev => ({
|
||||
...prev,
|
||||
loop: shouldLoop
|
||||
}));
|
||||
await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, context.trackVolumeObject);
|
||||
const result = fillTrackVolumeObject(mixer.id, mixer.mode, allMixers);
|
||||
if (!result) return;
|
||||
|
||||
const { volumeObj } = result;
|
||||
const updatedVolumeObj = { ...volumeObj, loop: shouldLoop };
|
||||
|
||||
setTrackVolumeObject(updatedVolumeObj);
|
||||
await context.jamClient.SessionSetTrackVolumeData(mixer.id, mixer.mode, updatedVolumeObj);
|
||||
|
||||
const updatedMixer = getMixer(mixer.id, mixer.mode, allMixers);
|
||||
updatedMixer.loop = context.trackVolumeObject.loop;
|
||||
updatedMixer.loop = shouldLoop;
|
||||
}, [getMixer, fillTrackVolumeObject, allMixers, setTrackVolumeObject]);
|
||||
|
||||
const percentFromMixerValue = useCallback((min, max, value) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue