/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2015 - ROLI Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE 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. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ class MidiInCollector { public: MidiInCollector (MidiInput* const input_, MidiInputCallback& callback_) : deviceHandle (0), input (input_), callback (callback_), concatenator (4096), isStarted (false), startTime (0) { } ~MidiInCollector() { stop(); if (deviceHandle != 0) { for (int count = 5; --count >= 0;) { if (midiInClose (deviceHandle) == MMSYSERR_NOERROR) break; Sleep (20); } } } //============================================================================== void handleMessage (const uint8* bytes, const uint32 timeStamp) { if (bytes[0] >= 0x80 && isStarted) { concatenator.pushMidiData (bytes, MidiMessage::getMessageLengthFromFirstByte (bytes[0]), convertTimeStamp (timeStamp), input, callback); writeFinishedBlocks(); } } void handleSysEx (MIDIHDR* const hdr, const uint32 timeStamp) { if (isStarted && hdr->dwBytesRecorded > 0) { concatenator.pushMidiData (hdr->lpData, (int) hdr->dwBytesRecorded, convertTimeStamp (timeStamp), input, callback); writeFinishedBlocks(); } } void start() { if (deviceHandle != 0 && ! isStarted) { activeMidiCollectors.addIfNotAlreadyThere (this); for (int i = 0; i < (int) numHeaders; ++i) { headers[i].prepare (deviceHandle); headers[i].write (deviceHandle); } startTime = Time::getMillisecondCounterHiRes(); MMRESULT res = midiInStart (deviceHandle); if (res == MMSYSERR_NOERROR) { concatenator.reset(); isStarted = true; } else { unprepareAllHeaders(); } } } void stop() { if (isStarted) { isStarted = false; midiInReset (deviceHandle); midiInStop (deviceHandle); activeMidiCollectors.removeFirstMatchingValue (this); unprepareAllHeaders(); concatenator.reset(); } } static void CALLBACK midiInCallback (HMIDIIN, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR midiMessage, DWORD_PTR timeStamp) { MidiInCollector* const collector = reinterpret_cast (dwInstance); if (activeMidiCollectors.contains (collector)) { if (uMsg == MIM_DATA) collector->handleMessage ((const uint8*) &midiMessage, (uint32) timeStamp); else if (uMsg == MIM_LONGDATA) collector->handleSysEx ((MIDIHDR*) midiMessage, (uint32) timeStamp); } } HMIDIIN deviceHandle; private: static Array activeMidiCollectors; MidiInput* input; MidiInputCallback& callback; MidiDataConcatenator concatenator; bool volatile isStarted; double startTime; class MidiHeader { public: MidiHeader() {} void prepare (HMIDIIN device) { zerostruct (hdr); hdr.lpData = data; hdr.dwBufferLength = (DWORD) numElementsInArray (data); midiInPrepareHeader (device, &hdr, sizeof (hdr)); } void unprepare (HMIDIIN device) { if ((hdr.dwFlags & WHDR_DONE) != 0) { int c = 10; while (--c >= 0 && midiInUnprepareHeader (device, &hdr, sizeof (hdr)) == MIDIERR_STILLPLAYING) Thread::sleep (20); jassert (c >= 0); } } void write (HMIDIIN device) { hdr.dwBytesRecorded = 0; midiInAddBuffer (device, &hdr, sizeof (hdr)); } void writeIfFinished (HMIDIIN device) { if ((hdr.dwFlags & WHDR_DONE) != 0) write (device); } private: MIDIHDR hdr; char data [256]; JUCE_DECLARE_NON_COPYABLE (MidiHeader) }; enum { numHeaders = 32 }; MidiHeader headers [numHeaders]; void writeFinishedBlocks() { for (int i = 0; i < (int) numHeaders; ++i) headers[i].writeIfFinished (deviceHandle); } void unprepareAllHeaders() { for (int i = 0; i < (int) numHeaders; ++i) headers[i].unprepare (deviceHandle); } double convertTimeStamp (uint32 timeStamp) { double t = startTime + timeStamp; const double now = Time::getMillisecondCounterHiRes(); if (t > now) { if (t > now + 2.0) startTime -= 1.0; t = now; } return t * 0.001; } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiInCollector) }; Array MidiInCollector::activeMidiCollectors; //============================================================================== StringArray MidiInput::getDevices() { StringArray s; const UINT num = midiInGetNumDevs(); for (UINT i = 0; i < num; ++i) { MIDIINCAPS mc = { 0 }; if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) s.add (String (mc.szPname, sizeof (mc.szPname))); } return s; } int MidiInput::getDefaultDeviceIndex() { return 0; } MidiInput* MidiInput::openDevice (const int index, MidiInputCallback* const callback) { if (callback == nullptr) return nullptr; UINT deviceId = MIDI_MAPPER; int n = 0; String name; const UINT num = midiInGetNumDevs(); for (UINT i = 0; i < num; ++i) { MIDIINCAPS mc = { 0 }; if (midiInGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) { if (index == n) { deviceId = i; name = String (mc.szPname, (size_t) numElementsInArray (mc.szPname)); break; } ++n; } } ScopedPointer in (new MidiInput (name)); ScopedPointer collector (new MidiInCollector (in, *callback)); HMIDIIN h; MMRESULT err = midiInOpen (&h, deviceId, (DWORD_PTR) &MidiInCollector::midiInCallback, (DWORD_PTR) (MidiInCollector*) collector, CALLBACK_FUNCTION); if (err == MMSYSERR_NOERROR) { collector->deviceHandle = h; in->internal = collector.release(); return in.release(); } return nullptr; } MidiInput::MidiInput (const String& name_) : name (name_), internal (0) { } MidiInput::~MidiInput() { delete static_cast (internal); } void MidiInput::start() { static_cast (internal)->start(); } void MidiInput::stop() { static_cast (internal)->stop(); } //============================================================================== struct MidiOutHandle { int refCount; UINT deviceId; HMIDIOUT handle; static Array activeHandles; private: JUCE_LEAK_DETECTOR (MidiOutHandle) }; Array MidiOutHandle::activeHandles; //============================================================================== StringArray MidiOutput::getDevices() { StringArray s; const UINT num = midiOutGetNumDevs(); for (UINT i = 0; i < num; ++i) { MIDIOUTCAPS mc = { 0 }; if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) s.add (String (mc.szPname, sizeof (mc.szPname))); } return s; } int MidiOutput::getDefaultDeviceIndex() { const UINT num = midiOutGetNumDevs(); int n = 0; for (UINT i = 0; i < num; ++i) { MIDIOUTCAPS mc = { 0 }; if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) { if ((mc.wTechnology & MOD_MAPPER) != 0) return n; ++n; } } return 0; } MidiOutput* MidiOutput::openDevice (int index) { UINT deviceId = MIDI_MAPPER; const UINT num = midiOutGetNumDevs(); int n = 0; String deviceName; for (UINT i = 0; i < num; ++i) { MIDIOUTCAPS mc = { 0 }; if (midiOutGetDevCaps (i, &mc, sizeof (mc)) == MMSYSERR_NOERROR) { String name = String (mc.szPname, sizeof (mc.szPname)); // use the microsoft sw synth as a default - best not to allow deviceId // to be MIDI_MAPPER, or else device sharing breaks if (name.containsIgnoreCase ("microsoft")) deviceId = i; if (index == n) { deviceName = name; deviceId = i; break; } ++n; } } for (int i = MidiOutHandle::activeHandles.size(); --i >= 0;) { MidiOutHandle* const han = MidiOutHandle::activeHandles.getUnchecked(i); if (han->deviceId == deviceId) { han->refCount++; MidiOutput* const out = new MidiOutput (deviceName); out->internal = han; return out; } } for (int i = 4; --i >= 0;) { HMIDIOUT h = 0; MMRESULT res = midiOutOpen (&h, deviceId, 0, 0, CALLBACK_NULL); if (res == MMSYSERR_NOERROR) { MidiOutHandle* const han = new MidiOutHandle(); han->deviceId = deviceId; han->refCount = 1; han->handle = h; MidiOutHandle::activeHandles.add (han); MidiOutput* const out = new MidiOutput (deviceName); out->internal = han; return out; } else if (res == MMSYSERR_ALLOCATED) { Sleep (100); } else { break; } } return nullptr; } MidiOutput::~MidiOutput() { stopBackgroundThread(); MidiOutHandle* const h = static_cast (internal); if (MidiOutHandle::activeHandles.contains (h) && --(h->refCount) == 0) { midiOutClose (h->handle); MidiOutHandle::activeHandles.removeFirstMatchingValue (h); delete h; } } void MidiOutput::sendMessageNow (const MidiMessage& message) { const MidiOutHandle* const handle = static_cast (internal); if (message.getRawDataSize() > 3 || message.isSysEx()) { MIDIHDR h = { 0 }; h.lpData = (char*) message.getRawData(); h.dwBytesRecorded = h.dwBufferLength = (DWORD) message.getRawDataSize(); if (midiOutPrepareHeader (handle->handle, &h, sizeof (MIDIHDR)) == MMSYSERR_NOERROR) { MMRESULT res = midiOutLongMsg (handle->handle, &h, sizeof (MIDIHDR)); if (res == MMSYSERR_NOERROR) { while ((h.dwFlags & MHDR_DONE) == 0) Sleep (1); int count = 500; // 1 sec timeout while (--count >= 0) { res = midiOutUnprepareHeader (handle->handle, &h, sizeof (MIDIHDR)); if (res == MIDIERR_STILLPLAYING) Sleep (2); else break; } } } } else { for (int i = 0; i < 50; ++i) { if (midiOutShortMsg (handle->handle, *(unsigned int*) message.getRawData()) != MIDIERR_NOTREADY) break; Sleep (1); } } }