From 89ba69ab2978287f64f5f551900dafb4a6f5e3d5 Mon Sep 17 00:00:00 2001 From: jules Date: Tue, 3 Nov 2015 10:37:52 +0000 Subject: [PATCH] Android Bluetooth MIDI, pro-audio i/o and improved openGL support --- examples/Demo/JuceDemo.jucer | 10 +- examples/Demo/Source/MainWindow.cpp | 4 + examples/MidiTest/MidiTest.jucer | 146 +++ examples/MidiTest/Source/Main.cpp | 120 +++ examples/MidiTest/Source/MainComponent.cpp | 457 ++++++++++ examples/MidiTest/Source/MainComponent.h | 99 +++ .../MidiTest/Source/Resources/Bluetooth.svg | 5 + .../jucer_ProjectExport_Android.h | 82 +- .../jucer_ProjectExport_AndroidStudio.h | 6 +- .../jucer_ProjectExport_CodeBlocks.h | 6 +- .../Project Saving/jucer_ProjectExport_MSVC.h | 16 +- .../Project Saving/jucer_ProjectExport_Make.h | 6 +- .../jucer_ProjectExport_XCode.h | 13 +- .../Project Saving/jucer_ProjectExporter.cpp | 11 +- .../Project Saving/jucer_ProjectExporter.h | 6 +- .../Project/jucer_ConfigTree_Exporter.h | 31 +- .../Project/jucer_GroupInformationComponent.h | 19 +- .../Source/Project/jucer_Project.cpp | 9 +- .../Introjucer/Source/Project/jucer_Project.h | 7 +- .../Source/Utility/jucer_PresetIDs.h | 2 + .../native/juce_android_Midi.cpp | 318 ++++++- .../native/juce_android_OpenSL.cpp | 329 ++++--- .../gui/juce_AudioDeviceSelectorComponent.cpp | 40 +- .../gui/juce_AudioDeviceSelectorComponent.h | 10 +- .../juce_BluetoothMidiDevicePairingDialogue.h | 76 ++ modules/juce_audio_utils/juce_audio_utils.cpp | 17 + modules/juce_audio_utils/juce_audio_utils.h | 1 + ...oid_BluetoothMidiDevicePairingDialogue.cpp | 438 +++++++++ ..._ios_BluetoothMidiDevicePairingDialogue.mm | 138 +++ ...nux_BluetoothMidiDevicePairingDialogue.cpp | 37 + ..._mac_BluetoothMidiDevicePairingDialogue.mm | 36 + ...win_BluetoothMidiDevicePairingDialogue.cpp | 37 + .../juce_core/native/java/AndroidMidi.java | 835 ++++++++++++++++++ .../native/java/AndroidMidiFallback.java | 81 ++ .../native/java/JuceAppActivity.java | 264 ++++-- .../native/juce_android_JNIHelpers.h | 161 +--- .../native/juce_android_SystemStats.cpp | 36 +- .../juce_core/native/juce_android_Threads.cpp | 232 +++++ .../juce_core/native/juce_posix_SharedCode.h | 23 +- modules/juce_core/system/juce_PlatformDefs.h | 64 +- .../native/juce_android_Messaging.cpp | 2 + .../native/juce_android_Windowing.cpp | 20 +- modules/juce_opengl/juce_opengl.h | 7 +- .../juce_opengl/native/juce_OpenGL_android.h | 322 ++++--- .../juce_opengl/opengl/juce_OpenGLContext.cpp | 178 ++-- 45 files changed, 4088 insertions(+), 669 deletions(-) create mode 100644 examples/MidiTest/MidiTest.jucer create mode 100644 examples/MidiTest/Source/Main.cpp create mode 100644 examples/MidiTest/Source/MainComponent.cpp create mode 100644 examples/MidiTest/Source/MainComponent.h create mode 100644 examples/MidiTest/Source/Resources/Bluetooth.svg create mode 100644 modules/juce_audio_utils/gui/juce_BluetoothMidiDevicePairingDialogue.h create mode 100644 modules/juce_audio_utils/native/juce_android_BluetoothMidiDevicePairingDialogue.cpp create mode 100644 modules/juce_audio_utils/native/juce_ios_BluetoothMidiDevicePairingDialogue.mm create mode 100644 modules/juce_audio_utils/native/juce_linux_BluetoothMidiDevicePairingDialogue.cpp create mode 100644 modules/juce_audio_utils/native/juce_mac_BluetoothMidiDevicePairingDialogue.mm create mode 100644 modules/juce_audio_utils/native/juce_win_BluetoothMidiDevicePairingDialogue.cpp create mode 100644 modules/juce_core/native/java/AndroidMidi.java create mode 100644 modules/juce_core/native/java/AndroidMidiFallback.java diff --git a/examples/Demo/JuceDemo.jucer b/examples/Demo/JuceDemo.jucer index d184e31109..11b1fca50e 100644 --- a/examples/Demo/JuceDemo.jucer +++ b/examples/Demo/JuceDemo.jucer @@ -168,11 +168,11 @@ + androidSDKPath="" androidNDKPath="" androidMinimumSDK="10" androidInternetNeeded="1" + androidKeyStore="${user.home}/.android/debug.keystore" androidKeyStorePass="android" + androidKeyAlias="androiddebugkey" androidKeyAliasPass="android" + androidMicNeeded="1" bigIcon="xycKOk" androidCpp11="1" smallIcon="BvyE0d" + androidVersionCode="1" androidBluetoothNeeded="0" androidTheme="@android:style/Theme.NoTitleBar"> diff --git a/examples/Demo/Source/MainWindow.cpp b/examples/Demo/Source/MainWindow.cpp index 64c4cb0d84..a5628e75f1 100644 --- a/examples/Demo/Source/MainWindow.cpp +++ b/examples/Demo/Source/MainWindow.cpp @@ -520,6 +520,10 @@ MainAppWindow::MainAppWindow() taskbarIcon = new DemoTaskbarComponent(); #endif + #if JUCE_ANDROID + setOpenGLRenderingEngine(); + #endif + triggerAsyncUpdate(); } diff --git a/examples/MidiTest/MidiTest.jucer b/examples/MidiTest/MidiTest.jucer new file mode 100644 index 0000000000..282957fce2 --- /dev/null +++ b/examples/MidiTest/MidiTest.jucer @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/MidiTest/Source/Main.cpp b/examples/MidiTest/Source/Main.cpp new file mode 100644 index 0000000000..eeb0816bfc --- /dev/null +++ b/examples/MidiTest/Source/Main.cpp @@ -0,0 +1,120 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +#include "../JuceLibraryCode/JuceHeader.h" +#include "MainComponent.h" + +//============================================================================== +class MidiTestApplication : public JUCEApplication +{ +public: + //========================================================================== + MidiTestApplication() {} + + const String getApplicationName() override { return ProjectInfo::projectName; } + const String getApplicationVersion() override { return ProjectInfo::versionString; } + bool moreThanOneInstanceAllowed() override { return true; } + + //========================================================================== + void initialise (const String& commandLine) override + { + // This method is where you should put your application's initialisation code.. + ignoreUnused (commandLine); + mainWindow = new MainWindow (getApplicationName()); + } + + void shutdown() override + { + // Add your application's shutdown code here.. + + mainWindow = nullptr; // (deletes our window) + } + + //========================================================================== + void systemRequestedQuit() override + { + // This is called when the app is being asked to quit: you can ignore this + // request and let the app carry on running, or call quit() to allow the app to close. + quit(); + } + + void anotherInstanceStarted (const String& commandLine) override + { + // When another instance of the app is launched while this one is running, + // this method is invoked, and the commandLine parameter tells you what + // the other instance's command-line arguments were. + ignoreUnused (commandLine); + } + + //========================================================================== + /* + This class implements the desktop window that contains an instance of + our MainContentComponent class. + */ + class MainWindow : public DocumentWindow + { + public: + MainWindow (String name) : DocumentWindow (name, + Colours::lightgrey, + DocumentWindow::allButtons) + { + setUsingNativeTitleBar (true); + setContentOwned (new MainContentComponent(), true); + + setResizable (true, false); + + #if JUCE_IOS || JUCE_ANDROID + setFullScreen (true); + #endif + + centreWithSize (getWidth(), getHeight()); + setVisible (true); + } + + void closeButtonPressed() override + { + // This is called when the user tries to close this window. Here, we'll just + // ask the app to quit when this happens, but you can change this to do + // whatever you need. + JUCEApplication::getInstance()->systemRequestedQuit(); + } + + /* Note: Be careful if you override any DocumentWindow methods - the base + class uses a lot of them, so by overriding you might break its functionality. + It's best to do all your work in your content component instead, but if + you really have to override any DocumentWindow methods, make sure your + subclass also calls the superclass's method. + */ + + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow) + }; + +private: + ScopedPointer mainWindow; +}; + +//============================================================================== +// This macro generates the main() routine that launches the app. +START_JUCE_APPLICATION (MidiTestApplication) diff --git a/examples/MidiTest/Source/MainComponent.cpp b/examples/MidiTest/Source/MainComponent.cpp new file mode 100644 index 0000000000..8e067d9849 --- /dev/null +++ b/examples/MidiTest/Source/MainComponent.cpp @@ -0,0 +1,457 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +#include "MainComponent.h" + +//============================================================================== +struct MidiDeviceListEntry : ReferenceCountedObject +{ + MidiDeviceListEntry (const String& deviceName) : name (deviceName) {} + + String name; + ScopedPointer inDevice; + ScopedPointer outDevice; + + typedef ReferenceCountedObjectPtr Ptr; +}; + +//============================================================================== +struct MidiCallbackMessage : public Message +{ + MidiCallbackMessage (const MidiMessage& msg) : message (msg) {} + MidiMessage message; +}; + +//============================================================================== +class MidiDeviceListBox : public ListBox, +private ListBoxModel +{ +public: + //========================================================================== + MidiDeviceListBox (const String& name, + MainContentComponent& contentComponent, + bool isInputDeviceList) + : ListBox (name, this), + parent (contentComponent), + isInput (isInputDeviceList) + { + setOutlineThickness (1); + setMultipleSelectionEnabled (true); + setClickingTogglesRowSelection (true); + } + + //========================================================================== + int getNumRows() override + { + return isInput ? parent.getNumMidiInputs() + : parent.getNumMidiOutputs(); + } + + //========================================================================== + void paintListBoxItem (int rowNumber, Graphics &g, + int width, int height, bool rowIsSelected) override + { + if (rowIsSelected) + g.fillAll (Colours::lightblue); + else if (rowNumber % 2) + g.fillAll (Colour (0xffeeeeee)); + + + g.setColour (Colours::black); + g.setFont (height * 0.7f); + + if (isInput) + { + if (rowNumber < parent.getNumMidiInputs()) + g.drawText (parent.getMidiDevice (rowNumber, true)->name, + 5, 0, width, height, + Justification::centredLeft, true); + } + else + { + if (rowNumber < parent.getNumMidiOutputs()) + g.drawText (parent.getMidiDevice (rowNumber, false)->name, + 5, 0, width, height, + Justification::centredLeft, true); + } + } + + //========================================================================== + void selectedRowsChanged (int lastRowSelected) override + { + ignoreUnused (lastRowSelected); + + SparseSet newSelectedItems = getSelectedRows(); + if (newSelectedItems != lastSelectedItems) + { + for (int i = 0; i < lastSelectedItems.size(); ++i) + { + if (! newSelectedItems.contains (lastSelectedItems[i])) + parent.closeDevice (isInput, lastSelectedItems[i]); + } + + for (int i = 0; i < newSelectedItems.size(); ++i) + { + if (! lastSelectedItems.contains (newSelectedItems[i])) + parent.openDevice (isInput, newSelectedItems[i]); + } + + lastSelectedItems = newSelectedItems; + } + } + + //========================================================================== + void syncSelectedItemsWithDeviceList (const ReferenceCountedArray& midiDevices) + { + SparseSet selectedRows; + for (int i = 0; i < midiDevices.size(); ++i) + if (midiDevices[i]->inDevice != nullptr || midiDevices[i]->outDevice != nullptr) + selectedRows.addRange (Range (i, i+1)); + + lastSelectedItems = selectedRows; + updateContent(); + setSelectedRows (selectedRows, dontSendNotification); + } + +private: + //========================================================================== + MainContentComponent& parent; + bool isInput; + SparseSet lastSelectedItems; +}; + +//============================================================================== +MainContentComponent::MainContentComponent () + : midiInputLabel ("Midi Input Label", "MIDI Input:"), + midiOutputLabel ("Midi Output Label", "MIDI Output:"), + incomingMidiLabel ("Incoming Midi Label", "Received MIDI messages:"), + outgoingMidiLabel ("Outgoing Midi Label", "Play the keyboard to send MIDI messages..."), + midiKeyboard (keyboardState, MidiKeyboardComponent::horizontalKeyboard), + midiMonitor ("MIDI Monitor"), + pairButton ("MIDI Bluetooth devices..."), + midiInputSelector (new MidiDeviceListBox ("Midi Input Selector", *this, true)), + midiOutputSelector (new MidiDeviceListBox ("Midi Input Selector", *this, false)) +{ + setSize (732, 520); + + addLabelAndSetStyle (midiInputLabel); + addLabelAndSetStyle (midiOutputLabel); + addLabelAndSetStyle (incomingMidiLabel); + addLabelAndSetStyle (outgoingMidiLabel); + + midiKeyboard.setName ("MIDI Keyboard"); + addAndMakeVisible (midiKeyboard); + + midiMonitor.setMultiLine (true); + midiMonitor.setReturnKeyStartsNewLine (false); + midiMonitor.setReadOnly (true); + midiMonitor.setScrollbarsShown (true); + midiMonitor.setCaretVisible (false); + midiMonitor.setPopupMenuEnabled (false); + midiMonitor.setText (String::empty); + addAndMakeVisible (midiMonitor); + + if (! BluetoothMidiDevicePairingDialogue::isAvailable()) + pairButton.setEnabled (false); + + addAndMakeVisible (pairButton); + pairButton.addListener (this); + keyboardState.addListener (this); + + addAndMakeVisible (midiInputSelector); + addAndMakeVisible (midiOutputSelector); + + startTimer (500); +} + +//============================================================================== +void MainContentComponent::addLabelAndSetStyle (Label& label) +{ + label.setFont (Font (15.00f, Font::plain)); + label.setJustificationType (Justification::centredLeft); + label.setEditable (false, false, false); + label.setColour (TextEditor::textColourId, Colours::black); + label.setColour (TextEditor::backgroundColourId, Colour (0x00000000)); + + addAndMakeVisible (label); +} + +//============================================================================== +MainContentComponent::~MainContentComponent() +{ + stopTimer(); + midiInputs.clear(); + midiOutputs.clear(); + keyboardState.removeListener (this); + + midiInputSelector = nullptr; + midiOutputSelector = nullptr; + midiOutputSelector = nullptr; +} + +//============================================================================== +void MainContentComponent::paint (Graphics& g) +{ + g.fillAll (Colours::white); +} + +//============================================================================== +void MainContentComponent::resized() +{ + const int margin = 10; + + midiInputLabel.setBounds (margin, margin, + (getWidth() / 2) - (2 * margin), 24); + + midiOutputLabel.setBounds ((getWidth() / 2) + margin, margin, + (getWidth() / 2) - (2 * margin), 24); + + midiInputSelector->setBounds (margin, (2 * margin) + 24, + (getWidth() / 2) - (2 * margin), + (getHeight() / 2) - ((4 * margin) + 24 + 24)); + + midiOutputSelector->setBounds ((getWidth() / 2) + margin, (2 * margin) + 24, + (getWidth() / 2) - (2 * margin), + (getHeight() / 2) - ((4 * margin) + 24 + 24)); + + pairButton.setBounds (margin, (getHeight() / 2) - (margin + 24), + getWidth() - (2 * margin), 24); + + outgoingMidiLabel.setBounds (margin, getHeight() / 2, getWidth() - (2*margin), 24); + midiKeyboard.setBounds (margin, (getHeight() / 2) + (24 + margin), getWidth() - (2*margin), 64); + + incomingMidiLabel.setBounds (margin, (getHeight() / 2) + (24 + (2 * margin) + 64), + getWidth() - (2*margin), 24); + + int y = (getHeight() / 2) + ((2 * 24) + (3 * margin) + 64); + midiMonitor.setBounds (margin, y, + getWidth() - (2*margin), getHeight() - y - margin); +} + +//============================================================================== +void MainContentComponent::buttonClicked (Button* buttonThatWasClicked) +{ + if (buttonThatWasClicked == &pairButton) + BluetoothMidiDevicePairingDialogue::open(); +} + +//============================================================================== +bool MainContentComponent::hasDeviceListChanged (const StringArray& deviceNames, bool isInputDevice) +{ + ReferenceCountedArray& midiDevices = isInputDevice ? midiInputs + : midiOutputs; + + if (deviceNames.size() != midiDevices.size()) + return true; + + for (int i = 0; i < deviceNames.size(); ++i) + if (deviceNames[i] != midiDevices[i]->name) + return true; + + return false; +} + +MidiDeviceListEntry::Ptr MainContentComponent::findDeviceWithName (const String& name, bool isInputDevice) const +{ + const ReferenceCountedArray& midiDevices = isInputDevice ? midiInputs + : midiOutputs; + + for (int i = 0; i < midiDevices.size(); ++i) + if (midiDevices[i]->name == name) + return midiDevices[i]; + + return nullptr; +} + +void MainContentComponent::closeUnpluggedDevices (StringArray& currentlyPluggedInDevices, bool isInputDevice) +{ + ReferenceCountedArray& midiDevices = isInputDevice ? midiInputs + : midiOutputs; + + for (int i = midiDevices.size(); --i >= 0;) + { + MidiDeviceListEntry& d = *midiDevices[i]; + + if (! currentlyPluggedInDevices.contains (d.name)) + { + if (isInputDevice ? d.inDevice != nullptr + : d.outDevice != nullptr) + closeDevice (isInputDevice, i); + + midiDevices.remove (i); + } + } +} + +void MainContentComponent::updateDeviceList (bool isInputDeviceList) +{ + StringArray newDeviceNames = isInputDeviceList ? MidiInput::getDevices() + : MidiOutput::getDevices(); + + if (hasDeviceListChanged (newDeviceNames, isInputDeviceList)) + { + + ReferenceCountedArray& midiDevices + = isInputDeviceList ? midiInputs : midiOutputs; + + closeUnpluggedDevices (newDeviceNames, isInputDeviceList); + + ReferenceCountedArray newDeviceList; + + // add all currently plugged-in devices to the device list + for (int i = 0; i < newDeviceNames.size(); ++i) + { + MidiDeviceListEntry::Ptr entry = findDeviceWithName (newDeviceNames[i], isInputDeviceList); + + if (entry == nullptr) + entry = new MidiDeviceListEntry (newDeviceNames[i]); + + newDeviceList.add (entry); + } + + // actually update the device list + midiDevices = newDeviceList; + + // update the selection status of the combo-box + if (MidiDeviceListBox* midiSelector = isInputDeviceList ? midiInputSelector : midiOutputSelector) + midiSelector->syncSelectedItemsWithDeviceList (midiDevices); + } +} + +//============================================================================== +void MainContentComponent::timerCallback () +{ + updateDeviceList (true); + updateDeviceList (false); +} + +//============================================================================== +void MainContentComponent::handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity)); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); + sendToOutputs (m); +} + +//============================================================================== +void MainContentComponent::handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) +{ + MidiMessage m (MidiMessage::noteOff (midiChannel, midiNoteNumber, velocity)); + m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001); + sendToOutputs (m); +} + +//============================================================================== +void MainContentComponent::sendToOutputs(const MidiMessage& msg) +{ + for (int i = 0; i < midiOutputs.size(); ++i) + if (midiOutputs[i]->outDevice != nullptr) + midiOutputs[i]->outDevice->sendMessageNow (msg); +} + +//============================================================================== +void MainContentComponent::handleIncomingMidiMessage (MidiInput* /*source*/, const MidiMessage &message) +{ + // This is called on the MIDI thread + + if (message.isNoteOnOrOff()) + postMessage (new MidiCallbackMessage (message)); +} + +//============================================================================== +void MainContentComponent::handleMessage (const Message& msg) +{ + // This is called on the message loop + + const MidiMessage& mm = dynamic_cast (msg).message; + String midiString; + midiString << (mm.isNoteOn() ? String ("Note on: ") : String ("Note off: ")); + midiString << (MidiMessage::getMidiNoteName (mm.getNoteNumber(), true, true, true)); + midiString << (String (" vel = ")); + midiString << static_cast(mm.getVelocity()); + midiString << "\n"; + + midiMonitor.insertTextAtCaret (midiString); +} + +//============================================================================== +void MainContentComponent::openDevice (bool isInput, int index) +{ + if (isInput) + { + jassert (midiInputs[index]->inDevice == nullptr); + midiInputs[index]->inDevice = MidiInput::openDevice (index, this); + + if (midiInputs[index]->inDevice == nullptr) + { + DBG ("MainContentComponent::openDevice: open input device for index = " << index << " failed!" ); + return; + } + + midiInputs[index]->inDevice->start(); + } + else + { + jassert (midiOutputs[index]->outDevice == nullptr); + midiOutputs[index]->outDevice = MidiOutput::openDevice (index); + + if (midiOutputs[index]->outDevice == nullptr) + DBG ("MainContentComponent::openDevice: open output device for index = " << index << " failed!" ); + } +} + +//============================================================================== +void MainContentComponent::closeDevice (bool isInput, int index) +{ + if (isInput) + { + jassert (midiInputs[index]->inDevice != nullptr); + midiInputs[index]->inDevice->stop(); + midiInputs[index]->inDevice = nullptr; + } + else + { + jassert (midiOutputs[index]->outDevice != nullptr); + midiOutputs[index]->outDevice = nullptr; + } +} + +//============================================================================== +int MainContentComponent::getNumMidiInputs() const noexcept +{ + return midiInputs.size(); +} + +//============================================================================== +int MainContentComponent::getNumMidiOutputs() const noexcept +{ + return midiOutputs.size(); +} + +//============================================================================== +ReferenceCountedObjectPtr +MainContentComponent::getMidiDevice (int index, bool isInput) const noexcept +{ + return isInput ? midiInputs[index] : midiOutputs[index]; +} diff --git a/examples/MidiTest/Source/MainComponent.h b/examples/MidiTest/Source/MainComponent.h new file mode 100644 index 0000000000..2c349619e5 --- /dev/null +++ b/examples/MidiTest/Source/MainComponent.h @@ -0,0 +1,99 @@ +/* + ============================================================================== + + 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. + + ============================================================================== +*/ + +#ifndef JUCE_MIDITEST_MAINCOMPONENT_H +#define JUCE_MIDITEST_MAINCOMPONENT_H + +#include "JuceHeader.h" + +//============================================================================== + +class MidiDeviceListBox; +struct MidiDeviceListEntry; + +//============================================================================== +class MainContentComponent : public Component, + private Timer, + private MidiKeyboardStateListener, + private MidiInputCallback, + private MessageListener, + private ButtonListener +{ +public: + //========================================================================== + MainContentComponent (); + ~MainContentComponent(); + + //========================================================================== + void timerCallback () override; + void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override; + void handleMessage (const Message& msg) override; + + void paint (Graphics& g) override; + void resized() override; + void buttonClicked (Button* buttonThatWasClicked) override; + + void openDevice (bool isInput, int index); + void closeDevice (bool isInput, int index); + + int getNumMidiInputs() const noexcept; + int getNumMidiOutputs() const noexcept; + + ReferenceCountedObjectPtr getMidiDevice (int index, bool isInputDevice) const noexcept; +private: + //========================================================================== + void handleIncomingMidiMessage (MidiInput *source, const MidiMessage &message) override; + void sendToOutputs(const MidiMessage& msg); + + //========================================================================== + bool hasDeviceListChanged (const StringArray& deviceNames, bool isInputDevice); + ReferenceCountedObjectPtr findDeviceWithName (const String& name, bool isInputDevice) const; + void closeUnpluggedDevices (StringArray& currentlyPluggedInDevices, bool isInputDevice); + void updateDeviceList (bool isInputDeviceList); + + //========================================================================== + void addLabelAndSetStyle (Label& label); + + //========================================================================== + Label midiInputLabel; + Label midiOutputLabel; + Label incomingMidiLabel; + Label outgoingMidiLabel; + MidiKeyboardState keyboardState; + MidiKeyboardComponent midiKeyboard; + TextEditor midiMonitor; + TextButton pairButton; + + ScopedPointer midiInputSelector; + ScopedPointer midiOutputSelector; + + ReferenceCountedArray midiInputs; + ReferenceCountedArray midiOutputs; + + //========================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) +}; + +#endif // JUCE_MIDITEST_MAINCOMPONENT_H diff --git a/examples/MidiTest/Source/Resources/Bluetooth.svg b/examples/MidiTest/Source/Resources/Bluetooth.svg new file mode 100644 index 0000000000..cb9ee695ba --- /dev/null +++ b/examples/MidiTest/Source/Resources/Bluetooth.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h index 224477da1a..ee244aeef2 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Android.h @@ -97,6 +97,9 @@ public: props.add (new BooleanPropertyComponent (getAudioRecordNeededValue(), "Audio Input Required", "Specify audio record permission in the manifest"), "If enabled, this will set the android.permission.RECORD_AUDIO flag in the manifest."); + props.add (new BooleanPropertyComponent (getBluetoothPermissionsValue(), "Bluetooth permissions Required", "Specify bluetooth permission (required for Bluetooth MIDI)"), + "If enabled, this will set the android.permission.BLUETOOTH and android.permission.BLUETOOTH_ADMIN flag in the manifest. This is required for Bluetooth MIDI on Android."); + props.add (new TextPropertyComponent (getOtherPermissionsValue(), "Custom permissions", 2048, false), "A space-separated list of other permission flags that should be added to the manifest."); @@ -145,6 +148,8 @@ public: bool getInternetNeeded() const { return settings [Ids::androidInternetNeeded]; } Value getAudioRecordNeededValue() { return getSetting (Ids::androidMicNeeded); } bool getAudioRecordNeeded() const { return settings [Ids::androidMicNeeded]; } + Value getBluetoothPermissionsValue() { return getSetting(Ids::androidBluetoothNeeded); } + bool getBluetoothPermissions() const { return settings[Ids::androidBluetoothNeeded]; } Value getMinimumSDKVersionValue() { return getSetting (Ids::androidMinimumSDK); } String getMinimumSDKVersionString() const { return settings [Ids::androidMinimumSDK]; } Value getOtherPermissionsValue() { return getSetting (Ids::androidOtherPermissions); } @@ -207,14 +212,47 @@ public: { File javaDestFile (targetFolder.getChildFile (className + ".java")); - File javaSourceFile (coreModule->getFolder().getChildFile ("native") - .getChildFile ("java") - .getChildFile ("JuceAppActivity.java")); + File javaSourceFolder (coreModule->getFolder().getChildFile ("native") + .getChildFile ("java")); + + String juceMidiCode, juceMidiImports; + + juceMidiImports << newLine; + + if (getMinimumSDKVersionString().getIntValue() >= 23) + { + File javaAndroidMidi (javaSourceFolder.getChildFile ("AndroidMidi.java")); + + juceMidiImports << "import android.media.midi.*;" << newLine + << "import android.bluetooth.*;" << newLine + << "import android.bluetooth.le.*;" << newLine; + + juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className); + } + else + { + juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java") + .loadFileAsString() + .replace ("JuceAppActivity", className); + } + + File javaSourceFile (javaSourceFolder.getChildFile ("JuceAppActivity.java")); + StringArray javaSourceLines (StringArray::fromLines (javaSourceFile.loadFileAsString())); MemoryOutputStream newFile; - newFile << javaSourceFile.loadFileAsString() - .replace ("JuceAppActivity", className) - .replace ("package com.juce;", "package " + package + ";"); + + for (int i = 0; i < javaSourceLines.size(); ++i) + { + const String& line = javaSourceLines[i]; + + if (line.contains ("$$JuceAndroidMidiImports$$")) + newFile << juceMidiImports; + else if (line.contains ("$$JuceAndroidMidiCode$$")) + newFile << juceMidiCode; + else + newFile << line.replace ("JuceAppActivity", className) + .replace ("package com.juce;", "package " + package + ";") << newLine; + } overwriteFileIfDifferentOrThrow (javaDestFile, newFile); } @@ -275,8 +313,17 @@ public: StringArray s; s.addTokens (getOtherPermissions(), ", ", ""); - if (getInternetNeeded()) s.add ("android.permission.INTERNET"); - if (getAudioRecordNeeded()) s.add ("android.permission.RECORD_AUDIO"); + if (getInternetNeeded()) + s.add ("android.permission.INTERNET"); + + if (getAudioRecordNeeded()) + s.add ("android.permission.RECORD_AUDIO"); + + if (getBluetoothPermissions()) + { + s.add ("android.permission.BLUETOOTH"); + s.add ("android.permission.BLUETOOTH_ADMIN"); + } return getCleanedStringArray (s); } @@ -422,7 +469,7 @@ class AndroidAntProjectExporter : public AndroidProjectExporterBase { public: //============================================================================== - static const char* getName() { return "Android Project"; } + static const char* getName() { return "Android Ant Project"; } static const char* getValueTreeTypeName() { return "ANDROID"; } static AndroidAntProjectExporter* createForSettings (Project& project, const ValueTree& settings) @@ -484,11 +531,16 @@ public: class AndroidBuildConfiguration : public BuildConfiguration { public: - AndroidBuildConfiguration (Project& p, const ValueTree& settings) - : BuildConfiguration (p, settings) + AndroidBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : BuildConfiguration (p, settings, e) { if (getArchitectures().isEmpty()) - getArchitecturesValue() = "armeabi armeabi-v7a"; + { + if (isDebug()) + getArchitecturesValue() = "armeabi x86"; + else + getArchitecturesValue() = "armeabi armeabi-v7a x86"; + } } Value getArchitecturesValue() { return getValue (Ids::androidArchitectures); } @@ -507,7 +559,7 @@ public: BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { - return new AndroidBuildConfiguration (project, v); + return new AndroidBuildConfiguration (project, v, *this); } private: @@ -545,7 +597,7 @@ private: struct Predicate { - bool operator() (const Project::Item& projectItem) const { return projectItem.shouldBeAddedToTargetProject(); } + bool operator() (const Project::Item& projectItem) const { return projectItem.shouldBeCompiled(); } }; for (int i = 0; i < getAllGroups().size(); ++i) @@ -629,7 +681,7 @@ private: String getLDLIBS (const AndroidBuildConfiguration& config) const { return " LOCAL_LDLIBS :=" + config.getGCCLibraryPathFlags() - + " -llog -lGLESv2 " + getExternalLibraryFlags (config) + + " -llog -lGLESv2 -landroid -lEGL" + getExternalLibraryFlags (config) + " " + replacePreprocessorTokens (config, getExtraLinkerFlagsString()); } diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h index 22f5a39a23..6920560062 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_AndroidStudio.h @@ -133,8 +133,8 @@ protected: class AndroidStudioBuildConfiguration : public BuildConfiguration { public: - AndroidStudioBuildConfiguration (Project& p, const ValueTree& settings) - : BuildConfiguration (p, settings) + AndroidStudioBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : BuildConfiguration (p, settings, e) { if (getArchitectures().isEmpty()) getArchitecturesValue() = "armeabi armeabi-v7a"; @@ -156,7 +156,7 @@ protected: BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { - return new AndroidStudioBuildConfiguration (project, v); + return new AndroidStudioBuildConfiguration (project, v, *this); } private: diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_CodeBlocks.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_CodeBlocks.h index 5482278265..5c1d35f9fb 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_CodeBlocks.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_CodeBlocks.h @@ -123,8 +123,8 @@ private: class CodeBlocksBuildConfiguration : public BuildConfiguration { public: - CodeBlocksBuildConfiguration (Project& p, const ValueTree& settings) - : BuildConfiguration (p, settings) + CodeBlocksBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : BuildConfiguration (p, settings, e) { } @@ -138,7 +138,7 @@ private: BuildConfiguration::Ptr createBuildConfig (const ValueTree& tree) const override { - return new CodeBlocksBuildConfiguration (project, tree); + return new CodeBlocksBuildConfiguration (project, tree, *this); } //============================================================================== diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_MSVC.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_MSVC.h index 45f8f2ae6b..e75533c576 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_MSVC.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_MSVC.h @@ -128,8 +128,8 @@ protected: class MSVCBuildConfiguration : public BuildConfiguration { public: - MSVCBuildConfiguration (Project& p, const ValueTree& settings) - : BuildConfiguration (p, settings) + MSVCBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : BuildConfiguration (p, settings, e) { if (getWarningLevel() == 0) getWarningLevelValue() = 4; @@ -237,7 +237,7 @@ protected: BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { - return new MSVCBuildConfiguration (project, v); + return new MSVCBuildConfiguration (project, v, *this); } //============================================================================== @@ -620,11 +620,11 @@ public: if (iconFile != File::nonexistent) { group.addFileAtIndex (iconFile, -1, true); - group.findItemForFile (iconFile).getShouldAddToResourceValue() = false; + group.findItemForFile (iconFile).getShouldAddToBinaryResourcesValue() = false; } group.addFileAtIndex (rcFile, -1, true); - group.findItemForFile (rcFile).getShouldAddToResourceValue() = false; + group.findItemForFile (rcFile).getShouldAddToBinaryResourcesValue() = false; break; } @@ -1048,8 +1048,8 @@ protected: class VC2010BuildConfiguration : public MSVCBuildConfiguration { public: - VC2010BuildConfiguration (Project& p, const ValueTree& settings) - : MSVCBuildConfiguration (p, settings) + VC2010BuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : MSVCBuildConfiguration (p, settings, e) { if (getArchitectureType().toString().isEmpty()) getArchitectureType() = get32BitArchName(); @@ -1085,7 +1085,7 @@ protected: BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { - return new VC2010BuildConfiguration (project, v); + return new VC2010BuildConfiguration (project, v, *this); } static bool is64Bit (const BuildConfiguration& config) diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Make.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Make.h index 41b9dba939..347c2acb3c 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Make.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_Make.h @@ -90,8 +90,8 @@ protected: class MakeBuildConfiguration : public BuildConfiguration { public: - MakeBuildConfiguration (Project& p, const ValueTree& settings) - : BuildConfiguration (p, settings) + MakeBuildConfiguration (Project& p, const ValueTree& settings, const ProjectExporter& e) + : BuildConfiguration (p, settings, e) { setValueIfVoid (getLibrarySearchPathValue(), "/usr/X11R6/lib/"); } @@ -116,7 +116,7 @@ protected: BuildConfiguration::Ptr createBuildConfig (const ValueTree& tree) const override { - return new MakeBuildConfiguration (project, tree); + return new MakeBuildConfiguration (project, tree, *this); } private: diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h index 9dea2d7f36..fbb44c0901 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExport_XCode.h @@ -166,8 +166,8 @@ protected: class XcodeBuildConfiguration : public BuildConfiguration { public: - XcodeBuildConfiguration (Project& p, const ValueTree& t, const bool isIOS) - : BuildConfiguration (p, t), iOS (isIOS) + XcodeBuildConfiguration (Project& p, const ValueTree& t, const bool isIOS, const ProjectExporter& e) + : BuildConfiguration (p, t, e), iOS (isIOS) { if (iOS) { @@ -296,7 +296,7 @@ protected: BuildConfiguration::Ptr createBuildConfig (const ValueTree& v) const override { - return new XcodeBuildConfiguration (project, v, iOS); + return new XcodeBuildConfiguration (project, v, iOS, *this); } private: @@ -1121,7 +1121,7 @@ private: return "file" + file.getFileExtension(); } - String addFile (const RelativePath& path, bool shouldBeCompiled, bool shouldBeAddedToBinaryResources, bool inhibitWarnings) const + String addFile (const RelativePath& path, bool shouldBeCompiled, bool shouldBeAddedToBinaryResources, bool shouldBeAddedToXcodeResources, bool inhibitWarnings) const { const String pathAsString (path.toUnixStyle()); const String refID (addFileReference (path.toUnixStyle())); @@ -1133,11 +1133,11 @@ private: else addBuildFile (pathAsString, refID, true, inhibitWarnings); } - else if (! shouldBeAddedToBinaryResources) + else if (! shouldBeAddedToBinaryResources || shouldBeAddedToXcodeResources) { const String fileType (getFileType (path)); - if (fileType.startsWith ("image.") || fileType.startsWith ("text.") || fileType.startsWith ("file.")) + if (shouldBeAddedToXcodeResources || fileType.startsWith ("image.") || fileType.startsWith ("text.") || fileType.startsWith ("file.")) { resourceIDs.add (addBuildFile (pathAsString, refID, false, false)); resourceFileRefs.add (refID); @@ -1175,6 +1175,7 @@ private: return addFile (path, projectItem.shouldBeCompiled(), projectItem.shouldBeAddedToBinaryResources(), + projectItem.shouldBeAddedToXcodeResources(), projectItem.shouldInhibitWarnings()); } diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp b/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp index 0cab1de7f7..d19aa817b0 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.cpp @@ -635,8 +635,8 @@ bool ProjectExporter::ConstConfigIterator::next() } //============================================================================== -ProjectExporter::BuildConfiguration::BuildConfiguration (Project& p, const ValueTree& configNode) - : config (configNode), project (p) +ProjectExporter::BuildConfiguration::BuildConfiguration (Project& p, const ValueTree& configNode, const ProjectExporter& e) + : config (configNode), project (p), exporter (e) { } @@ -686,14 +686,13 @@ void ProjectExporter::BuildConfiguration::addGCCOptimisationProperty (PropertyLi void ProjectExporter::BuildConfiguration::createPropertyEditors (PropertyListBuilder& props) { - props.add (new TextPropertyComponent (getNameValue(), "Name", 96, false), - "The name of this configuration."); + if (exporter.supportsUserDefinedConfigurations()) + props.add (new TextPropertyComponent (getNameValue(), "Name", 96, false), + "The name of this configuration."); props.add (new BooleanPropertyComponent (isDebugValue(), "Debug mode", "Debugging enabled"), "If enabled, this means that the configuration should be built with debug synbols."); -// addGCCOptimisationProperty (props); - props.add (new TextPropertyComponent (getTargetBinaryName(), "Binary name", 256, false), "The filename to use for the destination binary executable file. If you don't add a suffix to this name, " "a suitable platform-specific suffix will be added automatically."); diff --git a/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.h b/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.h index 8b9d213a13..eef60d9a31 100644 --- a/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.h +++ b/extras/Introjucer/Source/Project Saving/jucer_ProjectExporter.h @@ -66,6 +66,7 @@ public: virtual void create (const OwnedArray&) const = 0; // may throw a SaveError virtual bool shouldFileBeCompiledByDefault (const RelativePath& path) const; virtual bool canCopeWithDuplicateFiles() = 0; + virtual bool supportsUserDefinedConfigurations() const { return true; } virtual bool isXcode() const { return false; } virtual bool isVisualStudio() const { return false; } @@ -198,7 +199,7 @@ public: class BuildConfiguration : public ReferenceCountedObject { public: - BuildConfiguration (Project& project, const ValueTree& configNode); + BuildConfiguration (Project& project, const ValueTree& configNode, const ProjectExporter&); ~BuildConfiguration(); typedef ReferenceCountedObjectPtr Ptr; @@ -250,8 +251,7 @@ public: //============================================================================== ValueTree config; Project& project; - - protected: + const ProjectExporter& exporter; private: JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BuildConfiguration) diff --git a/extras/Introjucer/Source/Project/jucer_ConfigTree_Exporter.h b/extras/Introjucer/Source/Project/jucer_ConfigTree_Exporter.h index 03cf750c2f..15be8db10b 100644 --- a/extras/Introjucer/Source/Project/jucer_ConfigTree_Exporter.h +++ b/extras/Introjucer/Source/Project/jucer_ConfigTree_Exporter.h @@ -66,13 +66,13 @@ public: void addSubItems() override { for (ProjectExporter::ConfigIterator config (*exporter); config.next();) - addSubItem (new ConfigItem (config.config, exporter->getName())); + addSubItem (new ConfigItem (config.config, *exporter)); } void showPopupMenu() override { PopupMenu menu; - menu.addItem (1, "Add a new configuration"); + menu.addItem (1, "Add a new configuration", exporter->supportsUserDefinedConfigurations()); menu.addSeparator(); menu.addItem (2, "Delete this exporter"); @@ -179,8 +179,8 @@ private: class ConfigItem : public ConfigTreeItemBase { public: - ConfigItem (const ProjectExporter::BuildConfiguration::Ptr& conf, const String& expName) - : config (conf), exporterName (expName), configTree (config->config) + ConfigItem (const ProjectExporter::BuildConfiguration::Ptr& conf, ProjectExporter& e) + : config (conf), exporter (e), configTree (config->config) { jassert (config != nullptr); configTree.addListener (this); @@ -195,7 +195,7 @@ public: void setName (const String&) override {} Icon getIcon() const override { return Icon (getIcons().config, getContrastingColour (Colours::green, 0.5f)); } - void showDocument() override { showSettingsPage (new SettingsComp (config, exporterName)); } + void showDocument() override { showSettingsPage (new SettingsComp (config)); } void itemOpennessChanged (bool) override {} void deleteItem() override @@ -210,10 +210,12 @@ public: void showPopupMenu() override { + bool enabled = exporter.supportsUserDefinedConfigurations(); + PopupMenu menu; - menu.addItem (1, "Create a copy of this configuration"); + menu.addItem (1, "Create a copy of this configuration", enabled); menu.addSeparator(); - menu.addItem (2, "Delete this configuration"); + menu.addItem (2, "Delete this configuration", enabled); launchPopupMenu (menu); } @@ -226,14 +228,7 @@ public: } else if (resultCode == 1) { - for (Project::ExporterIterator exporter (config->project); exporter.next();) - { - if (config->config.isAChildOf (exporter->settings)) - { - exporter->addNewConfiguration (config); - break; - } - } + exporter.addNewConfiguration (config); } } @@ -246,21 +241,21 @@ public: private: ProjectExporter::BuildConfiguration::Ptr config; - String exporterName; + ProjectExporter& exporter; ValueTree configTree; //============================================================================== class SettingsComp : public Component { public: - SettingsComp (ProjectExporter::BuildConfiguration* conf, const String& expName) + SettingsComp (ProjectExporter::BuildConfiguration* conf) { addAndMakeVisible (group); PropertyListBuilder props; conf->createPropertyEditors (props); group.setProperties (props); - group.setName (expName + " / " + conf->getName()); + group.setName (conf->exporter.getName() + " / " + conf->getName()); parentSizeChanged(); } diff --git a/extras/Introjucer/Source/Project/jucer_GroupInformationComponent.h b/extras/Introjucer/Source/Project/jucer_GroupInformationComponent.h index 1d6c4a3650..b4896e1e5b 100644 --- a/extras/Introjucer/Source/Project/jucer_GroupInformationComponent.h +++ b/extras/Introjucer/Source/Project/jucer_GroupInformationComponent.h @@ -117,15 +117,19 @@ private: FileOptionComponent (const Project::Item& fileItem) : item (fileItem), compileButton ("Compile"), - resourceButton ("Add to Binary Resources") + binaryResourceButton ("Binary Resource"), + xcodeResourceButton ("Xcode Resource") { if (item.isFile()) { addAndMakeVisible (compileButton); compileButton.getToggleStateValue().referTo (item.getShouldCompileValue()); - addAndMakeVisible (resourceButton); - resourceButton.getToggleStateValue().referTo (item.getShouldAddToResourceValue()); + addAndMakeVisible (binaryResourceButton); + binaryResourceButton.getToggleStateValue().referTo (item.getShouldAddToBinaryResourcesValue()); + + addAndMakeVisible (xcodeResourceButton); + xcodeResourceButton.getToggleStateValue().referTo (item.getShouldAddToXcodeResourcesValue()); } } @@ -148,16 +152,15 @@ private: void resized() override { - int w = 180; - resourceButton.setBounds (getWidth() - w, 1, w, getHeight() - 2); - w = 100; - compileButton.setBounds (resourceButton.getX() - w, 1, w, getHeight() - 2); + binaryResourceButton.setBounds (getWidth() - 110, 1, 110, getHeight() - 2); + xcodeResourceButton.setBounds (binaryResourceButton.getX() - 110, 1, 110, getHeight() - 2); + compileButton.setBounds (xcodeResourceButton.getX() - 70, 1, 70, getHeight() - 2); } Project::Item item; private: - ToggleButton compileButton, resourceButton; + ToggleButton compileButton, binaryResourceButton, xcodeResourceButton; }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GroupInformationComponent) diff --git a/extras/Introjucer/Source/Project/jucer_Project.cpp b/extras/Introjucer/Source/Project/jucer_Project.cpp index fea6b486b4..36f4e05645 100644 --- a/extras/Introjucer/Source/Project/jucer_Project.cpp +++ b/extras/Introjucer/Source/Project/jucer_Project.cpp @@ -592,9 +592,12 @@ bool Project::Item::shouldBeAddedToTargetProject() const { return isFile(); } Value Project::Item::getShouldCompileValue() { return state.getPropertyAsValue (Ids::compile, getUndoManager()); } bool Project::Item::shouldBeCompiled() const { return state [Ids::compile]; } -Value Project::Item::getShouldAddToResourceValue() { return state.getPropertyAsValue (Ids::resource, getUndoManager()); } +Value Project::Item::getShouldAddToBinaryResourcesValue() { return state.getPropertyAsValue (Ids::resource, getUndoManager()); } bool Project::Item::shouldBeAddedToBinaryResources() const { return state [Ids::resource]; } +Value Project::Item::getShouldAddToXcodeResourcesValue() { return state.getPropertyAsValue (Ids::xcodeResource, getUndoManager()); } +bool Project::Item::shouldBeAddedToXcodeResources() const { return state [Ids::xcodeResource]; } + Value Project::Item::getShouldInhibitWarningsValue() { return state.getPropertyAsValue (Ids::noWarnings, getUndoManager()); } bool Project::Item::shouldInhibitWarnings() const { return state [Ids::noWarnings]; } @@ -872,7 +875,7 @@ void Project::Item::addFileUnchecked (const File& file, int insertIndex, const b item.initialiseMissingProperties(); item.getNameValue() = file.getFileName(); item.getShouldCompileValue() = shouldCompile && file.hasFileExtension (fileTypesToCompileByDefault); - item.getShouldAddToResourceValue() = project.shouldBeAddedToBinaryResourcesByDefault (file); + item.getShouldAddToBinaryResourcesValue() = project.shouldBeAddedToBinaryResourcesByDefault (file); if (canContain (item)) { @@ -887,7 +890,7 @@ bool Project::Item::addRelativeFile (const RelativePath& file, int insertIndex, item.initialiseMissingProperties(); item.getNameValue() = file.getFileName(); item.getShouldCompileValue() = shouldCompile; - item.getShouldAddToResourceValue() = project.shouldBeAddedToBinaryResourcesByDefault (file); + item.getShouldAddToBinaryResourcesValue() = project.shouldBeAddedToBinaryResourcesByDefault (file); if (canContain (item)) { diff --git a/extras/Introjucer/Source/Project/jucer_Project.h b/extras/Introjucer/Source/Project/jucer_Project.h index 04306dbc61..9c89c62055 100644 --- a/extras/Introjucer/Source/Project/jucer_Project.h +++ b/extras/Introjucer/Source/Project/jucer_Project.h @@ -167,8 +167,13 @@ public: bool shouldBeAddedToTargetProject() const; bool shouldBeCompiled() const; Value getShouldCompileValue(); + bool shouldBeAddedToBinaryResources() const; - Value getShouldAddToResourceValue(); + Value getShouldAddToBinaryResourcesValue(); + + bool shouldBeAddedToXcodeResources() const; + Value getShouldAddToXcodeResourcesValue(); + Value getShouldInhibitWarningsValue(); bool shouldInhibitWarnings() const; Value getShouldUseStdCallValue(); diff --git a/extras/Introjucer/Source/Utility/jucer_PresetIDs.h b/extras/Introjucer/Source/Utility/jucer_PresetIDs.h index 69a1c72c4d..f7b5063e53 100644 --- a/extras/Introjucer/Source/Utility/jucer_PresetIDs.h +++ b/extras/Introjucer/Source/Utility/jucer_PresetIDs.h @@ -114,6 +114,7 @@ namespace Ids DECLARE_ID (compile); DECLARE_ID (noWarnings); DECLARE_ID (resource); + DECLARE_ID (xcodeResource); DECLARE_ID (className); DECLARE_ID (classDesc); DECLARE_ID (controlPoint); @@ -144,6 +145,7 @@ namespace Ids DECLARE_ID (androidArchitectures); DECLARE_ID (androidCpp11); DECLARE_ID (androidMicNeeded); + DECLARE_ID (androidBluetoothNeeded); DECLARE_ID (androidMinimumSDK); DECLARE_ID (androidOtherPermissions); DECLARE_ID (androidKeyStore); diff --git a/modules/juce_audio_devices/native/juce_android_Midi.cpp b/modules/juce_audio_devices/native/juce_android_Midi.cpp index 59ddf3edb8..01a5ead549 100644 --- a/modules/juce_audio_devices/native/juce_android_Midi.cpp +++ b/modules/juce_audio_devices/native/juce_android_Midi.cpp @@ -22,11 +22,228 @@ ============================================================================== */ +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getJuceAndroidMidiInputDevices, "getJuceAndroidMidiInputDevices", "()[Ljava/lang/String;") \ + METHOD (getJuceAndroidMidiOutputDevices, "getJuceAndroidMidiOutputDevices", "()[Ljava/lang/String;") \ + METHOD (openMidiInputPortWithJuceIndex, "openMidiInputPortWithJuceIndex", "(IJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort;") \ + METHOD (openMidiOutputPortWithJuceIndex, "openMidiOutputPortWithJuceIndex", "(I)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort;") \ + METHOD (getInputPortNameForJuceIndex, "getInputPortNameForJuceIndex", "(I)Ljava/lang/String;") \ + METHOD (getOutputPortNameForJuceIndex, "getOutputPortNameForJuceIndex", "(I)Ljava/lang/String;") + DECLARE_JNI_CLASS (MidiDeviceManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$MidiDeviceManager") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (start, "start", "()V" )\ + METHOD (stop, "stop", "()V") \ + METHOD (close, "close", "()V") \ + METHOD (sendMidi, "sendMidi", "([BII)V") + DECLARE_JNI_CLASS (JuceMidiPort, JUCE_ANDROID_ACTIVITY_CLASSPATH "$JuceMidiPort") +#undef JNI_CLASS_MEMBERS + + +//============================================================================== +class AndroidMidiInput +{ +public: + AndroidMidiInput (MidiInput* midiInput, int portIdx, + juce::MidiInputCallback* midiInputCallback, jobject deviceManager) + : juceMidiInput (midiInput), + callback (midiInputCallback), + midiConcatenator (2048), + javaMidiDevice (getEnv()->CallObjectMethod (deviceManager, + MidiDeviceManager.openMidiInputPortWithJuceIndex, + (jint) portIdx, + (jlong) this)) + { + } + + ~AndroidMidiInput() + { + if (jobject d = javaMidiDevice.get()) + { + getEnv()->CallVoidMethod (d, JuceMidiPort.close); + javaMidiDevice.clear(); + } + } + + bool isOpen() const noexcept + { + return javaMidiDevice != nullptr; + } + + void start() + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, JuceMidiPort.start); + } + + void stop() + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, JuceMidiPort.stop); + + callback = nullptr; + } + + void receive (jbyteArray byteArray, jlong offset, jint len, jlong timestamp) + { + jassert (byteArray != nullptr); + jbyte* data = getEnv()->GetByteArrayElements (byteArray, nullptr); + + HeapBlock buffer (len); + std::memcpy (buffer.getData(), data + offset, len); + + midiConcatenator.pushMidiData (buffer.getData(), + len, static_cast (timestamp) * 1.0e-9, + juceMidiInput, *callback); + + getEnv()->ReleaseByteArrayElements (byteArray, data, 0); + } + +private: + MidiInput* juceMidiInput; + MidiInputCallback* callback; + GlobalRef javaMidiDevice; + MidiDataConcatenator midiConcatenator; +}; + +//============================================================================== +class AndroidMidiOutput +{ +public: + AndroidMidiOutput (jobject midiDevice) + : javaMidiDevice (midiDevice) + { + } + + ~AndroidMidiOutput() + { + if (jobject d = javaMidiDevice.get()) + { + getEnv()->CallVoidMethod (d, JuceMidiPort.close); + javaMidiDevice.clear(); + } + } + + void send (jbyteArray byteArray, jint offset, jint len) + { + if (jobject d = javaMidiDevice.get()) + getEnv()->CallVoidMethod (d, + JuceMidiPort.sendMidi, + byteArray, offset, len); + } + +private: + GlobalRef javaMidiDevice; +}; + +JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024JuceMidiInputPort), handleReceive, + void, (JNIEnv* env, jobject device, jlong host, jbyteArray byteArray, + jint offset, jint count, jlong timestamp)) +{ + // Java may create a Midi thread which JUCE doesn't know about and this callback may be + // received on this thread. Java will have already created a JNI Env for this new thread, + // which we need to tell Juce about + setEnv (env); + + reinterpret_cast (host)->receive (byteArray, offset, count, timestamp); +} + +//============================================================================== +class AndroidMidiDeviceManager +{ +public: + AndroidMidiDeviceManager () + : deviceManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidMidiDeviceManager)) + { + } + + String getInputPortNameForJuceIndex (int idx) + { + if (jobject dm = deviceManager.get()) + { + LocalRef string ((jstring) getEnv()->CallObjectMethod (dm, MidiDeviceManager.getInputPortNameForJuceIndex, idx)); + return juceString (string); + } + + return String(); + } + + String getOutputPortNameForJuceIndex (int idx) + { + if (jobject dm = deviceManager.get()) + { + LocalRef string ((jstring) getEnv()->CallObjectMethod (dm, MidiDeviceManager.getOutputPortNameForJuceIndex, idx)); + return juceString (string); + } + + return String(); + } + + StringArray getDevices (bool input) + { + if (jobject dm = deviceManager.get()) + { + jobjectArray jDevices + = (jobjectArray) getEnv()->CallObjectMethod (dm, input ? MidiDeviceManager.getJuceAndroidMidiInputDevices + : MidiDeviceManager.getJuceAndroidMidiOutputDevices); + + // Create a local reference as converting this + // to a JUCE string will call into JNI + LocalRef devices (jDevices); + return javaStringArrayToJuce (devices); + } + + return StringArray(); + } + + AndroidMidiInput* openMidiInputPortWithIndex (int idx, MidiInput* juceMidiInput, juce::MidiInputCallback* callback) + { + if (jobject dm = deviceManager.get()) + { + ScopedPointer androidMidiInput (new AndroidMidiInput (juceMidiInput, idx, callback, dm)); + + if (androidMidiInput->isOpen()) + return androidMidiInput.release(); + } + + return nullptr; + } + + AndroidMidiOutput* openMidiOutputPortWithIndex (int idx) + { + if (jobject dm = deviceManager.get()) + if (jobject javaMidiPort = getEnv()->CallObjectMethod (dm, MidiDeviceManager.openMidiOutputPortWithJuceIndex, (jint) idx)) + return new AndroidMidiOutput (javaMidiPort); + + return nullptr; + } + +private: + static StringArray javaStringArrayToJuce (jobjectArray jStrings) + { + StringArray retval; + + JNIEnv* env = getEnv(); + const int count = env->GetArrayLength (jStrings); + + for (int i = 0; i < count; ++i) + { + LocalRef string ((jstring) env->GetObjectArrayElement (jStrings, i)); + retval.add (juceString (string)); + } + + return retval; + } + + GlobalRef deviceManager; +}; + +//============================================================================== StringArray MidiOutput::getDevices() { - StringArray devices; - - return devices; + AndroidMidiDeviceManager manager; + return manager.getDevices (false); } int MidiOutput::getDefaultDeviceIndex() @@ -36,35 +253,65 @@ int MidiOutput::getDefaultDeviceIndex() MidiOutput* MidiOutput::openDevice (int index) { + if (index < 0) + return nullptr; + + AndroidMidiDeviceManager manager; + + String midiOutputName = manager.getOutputPortNameForJuceIndex (index); + + if (midiOutputName.isEmpty()) + { + // you supplied an invalid device index! + jassertfalse; + return nullptr; + } + + if (AndroidMidiOutput* midiOutput = manager.openMidiOutputPortWithIndex (index)) + { + MidiOutput* retval = new MidiOutput (midiOutputName); + retval->internal = midiOutput; + + return retval; + } + return nullptr; } MidiOutput::~MidiOutput() { stopBackgroundThread(); + + delete reinterpret_cast (internal); } -void MidiOutput::sendMessageNow (const MidiMessage&) +void MidiOutput::sendMessageNow (const MidiMessage& message) { + if (AndroidMidiOutput* androidMidi = reinterpret_cast(internal)) + { + JNIEnv* env = getEnv(); + const int messageSize = message.getRawDataSize(); + + LocalRef messageContent = LocalRef (env->NewByteArray (messageSize)); + jbyteArray content = messageContent.get(); + + jbyte* rawBytes = env->GetByteArrayElements (content, nullptr); + std::memcpy (rawBytes, message.getRawData(), messageSize); + env->ReleaseByteArrayElements (content, rawBytes, 0); + + androidMidi->send (content, (jint) 0, (jint) messageSize); + } } //============================================================================== -MidiInput::MidiInput (const String& name_) - : name (name_), - internal (0) +MidiInput::MidiInput (const String& nm) : name (nm) { } -MidiInput::~MidiInput() -{ -} - -void MidiInput::start() -{ -} - -void MidiInput::stop() +StringArray MidiInput::getDevices() { + AndroidMidiDeviceManager manager; + return manager.getDevices (true); } int MidiInput::getDefaultDeviceIndex() @@ -72,14 +319,43 @@ int MidiInput::getDefaultDeviceIndex() return 0; } -StringArray MidiInput::getDevices() +MidiInput* MidiInput::openDevice (int index, juce::MidiInputCallback* callback) { - StringArray devs; + if (index < 0) + return nullptr; - return devs; + AndroidMidiDeviceManager manager; + + String midiInputName = manager.getInputPortNameForJuceIndex (index); + + if (midiInputName.isEmpty()) + { + // you supplied an invalid device index! + jassertfalse; + return nullptr; + } + + ScopedPointer midiInput (new MidiInput (midiInputName)); + + midiInput->internal = manager.openMidiInputPortWithIndex (index, midiInput, callback); + + return midiInput->internal != nullptr ? midiInput.release() + : nullptr; } -MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback) +void MidiInput::start() { - return nullptr; + if (AndroidMidiInput* mi = reinterpret_cast (internal)) + mi->start(); +} + +void MidiInput::stop() +{ + if (AndroidMidiInput* mi = reinterpret_cast (internal)) + mi->stop(); +} + +MidiInput::~MidiInput() +{ + delete reinterpret_cast (internal); } diff --git a/modules/juce_audio_devices/native/juce_android_OpenSL.cpp b/modules/juce_audio_devices/native/juce_android_OpenSL.cpp index f4c09bb71f..514fa28192 100644 --- a/modules/juce_audio_devices/native/juce_android_OpenSL.cpp +++ b/modules/juce_audio_devices/native/juce_android_OpenSL.cpp @@ -32,7 +32,7 @@ bool isOpenSLAvailable() //============================================================================== class OpenSLAudioIODevice : public AudioIODevice, - public Thread + private Thread { public: OpenSLAudioIODevice (const String& deviceName) @@ -81,13 +81,28 @@ public: Array getAvailableSampleRates() override { static const double rates[] = { 8000.0, 16000.0, 32000.0, 44100.0, 48000.0 }; - return Array (rates, numElementsInArray (rates)); + Array retval (rates, numElementsInArray (rates)); + + // make sure the native sample rate is pafrt of the list + double native = getNativeSampleRate(); + if (native != 0.0 && ! retval.contains (native)) + retval.add (native); + + return retval; } Array getAvailableBufferSizes() override { - static const int sizes[] = { 256, 512, 768, 1024, 1280, 1600 }; // must all be multiples of the block size - return Array (sizes, numElementsInArray (sizes)); + // we need to offer the lowest possible buffer size which + // is the native buffer size + const int defaultNumMultiples = 8; + const int nativeBufferSize = getNativeBufferSize(); + + Array retval; + for (int i = 1; i < defaultNumMultiples; ++i) + retval.add (i * nativeBufferSize); + + return retval; } String open (const BigInteger& inputChannels, @@ -116,8 +131,28 @@ public: outputBuffer.setSize (jmax (1, numOutputChannels), actualBufferSize); outputBuffer.clear(); - recorder = engine.createRecorder (numInputChannels, sampleRate); - player = engine.createPlayer (numOutputChannels, sampleRate); + const int audioBuffersToEnqueue = hasLowLatencyAudioPath ? buffersToEnqueueForLowLatency + : buffersToEnqueueSlowAudio; + + DBG ("OpenSL: numInputChannels = " << numInputChannels + << ", numOutputChannels = " << numOutputChannels + << ", nativeBufferSize = " << getNativeBufferSize() + << ", nativeSampleRate = " << getNativeSampleRate() + << ", actualBufferSize = " << actualBufferSize + << ", audioBuffersToEnqueue = " << audioBuffersToEnqueue + << ", sampleRate = " << sampleRate); + + if (numInputChannels > 0) + recorder = engine.createRecorder (numInputChannels, sampleRate, + audioBuffersToEnqueue, actualBufferSize); + + if (numOutputChannels > 0) + player = engine.createPlayer (numOutputChannels, sampleRate, + audioBuffersToEnqueue, actualBufferSize); + + // pre-fill buffers + for (int i = 0; i < audioBuffersToEnqueue; ++i) + processBuffers(); startThread (8); @@ -134,18 +169,30 @@ public: player = nullptr; } - int getDefaultBufferSize() override { return 1024; } int getOutputLatencyInSamples() override { return outputLatency; } int getInputLatencyInSamples() override { return inputLatency; } bool isOpen() override { return deviceOpen; } int getCurrentBufferSizeSamples() override { return actualBufferSize; } int getCurrentBitDepth() override { return 16; } - double getCurrentSampleRate() override { return sampleRate; } BigInteger getActiveOutputChannels() const override { return activeOutputChans; } BigInteger getActiveInputChannels() const override { return activeInputChans; } String getLastError() override { return lastError; } bool isPlaying() override { return callback != nullptr; } + int getDefaultBufferSize() override + { + // Only on a Pro-Audio device will we set the lowest possible buffer size + // by default. We need to be more conservative on other devices + // as they may be low-latency, but still have a crappy CPU. + return (isProAudioDevice() ? 1 : 6) + * defaultBufferSizeIsMultipleOfNative * getNativeBufferSize(); + } + + double getCurrentSampleRate() override + { + return (sampleRate == 0.0 ? getNativeSampleRate() : sampleRate); + } + void start (AudioIODeviceCallback* newCallback) override { stop(); @@ -184,6 +231,55 @@ private: struct Player; struct Recorder; + enum + { + // The number of buffers to enqueue needs to be at least two for the audio to use the low-latency + // audio path (see "Performance" section in ndk/docs/Additional_library_docs/opensles/index.html) + buffersToEnqueueForLowLatency = 2, + buffersToEnqueueSlowAudio = 4, + defaultBufferSizeIsMultipleOfNative = 1 + }; + + //================================================================================================== + static String audioManagerGetProperty (const String& property) + { + const LocalRef jProperty (javaString (property)); + const LocalRef text ((jstring) android.activity.callObjectMethod (JuceAppActivity.audioManagerGetProperty, + jProperty.get())); + if (text.get() != 0) + return juceString (text); + + return String(); + } + + static bool androidHasSystemFeature (const String& property) + { + const LocalRef jProperty (javaString (property)); + return android.activity.callBooleanMethod (JuceAppActivity.hasSystemFeature, jProperty.get()); + } + + static double getNativeSampleRate() + { + return audioManagerGetProperty ("android.media.property.OUTPUT_SAMPLE_RATE").getDoubleValue(); + } + + static int getNativeBufferSize() + { + const int val = audioManagerGetProperty ("android.media.property.OUTPUT_FRAMES_PER_BUFFER").getIntValue(); + return val > 0 ? val : 512; + } + + static bool isProAudioDevice() + { + return androidHasSystemFeature ("android.hardware.audio.pro"); + } + + static bool hasLowLatencyAudioPath() + { + return androidHasSystemFeature ("android.hardware.audio.low_latency"); + } + + //================================================================================================== AudioIODeviceCallback* setCallback (AudioIODeviceCallback* const newCallback) { const ScopedLock sl (callbackLock); @@ -192,29 +288,45 @@ private: return oldCallback; } + void processBuffers() + { + if (recorder != nullptr) + recorder->readNextBlock (inputBuffer, *this); + + { + const ScopedLock sl (callbackLock); + + if (callback != nullptr) + callback->audioDeviceIOCallback (numInputChannels > 0 ? inputBuffer.getArrayOfReadPointers() : nullptr, numInputChannels, + numOutputChannels > 0 ? outputBuffer.getArrayOfWritePointers() : nullptr, numOutputChannels, + actualBufferSize); + else + outputBuffer.clear(); + } + + if (player != nullptr) + player->writeBuffer (outputBuffer, *this); + } + void run() override { + setThreadToAudioPriority (); + if (recorder != nullptr) recorder->start(); if (player != nullptr) player->start(); while (! threadShouldExit()) - { - if (player != nullptr) player->writeBuffer (outputBuffer, *this); - if (recorder != nullptr) recorder->readNextBlock (inputBuffer, *this); + processBuffers(); + } - const ScopedLock sl (callbackLock); + void setThreadToAudioPriority () + { + // see android.os.Process.THREAD_PRIORITY_AUDIO + const int THREAD_PRIORITY_AUDIO = -16; + jint priority = THREAD_PRIORITY_AUDIO; - if (callback != nullptr) - { - callback->audioDeviceIOCallback (numInputChannels > 0 ? inputBuffer.getArrayOfReadPointers() : nullptr, numInputChannels, - numOutputChannels > 0 ? outputBuffer.getArrayOfWritePointers() : nullptr, numOutputChannels, - actualBufferSize); - } - else - { - outputBuffer.clear(); - } - } + if (priority != android.activity.callIntMethod (JuceAppActivity.setCurrentThreadPriority, (jint) priority)) + DBG ("Unable to set audio thread priority: priority is still " << priority); } //================================================================================================== @@ -225,7 +337,8 @@ private: { if (library.open ("libOpenSLES.so")) { - typedef SLresult (*CreateEngineFunc) (SLObjectItf*, SLuint32, const SLEngineOption*, SLuint32, const SLInterfaceID*, const SLboolean*); + typedef SLresult (*CreateEngineFunc) (SLObjectItf*, SLuint32, const SLEngineOption*, + SLuint32, const SLInterfaceID*, const SLboolean*); if (CreateEngineFunc createEngine = (CreateEngineFunc) library.getFunction ("slCreateEngine")) { @@ -252,21 +365,21 @@ private: if (engineObject != nullptr) (*engineObject)->Destroy (engineObject); } - Player* createPlayer (const int numChannels, const int sampleRate) + Player* createPlayer (const int numChannels, const int sampleRate, const int numBuffers, const int bufferSize) { if (numChannels <= 0) return nullptr; - ScopedPointer player (new Player (numChannels, sampleRate, *this)); + ScopedPointer player (new Player (numChannels, sampleRate, *this, numBuffers, bufferSize)); return player->openedOk() ? player.release() : nullptr; } - Recorder* createRecorder (const int numChannels, const int sampleRate) + Recorder* createRecorder (const int numChannels, const int sampleRate, const int numBuffers, const int bufferSize) { if (numChannels <= 0) return nullptr; - ScopedPointer recorder (new Recorder (numChannels, sampleRate, *this)); + ScopedPointer recorder (new Recorder (numChannels, sampleRate, *this, numBuffers, bufferSize)); return recorder->openedOk() ? recorder.release() : nullptr; } @@ -288,12 +401,13 @@ private: //================================================================================================== struct BufferList { - BufferList (const int numChannels_) - : numChannels (numChannels_), bufferSpace (numChannels_ * numSamples * numBuffers), nextBlock (0) + BufferList (const int numChannels_, const int numBuffers_, const int numSamples_) + : numChannels (numChannels_), numBuffers (numBuffers_), numSamples (numSamples_), + bufferSpace (numChannels_ * numSamples * numBuffers), nextBlock (0) { } - int16* waitForFreeBuffer (Thread& threadToCheck) + int16* waitForFreeBuffer (Thread& threadToCheck) noexcept { while (numBlocksOut.get() == numBuffers) { @@ -306,7 +420,7 @@ private: return getNextBuffer(); } - int16* getNextBuffer() + int16* getNextBuffer() noexcept { if (++nextBlock == numBuffers) nextBlock = 0; @@ -314,13 +428,12 @@ private: return bufferSpace + nextBlock * numChannels * numSamples; } - void bufferReturned() { --numBlocksOut; dataArrived.signal(); } - void bufferSent() { ++numBlocksOut; dataArrived.signal(); } + void bufferReturned() noexcept { --numBlocksOut; dataArrived.signal(); } + void bufferSent() noexcept { ++numBlocksOut; dataArrived.signal(); } - int getBufferSizeBytes() const { return numChannels * numSamples * sizeof (int16); } + int getBufferSizeBytes() const noexcept { return numChannels * numSamples * sizeof (int16); } - const int numChannels; - enum { numSamples = 256, numBuffers = 16 }; + const int numChannels, numBuffers, numSamples; private: HeapBlock bufferSpace; @@ -332,24 +445,23 @@ private: //================================================================================================== struct Player { - Player (int numChannels, int sampleRate, Engine& engine) + Player (int numChannels, int sampleRate, Engine& engine, int playerNumBuffers, int playerBufferSize) : playerObject (nullptr), playerPlay (nullptr), playerBufferQueue (nullptr), - bufferList (numChannels) + bufferList (numChannels, playerNumBuffers, playerBufferSize) { - jassert (numChannels == 2); - SLDataFormat_PCM pcmFormat = { SL_DATAFORMAT_PCM, (SLuint32) numChannels, - (SLuint32) (sampleRate * 1000), // (sample rate units are millihertz) + (SLuint32) (sampleRate * 1000), SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, - SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, + (numChannels == 1) ? SL_SPEAKER_FRONT_CENTER : (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT), SL_BYTEORDER_LITTLEENDIAN }; - SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, bufferList.numBuffers }; + SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + static_cast (bufferList.numBuffers) }; SLDataSource audioSrc = { &bufferQueue, &pcmFormat }; SLDataLocator_OutputMix outputMix = { SL_DATALOCATOR_OUTPUTMIX, engine.outputMixObject }; @@ -385,10 +497,11 @@ private: void start() { jassert (openedOk()); + check ((*playerPlay)->SetPlayState (playerPlay, SL_PLAYSTATE_PLAYING)); } - void writeBuffer (const AudioSampleBuffer& buffer, Thread& thread) + void writeBuffer (const AudioSampleBuffer& buffer, Thread& thread) noexcept { jassert (buffer.getNumChannels() == bufferList.numChannels); jassert (buffer.getNumSamples() < bufferList.numSamples * bufferList.numBuffers); @@ -398,26 +511,27 @@ private: while (numSamples > 0) { - int16* const destBuffer = bufferList.waitForFreeBuffer (thread); - - if (destBuffer == nullptr) - break; - - for (int i = 0; i < bufferList.numChannels; ++i) + if (int16* const destBuffer = bufferList.waitForFreeBuffer (thread)) { - typedef AudioData::Pointer DstSampleType; - typedef AudioData::Pointer SrcSampleType; + for (int i = 0; i < bufferList.numChannels; ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; - DstSampleType dstData (destBuffer + i, bufferList.numChannels); - SrcSampleType srcData (buffer.getReadPointer (i, offset)); - dstData.convertSamples (srcData, bufferList.numSamples); + DstSampleType dstData (destBuffer + i, bufferList.numChannels); + SrcSampleType srcData (buffer.getReadPointer (i, offset)); + dstData.convertSamples (srcData, bufferList.numSamples); + } + + enqueueBuffer (destBuffer); + + numSamples -= bufferList.numSamples; + offset += bufferList.numSamples; + } + else + { + break; } - - check ((*playerBufferQueue)->Enqueue (playerBufferQueue, destBuffer, bufferList.getBufferSizeBytes())); - bufferList.bufferSent(); - - numSamples -= bufferList.numSamples; - offset += bufferList.numSamples; } } @@ -428,10 +542,16 @@ private: BufferList bufferList; - static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) + void enqueueBuffer (int16* buffer) noexcept { - jassert (queue == static_cast (context)->playerBufferQueue); (void) queue; - static_cast (context)->bufferList.bufferReturned(); + check ((*playerBufferQueue)->Enqueue (playerBufferQueue, buffer, bufferList.getBufferSizeBytes())); + bufferList.bufferSent(); + } + + static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept + { + jassert (queue == static_cast (context)->playerBufferQueue); (void) queue; + static_cast (context)->bufferList.bufferReturned(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Player) @@ -440,13 +560,11 @@ private: //================================================================================================== struct Recorder { - Recorder (int numChannels, int sampleRate, Engine& engine) + Recorder (int numChannels, int sampleRate, Engine& engine, const int numBuffers, const int numSamples) : recorderObject (nullptr), recorderRecord (nullptr), recorderBufferQueue (nullptr), configObject (nullptr), - bufferList (numChannels) + bufferList (numChannels, numBuffers, numSamples) { - jassert (numChannels == 1); // STEREO doesn't always work!! - SLDataFormat_PCM pcmFormat = { SL_DATAFORMAT_PCM, @@ -461,7 +579,8 @@ private: SLDataLocator_IODevice ioDevice = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr }; SLDataSource audioSrc = { &ioDevice, nullptr }; - SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, bufferList.numBuffers }; + SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, + static_cast (bufferList.numBuffers) }; SLDataSink audioSink = { &bufferQueue, &pcmFormat }; const SLInterfaceID interfaceIDs[] = { *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE }; @@ -474,16 +593,14 @@ private: { check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_RECORD, &recorderRecord)); check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue)); - check ((*recorderObject)->GetInterface (recorderObject, *engine.SL_IID_ANDROIDCONFIGURATION, &configObject)); + // not all android versions seem to have a config object + SLresult result = (*recorderObject)->GetInterface (recorderObject, + *engine.SL_IID_ANDROIDCONFIGURATION, &configObject); + if (result != SL_RESULT_SUCCESS) + configObject = nullptr; + check ((*recorderBufferQueue)->RegisterCallback (recorderBufferQueue, staticCallback, this)); check ((*recorderRecord)->SetRecordState (recorderRecord, SL_RECORDSTATE_STOPPED)); - - for (int i = bufferList.numBuffers; --i >= 0;) - { - int16* const buffer = bufferList.getNextBuffer(); - jassert (buffer != nullptr); - enqueueBuffer (buffer); - } } } } @@ -519,25 +636,27 @@ private: while (numSamples > 0) { - int16* const srcBuffer = bufferList.waitForFreeBuffer (thread); - - if (srcBuffer == nullptr) - break; - - for (int i = 0; i < bufferList.numChannels; ++i) + if (int16* const srcBuffer = bufferList.waitForFreeBuffer (thread)) { - typedef AudioData::Pointer DstSampleType; - typedef AudioData::Pointer SrcSampleType; + for (int i = 0; i < bufferList.numChannels; ++i) + { + typedef AudioData::Pointer DstSampleType; + typedef AudioData::Pointer SrcSampleType; - DstSampleType dstData (buffer.getWritePointer (i, offset)); - SrcSampleType srcData (srcBuffer + i, bufferList.numChannels); - dstData.convertSamples (srcData, bufferList.numSamples); + DstSampleType dstData (buffer.getWritePointer (i, offset)); + SrcSampleType srcData (srcBuffer + i, bufferList.numChannels); + dstData.convertSamples (srcData, bufferList.numSamples); + } + + enqueueBuffer (srcBuffer); + + numSamples -= bufferList.numSamples; + offset += bufferList.numSamples; + } + else + { + break; } - - enqueueBuffer (srcBuffer); - - numSamples -= bufferList.numSamples; - offset += bufferList.numSamples; } } @@ -558,16 +677,16 @@ private: BufferList bufferList; - void enqueueBuffer (int16* buffer) + void enqueueBuffer (int16* buffer) noexcept { check ((*recorderBufferQueue)->Enqueue (recorderBufferQueue, buffer, bufferList.getBufferSizeBytes())); bufferList.bufferSent(); } - static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) + static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept { - jassert (queue == static_cast (context)->recorderBufferQueue); (void) queue; - static_cast (context)->bufferList.bufferReturned(); + jassert (queue == static_cast (context)->recorderBufferQueue); (void) queue; + static_cast (context)->bufferList.bufferReturned(); } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Recorder) @@ -581,7 +700,7 @@ private: ScopedPointer recorder; //============================================================================== - static bool check (const SLresult result) + static bool check (const SLresult result) noexcept { jassert (result == SL_RESULT_SUCCESS); return result == SL_RESULT_SUCCESS; @@ -598,14 +717,14 @@ public: OpenSLAudioDeviceType() : AudioIODeviceType (openSLTypeName) {} //============================================================================== - void scanForDevices() {} - StringArray getDeviceNames (bool wantInputNames) const { return StringArray (openSLTypeName); } - int getDefaultDeviceIndex (bool forInput) const { return 0; } - int getIndexOfDevice (AudioIODevice* device, bool asInput) const { return device != nullptr ? 0 : -1; } - bool hasSeparateInputsAndOutputs() const { return false; } + void scanForDevices() override {} + StringArray getDeviceNames (bool wantInputNames) const override { return StringArray (openSLTypeName); } + int getDefaultDeviceIndex (bool forInput) const override { return 0; } + int getIndexOfDevice (AudioIODevice* device, bool asInput) const override { return device != nullptr ? 0 : -1; } + bool hasSeparateInputsAndOutputs() const override { return false; } AudioIODevice* createDevice (const String& outputDeviceName, - const String& inputDeviceName) + const String& inputDeviceName) override { ScopedPointer dev; diff --git a/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp b/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp index 6c39d163de..23d1eb0399 100644 --- a/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp +++ b/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp @@ -80,12 +80,16 @@ public: deviceManager (dm), noItemsMessage (noItems) { - items = MidiInput::getDevices(); - + updateDevices(); setModel (this); setOutlineThickness (1); } + void updateDevices() + { + items = MidiInput::getDevices(); + } + int getNumRows() override { return items.size(); @@ -1011,11 +1015,19 @@ AudioDeviceSelectorComponent::AudioDeviceSelectorComponent (AudioDeviceManager& midiInputsLabel = new Label (String::empty, TRANS ("Active MIDI inputs:")); midiInputsLabel->setJustificationType (Justification::topRight); midiInputsLabel->attachToComponent (midiInputsList, true); + + if (BluetoothMidiDevicePairingDialogue::isAvailable()) + { + addAndMakeVisible (bluetoothButton = new TextButton (TRANS("Bluetooth MIDI"), + TRANS("Scan for bluetooth MIDI devices"))); + bluetoothButton->addListener (this); + } } else { midiInputsList = nullptr; midiInputsLabel = nullptr; + bluetoothButton = nullptr; } if (showMidiOutputSelector) @@ -1034,6 +1046,7 @@ AudioDeviceSelectorComponent::AudioDeviceSelectorComponent (AudioDeviceManager& deviceManager.addChangeListener (this); updateAllControls(); + startTimer (1000); } AudioDeviceSelectorComponent::~AudioDeviceSelectorComponent() @@ -1073,10 +1086,26 @@ void AudioDeviceSelectorComponent::resized() r.removeFromTop (space); } + if (bluetoothButton != nullptr) + { + bluetoothButton->setBounds (r.removeFromTop (24)); + r.removeFromTop (space); + } + if (midiOutputSelector != nullptr) midiOutputSelector->setBounds (r.removeFromTop (itemHeight)); } +void AudioDeviceSelectorComponent::timerCallback() +{ + // TODO + // unfortunately, the AudioDeviceManager only gives us changeListenerCallbacks + // if an audio device has changed, but not if a MIDI device has changed. + // This needs to be implemented properly. Until then, we use a workaround + // where we update the whole component once per second on a timer callback. + updateAllControls(); +} + void AudioDeviceSelectorComponent::comboBoxChanged (ComboBox* comboBoxThatHasChanged) { if (comboBoxThatHasChanged == deviceTypeDropDown) @@ -1133,6 +1162,7 @@ void AudioDeviceSelectorComponent::updateAllControls() if (midiInputsList != nullptr) { + midiInputsList->updateDevices(); midiInputsList->updateContent(); midiInputsList->repaint(); } @@ -1159,3 +1189,9 @@ void AudioDeviceSelectorComponent::updateAllControls() resized(); } + +void AudioDeviceSelectorComponent::buttonClicked (Button* btn) +{ + if (bluetoothButton == btn) + BluetoothMidiDevicePairingDialogue::open(); +} diff --git a/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h b/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h index f11a640b0d..8a0e35d029 100644 --- a/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h +++ b/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.h @@ -37,7 +37,9 @@ */ class JUCE_API AudioDeviceSelectorComponent : public Component, private ComboBoxListener, // (can't use ComboBox::Listener due to idiotic VC2005 bug) - private ChangeListener + private ChangeListener, + private Button::Listener, + private Timer { public: //============================================================================== @@ -84,8 +86,13 @@ public: //============================================================================== /** @internal */ void resized() override; + /** @internal */ + void timerCallback() override; private: + //============================================================================== + void buttonClicked (Button*) override; + //============================================================================== ScopedPointer deviceTypeDropDown; ScopedPointer