Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/JSON-RPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,40 @@ Results:
| result.clients | array | The client list. See jamulusclient/clientListReceived for the format. |


### jamulusclient/getMidiDevices

Returns a list of available MIDI input devices.

Parameters:

| Name | Type | Description |
| --- | --- | --- |
| params | object | No parameters (empty object). |

Results:

| Name | Type | Description |
| --- | --- | --- |
| result | array | Array of MIDI device name strings. |


### jamulusclient/getMidiSettings

Returns all MIDI controller settings.

Parameters:

| Name | Type | Description |
| --- | --- | --- |
| params | object | No parameters (empty object). |

Results:

| Name | Type | Description |
| --- | --- | --- |
| result | object | MIDI settings object. |


### jamulusclient/pollServerList

Request list of servers in a directory.
Expand Down Expand Up @@ -240,6 +274,23 @@ Results:
| result | string | Always "ok". |


### jamulusclient/setMidiSettings

Sets one or more MIDI controller settings.

Parameters:

| Name | Type | Description |
| --- | --- | --- |
| params | object | Any subset of MIDI settings fields to set. |

Results:

| Name | Type | Description |
| --- | --- | --- |
| result | string | Always "ok". |


### jamulusclient/setName

Sets your name.
Expand Down
177 changes: 177 additions & 0 deletions src/audiomixerboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,92 @@
\******************************************************************************/

#include "audiomixerboard.h"
#include <chrono>
#include <deque>

namespace
{
// Per-channel MIDI pickup state
struct MidiPickupState
{
std::deque<int> recentFader;
std::deque<int> recentPan;
std::chrono::steady_clock::time_point lastMidiTimeFader;
std::chrono::steady_clock::time_point lastMidiTimePan;
};

std::vector<MidiPickupState> g_midiPickupStates ( MAX_NUM_CHANNELS );
std::vector<bool> g_midiPickupInitialized ( MAX_NUM_CHANNELS, false );
std::vector<bool> g_midiPickupWaitingForPickup ( MAX_NUM_CHANNELS, false );

// Check for inactivity and reset pickup state if needed
static void midiPickupInactivityCheck ( int iChannelIdx,
std::chrono::steady_clock::time_point& lastMidiTime,
std::deque<int>& pickupBuffer,
std::vector<bool>& waitingFlag )
{
auto now = std::chrono::steady_clock::now();
if ( lastMidiTime.time_since_epoch().count() > 0 )
{
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds> ( now - lastMidiTime ).count();
if ( elapsed > MIDI_PICKUP_INACTIVITY_TIMEOUT_MS )
{
// Reset pickup state after inactivity
waitingFlag[iChannelIdx] = true;
pickupBuffer.clear();
}
}
lastMidiTime = now;
}

// Determine if MIDI value should be applied based on pickup logic
template<typename T>
static bool midiPickupShouldApply ( int midiValue, int currentValue, int tolerance, const std::deque<T>& recentMidiValues )
{
// Accept if within tolerance
if ( std::abs ( midiValue - currentValue ) <= tolerance )
return true;

// If buffer has at least 2 values, check if we're crossing the current value
// Handles the case where rapid movement causes MIDI values to "skip over" the software value
if ( recentMidiValues.size() >= 2 )
{
int prevMidi = recentMidiValues.back();
// Check if current and previous MIDI values bracket the software value
if ( ( prevMidi <= currentValue && midiValue >= currentValue ) || ( prevMidi >= currentValue && midiValue <= currentValue ) )
return true;
}

return false;
}

// Try to apply MIDI value with pickup logic
template<typename T>
static bool midiPickupTryApply ( int midiValue, int currentValue, int tolerance, std::deque<T>& pickupBuffer, bool waitingForPickup )
{
if ( waitingForPickup )
{
// Create temp buffer with new value to test pickup logic
std::deque<int> tempPickup = pickupBuffer;
if ( tempPickup.size() >= MIDI_PICKUP_HISTORY )
tempPickup.pop_front();
tempPickup.push_back ( midiValue );

if ( !midiPickupShouldApply ( midiValue, currentValue, tolerance, tempPickup ) )
return true; // Still waiting for pickup

// Picked up. Stop waiting
waitingForPickup = false;
}

// Update the pickup buffer
if ( pickupBuffer.size() >= MIDI_PICKUP_HISTORY )
pickupBuffer.pop_front();
pickupBuffer.push_back ( midiValue );

return waitingForPickup;
}
} // namespace

/******************************************************************************\
* CChanneFader *
Expand Down Expand Up @@ -1334,6 +1420,9 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector<CChannelInfo>& vecChanInf
}
Mutex.unlock(); // release mutex

// Ensure MIDI state is applied to faders during the connection process
SetMIDICtrlUsed ( pSettings->bUseMIDIController );

// sort the channels according to the selected sorting type
ChangeFaderOrder ( eChSortType );

Expand All @@ -1348,6 +1437,33 @@ void CAudioMixerBoard::SetFaderLevel ( const int iChannelIdx, const int iValue )
{
if ( vecpChanFader[static_cast<size_t> ( iChannelIdx )]->IsVisible() )
{
// Check for MIDI pickup mode
if ( pSettings && pSettings->bMIDIPickupMode && g_midiPickupInitialized[iChannelIdx] )
{
auto& midiState = g_midiPickupStates[iChannelIdx];
midiPickupInactivityCheck ( iChannelIdx, midiState.lastMidiTimeFader, midiState.recentFader, g_midiPickupWaitingForPickup );
}

if ( pSettings && pSettings->bMIDIPickupMode )
{
// Initialize pickup state on first use
if ( !g_midiPickupInitialized[iChannelIdx] )
{
g_midiPickupInitialized[iChannelIdx] = true;
g_midiPickupWaitingForPickup[iChannelIdx] = true;
g_midiPickupStates[iChannelIdx].recentFader.clear();
}

auto& pickup = g_midiPickupStates[iChannelIdx].recentFader;
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetFaderLevel();
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;

if ( waiting )
return; // Don't apply the value yet, still waiting for pickup
}

vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetFaderLevel ( iValue );
}
}
Expand All @@ -1360,6 +1476,33 @@ void CAudioMixerBoard::SetPanValue ( const int iChannelIdx, const int iValue )
{
if ( vecpChanFader[static_cast<size_t> ( iChannelIdx )]->IsVisible() )
{
// Check for MIDI pickup mode
if ( pSettings && pSettings->bMIDIPickupMode && g_midiPickupInitialized[iChannelIdx] )
{
auto& midiState = g_midiPickupStates[iChannelIdx];
midiPickupInactivityCheck ( iChannelIdx, midiState.lastMidiTimePan, midiState.recentPan, g_midiPickupWaitingForPickup );
}

if ( pSettings && pSettings->bMIDIPickupMode )
{
// Initialize pickup state on first use
if ( !g_midiPickupInitialized[iChannelIdx] )
{
g_midiPickupInitialized[iChannelIdx] = true;
g_midiPickupWaitingForPickup[iChannelIdx] = true;
g_midiPickupStates[iChannelIdx].recentPan.clear();
}

auto& pickup = g_midiPickupStates[iChannelIdx].recentPan;
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetPanValue();
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;

if ( waiting )
return; // Don't apply the value yet, still waiting for pickup
}

vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetPanValue ( iValue );
}
}
Expand Down Expand Up @@ -1403,6 +1546,13 @@ void CAudioMixerBoard::SetAllFaderLevelsToNewClientLevel()
// update flag to make sure the group values are all set to the
// same fader level now
vecpChanFader[i]->SetFaderLevel ( pSettings->iNewClientFaderLevel / 100.0 * AUD_MIX_FADER_MAX, true );

// Reset MIDI pickup state so pickup mode works with the new value
if ( g_midiPickupInitialized[i] )
{
g_midiPickupWaitingForPickup[i] = true;
g_midiPickupStates[i].recentFader.clear();
}
}
}
}
Expand Down Expand Up @@ -1552,6 +1702,13 @@ void CAudioMixerBoard::AutoAdjustAllFaderLevels()

// set fader level
vecpChanFader[i]->SetFaderLevel ( newFaderLevel, true );

// Reset MIDI pickup state so pickup mode works with the auto-adjusted value
if ( g_midiPickupInitialized[i] )
{
g_midiPickupWaitingForPickup[i] = true;
g_midiPickupStates[i].recentFader.clear();
}
}
}
}
Expand All @@ -1565,6 +1722,16 @@ void CAudioMixerBoard::SetMIDICtrlUsed ( const bool bMIDICtrlUsed )
{
vecpChanFader[i]->SetMIDICtrlUsed ( bMIDICtrlUsed );
}

// Reset MIDI pickup state when toggling MIDI control to
// ensure pickup mode works correctly when re-enabling MIDI
for ( size_t i = 0; i < MAX_NUM_CHANNELS; i++ )
{
g_midiPickupInitialized[i] = false;
g_midiPickupWaitingForPickup[i] = false;
g_midiPickupStates[i].recentFader.clear();
g_midiPickupStates[i].recentPan.clear();
}
}

void CAudioMixerBoard::StoreAllFaderSettings()
Expand Down Expand Up @@ -1601,6 +1768,16 @@ void CAudioMixerBoard::LoadAllFaderSettings()
vecpChanFader[i]->SetFaderIsSolo ( bStoredFaderIsSolo );
vecpChanFader[i]->SetFaderIsMute ( bStoredFaderIsMute );
vecpChanFader[i]->SetGroupID ( iGroupID ); // Must be the last to be set in the fader!

// Reset MIDI pickup state for this channel so pickup mode works with restored values
// Without this, if MIDI values arrived before settings were loaded, pickup might have
// already occurred at the wrong value, causing jumps when the controller is moved
if ( g_midiPickupInitialized[i] )
{
g_midiPickupWaitingForPickup[i] = true;
g_midiPickupStates[i].recentFader.clear();
g_midiPickupStates[i].recentPan.clear();
}
}
}
}
Expand Down
36 changes: 32 additions & 4 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@
\******************************************************************************/

#include "client.h"
#include "settings.h"
#include "util.h"

/* Implementation *************************************************************/
CClient::CClient ( const quint16 iPortNumber,
const quint16 iQosNumber,
const QString& strConnOnStartupAddress,
const QString& strMIDISetup,
const bool bNoAutoJackConnect,
const QString& strNClientName,
const bool bNEnableIPv6,
const bool bNMuteMeInPersonalMix ) :
ChannelInfo(),
strClientName ( strNClientName ),
pSignalHandler ( CSignalHandler::getSingletonP() ),
pSettings ( nullptr ),
Channel ( false ), /* we need a client channel -> "false" */
CurOpusEncoder ( nullptr ),
CurOpusDecoder ( nullptr ),
Expand All @@ -49,7 +51,7 @@ CClient::CClient ( const quint16 iPortNumber,
bMuteOutStream ( false ),
fMuteOutStreamGain ( 1.0f ),
Socket ( &Channel, iPortNumber, iQosNumber, "", bNEnableIPv6 ),
Sound ( AudioCallback, this, strMIDISetup, bNoAutoJackConnect, strNClientName ),
Sound ( AudioCallback, this, bNoAutoJackConnect, strNClientName ),
iAudioInFader ( AUD_FADER_IN_MIDDLE ),
bReverbOnLeftChan ( false ),
iReverbLevel ( 0 ),
Expand All @@ -68,8 +70,7 @@ CClient::CClient ( const quint16 iPortNumber,
bJitterBufferOK ( true ),
bEnableIPv6 ( bNEnableIPv6 ),
bMuteMeInPersonalMix ( bNMuteMeInPersonalMix ),
iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ),
pSignalHandler ( CSignalHandler::getSingletonP() )
iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL )
{
int iOpusError;

Expand Down Expand Up @@ -173,6 +174,8 @@ CClient::CClient ( const quint16 iPortNumber,

QObject::connect ( pSignalHandler, &CSignalHandler::HandledSignal, this, &CClient::OnHandledSignal );

QObject::connect ( &Sound, &CSoundBase::MidiCCReceived, this, [this] ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } );

// start timer so that elapsed time works
PreciseTime.start();

Expand All @@ -193,6 +196,29 @@ CClient::CClient ( const quint16 iPortNumber,
}
}

// MIDI setup will be handled after settings are assigned
void CClient::SetSettings ( CClientSettings* settings )
{
pSettings = settings;

// Apply MIDI settings
Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel );
Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset,
pSettings->bMidiFaderEnabled ? pSettings->iMidiFaderCount : 0,
pSettings->iMidiPanOffset,
pSettings->bMidiPanEnabled ? pSettings->iMidiPanCount : 0,
pSettings->iMidiSoloOffset,
pSettings->bMidiSoloEnabled ? pSettings->iMidiSoloCount : 0,
pSettings->iMidiMuteOffset,
pSettings->bMidiMuteEnabled ? pSettings->iMidiMuteCount : 0,
pSettings->bMidiMuteMyselfEnabled ? pSettings->iMidiMuteMyself : 0 );
if ( !pSettings->strMidiDevice.isEmpty() )
{
Sound.SetMIDIDevice ( pSettings->strMidiDevice );
}
Sound.EnableMIDI ( pSettings->bUseMIDIController );
}

CClient::~CClient()
{
// if we were running, stop sound device
Expand Down Expand Up @@ -1547,6 +1573,8 @@ void CClient::FreeClientChannel ( const int iServerChannelID )
*/
}

void CClient::OnMidiCCReceived ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); }

// find, and optionally create, a client channel for the supplied server channel ID
// returns a client channel ID or INVALID_INDEX
int CClient::FindClientChannel ( const int iServerChannelID, const bool bCreateIfNew )
Expand Down
Loading