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:
Nuwan 2026-02-16 15:56:46 +05:30
parent aa731c96d0
commit 78cdafbb8a
5 changed files with 91 additions and 57 deletions

View File

@ -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}

View File

@ -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">

View File

@ -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}

View File

@ -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;

View File

@ -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) => {