14 KiB
Track Configuration API - Open Questions Answered
Date: 2026-01-21 Status: ✅ All questions resolved through codebase exploration
Question 1: How do we get track data from native client (jamClient)?
✅ ANSWER: Use jamClient.SessionGetAllControlState()
Primary Method:
const allTracks = await jamClient.SessionGetAllControlState(true);
// Parameter: true for master mix, false for personal mix
Returns: Array of track objects with full mixer state
Concrete Implementation: src/hooks/useTrackHelpers.js:152-162
const getTracks = useCallback(async (groupId, allTracks) => {
const tracks = [];
if (!allTracks) {
allTracks = await jamClient.SessionGetAllControlState(true);
}
for (const track of allTracks) {
if (track.group_id === groupId) {
tracks.push(track);
}
}
return tracks;
}, [jamClient]);
Track Object Structure (from native client)
Source: src/fakeJamClient.js:807-826
{
client_id: "a2d34590-5e77-4f04-a47e-a2cbb2871baf", // Client UUID
group_id: 4, // ChannelGroupIds.AudioInputMusicGroup
id: "mixer-uuid", // Mixer ID
master: true, // Master vs personal mix
media_type: "AudioInputMusic", // Track type
monitor: false,
mute: false,
name: "Guitar",
range_high: 20,
range_low: -80,
record: true,
stereo: true, // true = stereo, false = mono
volume_left: 0,
volume_right: 0,
pan: 0,
instrument_id: 10, // Client-side instrument ID (numeric)
mode: true,
rid: "resource-uuid" // Resource ID
}
Filtering Tracks by Type
Use group_id from src/helpers/globals.js:338-382:
// User audio tracks
const userTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.AudioInputMusicGroup); // 4
// Metronome track
const metronome = allTracks.find(t => t.group_id === ChannelGroupIds.MetronomeGroup); // 16
// Backing tracks
const backingTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.BackingTrackGroup); // 6
// JamTracks
const jamTracks = allTracks.filter(t => t.group_id === ChannelGroupIds.JamTrackGroup); // 15
Converting Native Tracks to API Format
Source: src/hooks/useTrackHelpers.js:71-91
const buildTrackForAPI = (track) => {
// Map client instrument ID (numeric) to server instrument ID (string)
const instrumentId = getInstrumentServerIdFromClientId(track.instrument_id);
return {
client_track_id: track.id, // Mixer ID
client_resource_id: track.rid, // Resource ID
instrument_id: instrumentId, // "acoustic guitar", "drums", etc.
sound: track.stereo ? "stereo" : "mono"
};
};
Instrument ID Mapping: src/helpers/globals.js:183-260
// Example mappings:
server_to_client_instrument_map = {
"Acoustic Guitar": { client_id: 10, server_id: "acoustic guitar" },
"Bass Guitar": { client_id: 20, server_id: "bass guitar" },
"Drums": { client_id: 40, server_id: "drums" },
"Electric Guitar": { client_id: 50, server_id: "electric guitar" },
"Piano": { client_id: 140, server_id: "piano" },
"Voice": { client_id: 240, server_id: "vocals" },
// ... 27 instruments total
};
// Helper function:
export const getInstrumentServerIdFromClientId = (clientId) => {
const instrument = Object.values(server_to_client_instrument_map)
.find(inst => inst.client_id === clientId);
return instrument?.server_id || 'other';
};
Other Track-Related Methods
JamClient proxy methods (src/jamClientProxy.js):
// Backing tracks
jamClient.getBackingTrackList() // Get available backing track files
// JamTracks
jamClient.JamTrackGetTracks() // Get JamTrack library
// Individual track info
jamClient.TrackGetInstrument(trackNumber) // Get track's instrument
jamClient.TrackSetInstrument(trackNumber, instrumentId) // Set track's instrument
Question 2: Should we optimize the 3-call pattern?
✅ RECOMMENDATION: Start with 3 calls, optimize to 1 later
Legacy pattern (from test fixtures):
- Call 1: ~1 second after joining (initial track setup)
- Call 2: ~400ms later at 1.4s (track refinement)
- Call 3: ~5 seconds later at 6s (final configuration)
Why 3 calls in legacy?
Hypothesis from examining the flow:
- First call (1s): Wait for native client initialization and initial mixer setup
- Second call (1.4s): React to any auto-configuration from native client
- Third call (6s): Finalize after all components loaded and WebSocket fully established
This pattern likely evolved organically to handle:
- Async native client initialization
- Gradual mixer state population
- WebSocket connection establishment timing
- Component mount order dependencies
Implementation Strategy
Phase 1: Match legacy exactly (safety first)
// In JKSessionScreen.js or useSessionModel.js
useEffect(() => {
if (sessionJoined && clientId) {
// First sync: Initial setup
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 1000);
// Second sync: Refinement
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 1400);
// Third sync: Final config
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 6000);
}
}, [sessionJoined, clientId]);
Phase 2: Optimize (after verification)
Once we confirm tracks sync correctly, test reducing to single call:
useEffect(() => {
if (sessionJoined && clientId && nativeClientReady) {
// Single sync after everything initialized
setTimeout(() => {
dispatch(syncTracksToServer(sessionId));
}, 2000); // Wait for full initialization
}
}, [sessionJoined, clientId, nativeClientReady]);
Monitor:
- Does single call capture all tracks correctly?
- Are there any race conditions?
- Do other participants see tracks immediately?
Decision: ✅ Start with 3 calls
Rationale:
- Legacy pattern is battle-tested
- Minimal risk of missing tracks
- Can optimize later without breaking functionality
- Matches existing behavior for easier debugging
Future optimization triggers:
- After successful deployment and monitoring
- If performance metrics show excessive API calls
- If we can reliably detect "all initialized" state
Question 3: Instrument selection UI - does it exist?
✅ ANSWER: YES, fully implemented!
Component: src/components/client/JKSessionInstrumentModal.js
How It Works
1. User clicks on their track (src/components/client/JKSessionMyTrack.js:81-86)
const handleInstrumentSelect = () => {
setShowInstrumentModal(true);
};
<div onClick={handleInstrumentSelect}>
<InstrumentIcon instrumentId={instrumentId} />
</div>
2. Modal opens with instrument list (27 instruments total)
// JKSessionInstrumentModal.js:24
const instrumentList = listInstruments().sort((a, b) =>
a.description.localeCompare(b.description)
);
// Displays:
// - Acoustic Guitar
// - Bass Guitar
// - Cello
// - Drums
// - Electric Guitar
// - ... (27 total)
3. User selects new instrument
// JKSessionInstrumentModal.js:117
const handleSave = async () => {
await jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, selectedInstrument);
onSave(selectedInstrument);
onRequestClose();
};
4. Parent component receives callback (JKSessionMyTrack.js:115-118)
const handleInstrumentSave = instrumentId => {
jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentId);
setShowInstrumentModal(false);
// TODO: Trigger track sync here!
};
Where to Add Track Sync
Modify JKSessionMyTrack.js:115-118:
const handleInstrumentSave = async (instrumentId) => {
// Update native client
await jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentId);
// Sync to server
dispatch(syncTracksToServer(sessionId)); // ADD THIS LINE
// Close modal
setShowInstrumentModal(false);
};
Instrument List Source
Function: src/helpers/globals.js:183-260
export const listInstruments = () => [
{ id: 10, description: "Acoustic Guitar", server_id: "acoustic guitar" },
{ id: 20, description: "Bass Guitar", server_id: "bass guitar" },
{ id: 30, description: "Cello", server_id: "cello" },
{ id: 40, description: "Drums", server_id: "drums" },
{ id: 50, description: "Electric Guitar", server_id: "electric guitar" },
{ id: 60, description: "Fiddle", server_id: "fiddle" },
// ... 21 more instruments
{ id: 250, description: "Other", server_id: "other" }
];
Current Limitation
⚠️ Instrument changes are NOT currently synced to server!
After user changes instrument:
- ✅ Native client is updated (
TrackSetInstrument) - ✅ UI shows new instrument icon
- ❌ Server is NOT notified (missing
syncTracksToServercall) - ❌ Other participants don't see the change
This is exactly what we need to fix!
Question 4: What if native client not initialized yet?
✅ ANSWER: Check jamClient.clientID before syncing
Client ID Initialization Flow
1. Initial UUID generation (src/hooks/useJamServer.js:69-70)
// Temporary UUID until login completes
const clientId = generateUUID(); // Not from jamClient.clientID() yet
server.current.clientId = clientId;
Note: jamClient.clientID() currently returns empty string, so we generate UUID as fallback.
2. Login assigns official clientId (src/helpers/JamServer.js:228-240)
loggedIn(header, payload) {
this.clientID = payload.client_id; // From WebSocket handshake
if (typeof window !== 'undefined' && window.jamClient !== undefined) {
window.jamClient.clientID = this.clientID; // Set on jamClient proxy
}
this.app.clientId = payload.client_id;
}
3. Access throughout app (src/hooks/useSessionModel.js:122)
const clientId = jamClient?.clientID || jamClient?.GetClientID() || 'unknown';
Guard Conditions for Track Sync
Check these conditions before calling API:
const shouldSyncTracks = () => {
// 1. Client ID must exist
if (!jamClient?.clientID && !jamClient?.GetClientID()) {
console.warn('Track sync skipped: clientId not available');
return false;
}
// 2. Session must be active
if (!sessionId) {
console.warn('Track sync skipped: no active session');
return false;
}
// 3. User must have joined session
if (!sessionJoined) {
console.warn('Track sync skipped: session not joined yet');
return false;
}
// 4. Native client must be connected
if (!jamClient || !window.jamClient) {
console.warn('Track sync skipped: native client not available');
return false;
}
return true;
};
// Usage:
const syncTracks = async () => {
if (!shouldSyncTracks()) return;
try {
await dispatch(syncTracksToServer(sessionId));
} catch (error) {
console.error('Track sync failed:', error);
}
};
Recommended Implementation
In src/services/trackSyncService.js:
export const syncTracksToServer = (sessionId) => async (dispatch, getState) => {
const state = getState();
const { jamClient } = state.jamClient;
const { sessionJoined } = state.activeSession;
// Guard: Check all prerequisites
const clientId = jamClient?.clientID || jamClient?.GetClientID();
if (!clientId) {
console.warn('[Track Sync] Skipped: Client ID not available');
return { skipped: true, reason: 'no_client_id' };
}
if (!sessionId) {
console.warn('[Track Sync] Skipped: No session ID');
return { skipped: true, reason: 'no_session_id' };
}
if (!sessionJoined) {
console.warn('[Track Sync] Skipped: Session not joined');
return { skipped: true, reason: 'session_not_joined' };
}
// Proceed with sync
const payload = await buildTrackSyncPayload(state, sessionId);
try {
dispatch(setTrackSyncStatus('syncing'));
const response = await putTrackSyncChange({ id: sessionId, ...payload });
dispatch(setTrackSyncStatus('success'));
dispatch(setUserTracks(response.tracks));
dispatch(setBackingTrackData(response.backing_tracks));
console.log('[Track Sync] Success:', response);
return { success: true, response };
} catch (error) {
dispatch(setTrackSyncStatus('error'));
dispatch(showError('Failed to sync tracks'));
console.error('[Track Sync] Failed:', error);
return { success: false, error };
}
};
Initialization Timeline
Typical initialization sequence:
- App loads → jamClient proxy created (fake or real)
- User logs in → Temporary UUID assigned
- WebSocket connects → Official clientId received in
loggedIncallback - Session joined →
sessionJoined = truein Redux - ✅ Ready to sync tracks → All guards pass
Wait for initialization:
useEffect(() => {
if (sessionJoined && jamClient?.clientID) {
// All prerequisites met, safe to sync
const timer = setTimeout(() => {
syncTracks();
}, 1000);
return () => clearTimeout(timer);
}
}, [sessionJoined, jamClient?.clientID]);
Summary: Implementation Checklist
✅ Question 1: Getting Track Data
- Use
jamClient.SessionGetAllControlState(true) - Filter by
group_idto categorize tracks - Map client instrument IDs to server IDs
- Build API payload with
client_track_id,client_resource_id,instrument_id,sound
✅ Question 2: 3-Call Pattern
- Start with legacy's 3-call pattern (1s, 1.4s, 6s)
- Optimize to 1 call after verification
- Monitor for race conditions
✅ Question 3: Instrument Selection
- UI exists:
JKSessionInstrumentModal.js - Add track sync after instrument change
- Modify
handleInstrumentSaveinJKSessionMyTrack.js
✅ Question 4: Initialization Guards
- Check
jamClient?.clientIDexists - Check
sessionIdexists - Check
sessionJoined === true - Return early if any prerequisite missing
- Log skip reasons for debugging
Next Steps
- Create
src/services/trackSyncService.jswith guards and payload builder - Create
src/hooks/useTrackSync.jswith debouncing - Modify
JKSessionScreen.jsto add 3-call pattern on join - Modify
JKSessionMyTrack.jsto sync on instrument change - Modify
useMediaActions.jsto sync on media toggle - Write tests in
test/track-sync/track-configuration.spec.ts
All questions answered! Ready to implement. 🚀
Generated: 2026-01-21 Status: ✅ All questions resolved