Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Windows native MIDI support #3431

Merged
merged 9 commits into from
Dec 6, 2024
5 changes: 5 additions & 0 deletions Jamulus.pro
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ win32 {
INCLUDEPATH += "$${programfilesdir}/JACK2/include"
LIBS += "$${programfilesdir}/JACK2/lib/$${libjackname}"
} else {
message(Using native Windows MIDI.)

HEADERS += src/sound/midi-win/midi.h
SOURCES += src/sound/midi-win/midi.cpp

message(Using ASIO.)
message(Please review the ASIO SDK licence.)

Expand Down
17 changes: 17 additions & 0 deletions src/sound/asio/sound.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,23 @@ CSound::CSound ( void ( *fpNewCallback ) ( CVector<int16_t>& psData, void* arg )
asioCallbacks.sampleRateDidChange = &sampleRateChanged;
asioCallbacks.asioMessage = &asioMessages;
asioCallbacks.bufferSwitchTimeInfo = &bufferSwitchTimeInfo;

// Optional MIDI initialization --------------------------------------------
if ( iCtrlMIDIChannel != INVALID_MIDI_CH )
{
Midi.MidiStart();
}
}

CSound::~CSound()
{
// stop MIDI if running
if ( iCtrlMIDIChannel != INVALID_MIDI_CH )
{
Midi.MidiStop();
}

UnloadCurrentDriver();
}

void CSound::ResetChannelMapping()
Expand Down
6 changes: 5 additions & 1 deletion src/sound/asio/sound.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "../../util.h"
#include "../../global.h"
#include "../soundbase.h"
#include "../midi-win/midi.h"

// The following includes require the ASIO SDK to be placed in
// libs/ASIOSDK2 during build.
Expand Down Expand Up @@ -56,7 +57,7 @@ class CSound : public CSoundBase
public:
CSound ( void ( *fpNewCallback ) ( CVector<int16_t>& psData, void* arg ), void* arg, const QString& strMIDISetup, const bool, const QString& );

virtual ~CSound() { UnloadCurrentDriver(); }
virtual ~CSound();

virtual int Init ( const int iNewPrefMonoBufferSize );
virtual void Start();
Expand Down Expand Up @@ -134,4 +135,7 @@ class CSound : public CSoundBase
static long asioMessages ( long selector, long value, void* message, double* opt );

char* cDriverNames[MAX_NUMBER_SOUND_CARDS];

// Windows native MIDI support
CMidi Midi;
};
130 changes: 130 additions & 0 deletions src/sound/midi-win/midi.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/******************************************************************************\
* Copyright (c) 2024
*
* Author(s):
* Tony Mountifield
*
* Description:
* MIDI interface for Windows operating systems
*
******************************************************************************
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
\******************************************************************************/

#include "midi.h"

/* Implementation *************************************************************/

// pointer to the sound object (for passing received MIDI messages upwards)
extern CSound* pSound;

//---------------------------------------------------------------------------------------
// Windows Native MIDI support
//
// For API, see https://learn.microsoft.com/en-gb/windows/win32/multimedia/midi-reference

void CMidi::MidiStart()
{
QString selMIDIDevice = pSound->GetMIDIDevice();

/* Get the number of MIDI In devices in this computer */
iMidiDevs = midiInGetNumDevs();

qInfo() << qUtf8Printable ( QString ( "- MIDI devices found: %1" ).arg ( iMidiDevs ) );

// open all connected MIDI devices and set the callback function to handle incoming messages
for ( int i = 0; i < iMidiDevs; i++ )
{
HMIDIIN hMidiIn; // windows handle
MIDIINCAPS mic; // device name and capabilities

MMRESULT result = midiInGetDevCaps ( i, &mic, sizeof ( MIDIINCAPS ) );

if ( result != MMSYSERR_NOERROR )
{
qWarning() << qUtf8Printable ( QString ( "! Failed to identify MIDI input device %1. Error code: %2" ).arg ( i ).arg ( result ) );
continue; // try next device, if any
}

QString midiDev ( mic.szPname );

if ( !selMIDIDevice.isEmpty() && selMIDIDevice != midiDev )
{
qInfo() << qUtf8Printable ( QString ( " %1: %2 (ignored)" ).arg ( i ).arg ( midiDev ) );
continue; // try next device, if any
}

qInfo() << qUtf8Printable ( QString ( " %1: %2" ).arg ( i ).arg ( midiDev ) );

result = midiInOpen ( &hMidiIn, i, (DWORD_PTR) MidiCallback, 0, CALLBACK_FUNCTION );

if ( result != MMSYSERR_NOERROR )
{
qWarning() << qUtf8Printable ( QString ( "! Failed to open MIDI input device %1. Error code: %2" ).arg ( i ).arg ( result ) );
continue; // try next device, if any
}

result = midiInStart ( hMidiIn );

if ( result != MMSYSERR_NOERROR )
{
qWarning() << qUtf8Printable ( QString ( "! Failed to start MIDI input device %1. Error code: %2" ).arg ( i ).arg ( result ) );
midiInClose ( hMidiIn );
continue; // try next device, if any
}

// success, add it to list of open handles
vecMidiInHandles.append ( hMidiIn );
}
}

void CMidi::MidiStop()
{
// stop MIDI if running
for ( int i = 0; i < vecMidiInHandles.size(); i++ )
{
midiInStop ( vecMidiInHandles.at ( i ) );
midiInClose ( vecMidiInHandles.at ( i ) );
}
}

// See https://learn.microsoft.com/en-us/previous-versions//dd798460(v=vs.85)
// for the definition of the MIDI input callback function.
void CALLBACK CMidi::MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 )
{
Q_UNUSED ( hMidiIn );
Q_UNUSED ( dwInstance );
Q_UNUSED ( dwParam2 );

if ( wMsg == MIM_DATA )
{
// See https://learn.microsoft.com/en-gb/windows/win32/multimedia/mim-data
// The three bytes of a MIDI message are encoded into the 32-bit dwParam1 parameter.
BYTE status = dwParam1 & 0xFF;
BYTE data1 = ( dwParam1 >> 8 ) & 0xFF;
BYTE data2 = ( dwParam1 >> 16 ) & 0xFF;

// copy packet and send it to the MIDI parser
CVector<uint8_t> vMIDIPaketBytes ( 3 );

vMIDIPaketBytes[0] = static_cast<uint8_t> ( status );
vMIDIPaketBytes[1] = static_cast<uint8_t> ( data1 );
vMIDIPaketBytes[2] = static_cast<uint8_t> ( data2 );

pSound->ParseMIDIMessage ( vMIDIPaketBytes );
}
}
54 changes: 54 additions & 0 deletions src/sound/midi-win/midi.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/******************************************************************************\
* Copyright (c) 2024
*
* Author(s):
* Tony Mountifield
*
******************************************************************************
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*
\******************************************************************************/

#pragma once

#include "../../util.h"
#include "../../global.h"

/* Classes ********************************************************************/
class CMidi
{
public:
CMidi() {}

virtual ~CMidi() {}

void MidiStart();
void MidiStop();

protected:
int iMidiDevs;
QVector<HMIDIIN> vecMidiInHandles; // windows handles

static void CALLBACK MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 );
};

// Provide the definition of CSound for the MIDI callback
// This must be after the above definition of CMidi
#if defined( JACK_ON_WINDOWS )
# include "sound/jack/sound.h"
#else
# include "sound/asio/sound.h"
#endif
27 changes: 18 additions & 9 deletions src/sound/soundbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ char const sMidiCtlChar[] = {
/* [EMidiCtlType::Solo] = */ 's',
/* [EMidiCtlType::Mute] = */ 'm',
/* [EMidiCtlType::MuteMyself] = */ 'o',
/* [EMidiCtlType::Device] = */ 'd',
/* [EMidiCtlType::None] = */ '\0' };

/* Implementation *************************************************************/
Expand Down Expand Up @@ -309,16 +310,24 @@ void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup )
continue;
EMidiCtlType eTyp = static_cast<EMidiCtlType> ( iCtrl );

const QStringList slP = sParm.mid ( 1 ).split ( '*' );
int iFirst = slP[0].toUInt();
int iNum = ( slP.count() > 1 ) ? slP[1].toUInt() : 1;
for ( int iOff = 0; iOff < iNum; iOff++ )
if ( eTyp == Device )
{
if ( iOff >= MAX_NUM_CHANNELS )
break;
if ( iFirst + iOff >= 128 )
break;
aMidiCtls[iFirst + iOff] = { eTyp, iOff };
// save MIDI device name to select
strMIDIDevice = sParm.mid ( 1 );
}
else
{
const QStringList slP = sParm.mid ( 1 ).split ( '*' );
int iFirst = slP[0].toUInt();
int iNum = ( slP.count() > 1 ) ? slP[1].toUInt() : 1;
for ( int iOff = 0; iOff < iNum; iOff++ )
{
if ( iOff >= MAX_NUM_CHANNELS )
break;
if ( iFirst + iOff >= 128 )
break;
aMidiCtls[iFirst + iOff] = { eTyp, iOff };
}
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions src/sound/soundbase.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ enum EMidiCtlType
Solo,
Mute,
MuteMyself,
Device,
None
};

Expand Down Expand Up @@ -106,13 +107,18 @@ class CSoundBase : public QThread

virtual void OpenDriverSetup() {}

virtual const QString& GetMIDIDevice() { return strMIDIDevice; }

bool IsRunning() const { return bRun; }
bool IsCallbackEntered() const { return bCallbackEntered; }

// TODO this should be protected but since it is used
// in a callback function it has to be public -> better solution
void EmitReinitRequestSignal ( const ESndCrdResetType eSndCrdResetType ) { emit ReinitRequest ( eSndCrdResetType ); }

// this needs to be public so that it can be called from CMidi
void ParseMIDIMessage ( const CVector<uint8_t>& vMIDIPaketBytes );

protected:
virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; }
virtual void UnloadCurrentDriver() {}
Expand Down Expand Up @@ -151,8 +157,6 @@ class CSoundBase : public QThread
( *fpProcessCallback ) ( psData, pProcessCallbackArg );
}

void ParseMIDIMessage ( const CVector<uint8_t>& vMIDIPaketBytes );

bool bRun;
bool bCallbackEntered;
QMutex MutexAudioProcessCallback;
Expand All @@ -166,6 +170,8 @@ class CSoundBase : public QThread
QString strCurDevName;
QString strDriverNames[MAX_NUMBER_SOUND_CARDS];

QString strMIDIDevice;

signals:
void ReinitRequest ( int iSndCrdResetType );
void ControllerInFaderLevel ( int iChannelIdx, int iValue );
Expand Down
Loading