Skip to content

Commit 8d1cf47

Browse files
authored
Merge pull request jamulussoftware#3502 from ignotus666/midi-gui-learn
Add MIDI GUI tab and learn function
2 parents 2f741e6 + b03c422 commit 8d1cf47

29 files changed

+2673
-212
lines changed

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: 177 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,33 @@ 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, midiState.lastMidiTimeFader, midiState.recentFader, g_midiPickupWaitingForPickup );
1445+
}
1446+
1447+
if ( pSettings && pSettings->bMIDIPickupMode )
1448+
{
1449+
// Initialize pickup state on first use
1450+
if ( !g_midiPickupInitialized[iChannelIdx] )
1451+
{
1452+
g_midiPickupInitialized[iChannelIdx] = true;
1453+
g_midiPickupWaitingForPickup[iChannelIdx] = true;
1454+
g_midiPickupStates[iChannelIdx].recentFader.clear();
1455+
}
1456+
1457+
auto& pickup = g_midiPickupStates[iChannelIdx].recentFader;
1458+
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetFaderLevel();
1459+
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
1460+
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
1461+
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;
1462+
1463+
if ( waiting )
1464+
return; // Don't apply the value yet, still waiting for pickup
1465+
}
1466+
13511467
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetFaderLevel ( iValue );
13521468
}
13531469
}
@@ -1360,6 +1476,33 @@ void CAudioMixerBoard::SetPanValue ( const int iChannelIdx, const int iValue )
13601476
{
13611477
if ( vecpChanFader[static_cast<size_t> ( iChannelIdx )]->IsVisible() )
13621478
{
1479+
// Check for MIDI pickup mode
1480+
if ( pSettings && pSettings->bMIDIPickupMode && g_midiPickupInitialized[iChannelIdx] )
1481+
{
1482+
auto& midiState = g_midiPickupStates[iChannelIdx];
1483+
midiPickupInactivityCheck ( iChannelIdx, midiState.lastMidiTimePan, midiState.recentPan, g_midiPickupWaitingForPickup );
1484+
}
1485+
1486+
if ( pSettings && pSettings->bMIDIPickupMode )
1487+
{
1488+
// Initialize pickup state on first use
1489+
if ( !g_midiPickupInitialized[iChannelIdx] )
1490+
{
1491+
g_midiPickupInitialized[iChannelIdx] = true;
1492+
g_midiPickupWaitingForPickup[iChannelIdx] = true;
1493+
g_midiPickupStates[iChannelIdx].recentPan.clear();
1494+
}
1495+
1496+
auto& pickup = g_midiPickupStates[iChannelIdx].recentPan;
1497+
int current = vecpChanFader[static_cast<size_t> ( iChannelIdx )]->GetPanValue();
1498+
bool waiting = g_midiPickupWaitingForPickup[iChannelIdx];
1499+
waiting = midiPickupTryApply ( iValue, current, MIDI_PICKUP_TOLERANCE, pickup, waiting );
1500+
g_midiPickupWaitingForPickup[iChannelIdx] = waiting;
1501+
1502+
if ( waiting )
1503+
return; // Don't apply the value yet, still waiting for pickup
1504+
}
1505+
13631506
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetPanValue ( iValue );
13641507
}
13651508
}
@@ -1403,6 +1546,13 @@ void CAudioMixerBoard::SetAllFaderLevelsToNewClientLevel()
14031546
// update flag to make sure the group values are all set to the
14041547
// same fader level now
14051548
vecpChanFader[i]->SetFaderLevel ( pSettings->iNewClientFaderLevel / 100.0 * AUD_MIX_FADER_MAX, true );
1549+
1550+
// Reset MIDI pickup state so pickup mode works with the new value
1551+
if ( g_midiPickupInitialized[i] )
1552+
{
1553+
g_midiPickupWaitingForPickup[i] = true;
1554+
g_midiPickupStates[i].recentFader.clear();
1555+
}
14061556
}
14071557
}
14081558
}
@@ -1552,6 +1702,13 @@ void CAudioMixerBoard::AutoAdjustAllFaderLevels()
15521702

15531703
// set fader level
15541704
vecpChanFader[i]->SetFaderLevel ( newFaderLevel, true );
1705+
1706+
// Reset MIDI pickup state so pickup mode works with the auto-adjusted value
1707+
if ( g_midiPickupInitialized[i] )
1708+
{
1709+
g_midiPickupWaitingForPickup[i] = true;
1710+
g_midiPickupStates[i].recentFader.clear();
1711+
}
15551712
}
15561713
}
15571714
}
@@ -1565,6 +1722,16 @@ void CAudioMixerBoard::SetMIDICtrlUsed ( const bool bMIDICtrlUsed )
15651722
{
15661723
vecpChanFader[i]->SetMIDICtrlUsed ( bMIDICtrlUsed );
15671724
}
1725+
1726+
// Reset MIDI pickup state when toggling MIDI control to
1727+
// ensure pickup mode works correctly when re-enabling MIDI
1728+
for ( size_t i = 0; i < MAX_NUM_CHANNELS; i++ )
1729+
{
1730+
g_midiPickupInitialized[i] = false;
1731+
g_midiPickupWaitingForPickup[i] = false;
1732+
g_midiPickupStates[i].recentFader.clear();
1733+
g_midiPickupStates[i].recentPan.clear();
1734+
}
15681735
}
15691736

15701737
void CAudioMixerBoard::StoreAllFaderSettings()
@@ -1601,6 +1768,16 @@ void CAudioMixerBoard::LoadAllFaderSettings()
16011768
vecpChanFader[i]->SetFaderIsSolo ( bStoredFaderIsSolo );
16021769
vecpChanFader[i]->SetFaderIsMute ( bStoredFaderIsMute );
16031770
vecpChanFader[i]->SetGroupID ( iGroupID ); // Must be the last to be set in the fader!
1771+
1772+
// Reset MIDI pickup state for this channel so pickup mode works with restored values
1773+
// Without this, if MIDI values arrived before settings were loaded, pickup might have
1774+
// already occurred at the wrong value, causing jumps when the controller is moved
1775+
if ( g_midiPickupInitialized[i] )
1776+
{
1777+
g_midiPickupWaitingForPickup[i] = true;
1778+
g_midiPickupStates[i].recentFader.clear();
1779+
g_midiPickupStates[i].recentPan.clear();
1780+
}
16041781
}
16051782
}
16061783
}

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 : -1 );
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)