From 7022c4009fafa72fffa0fcf45aac9919557a280f Mon Sep 17 00:00:00 2001 From: Burt Sloane Date: Thu, 15 Jun 2017 15:18:06 -0700 Subject: [PATCH] midi in and out --- includes/BuildInfo.h | 29 +++ interface/CMakeLists.txt | 2 +- interface/src/Application.cpp | 14 +- libraries/midi/CMakeLists.txt | 3 + libraries/midi/src/Midi.cpp | 223 ++++++++++++++++++ libraries/midi/src/Midi.h | 68 ++++++ libraries/script-engine/src/ScriptEngine.cpp | 3 + libraries/shared/src/USBEventListener.cpp | 48 ++++ libraries/shared/src/USBEventListener.h | 28 +++ scripts/tutorials/createMidiSphere.js | 49 ++++ .../tutorials/entity_scripts/midiSphere.js | 52 ++++ 11 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 includes/BuildInfo.h create mode 100644 libraries/midi/CMakeLists.txt create mode 100644 libraries/midi/src/Midi.cpp create mode 100644 libraries/midi/src/Midi.h create mode 100644 libraries/shared/src/USBEventListener.cpp create mode 100644 libraries/shared/src/USBEventListener.h create mode 100644 scripts/tutorials/createMidiSphere.js create mode 100644 scripts/tutorials/entity_scripts/midiSphere.js diff --git a/includes/BuildInfo.h b/includes/BuildInfo.h new file mode 100644 index 0000000000..76faf1fe84 --- /dev/null +++ b/includes/BuildInfo.h @@ -0,0 +1,29 @@ +// +// BuildInfo.h.in +// cmake/templates +// +// Created by Stephen Birarda on 1/14/16. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#define USE_STABLE_GLOBAL_SERVICES 0 + +#include + +namespace BuildInfo { + // WARNING: This file has been auto-generated. + // Check cmake/templates/BuildInfo.h.in if you want to modify it. + + const QString INTERFACE_NAME = "Interface"; + const QString ASSIGNMENT_CLIENT_NAME = "assignment-client"; + const QString DOMAIN_SERVER_NAME = "domain-server"; + const QString AC_CLIENT_SERVER_NAME = "ac-client"; + const QString MODIFIED_ORGANIZATION = "High Fidelity - dev"; + const QString ORGANIZATION_DOMAIN = "highfidelity.io"; + const QString VERSION = "dev"; + const QString BUILD_BRANCH = ""; + const QString BUILD_GLOBAL_SERVICES = "DEVELOPMENT"; +} diff --git a/interface/CMakeLists.txt b/interface/CMakeLists.txt index 71341f3f11..79758c625b 100644 --- a/interface/CMakeLists.txt +++ b/interface/CMakeLists.txt @@ -193,7 +193,7 @@ link_hifi_libraries( shared octree ktx gpu gl gpu-gl procedural model render recording fbx networking model-networking entities avatars trackers audio audio-client animation script-engine physics - render-utils entities-renderer avatars-renderer ui auto-updater + render-utils entities-renderer avatars-renderer ui auto-updater midi controllers plugins image trackers ui-plugins display-plugins input-plugins ${NON_ANDROID_LIBRARIES} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 890b5cb455..d54c0da371 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -61,6 +61,7 @@ #include #include #include +#include #include #include #include @@ -395,7 +396,11 @@ public: return true; } } - } + + if (message->message == WM_DEVICECHANGE) { + Midi::USBchanged(); // re-scan the MIDI bus + } + } return false; } }; @@ -518,8 +523,9 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); - DependencyManager::set(); - DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -640,7 +646,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo _entityClipboard->createRootElement(); #ifdef Q_OS_WIN - installNativeEventFilter(&MyNativeEventFilter::getInstance()); + installNativeEventFilter(&MyNativeEventFilter::getInstance()); #endif _logger = new FileLogger(this); // After setting organization name in order to get correct directory diff --git a/libraries/midi/CMakeLists.txt b/libraries/midi/CMakeLists.txt new file mode 100644 index 0000000000..dc54819c2b --- /dev/null +++ b/libraries/midi/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME midi) +setup_hifi_library(Network) +link_hifi_libraries(shared networking) diff --git a/libraries/midi/src/Midi.cpp b/libraries/midi/src/Midi.cpp new file mode 100644 index 0000000000..5f5b4fa2f3 --- /dev/null +++ b/libraries/midi/src/Midi.cpp @@ -0,0 +1,223 @@ +// +// Midi.cpp +// libraries/midi/src +// +// Created by Burt Sloane +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "Midi.h" + + +#include + + +#if defined Q_OS_WIN32 +#include "Windows.h" +#endif + + +static Midi* instance = NULL; // communicate this to non-class callbacks + + +#if defined Q_OS_WIN32 + +#pragma comment(lib, "Winmm.lib") + +// +std::vector midihin; +std::vector midihout; + +std::vector Midi::midiinexclude; +std::vector Midi::midioutexclude; + + +void CALLBACK MidiInProc( + HMIDIIN hMidiIn, + UINT wMsg, + DWORD_PTR dwInstance, + DWORD_PTR dwParam1, + DWORD_PTR dwParam2 +) { + if (wMsg == MIM_OPEN) { + } + else if (wMsg == MIM_CLOSE) { + for (int i = 0; i < midihin.size(); i++) if (midihin[i] == hMidiIn) { + midihin[i] = NULL; + instance->sendNote(0xb0, 0x7b, 0); // all notes off + } + } + else if (wMsg == MIM_DATA) { + int status = 0x0ff & dwParam1; + int note = 0x0ff & (dwParam1 >> 8); + int vel = 0x0ff & (dwParam1 >> 16); +//sendNote(status, note, vel); // NOTE: relay the note on to all other midi devices + instance->Midi::noteReceived(status, note, vel); + } +} + + +void CALLBACK MidiOutProc( + HMIDIOUT hmo, + UINT wMsg, + DWORD_PTR dwInstance, + DWORD_PTR dwParam1, + DWORD_PTR dwParam2 + ) { + if (wMsg == MOM_CLOSE) { + for (int i = 0; i < midihout.size(); i++) if (midihout[i] == hmo) { + midihout[i] = NULL; + instance->sendNote(0xb0, 0x7b, 0); // all notes off + } + } +} + + +void Midi::sendNote(int status, int note, int vel) { + for (int i = 0; i < midihout.size(); i++) if (midihout[i] != NULL) { + midiOutShortMsg(midihout[i], status + (note << 8) + (vel << 16)); + } +} + +void Midi::noteReceived(int status, int note, int velocity) { + if (status >= 0x0a0) return; // NOTE: only sending note-on and note-off to Javascript + + QVariantMap eventData; + eventData["status"] = status; + eventData["note"] = note; + eventData["velocity"] = velocity; + emit midiNote(eventData); +} + + +void Midi::MidiSetup() { + midihin.clear(); + midihout.clear(); + + MIDIINCAPS incaps; + for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { + midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); + + bool found = false; + for (int j = 0; j < midiinexclude.size(); j++) { + if (lstrcmp(midiinexclude[j].toStdString().c_str(), incaps.szPname) == 0) found = true; + } + if (!found) // EXCLUDE AN INPUT BY NAME + { + HMIDIIN tmphin; + midiInOpen(&tmphin, i, (DWORD_PTR)MidiInProc, NULL, CALLBACK_FUNCTION); + midiInStart(tmphin); + midihin.push_back(tmphin); + } + + } + + MIDIOUTCAPS outcaps; + for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { + midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); + + bool found = false; + for (int j = 0; j < midioutexclude.size(); j++) { + if (lstrcmp(midioutexclude[j].toStdString().c_str(), outcaps.szPname) == 0) found = true; + } + if (!found) // EXCLUDE AN OUTPUT BY NAME + { + HMIDIOUT tmphout; + midiOutOpen(&tmphout, i, (DWORD_PTR)MidiOutProc, NULL, CALLBACK_FUNCTION); + midihout.push_back(tmphout); + } + } + + sendNote(0xb0, 0x7b, 0); // all notes off +} + +void Midi::MidiCleanup() { + sendNote(0xb0, 0x7b, 0); // all notes off + + for (int i = 0; i < midihin.size(); i++) if (midihin[i] != NULL) { + midiInStop(midihin[i]); + midiInClose(midihin[i]); + } + for (int i = 0; i < midihout.size(); i++) if (midihout[i] != NULL) { + midiOutClose(midihout[i]); + } + midihin.clear(); + midihout.clear(); +} +#endif + +// + +Midi::Midi() { + instance = this; + midioutexclude.push_back("Microsoft GS Wavetable Synth"); // we don't want to hear this thing + MidiSetup(); +} + +void Midi::playMidiNote(int status, int note, int velocity) { + sendNote(status, note, velocity); +} + +void Midi::allNotesOff() { + sendNote(0xb0, 0x7b, 0); // all notes off +} + +void Midi::resetDevices() { + MidiCleanup(); + MidiSetup(); +} + +void Midi::USBchanged() { + instance->MidiCleanup(); + instance->MidiSetup(); +} + +// + +QStringList Midi::listMidiDevices(bool output) { + QStringList rv; + if (output) { + MIDIOUTCAPS outcaps; + for (unsigned int i = 0; i < midiOutGetNumDevs(); i++) { + midiOutGetDevCaps(i, &outcaps, sizeof(MIDIINCAPS)); + rv.append(outcaps.szPname); + } + } + else { + MIDIINCAPS incaps; + for (unsigned int i = 0; i < midiInGetNumDevs(); i++) { + midiInGetDevCaps(i, &incaps, sizeof(MIDIINCAPS)); + rv.append(incaps.szPname); + } + } + return rv; +} + +void Midi::unblockMidiDevice(QString name, bool output) { + if (output) { + for (int i = 0; i < midioutexclude.size(); i++) if (midioutexclude[i].toStdString().compare(name.toStdString()) == 0) { + midioutexclude.erase(midioutexclude.begin() + i); + break; + } + } + else { + for (int i = 0; i < midiinexclude.size(); i++) if (midiinexclude[i].toStdString().compare(name.toStdString()) == 0) { + midiinexclude.erase(midiinexclude.begin() + i); + break; + } + } +} + +void Midi::blockMidiDevice(QString name, bool output) { + unblockMidiDevice(name, output); // make sure it's only in there once + if (output) { + midioutexclude.push_back(name); + } + else { + midiinexclude.push_back(name); + } +} + diff --git a/libraries/midi/src/Midi.h b/libraries/midi/src/Midi.h new file mode 100644 index 0000000000..66e45f0a53 --- /dev/null +++ b/libraries/midi/src/Midi.h @@ -0,0 +1,68 @@ +// +// Midi.h +// libraries/midi/src +// +// Created by Burt Sloane +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_Midi_h +#define hifi_Midi_h + +#include +#include +#include + +#include +#include + +class Midi : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + Midi(); + +public: + void noteReceived(int status, int note, int velocity); // relay a note to Javascript + void sendNote(int status, int note, int vel); // relay a note to MIDI outputs + static void USBchanged(); + +private: + static std::vector midiinexclude; + static std::vector midioutexclude; + +private: + void MidiSetup(); + void MidiCleanup(); + +signals: + void midiNote(QVariantMap eventData); + +public slots: +/// play a note on all connected devices +/// @param {int} status: 0x80 is noteoff, 0x90 is noteon (if velocity=0, noteoff), etc +/// @param {int} note: midi note number +/// @param {int} velocity: note velocity (0 means noteoff) +Q_INVOKABLE void playMidiNote(int status, int note, int velocity); + +/// turn off all notes on all connected devices +Q_INVOKABLE void allNotesOff(); + +/// clean up and re-discover attached devices +Q_INVOKABLE void resetDevices(); + +/// ask for a list of inputs/outputs +Q_INVOKABLE QStringList listMidiDevices(bool output); + +/// block an input/output by name +Q_INVOKABLE void blockMidiDevice(QString name, bool output); + +/// unblock an input/output by name +Q_INVOKABLE void unblockMidiDevice(QString name, bool output); +}; + +#endif // hifi_Midi_h diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index 61bf601019..ba162ba502 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -76,6 +76,7 @@ #include +#include "../../midi/src/Midi.h" // FIXME why won't a simpler include work? #include "MIDIEvent.h" const QString ScriptEngine::_SETTINGS_ENABLE_EXTENDED_EXCEPTIONS { @@ -662,6 +663,8 @@ void ScriptEngine::init() { registerGlobalObject("Audio", DependencyManager::get().data()); + registerGlobalObject("Midi", DependencyManager::get().data()); + registerGlobalObject("Entities", entityScriptingInterface.data()); registerGlobalObject("Quat", &_quatLibrary); registerGlobalObject("Vec3", &_vec3Library); diff --git a/libraries/shared/src/USBEventListener.cpp b/libraries/shared/src/USBEventListener.cpp new file mode 100644 index 0000000000..c71277dfed --- /dev/null +++ b/libraries/shared/src/USBEventListener.cpp @@ -0,0 +1,48 @@ +// +// USBEventListener.cpp +// libraries/shared/src +// +// Created by Ryan Huffman on 09/03/14. +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "USBEventListener.h" + +#ifdef Q_OS_WIN +#include +#else +#include +#endif + +#include +#include + +USBEventListener& USBEventListener::getInstance() { + static USBEventListener staticInstance; + return staticInstance; +} + +void signalHandler(int param) { + // tell the qApp it should quit + QMetaObject::invokeMethod(qApp, "quit"); +} + +USBEventListener::USBEventListener(QObject* parent) : QObject(parent) { +#ifndef Q_OS_WIN +#endif +} + + +bool USBEventListener::nativeEventFilter(const QByteArray &eventType, void* msg, long* result) { +#ifdef Q_OS_WIN + if (eventType == "windows_generic_MSG") { + MSG* message = (MSG*)msg; + if (message->message == WM_DEVICECHANGE) { + } + } +#endif + return false; +} diff --git a/libraries/shared/src/USBEventListener.h b/libraries/shared/src/USBEventListener.h new file mode 100644 index 0000000000..9ab008256e --- /dev/null +++ b/libraries/shared/src/USBEventListener.h @@ -0,0 +1,28 @@ +// +// USBEventListener.h +// libraries/midi +// +// Created by Burt Sloane +// Copyright 2014 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_USBEventListener_h +#define hifi_USBEventListener_h + +#include +#include + +class USBEventListener : public QObject, public QAbstractNativeEventFilter { + Q_OBJECT +public: + static USBEventListener& getInstance(); + + virtual bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override; +private: + USBEventListener(QObject* parent = 0); +}; + +#endif // hifi_USBEventListener_h diff --git a/scripts/tutorials/createMidiSphere.js b/scripts/tutorials/createMidiSphere.js new file mode 100644 index 0000000000..705acac9de --- /dev/null +++ b/scripts/tutorials/createMidiSphere.js @@ -0,0 +1,49 @@ +// +// Created by James B. Pollack @imgntn on April 18, 2016. +// Adapted by Burt +// Copyright 2016 High Fidelity, Inc. +// +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +var SCRIPT_URL = "file:///e:/hifi/scripts/tutorials/entity_scripts/midiSphere.js"; +var center = Vec3.sum(MyAvatar.position, Vec3.multiply(0.5, Quat.getForward(Camera.getOrientation()))); + +var BALL_GRAVITY = { + x: 0, + y: 0, + z: 0 +}; + +var BALL_DIMENSIONS = { + x: 0.4, + y: 0.4, + z: 0.4 +}; + + +var BALL_COLOR = { + red: 255, + green: 0, + blue: 0 +}; + +var midiSphereProperties = { + name: 'MIDI Sphere', + shapeType: 'sphere', + type: 'Sphere', + script: SCRIPT_URL, + color: BALL_COLOR, + dimensions: BALL_DIMENSIONS, + gravity: BALL_GRAVITY, + dynamic: false, + position: center, + collisionless: false, + ignoreForCollisions: true +}; + +var midiSphere = Entities.addEntity(midiSphereProperties); + +Script.stop(); diff --git a/scripts/tutorials/entity_scripts/midiSphere.js b/scripts/tutorials/entity_scripts/midiSphere.js new file mode 100644 index 0000000000..5ed29a330f --- /dev/null +++ b/scripts/tutorials/entity_scripts/midiSphere.js @@ -0,0 +1,52 @@ +// midiSphere.js +// +// Script Type: Entity +// Created by James B. Pollack @imgntn on 9/21/2015 +// Adapted by Burt +// Copyright 2015 High Fidelity, Inc. +// +// This script listens to MIDI and makes the ball change color. +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +(function() { + var _this; + + function MidiSphere() { + _this = this; + this.clicked = false; + return; + } + + MidiSphere.prototype = { + preload: function(entityID) { + this.entityID = entityID; + Midi.midiNote.connect(function(eventData) { + print("MidiSphere.noteReceived: "+JSON.stringify(eventData)); + Entities.editEntity(entityID, { color: { red: 2*eventData.note, green: 2*eventData.note, blue: 2*eventData.note} }); + }); + print("MidiSphere.preload"); + }, + unload: function(entityID) { + print("MidiSphere.unload"); + }, + + clickDownOnEntity: function(entityID, mouseEvent) { + print("MidiSphere.clickDownOnEntity"); + if (this.clicked) { + Entities.editEntity(entityID, { color: { red: 0, green: 255, blue: 255} }); + this.clicked = false; + Midi.playMidiNote(144, 64, 0); + } else { + Entities.editEntity(entityID, { color: { red: 255, green: 255, blue: 0} }); + this.clicked = true; + Midi.playMidiNote(144, 64, 100); + } + } + + }; + + // entity scripts should return a newly constructed object of our type + return new MidiSphere(); +}); \ No newline at end of file