diff --git a/Jamulus.pro b/Jamulus.pro index 3176affb22..3d1d5acd45 100644 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -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.) diff --git a/src/sound/asio/sound.cpp b/src/sound/asio/sound.cpp index e23609e7af..a1d1274b69 100644 --- a/src/sound/asio/sound.cpp +++ b/src/sound/asio/sound.cpp @@ -577,6 +577,23 @@ CSound::CSound ( void ( *fpNewCallback ) ( CVector& 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() diff --git a/src/sound/asio/sound.h b/src/sound/asio/sound.h index 3765a2f633..efd08cfbe5 100644 --- a/src/sound/asio/sound.h +++ b/src/sound/asio/sound.h @@ -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. @@ -56,7 +57,7 @@ class CSound : public CSoundBase public: CSound ( void ( *fpNewCallback ) ( CVector& 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(); @@ -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; }; diff --git a/src/sound/midi-win/midi.cpp b/src/sound/midi-win/midi.cpp new file mode 100644 index 0000000000..6d58b771ee --- /dev/null +++ b/src/sound/midi-win/midi.cpp @@ -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 vMIDIPaketBytes ( 3 ); + + vMIDIPaketBytes[0] = static_cast ( status ); + vMIDIPaketBytes[1] = static_cast ( data1 ); + vMIDIPaketBytes[2] = static_cast ( data2 ); + + pSound->ParseMIDIMessage ( vMIDIPaketBytes ); + } +} diff --git a/src/sound/midi-win/midi.h b/src/sound/midi-win/midi.h new file mode 100644 index 0000000000..5ad1ab9c8d --- /dev/null +++ b/src/sound/midi-win/midi.h @@ -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 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 diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index 464b81bd41..3d3be8d8a2 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -33,6 +33,7 @@ char const sMidiCtlChar[] = { /* [EMidiCtlType::Solo] = */ 's', /* [EMidiCtlType::Mute] = */ 'm', /* [EMidiCtlType::MuteMyself] = */ 'o', + /* [EMidiCtlType::Device] = */ 'd', /* [EMidiCtlType::None] = */ '\0' }; /* Implementation *************************************************************/ @@ -309,16 +310,24 @@ void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) continue; EMidiCtlType eTyp = static_cast ( 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 }; + } } } } diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index 4aa6c629bc..bcfa21243a 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -49,6 +49,7 @@ enum EMidiCtlType Solo, Mute, MuteMyself, + Device, None }; @@ -106,6 +107,8 @@ class CSoundBase : public QThread virtual void OpenDriverSetup() {} + virtual const QString& GetMIDIDevice() { return strMIDIDevice; } + bool IsRunning() const { return bRun; } bool IsCallbackEntered() const { return bCallbackEntered; } @@ -113,6 +116,9 @@ class CSoundBase : public QThread // 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& vMIDIPaketBytes ); + protected: virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } virtual void UnloadCurrentDriver() {} @@ -151,8 +157,6 @@ class CSoundBase : public QThread ( *fpProcessCallback ) ( psData, pProcessCallbackArg ); } - void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); - bool bRun; bool bCallbackEntered; QMutex MutexAudioProcessCallback; @@ -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 );