jam-cloud/jam-ui/TRACK_CONFIG_QUESTIONS_ANSW...

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';
};

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:

  1. First call (1s): Wait for native client initialization and initial mixer setup
  2. Second call (1.4s): React to any auto-configuration from native client
  3. 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 syncTracksToServer call)
  • 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);
  }
};

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:

  1. App loads → jamClient proxy created (fake or real)
  2. User logs in → Temporary UUID assigned
  3. WebSocket connects → Official clientId received in loggedIn callback
  4. Session joinedsessionJoined = true in Redux
  5. 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_id to 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 handleInstrumentSave in JKSessionMyTrack.js

Question 4: Initialization Guards

  • Check jamClient?.clientID exists
  • Check sessionId exists
  • Check sessionJoined === true
  • Return early if any prerequisite missing
  • Log skip reasons for debugging

Next Steps

  1. Create src/services/trackSyncService.js with guards and payload builder
  2. Create src/hooks/useTrackSync.js with debouncing
  3. Modify JKSessionScreen.js to add 3-call pattern on join
  4. Modify JKSessionMyTrack.js to sync on instrument change
  5. Modify useMediaActions.js to sync on media toggle
  6. Write tests in test/track-sync/track-configuration.spec.ts

All questions answered! Ready to implement. 🚀


Generated: 2026-01-21 Status: All questions resolved