Skip to content

Commit e994e34

Browse files
committed
Add MIDI GUI tab and learn function
1 parent 043dafe commit e994e34

29 files changed

Lines changed: 2679 additions & 212 deletions

docs/JSON-RPC.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,40 @@ Results:
188188
| result.clients | array | The client list. See jamulusclient/clientListReceived for the format. |
189189

190190

191+
### jamulusclient/getMidiDevices
192+
193+
Returns a list of available MIDI input devices.
194+
195+
Parameters:
196+
197+
| Name | Type | Description |
198+
| --- | --- | --- |
199+
| params | object | No parameters (empty object). |
200+
201+
Results:
202+
203+
| Name | Type | Description |
204+
| --- | --- | --- |
205+
| result | array | Array of MIDI device name strings. |
206+
207+
208+
### jamulusclient/getMidiSettings
209+
210+
Returns all MIDI controller settings.
211+
212+
Parameters:
213+
214+
| Name | Type | Description |
215+
| --- | --- | --- |
216+
| params | object | No parameters (empty object). |
217+
218+
Results:
219+
220+
| Name | Type | Description |
221+
| --- | --- | --- |
222+
| result | object | MIDI settings object. |
223+
224+
191225
### jamulusclient/pollServerList
192226

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

242276

277+
### jamulusclient/setMidiSettings
278+
279+
Sets one or more MIDI controller settings.
280+
281+
Parameters:
282+
283+
| Name | Type | Description |
284+
| --- | --- | --- |
285+
| params | object | Any subset of MIDI settings fields to set. |
286+
287+
Results:
288+
289+
| Name | Type | Description |
290+
| --- | --- | --- |
291+
| result | string | Always "ok". |
292+
293+
243294
### jamulusclient/setName
244295

245296
Sets your name.

src/audiomixerboard.cpp

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,92 @@
2323
\******************************************************************************/
2424

2525
#include "audiomixerboard.h"
26+
#include <chrono>
27+
#include <deque>
28+
29+
namespace
30+
{
31+
// Per-channel MIDI pickup state
32+
struct MidiPickupState
33+
{
34+
std::deque<int> recentFader;
35+
std::deque<int> recentPan;
36+
std::chrono::steady_clock::time_point lastMidiTimeFader;
37+
std::chrono::steady_clock::time_point lastMidiTimePan;
38+
};
39+
40+
std::vector<MidiPickupState> g_midiPickupStates ( MAX_NUM_CHANNELS );
41+
std::vector<bool> g_midiPickupInitialized ( MAX_NUM_CHANNELS, false );
42+
std::vector<bool> g_midiPickupWaitingForPickup ( MAX_NUM_CHANNELS, false );
43+
44+
// Check for inactivity and reset pickup state if needed
45+
static void midiPickupInactivityCheck ( int iChannelIdx,
46+
std::chrono::steady_clock::time_point& lastMidiTime,
47+
std::deque<int>& pickupBuffer,
48+
std::vector<bool>& waitingFlag )
49+
{
50+
auto now = std::chrono::steady_clock::now();
51+
if ( lastMidiTime.time_since_epoch().count() > 0 )
52+
{
53+
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds> ( now - lastMidiTime ).count();
54+
if ( elapsed > MIDI_PICKUP_INACTIVITY_TIMEOUT_MS )
55+
{
56+
// Reset pickup state after inactivity
57+
waitingFlag[iChannelIdx] = true;
58+
pickupBuffer.clear();
59+
}
60+
}
61+
lastMidiTime = now;
62+
}
63+
64+
// Determine if MIDI value should be applied based on pickup logic
65+
template<typename T>
66+
static bool midiPickupShouldApply ( int midiValue, int currentValue, int tolerance, const std::deque<T>& recentMidiValues )
67+
{
68+
// Accept if within tolerance
69+
if ( std::abs ( midiValue - currentValue ) <= tolerance )
70+
return true;
71+
72+
// If buffer has at least 2 values, check if we're crossing the current value
73+
// Handles the case where rapid movement causes MIDI values to "skip over" the software value
74+
if ( recentMidiValues.size() >= 2 )
75+
{
76+
int prevMidi = recentMidiValues.back();
77+
// Check if current and previous MIDI values bracket the software value
78+
if ( ( prevMidi <= currentValue && midiValue >= currentValue ) || ( prevMidi >= currentValue && midiValue <= currentValue ) )
79+
return true;
80+
}
81+
82+
return false;
83+
}
84+
85+
// Try to apply MIDI value with pickup logic
86+
template<typename T>
87+
static bool midiPickupTryApply ( int midiValue, int currentValue, int tolerance, std::deque<T>& pickupBuffer, bool waitingForPickup )
88+
{
89+
if ( waitingForPickup )
90+
{
91+
// Create temp buffer with new value to test pickup logic
92+
std::deque<int> tempPickup = pickupBuffer;
93+
if ( tempPickup.size() >= MIDI_PICKUP_HISTORY )
94+
tempPickup.pop_front();
95+
tempPickup.push_back ( midiValue );
96+
97+
if ( !midiPickupShouldApply ( midiValue, currentValue, tolerance, tempPickup ) )
98+
return true; // Still waiting for pickup
99+
100+
// Picked up. Stop waiting
101+
waitingForPickup = false;
102+
}
103+
104+
// Update the pickup buffer
105+
if ( pickupBuffer.size() >= MIDI_PICKUP_HISTORY )
106+
pickupBuffer.pop_front();
107+
pickupBuffer.push_back ( midiValue );
108+
109+
return waitingForPickup;
110+
}
111+
} // namespace
26112

27113
/******************************************************************************\
28114
* CChanneFader *
@@ -1334,6 +1420,9 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector<CChannelInfo>& vecChanInf
13341420
}
13351421
Mutex.unlock(); // release mutex
13361422

1423+
// Ensure MIDI state is applied to faders during the connection process
1424+
SetMIDICtrlUsed ( pSettings->bUseMIDIController );
1425+
13371426
// sort the channels according to the selected sorting type
13381427
ChangeFaderOrder ( eChSortType );
13391428

@@ -1348,6 +1437,36 @@ void CAudioMixerBoard::SetFaderLevel ( const int iChannelIdx, const int iValue )
13481437
{
13491438
if ( vecpChanFader[static_cast<size_t> ( iChannelIdx )]->IsVisible() )
13501439
{
1440+
// Check for MIDI pickup mode
1441+
if ( pSettings && pSettings->bMIDIPickupMode && g_midiPickupInitialized[iChannelIdx] )
1442+
{
1443+
auto& midiState = g_midiPickupStates[iChannelIdx];
1444+
midiPickupInactivityCheck ( iChannelIdx,
1445+
midiState.lastMidiTimeFader,
1446+
midiState.recentFader,
1447+
g_midiPickupWaitingForPickup );
1448+
}
1449+
1450+
if ( pSettings && pSettings->bMIDIPickupMode )
1451+
{
1452+
// Initialize pickup state on first use
1453+
if ( !g_midiPickupInitialized[iChannelIdx] )
1454+
{
1455+
g_midiPickupInitialized[iChannelIdx] = true;
1456+
g_midiPickupWaitingForPickup[iChannelIdx] = true;
1457+
g_midiPickupStates[iChannelIdx].recentFader.clear();
1458+
}
1459+
1460+
auto& pickup = g_midiPickupStates[iChannelIdx].recentFader;
1461+
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetFaderLevel();
1462+
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
1463+
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
1464+
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;
1465+
1466+
if ( waiting )
1467+
return; // Don't apply the value yet, still waiting for pickup
1468+
}
1469+
13511470
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetFaderLevel ( iValue );
13521471
}
13531472
}
@@ -1360,6 +1479,36 @@ void CAudioMixerBoard::SetPanValue ( const int iChannelIdx, const int iValue )
13601479
{
13611480
if ( vecpChanFader[static_cast<size_t> ( iChannelIdx )]->IsVisible() )
13621481
{
1482+
// Check for MIDI pickup mode
1483+
if ( pSettings && pSettings->bMIDIPickupMode && g_midiPickupInitialized[iChannelIdx] )
1484+
{
1485+
auto& midiState = g_midiPickupStates[iChannelIdx];
1486+
midiPickupInactivityCheck ( iChannelIdx,
1487+
midiState.lastMidiTimePan,
1488+
midiState.recentPan,
1489+
g_midiPickupWaitingForPickup );
1490+
}
1491+
1492+
if ( pSettings && pSettings->bMIDIPickupMode )
1493+
{
1494+
// Initialize pickup state on first use
1495+
if ( !g_midiPickupInitialized[iChannelIdx] )
1496+
{
1497+
g_midiPickupInitialized[iChannelIdx] = true;
1498+
g_midiPickupWaitingForPickup[iChannelIdx] = true;
1499+
g_midiPickupStates[iChannelIdx].recentPan.clear();
1500+
}
1501+
1502+
auto& pickup = g_midiPickupStates[iChannelIdx].recentPan;
1503+
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetPanValue();
1504+
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
1505+
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
1506+
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;
1507+
1508+
if ( waiting )
1509+
return; // Don't apply the value yet, still waiting for pickup
1510+
}
1511+
13631512
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetPanValue ( iValue );
13641513
}
13651514
}
@@ -1403,6 +1552,13 @@ void CAudioMixerBoard::SetAllFaderLevelsToNewClientLevel()
14031552
// update flag to make sure the group values are all set to the
14041553
// same fader level now
14051554
vecpChanFader[i]->SetFaderLevel ( pSettings->iNewClientFaderLevel / 100.0 * AUD_MIX_FADER_MAX, true );
1555+
1556+
// Reset MIDI pickup state so pickup mode works with the new value
1557+
if ( g_midiPickupInitialized[i] )
1558+
{
1559+
g_midiPickupWaitingForPickup[i] = true;
1560+
g_midiPickupStates[i].recentFader.clear();
1561+
}
14061562
}
14071563
}
14081564
}
@@ -1552,6 +1708,13 @@ void CAudioMixerBoard::AutoAdjustAllFaderLevels()
15521708

15531709
// set fader level
15541710
vecpChanFader[i]->SetFaderLevel ( newFaderLevel, true );
1711+
1712+
// Reset MIDI pickup state so pickup mode works with the auto-adjusted value
1713+
if ( g_midiPickupInitialized[i] )
1714+
{
1715+
g_midiPickupWaitingForPickup[i] = true;
1716+
g_midiPickupStates[i].recentFader.clear();
1717+
}
15551718
}
15561719
}
15571720
}
@@ -1565,6 +1728,16 @@ void CAudioMixerBoard::SetMIDICtrlUsed ( const bool bMIDICtrlUsed )
15651728
{
15661729
vecpChanFader[i]->SetMIDICtrlUsed ( bMIDICtrlUsed );
15671730
}
1731+
1732+
// Reset MIDI pickup state when toggling MIDI control to
1733+
// ensure pickup mode works correctly when re-enabling MIDI
1734+
for ( size_t i = 0; i < MAX_NUM_CHANNELS; i++ )
1735+
{
1736+
g_midiPickupInitialized[i] = false;
1737+
g_midiPickupWaitingForPickup[i] = false;
1738+
g_midiPickupStates[i].recentFader.clear();
1739+
g_midiPickupStates[i].recentPan.clear();
1740+
}
15681741
}
15691742

15701743
void CAudioMixerBoard::StoreAllFaderSettings()
@@ -1601,6 +1774,16 @@ void CAudioMixerBoard::LoadAllFaderSettings()
16011774
vecpChanFader[i]->SetFaderIsSolo ( bStoredFaderIsSolo );
16021775
vecpChanFader[i]->SetFaderIsMute ( bStoredFaderIsMute );
16031776
vecpChanFader[i]->SetGroupID ( iGroupID ); // Must be the last to be set in the fader!
1777+
1778+
// Reset MIDI pickup state for this channel so pickup mode works with restored values
1779+
// Without this, if MIDI values arrived before settings were loaded, pickup might have
1780+
// already occurred at the wrong value, causing jumps when the controller is moved
1781+
if ( g_midiPickupInitialized[i] )
1782+
{
1783+
g_midiPickupWaitingForPickup[i] = true;
1784+
g_midiPickupStates[i].recentFader.clear();
1785+
g_midiPickupStates[i].recentPan.clear();
1786+
}
16041787
}
16051788
}
16061789
}

src/client.cpp

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@
2323
\******************************************************************************/
2424

2525
#include "client.h"
26+
#include "settings.h"
2627
#include "util.h"
2728

2829
/* Implementation *************************************************************/
2930
CClient::CClient ( const quint16 iPortNumber,
3031
const quint16 iQosNumber,
3132
const QString& strConnOnStartupAddress,
32-
const QString& strMIDISetup,
3333
const bool bNoAutoJackConnect,
3434
const QString& strNClientName,
3535
const bool bNEnableIPv6,
3636
const bool bNMuteMeInPersonalMix ) :
3737
ChannelInfo(),
3838
strClientName ( strNClientName ),
39+
pSignalHandler ( CSignalHandler::getSingletonP() ),
40+
pSettings ( nullptr ),
3941
Channel ( false ), /* we need a client channel -> "false" */
4042
CurOpusEncoder ( nullptr ),
4143
CurOpusDecoder ( nullptr ),
@@ -49,7 +51,7 @@ CClient::CClient ( const quint16 iPortNumber,
4951
bMuteOutStream ( false ),
5052
fMuteOutStreamGain ( 1.0f ),
5153
Socket ( &Channel, iPortNumber, iQosNumber, "", bNEnableIPv6 ),
52-
Sound ( AudioCallback, this, strMIDISetup, bNoAutoJackConnect, strNClientName ),
54+
Sound ( AudioCallback, this, bNoAutoJackConnect, strNClientName ),
5355
iAudioInFader ( AUD_FADER_IN_MIDDLE ),
5456
bReverbOnLeftChan ( false ),
5557
iReverbLevel ( 0 ),
@@ -68,8 +70,7 @@ CClient::CClient ( const quint16 iPortNumber,
6870
bJitterBufferOK ( true ),
6971
bEnableIPv6 ( bNEnableIPv6 ),
7072
bMuteMeInPersonalMix ( bNMuteMeInPersonalMix ),
71-
iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ),
72-
pSignalHandler ( CSignalHandler::getSingletonP() )
73+
iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL )
7374
{
7475
int iOpusError;
7576

@@ -173,6 +174,8 @@ CClient::CClient ( const quint16 iPortNumber,
173174

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

177+
QObject::connect ( &Sound, &CSoundBase::MidiCCReceived, this, [this] ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } );
178+
176179
// start timer so that elapsed time works
177180
PreciseTime.start();
178181

@@ -193,6 +196,29 @@ CClient::CClient ( const quint16 iPortNumber,
193196
}
194197
}
195198

199+
// MIDI setup will be handled after settings are assigned
200+
void CClient::SetSettings ( CClientSettings* settings )
201+
{
202+
pSettings = settings;
203+
204+
// Apply MIDI settings
205+
Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel );
206+
Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset,
207+
pSettings->bMidiFaderEnabled ? pSettings->iMidiFaderCount : 0,
208+
pSettings->iMidiPanOffset,
209+
pSettings->bMidiPanEnabled ? pSettings->iMidiPanCount : 0,
210+
pSettings->iMidiSoloOffset,
211+
pSettings->bMidiSoloEnabled ? pSettings->iMidiSoloCount : 0,
212+
pSettings->iMidiMuteOffset,
213+
pSettings->bMidiMuteEnabled ? pSettings->iMidiMuteCount : 0,
214+
pSettings->bMidiMuteMyselfEnabled ? pSettings->iMidiMuteMyself : 0 );
215+
if ( !pSettings->strMidiDevice.isEmpty() )
216+
{
217+
Sound.SetMIDIDevice ( pSettings->strMidiDevice );
218+
}
219+
Sound.EnableMIDI ( pSettings->bUseMIDIController );
220+
}
221+
196222
CClient::~CClient()
197223
{
198224
// if we were running, stop sound device
@@ -1547,6 +1573,8 @@ void CClient::FreeClientChannel ( const int iServerChannelID )
15471573
*/
15481574
}
15491575

1576+
void CClient::OnMidiCCReceived ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); }
1577+
15501578
// find, and optionally create, a client channel for the supplied server channel ID
15511579
// returns a client channel ID or INVALID_INDEX
15521580
int CClient::FindClientChannel ( const int iServerChannelID, const bool bCreateIfNew )

0 commit comments

Comments
 (0)