1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Android Bluetooth MIDI, pro-audio i/o and improved openGL support

This commit is contained in:
jules 2015-11-03 10:37:52 +00:00
parent 02041328dc
commit 89ba69ab29
45 changed files with 4088 additions and 669 deletions

View file

@ -168,11 +168,11 @@
</MODULEPATHS>
</VS2015>
<ANDROID targetFolder="Builds/Android" androidActivityClass="com.juce.jucedemo.JuceDemo"
androidSDKPath="${user.home}/SDKs/android-sdk" androidNDKPath="${user.home}/SDKs/android-ndk"
androidMinimumSDK="9" androidInternetNeeded="1" androidKeyStore="${user.home}/.android/debug.keystore"
androidKeyStorePass="android" androidKeyAlias="androiddebugkey"
androidKeyAliasPass="android" androidMicNeeded="1" bigIcon="xycKOk"
androidCpp11="1" smallIcon="BvyE0d" androidVersionCode="1">
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">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" androidArchitectures="armeabi armeabi-v7a" isDebug="1"
optimisation="1" targetName="JuceDemo"/>

View file

@ -520,6 +520,10 @@ MainAppWindow::MainAppWindow()
taskbarIcon = new DemoTaskbarComponent();
#endif
#if JUCE_ANDROID
setOpenGLRenderingEngine();
#endif
triggerAsyncUpdate();
}

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<JUCERPROJECT id="wHE0ay" name="MidiTest" projectType="guiapp" version="1.0.0"
bundleIdentifier="com.yourcompany.MidiTest" includeBinaryInAppConfig="1"
jucerVersion="3.2.0">
<MAINGROUP id="s3xxCh" name="MidiTest">
<GROUP id="{7D29F5BC-1B05-AE8F-9202-5CF152AB1103}" name="Source">
<FILE id="kpmJ3T" name="Main.cpp" compile="1" resource="0" file="Source/Main.cpp"/>
<FILE id="lYMuLe" name="MainComponent.cpp" compile="1" resource="0"
file="Source/MainComponent.cpp"/>
<FILE id="YoRllh" name="MainComponent.h" compile="0" resource="0" file="Source/MainComponent.h"/>
</GROUP>
</MAINGROUP>
<EXPORTFORMATS>
<VS2015 targetFolder="Builds/VisualStudio2015">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" winWarningLevel="4" generateManifest="1" winArchitecture="32-bit"
isDebug="1" optimisation="1" targetName="MidiTest"/>
<CONFIGURATION name="Release" winWarningLevel="4" generateManifest="1" winArchitecture="32-bit"
isDebug="0" optimisation="3" targetName="MidiTest"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_gui_extra" path="../../modules"/>
<MODULEPATH id="juce_gui_basics" path="../../modules"/>
<MODULEPATH id="juce_graphics" path="../../modules"/>
<MODULEPATH id="juce_events" path="../../modules"/>
<MODULEPATH id="juce_dsp" path="../../modules"/>
<MODULEPATH id="juce_data_structures" path="../../modules"/>
<MODULEPATH id="juce_core" path="../../modules"/>
<MODULEPATH id="juce_audio_utils" path="../../modules"/>
<MODULEPATH id="juce_audio_processors" path="../../modules"/>
<MODULEPATH id="juce_audio_formats" path="../../modules"/>
<MODULEPATH id="juce_audio_devices" path="../../modules"/>
<MODULEPATH id="juce_audio_basics" path="../../modules"/>
</MODULEPATHS>
</VS2015>
<LINUX_MAKE targetFolder="Builds/LinuxMakefile" extraCompilerFlags="">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" libraryPath="/usr/X11R6/lib/" isDebug="1" optimisation="1"
targetName="MidiTest"/>
<CONFIGURATION name="Release" libraryPath="/usr/X11R6/lib/" isDebug="0" optimisation="3"
targetName="MidiTest"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_core" path="../../modules"/>
<MODULEPATH id="juce_events" path="../../modules"/>
<MODULEPATH id="juce_graphics" path="../../modules"/>
<MODULEPATH id="juce_data_structures" path="../../modules"/>
<MODULEPATH id="juce_gui_basics" path="../../modules"/>
<MODULEPATH id="juce_audio_basics" path="../../modules"/>
<MODULEPATH id="juce_audio_devices" path="../../modules"/>
<MODULEPATH id="juce_audio_formats" path="../../modules"/>
<MODULEPATH id="juce_gui_extra" path="../../modules"/>
<MODULEPATH id="juce_audio_utils" path="../../modules"/>
<MODULEPATH id="juce_audio_processors" path="../../modules"/>
<MODULEPATH id="juce_dsp" path="../../modules"/>
</MODULEPATHS>
</LINUX_MAKE>
<XCODE_MAC targetFolder="Builds/MacOSX" extraCompilerFlags="-std=c++14 -Wreorder -Wconstant-conversion -Wint-conversion -Woverloaded-virtual -Wuninitialized -Wunused-parameter -Wshorten-64-to-32 -Wstrict-aliasing -Wshadow -Wconversion -Wsign-compare -Wsign-conversion">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" osxSDK="default" osxCompatibility="10.9 SDK" osxArchitecture="default"
isDebug="1" optimisation="1" targetName="MidiTest"/>
<CONFIGURATION name="Release" osxSDK="default" osxCompatibility="10.9 SDK" osxArchitecture="default"
isDebug="0" optimisation="3" targetName="MidiTest"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_gui_extra" path="../../modules"/>
<MODULEPATH id="juce_gui_basics" path="../../modules"/>
<MODULEPATH id="juce_graphics" path="../../modules"/>
<MODULEPATH id="juce_events" path="../../modules"/>
<MODULEPATH id="juce_data_structures" path="../../modules"/>
<MODULEPATH id="juce_core" path="../../modules"/>
<MODULEPATH id="juce_audio_utils" path="../../modules"/>
<MODULEPATH id="juce_audio_processors" path="../../modules"/>
<MODULEPATH id="juce_audio_formats" path="../../modules"/>
<MODULEPATH id="juce_audio_devices" path="../../modules"/>
<MODULEPATH id="juce_audio_basics" path="../../modules"/>
<MODULEPATH id="juce_dsp" path="../../modules"/>
</MODULEPATHS>
</XCODE_MAC>
<XCODE_IPHONE targetFolder="Builds/iOS">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" iosCompatibility="8.0" isDebug="1" optimisation="1"
targetName="MidiTest"/>
<CONFIGURATION name="Release" iosCompatibility="8.0" isDebug="0" optimisation="3"
targetName="MidiTest"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_gui_extra" path="../../modules"/>
<MODULEPATH id="juce_gui_basics" path="../../modules"/>
<MODULEPATH id="juce_graphics" path="../../modules"/>
<MODULEPATH id="juce_events" path="../../modules"/>
<MODULEPATH id="juce_dsp" path="../../modules"/>
<MODULEPATH id="juce_data_structures" path="../../modules"/>
<MODULEPATH id="juce_core" path="../../modules"/>
<MODULEPATH id="juce_audio_utils" path="../../modules"/>
<MODULEPATH id="juce_audio_processors" path="../../modules"/>
<MODULEPATH id="juce_audio_formats" path="../../modules"/>
<MODULEPATH id="juce_audio_devices" path="../../modules"/>
<MODULEPATH id="juce_audio_basics" path="../../modules"/>
</MODULEPATHS>
</XCODE_IPHONE>
<ANDROID targetFolder="Builds/Android" androidVersionCode="1" androidActivityClass="com.yourcompany.miditest.MidiTest"
androidMinimumSDK="23" androidInternetNeeded="1" androidKeyStore="${user.home}/.android/debug.keystore"
androidKeyStorePass="android" androidKeyAlias="androiddebugkey"
androidKeyAliasPass="android" androidCpp11="1" androidSDKPath=""
androidNDKPath="" androidMicNeeded="1" androidOtherPermissions=""
androidBluetoothNeeded="1">
<CONFIGURATIONS>
<CONFIGURATION name="Debug" androidArchitectures="armeabi-v7a" isDebug="1" optimisation="1"
targetName="MidiTest"/>
<CONFIGURATION name="Release" androidArchitectures="armeabi-v7a" isDebug="0"
optimisation="3" targetName="MidiTest"/>
</CONFIGURATIONS>
<MODULEPATHS>
<MODULEPATH id="juce_core" path="../../modules"/>
<MODULEPATH id="juce_events" path="../../modules"/>
<MODULEPATH id="juce_graphics" path="../../modules"/>
<MODULEPATH id="juce_data_structures" path="../../modules"/>
<MODULEPATH id="juce_gui_basics" path="../../modules"/>
<MODULEPATH id="juce_audio_basics" path="../../modules"/>
<MODULEPATH id="juce_audio_devices" path="../../modules"/>
<MODULEPATH id="juce_audio_formats" path="../../modules"/>
<MODULEPATH id="juce_gui_extra" path="../../modules"/>
<MODULEPATH id="juce_audio_utils" path="../../modules"/>
<MODULEPATH id="juce_audio_processors" path="../../modules"/>
<MODULEPATH id="juce_dsp" path="../../modules"/>
</MODULEPATHS>
</ANDROID>
</EXPORTFORMATS>
<MODULES>
<MODULE id="juce_audio_basics" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_audio_devices" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_audio_formats" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_audio_processors" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_audio_utils" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_core" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_data_structures" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_dsp" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_events" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_graphics" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_gui_basics" showAllCode="1" useLocalCopy="0"/>
<MODULE id="juce_gui_extra" showAllCode="1" useLocalCopy="0"/>
</MODULES>
<JUCEOPTIONS/>
</JUCERPROJECT>

View file

@ -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> mainWindow;
};
//==============================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (MidiTestApplication)

View file

@ -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<MidiInput> inDevice;
ScopedPointer<MidiOutput> outDevice;
typedef ReferenceCountedObjectPtr<MidiDeviceListEntry> 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<int> 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<MidiDeviceListEntry>& midiDevices)
{
SparseSet<int> selectedRows;
for (int i = 0; i < midiDevices.size(); ++i)
if (midiDevices[i]->inDevice != nullptr || midiDevices[i]->outDevice != nullptr)
selectedRows.addRange (Range<int> (i, i+1));
lastSelectedItems = selectedRows;
updateContent();
setSelectedRows (selectedRows, dontSendNotification);
}
private:
//==========================================================================
MainContentComponent& parent;
bool isInput;
SparseSet<int> 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<MidiDeviceListEntry>& 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<MidiDeviceListEntry>& 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<MidiDeviceListEntry>& 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<MidiDeviceListEntry>& midiDevices
= isInputDeviceList ? midiInputs : midiOutputs;
closeUnpluggedDevices (newDeviceNames, isInputDeviceList);
ReferenceCountedArray<MidiDeviceListEntry> 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<const MidiCallbackMessage&> (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<int>(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<MidiDeviceListEntry>
MainContentComponent::getMidiDevice (int index, bool isInput) const noexcept
{
return isInput ? midiInputs[index] : midiOutputs[index];
}

View file

@ -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<MidiDeviceListEntry> 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<MidiDeviceListEntry> 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<MidiDeviceListBox> midiInputSelector;
ScopedPointer<MidiDeviceListBox> midiOutputSelector;
ReferenceCountedArray<MidiDeviceListEntry> midiInputs;
ReferenceCountedArray<MidiDeviceListEntry> midiOutputs;
//==========================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
#endif // JUCE_MIDITEST_MAINCOMPONENT_H

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 976">
<rect ry="291" height="976" width="640" fill="#0a3d91"/>
<path d="m157,330,305,307-147,178v-636l147,170-305,299" stroke="#FFF" stroke-width="53" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View file

@ -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());
}

View file

@ -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:

View file

@ -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);
}
//==============================================================================

View file

@ -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)

View file

@ -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:

View file

@ -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());
}

View file

@ -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)
{
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.");

View file

@ -66,6 +66,7 @@ public:
virtual void create (const OwnedArray<LibraryModule>&) 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<BuildConfiguration> Ptr;
@ -250,8 +251,7 @@ public:
//==============================================================================
ValueTree config;
Project& project;
protected:
const ProjectExporter& exporter;
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BuildConfiguration)

View file

@ -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();
}

View file

@ -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)

View file

@ -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))
{

View file

@ -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();

View file

@ -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);

View file

@ -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<uint8> buffer (len);
std::memcpy (buffer.getData(), data + offset, len);
midiConcatenator.pushMidiData (buffer.getData(),
len, static_cast<double> (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<AndroidMidiInput*> (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<jstring> string ((jstring) getEnv()->CallObjectMethod (dm, MidiDeviceManager.getInputPortNameForJuceIndex, idx));
return juceString (string);
}
return String();
}
String getOutputPortNameForJuceIndex (int idx)
{
if (jobject dm = deviceManager.get())
{
LocalRef<jstring> 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<jobjectArray> devices (jDevices);
return javaStringArrayToJuce (devices);
}
return StringArray();
}
AndroidMidiInput* openMidiInputPortWithIndex (int idx, MidiInput* juceMidiInput, juce::MidiInputCallback* callback)
{
if (jobject dm = deviceManager.get())
{
ScopedPointer<AndroidMidiInput> 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<jstring> 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<AndroidMidiOutput*> (internal);
}
void MidiOutput::sendMessageNow (const MidiMessage&)
void MidiOutput::sendMessageNow (const MidiMessage& message)
{
if (AndroidMidiOutput* androidMidi = reinterpret_cast<AndroidMidiOutput*>(internal))
{
JNIEnv* env = getEnv();
const int messageSize = message.getRawDataSize();
LocalRef<jbyteArray> messageContent = LocalRef<jbyteArray> (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;
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback)
String midiInputName = manager.getInputPortNameForJuceIndex (index);
if (midiInputName.isEmpty())
{
// you supplied an invalid device index!
jassertfalse;
return nullptr;
}
ScopedPointer<MidiInput> midiInput (new MidiInput (midiInputName));
midiInput->internal = manager.openMidiInputPortWithIndex (index, midiInput, callback);
return midiInput->internal != nullptr ? midiInput.release()
: nullptr;
}
void MidiInput::start()
{
if (AndroidMidiInput* mi = reinterpret_cast<AndroidMidiInput*> (internal))
mi->start();
}
void MidiInput::stop()
{
if (AndroidMidiInput* mi = reinterpret_cast<AndroidMidiInput*> (internal))
mi->stop();
}
MidiInput::~MidiInput()
{
delete reinterpret_cast<AndroidMidiInput*> (internal);
}

View file

@ -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<double> getAvailableSampleRates() override
{
static const double rates[] = { 8000.0, 16000.0, 32000.0, 44100.0, 48000.0 };
return Array<double> (rates, numElementsInArray (rates));
Array<double> 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<int> getAvailableBufferSizes() override
{
static const int sizes[] = { 256, 512, 768, 1024, 1280, 1600 }; // must all be multiples of the block size
return Array<int> (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<int> 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<jstring> jProperty (javaString (property));
const LocalRef<jstring> 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<jstring> 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> player (new Player (numChannels, sampleRate, *this));
ScopedPointer<Player> 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> recorder (new Recorder (numChannels, sampleRate, *this));
ScopedPointer<Recorder> 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<int16> 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<SLuint32> (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,11 +511,8 @@ private:
while (numSamples > 0)
{
int16* const destBuffer = bufferList.waitForFreeBuffer (thread);
if (destBuffer == nullptr)
break;
if (int16* const destBuffer = bufferList.waitForFreeBuffer (thread))
{
for (int i = 0; i < bufferList.numChannels; ++i)
{
typedef AudioData::Pointer<AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> DstSampleType;
@ -413,12 +523,16 @@ private:
dstData.convertSamples (srcData, bufferList.numSamples);
}
check ((*playerBufferQueue)->Enqueue (playerBufferQueue, destBuffer, bufferList.getBufferSizeBytes()));
bufferList.bufferSent();
enqueueBuffer (destBuffer);
numSamples -= bufferList.numSamples;
offset += bufferList.numSamples;
}
else
{
break;
}
}
}
private:
@ -428,7 +542,13 @@ private:
BufferList bufferList;
static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context)
void enqueueBuffer (int16* buffer) noexcept
{
check ((*playerBufferQueue)->Enqueue (playerBufferQueue, buffer, bufferList.getBufferSizeBytes()));
bufferList.bufferSent();
}
static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept
{
jassert (queue == static_cast<Player*> (context)->playerBufferQueue); (void) queue;
static_cast<Player*> (context)->bufferList.bufferReturned();
@ -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<SLuint32> (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,11 +636,8 @@ private:
while (numSamples > 0)
{
int16* const srcBuffer = bufferList.waitForFreeBuffer (thread);
if (srcBuffer == nullptr)
break;
if (int16* const srcBuffer = bufferList.waitForFreeBuffer (thread))
{
for (int i = 0; i < bufferList.numChannels; ++i)
{
typedef AudioData::Pointer<AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DstSampleType;
@ -539,6 +653,11 @@ private:
numSamples -= bufferList.numSamples;
offset += bufferList.numSamples;
}
else
{
break;
}
}
}
bool setAudioPreprocessingEnabled (bool enable)
@ -558,13 +677,13 @@ 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<Recorder*> (context)->recorderBufferQueue); (void) queue;
static_cast<Recorder*> (context)->bufferList.bufferReturned();
@ -581,7 +700,7 @@ private:
ScopedPointer<Recorder> 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<OpenSLAudioIODevice> dev;

View file

@ -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();
}

View file

@ -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<ComboBox> deviceTypeDropDown;
ScopedPointer<Label> deviceTypeDropDownLabel;
@ -101,6 +108,7 @@ private:
ScopedPointer<MidiInputSelectorComponentListBox> midiInputsList;
ScopedPointer<ComboBox> midiOutputSelector;
ScopedPointer<Label> midiInputsLabel, midiOutputLabel;
ScopedPointer<TextButton> bluetoothButton;
void comboBoxChanged (ComboBox*) override;
void changeListenerCallback (ChangeBroadcaster*) override;

View file

@ -0,0 +1,76 @@
/*
==============================================================================
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_BLUETOOTHMIDIDEVICPAIRINGCOMPONENT_H_INCLUDED
#define JUCE_BLUETOOTHMIDIDEVICPAIRINGCOMPONENT_H_INCLUDED
class BluetoothMidiSelectorOverlay;
//==============================================================================
/**
Opens a Bluetooth MIDI pairing dialogue that allows the user to view and
connect to Bluetooth MIDI devices that are currently found nearby.
The dialogue will ignore non-MIDI Bluetooth devices.
Only after a Bluetooth MIDI device has been paired will its MIDI ports
be available through JUCE's MidiInput and MidiOutput classes.
This dialogue is currently only available on iOS and Android. On OSX,
you should instead pair Bluetooth MIDI devices using the "Audio MIDI Setup"
app (located in /Applications/Utilities). On Windows, you should use
the system settings. On Linux, Bluetooth MIDI devices are currently not
supported.
*/
class BluetoothMidiDevicePairingDialogue
{
public:
/** Opens the Bluetooth MIDI pairing dialogue, if it is available.
@return true if the dialogue was opened, false on error.
*/
static bool open();
/** Checks if a Bluetooth MIDI pairing dialogue is available on this
platform.
On iOS, this will be true for iOS versions 8.0 and higher.
On Android, this will be true only for Android SDK versions 23 and
higher, and additionally only if the device itself supports MIDI
over Bluetooth.
On deskrop platforms, this will typically be false as the bluetooth
pairing is not done inside the app but by other means.
@return true if the Bluetooth MIDI pairing dialogue is available,
false otherwise.
*/
static bool isAvailable();
};
#endif // JUCE_BLUETOOTHMIDIDEVICPAIRINGCOMPONENT_H_INCLUDED

View file

@ -49,4 +49,21 @@ namespace juce
#include "gui/juce_AudioAppComponent.cpp"
#include "players/juce_AudioProcessorPlayer.cpp"
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
#include "../juce_gui_extra/embedding/juce_UIViewComponent.h"
#endif
#if JUCE_MAC
#include "native/juce_mac_BluetoothMidiDevicePairingDialogue.mm"
#elif JUCE_IOS
#include "native/juce_ios_BluetoothMidiDevicePairingDialogue.mm"
#elif JUCE_ANDROID
#include "../juce_core/native/juce_android_JNIHelpers.h"
#include "native/juce_android_BluetoothMidiDevicePairingDialogue.cpp"
#elif JUCE_LINUX
#include "native/juce_linux_BluetoothMidiDevicePairingDialogue.cpp"
#elif JUCE_WINDOWS
#include "native/juce_win_BluetoothMidiDevicePairingDialogue.cpp"
#endif
}

View file

@ -41,6 +41,7 @@ namespace juce
#include "gui/juce_AudioVisualiserComponent.h"
#include "gui/juce_MidiKeyboardComponent.h"
#include "gui/juce_AudioAppComponent.h"
#include "gui/juce_BluetoothMidiDevicePairingDialogue.h"
#include "players/juce_AudioProcessorPlayer.h"
}

View file

@ -0,0 +1,438 @@
/*
==============================================================================
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.
==============================================================================
*/
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (getMidiBluetoothAddresses, "getMidiBluetoothAddresses", "()[Ljava/lang/String;") \
METHOD (pairBluetoothMidiDevice, "pairBluetoothMidiDevice", "(Ljava/lang/String;)Z") \
METHOD (unpairBluetoothMidiDevice, "unpairBluetoothMidiDevice", "(Ljava/lang/String;)V") \
METHOD (getHumanReadableStringForBluetoothAddress, "getHumanReadableStringForBluetoothAddress", "(Ljava/lang/String;)Ljava/lang/String;") \
METHOD (isBluetoothDevicePaired, "isBluetoothDevicePaired", "(Ljava/lang/String;)Z")
DECLARE_JNI_CLASS (AndroidBluetoothManager, JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager");
#undef JNI_CLASS_MEMBERS
//==============================================================================
struct AndroidBluetoothMidiInterface
{
static StringArray getBluetoothMidiDevicesNearby()
{
StringArray retval;
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
// if this is null then bluetooth is not enabled
if (btManager.get() == nullptr)
return StringArray();
jobjectArray jDevices = (jobjectArray) env->CallObjectMethod (btManager.get(),
AndroidBluetoothManager.getMidiBluetoothAddresses);
LocalRef<jobjectArray> devices (jDevices);
const int count = env->GetArrayLength (devices.get());
for (int i = 0; i < count; ++i)
{
LocalRef<jstring> string ((jstring) env->GetObjectArrayElement (devices.get(), i));
retval.add (juceString (string));
}
return retval;
}
//==========================================================================
static bool pairBluetoothMidiDevice (const String& bluetoothAddress)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
if (btManager.get() == nullptr)
return false;
jboolean result = env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.pairBluetoothMidiDevice,
javaString (bluetoothAddress).get());
return result;
}
static void unpairBluetoothMidiDevice (const String& bluetoothAddress)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
if (btManager.get() != nullptr)
env->CallVoidMethod (btManager.get(), AndroidBluetoothManager.unpairBluetoothMidiDevice,
javaString (bluetoothAddress).get());
}
//==========================================================================
static String getHumanReadableStringForBluetoothAddress (const String& address)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
if (btManager.get() == nullptr)
return address;
LocalRef<jstring> string ((jstring) env->CallObjectMethod (btManager.get(),
AndroidBluetoothManager.getHumanReadableStringForBluetoothAddress,
javaString (address).get()));
if (string.get() == nullptr)
return address;
return juceString (string);
}
//==========================================================================
static bool isBluetoothDevicePaired (const String& address)
{
JNIEnv* env = getEnv();
LocalRef<jobject> btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
if (btManager.get() == nullptr)
return false;
return env->CallBooleanMethod (btManager.get(), AndroidBluetoothManager.isBluetoothDevicePaired,
javaString (address).get());
}
};
//==============================================================================
struct AndroidBluetoothMidiDevice
{
enum ConnectionStatus
{
offline,
connected,
disconnected,
connecting,
disconnecting
};
AndroidBluetoothMidiDevice (String deviceName, String address, ConnectionStatus status)
: name (deviceName), bluetoothAddress (address), connectionStatus (status)
{
// can't create a device without a valid name and bluetooth address!
jassert (! name.isEmpty());
jassert (! bluetoothAddress.isEmpty());
}
bool operator== (const AndroidBluetoothMidiDevice& other) const noexcept
{
return bluetoothAddress == other.bluetoothAddress;
}
bool operator!= (const AndroidBluetoothMidiDevice& other) const noexcept
{
return ! operator== (other);
}
const String name, bluetoothAddress;
ConnectionStatus connectionStatus;
};
//==============================================================================
class AndroidBluetoothMidiDevicesListBox : public ListBox,
private ListBoxModel,
private Timer
{
public:
//==========================================================================
AndroidBluetoothMidiDevicesListBox()
: timerPeriodInMs (1000)
{
setRowHeight (40);
setModel (this);
setOutlineThickness (1);
updateDeviceList();
startTimer (timerPeriodInMs);
}
void pairDeviceThreadFinished() // callback from PairDeviceThread
{
updateDeviceList();
startTimer (timerPeriodInMs);
}
private:
//==========================================================================
typedef AndroidBluetoothMidiDevice::ConnectionStatus DeviceStatus;
int getNumRows() override
{
return devices.size();
}
void paintListBoxItem (int rowNumber, Graphics& g,
int width, int height, bool rowIsSelected) override
{
if (isPositiveAndBelow (rowNumber, devices.size()))
{
const AndroidBluetoothMidiDevice& device = devices.getReference (rowNumber);
const String statusString (getDeviceStatusString (device.connectionStatus));
g.fillAll (Colours::white);
const float xmargin = 3.0f;
const float ymargin = 3.0f;
const float fontHeight = 0.4f * height;
const float deviceNameWidth = 0.6f * width;
g.setFont (fontHeight);
g.setColour (getDeviceNameFontColour (device.connectionStatus));
g.drawText (device.name,
xmargin, ymargin,
deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin),
Justification::topLeft, true);
g.setColour (getDeviceStatusFontColour (device.connectionStatus));
g.drawText (statusString,
deviceNameWidth + xmargin, ymargin,
width - deviceNameWidth - (2.0f * xmargin), height - (2.0f * ymargin),
Justification::topRight, true);
g.setColour (Colours::grey);
g.drawHorizontalLine (height - 1, xmargin, width);
}
}
//==========================================================================
static Colour getDeviceNameFontColour (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline)
return Colours::grey;
return Colours::black;
}
static Colour getDeviceStatusFontColour (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline
|| deviceStatus == AndroidBluetoothMidiDevice::connecting
|| deviceStatus == AndroidBluetoothMidiDevice::disconnecting)
return Colours::grey;
if (deviceStatus == AndroidBluetoothMidiDevice::connected)
return Colours::green;
return Colours::black;
}
static String getDeviceStatusString (DeviceStatus deviceStatus) noexcept
{
if (deviceStatus == AndroidBluetoothMidiDevice::offline) return "Offline";
if (deviceStatus == AndroidBluetoothMidiDevice::connected) return "Connected";
if (deviceStatus == AndroidBluetoothMidiDevice::disconnected) return "Not connected";
if (deviceStatus == AndroidBluetoothMidiDevice::connecting) return "Connecting...";
if (deviceStatus == AndroidBluetoothMidiDevice::disconnecting) return "Disconnecting...";
// unknown device state!
jassertfalse;
return "Status unknown";
}
//==========================================================================
void listBoxItemClicked (int row, const MouseEvent&) override
{
const AndroidBluetoothMidiDevice& device = devices.getReference (row);
if (device.connectionStatus == AndroidBluetoothMidiDevice::disconnected)
disconnectedDeviceClicked (row);
else if (device.connectionStatus == AndroidBluetoothMidiDevice::connected)
connectedDeviceClicked (row);
}
void timerCallback() override
{
updateDeviceList();
}
//==========================================================================
struct PairDeviceThread : public Thread,
private AsyncUpdater
{
PairDeviceThread (const String& bluetoothAddressOfDeviceToPair,
AndroidBluetoothMidiDevicesListBox& ownerListBox)
: Thread ("JUCE Bluetooth MIDI Device Pairing Thread"),
bluetoothAddress (bluetoothAddressOfDeviceToPair),
owner (&ownerListBox)
{
startThread();
}
void run() override
{
AndroidBluetoothMidiInterface::pairBluetoothMidiDevice (bluetoothAddress);
triggerAsyncUpdate();
}
void handleAsyncUpdate() override
{
if (owner != nullptr)
owner->pairDeviceThreadFinished();
delete this;
}
private:
String bluetoothAddress;
Component::SafePointer<AndroidBluetoothMidiDevicesListBox> owner;
};
//==========================================================================
void disconnectedDeviceClicked (int row)
{
stopTimer();
AndroidBluetoothMidiDevice& device = devices.getReference (row);
device.connectionStatus = AndroidBluetoothMidiDevice::connecting;
updateContent();
repaint();
new PairDeviceThread (device.bluetoothAddress, *this);
}
void connectedDeviceClicked (int row)
{
AndroidBluetoothMidiDevice& device = devices.getReference (row);
device.connectionStatus = AndroidBluetoothMidiDevice::disconnecting;
updateContent();
repaint();
AndroidBluetoothMidiInterface::unpairBluetoothMidiDevice (device.bluetoothAddress);
}
//==========================================================================
void updateDeviceList()
{
StringArray bluetoothAddresses = AndroidBluetoothMidiInterface::getBluetoothMidiDevicesNearby();
Array<AndroidBluetoothMidiDevice> newDevices;
for (String* address = bluetoothAddresses.begin();
address != bluetoothAddresses.end(); ++address)
{
String name = AndroidBluetoothMidiInterface::getHumanReadableStringForBluetoothAddress (*address);
DeviceStatus status = AndroidBluetoothMidiInterface::isBluetoothDevicePaired (*address)
? AndroidBluetoothMidiDevice::connected
: AndroidBluetoothMidiDevice::disconnected;
newDevices.add (AndroidBluetoothMidiDevice (name, *address, status));
}
devices.swapWith (newDevices);
updateContent();
repaint();
}
Array<AndroidBluetoothMidiDevice> devices;
const int timerPeriodInMs;
};
//==============================================================================
class BluetoothMidiSelectorOverlay : public Component
{
public:
BluetoothMidiSelectorOverlay()
{
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (ComponentPeer::windowHasDropShadow);
setBounds (0, 0, getParentWidth(), getParentHeight());
toFront (true);
addAndMakeVisible (bluetoothDevicesList);
enterModalState (true, nullptr, true);
}
void paint (Graphics& g) override
{
g.fillAll (Colours::black.withAlpha (0.6f));
g.setColour (Colour (0xffdfdfdf));
Rectangle<int> overlayBounds = getOverlayBounds();
g.fillRect (overlayBounds);
g.setColour (Colours::black);
g.setFont (16);
g.drawText ("Bluetooth MIDI Devices",
overlayBounds.removeFromTop (20).reduced (3, 3),
Justification::topLeft, true);
overlayBounds.removeFromTop (2);
g.setFont (12);
g.drawText ("tap to connect/disconnect",
overlayBounds.removeFromTop (18).reduced (3, 3),
Justification::topLeft, true);
}
void inputAttemptWhenModal() override { exitModalState (0); }
void mouseDrag (const MouseEvent&) override {}
void mouseDown (const MouseEvent&) override { exitModalState (0); }
void resized() override { update(); }
void parentSizeChanged() override { update(); }
private:
void update()
{
setBounds (0, 0, getParentWidth(), getParentHeight());
bluetoothDevicesList.setBounds (getOverlayBounds().withTrimmedTop (40));
}
Rectangle<int> getOverlayBounds() const noexcept
{
const int pw = getParentWidth();
const int ph = getParentHeight();
return Rectangle<int> (pw, ph).withSizeKeepingCentre (jmin (400, pw - 14),
jmin (300, ph - 40));
}
AndroidBluetoothMidiDevicesListBox bluetoothDevicesList;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
};
//==============================================================================
bool BluetoothMidiDevicePairingDialogue::open()
{
BluetoothMidiSelectorOverlay* overlay = new BluetoothMidiSelectorOverlay;
return true;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
jobject btManager (android.activity.callObjectMethod (JuceAppActivity.getAndroidBluetoothManager));
return btManager != nullptr;
}

View file

@ -0,0 +1,138 @@
/*
==============================================================================
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.
==============================================================================
*/
// Note: for the Bluetooth Midi selector overlay, we need the class
// UIViewComponent from the juce_gui_extra module. If this module is not
// included in your app, BluetoothMidiDevicePairingDialogue::open() will fail
// and return false.
// It is also not available in the iPhone/iPad simulator.
#if JUCE_MODULE_AVAILABLE_juce_gui_extra && ! TARGET_IPHONE_SIMULATOR
} // (juce namespace)
#include <CoreAudioKit/CoreAudioKit.h>
//==============================================================================
@interface BluetoothSelectorView : NSObject
@property CABTMIDICentralViewController *central;
- (UIView*) getView;
@end
//==============================================================================
@implementation BluetoothSelectorView
- (instancetype) init
{
self = [super init];
self.central = [[CABTMIDICentralViewController alloc] init];
return self;
}
- (UIView*) getView
{
return self.central.view;
}
@end
namespace juce
{
//==============================================================================
class BluetoothMidiSelectorOverlay : public Component
{
public:
BluetoothMidiSelectorOverlay()
{
setAlwaysOnTop (true);
setVisible (true);
addToDesktop (ComponentPeer::windowHasDropShadow);
setBounds (0, 0, getParentWidth(), getParentHeight());
toFront (true);
nativeSelectorComponent.setView ([[[BluetoothSelectorView alloc] init] getView]);
addAndMakeVisible (nativeSelectorComponent);
enterModalState (true, nullptr, true);
}
void paint (Graphics& g) override
{
g.fillAll (Colours::black.withAlpha (0.5f));
}
void inputAttemptWhenModal() override { close(); }
void mouseDrag (const MouseEvent&) override {}
void mouseDown (const MouseEvent&) override { close(); }
void resized () override { update(); }
void parentSizeChanged() override { update(); }
private:
void update()
{
const int pw = getParentWidth();
const int ph = getParentHeight();
nativeSelectorComponent.setBounds (Rectangle<int> (pw, ph)
.withSizeKeepingCentre (jmin (400, pw - 14),
jmin (500, ph - 40)));
}
void close()
{
exitModalState (0);
setVisible (false);
}
UIViewComponent nativeSelectorComponent;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BluetoothMidiSelectorOverlay)
};
#endif // JUCE_MODULE_AVAILABLE_juce_gui_extra && ! TARGET_IPHONE_SIMULATOR
//==============================================================================
bool BluetoothMidiDevicePairingDialogue::open()
{
#if JUCE_MODULE_AVAILABLE_juce_gui_extra && ! TARGET_IPHONE_SIMULATOR
if (isAvailable())
{
new BluetoothMidiSelectorOverlay();
return true;
}
#endif
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
#if JUCE_MODULE_AVAILABLE_juce_gui_extra && ! TARGET_IPHONE_SIMULATOR
return NSClassFromString ([NSString stringWithUTF8String: "CABTMIDICentralViewController"]) != nil;
#else
return false;
#endif
}

View file

@ -0,0 +1,37 @@
/*
==============================================================================
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.
==============================================================================
*/
bool BluetoothMidiDevicePairingDialogue::open()
{
// not implemented on Linux yet!
// You should check whether the dialogue is available on your system
// using isAvailable() before calling open().
jassertfalse;
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return false;
}

View file

@ -0,0 +1,36 @@
/*
==============================================================================
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.
==============================================================================
*/
bool BluetoothMidiDevicePairingDialogue::open()
{
// Do not call this on OSX. Instead, you should pair Bluetooth MIDI devices
// using the "Audio MIDI Setup" app (located in /Applications/Utilities).
jassertfalse;
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return false;
}

View file

@ -0,0 +1,37 @@
/*
==============================================================================
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.
==============================================================================
*/
bool BluetoothMidiDevicePairingDialogue::open()
{
// not implemented on Windows yet!
// You should check whether the dialogue is available on your system
// using isAvailable() before calling open().
jassertfalse;
return false;
}
bool BluetoothMidiDevicePairingDialogue::isAvailable()
{
return false;
}

View file

@ -0,0 +1,835 @@
//==============================================================================
public class BluetoothManager extends ScanCallback
{
BluetoothManager()
{
ScanFilter.Builder scanFilterBuilder = new ScanFilter.Builder();
scanFilterBuilder.setServiceUuid (ParcelUuid.fromString (bluetoothLEMidiServiceUUID));
ScanSettings.Builder scanSettingsBuilder = new ScanSettings.Builder();
scanSettingsBuilder.setCallbackType (ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setScanMode (ScanSettings.SCAN_MODE_LOW_POWER)
.setScanMode (ScanSettings.MATCH_MODE_STICKY);
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null)
{
Log.d ("JUCE", "BluetoothManager error: could not get default Bluetooth adapter");
return;
}
BluetoothLeScanner bluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
if (bluetoothLeScanner == null)
{
Log.d ("JUCE", "BluetoothManager error: could not get Bluetooth LE scanner");
return;
}
bluetoothLeScanner.startScan (Arrays.asList (scanFilterBuilder.build()),
scanSettingsBuilder.build(),
this);
}
public String[] getMidiBluetoothAddresses()
{
return bluetoothMidiDevices.toArray (new String[bluetoothMidiDevices.size()]);
}
public String getHumanReadableStringForBluetoothAddress (String address)
{
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address);
return btDevice.getName();
}
public boolean isBluetoothDevicePaired (String address)
{
return getAndroidMidiDeviceManager().isBluetoothDevicePaired (address);
}
public boolean pairBluetoothMidiDevice(String address)
{
BluetoothDevice btDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice (address);
if (btDevice == null)
{
Log.d ("JUCE", "failed to create buletooth device from address");
return false;
}
MidiManager mm = (MidiManager) getSystemService (MIDI_SERVICE);
PhysicalMidiDevice midiDevice = PhysicalMidiDevice.fromBluetoothLeDevice (btDevice, mm);
if (midiDevice != null)
{
getAndroidMidiDeviceManager().addDeviceToList (midiDevice);
return true;
}
return false;
}
public void unpairBluetoothMidiDevice (String address)
{
getAndroidMidiDeviceManager().unpairBluetoothDevice (address);
}
public void onScanFailed (int errorCode)
{
}
public void onScanResult (int callbackType, ScanResult result)
{
if (callbackType == ScanSettings.CALLBACK_TYPE_ALL_MATCHES
|| callbackType == ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
{
BluetoothDevice device = result.getDevice();
if (device != null)
bluetoothMidiDevices.add (device.getAddress());
}
if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST)
{
Log.d ("JUCE", "ScanSettings.CALLBACK_TYPE_MATCH_LOST");
BluetoothDevice device = result.getDevice();
if (device != null)
{
bluetoothMidiDevices.remove (device.getAddress());
unpairBluetoothMidiDevice (device.getAddress());
}
}
}
public void onBatchScanResults (List<ScanResult> results)
{
for (ScanResult result : results)
onScanResult (ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result);
}
private BluetoothLeScanner scanner;
private static final String bluetoothLEMidiServiceUUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700";
private HashSet<String> bluetoothMidiDevices = new HashSet<String>();
}
public static class JuceMidiInputPort extends MidiReceiver implements JuceMidiPort
{
private native void handleReceive (long host, byte[] msg, int offset, int count, long timestamp);
public JuceMidiInputPort (PhysicalMidiDevice device, long host, MidiOutputPort midiPort)
{
parent = device;
juceHost = host;
port = midiPort;
}
@Override
public boolean isInputPort()
{
return true;
}
@Override
public void start()
{
port.connect (this);
}
@Override
public void stop()
{
port.disconnect (this);
}
@Override
public void close()
{
stop();
try
{
port.close();
}
catch (IOException e)
{
Log.d ("JUCE", "JuceMidiInputPort::close: IOException = " + e.toString());
}
if (parent != null)
{
parent.removePort (port.getPortNumber(), true);
parent = null;
}
}
public void onSend (byte[] msg, int offset, int count, long timestamp)
{
if (count > 0)
handleReceive (juceHost, msg, offset, count, timestamp);
}
@Override
public MidiPortID getPortId()
{
return new MidiPortID (port.getPortNumber(), true);
}
@Override
public void sendMidi (byte[] msg, int offset, int count)
{
}
private PhysicalMidiDevice parent = null;
private long juceHost = 0;
private MidiOutputPort port;
}
public static class JuceMidiOutputPort implements JuceMidiPort
{
public JuceMidiOutputPort (PhysicalMidiDevice device, MidiInputPort midiPort)
{
parent = device;
port = midiPort;
}
@Override
public boolean isInputPort()
{
return false;
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
@Override
public void sendMidi (byte[] msg, int offset, int count)
{
try
{
port.send(msg, offset, count);
}
catch (IOException e)
{
Log.d ("JUCE", "JuceMidiOutputPort::sendMidi: IOException = " + e.toString());
}
}
@Override
public void close()
{
try
{
port.close();
}
catch (IOException e)
{
Log.d ("JUCE", "JuceMidiOutputPort::close: IOException = " + e.toString());
}
if (parent != null)
{
parent.removePort (port.getPortNumber(), false);
parent = null;
}
}
@Override
public MidiPortID getPortId()
{
return new MidiPortID (port.getPortNumber(), false);
}
private PhysicalMidiDevice parent = null;
private MidiInputPort port;
}
public static class PhysicalMidiDevice
{
private static class MidiDeviceThread extends Thread
{
public Handler handler = null;
public Object sync = null;
public MidiDeviceThread (Object syncrhonization)
{
sync = syncrhonization;
}
public void run()
{
Looper.prepare();
synchronized (sync)
{
handler = new Handler();
sync.notifyAll();
}
Looper.loop();
}
}
private static class MidiDeviceOpenCallback implements MidiManager.OnDeviceOpenedListener
{
public Object sync = null;
public boolean isWaiting = true;
public android.media.midi.MidiDevice theDevice = null;
public MidiDeviceOpenCallback (Object waiter)
{
sync = waiter;
}
public void onDeviceOpened (MidiDevice device)
{
synchronized (sync)
{
theDevice = device;
isWaiting = false;
sync.notifyAll();
}
}
}
public static PhysicalMidiDevice fromBluetoothLeDevice (BluetoothDevice bluetoothDevice, MidiManager mm)
{
Object waitForCreation = new Object();
MidiDeviceThread thread = new MidiDeviceThread (waitForCreation);
thread.start();
synchronized (waitForCreation)
{
while (thread.handler == null)
{
try
{
waitForCreation.wait();
}
catch (InterruptedException e)
{
Log.d ("JUCE", "Wait was interrupted but we don't care");
}
}
}
Object waitForDevice = new Object();
MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice);
synchronized (waitForDevice)
{
mm.openBluetoothDevice (bluetoothDevice, openCallback, thread.handler);
while (openCallback.isWaiting)
{
try
{
waitForDevice.wait();
}
catch (InterruptedException e)
{
Log.d ("JUCE", "Wait was interrupted but we don't care");
}
}
}
if (openCallback.theDevice == null)
{
Log.d ("JUCE", "openBluetoothDevice failed");
return null;
}
PhysicalMidiDevice device = new PhysicalMidiDevice();
device.handle = openCallback.theDevice;
device.info = device.handle.getInfo();
device.bluetoothAddress = bluetoothDevice.getAddress();
device.midiManager = mm;
return device;
}
public void unpair()
{
if (! bluetoothAddress.equals ("") && handle != null)
{
JuceMidiPort ports[] = new JuceMidiPort[0];
ports = juceOpenedPorts.values().toArray(ports);
for (int i = 0; i < ports.length; ++i)
ports[i].close();
juceOpenedPorts.clear();
try
{
handle.close();
}
catch (IOException e)
{
Log.d ("JUCE", "handle.close(): IOException = " + e.toString());
}
handle = null;
}
}
public static PhysicalMidiDevice fromMidiDeviceInfo (MidiDeviceInfo info, MidiManager mm)
{
PhysicalMidiDevice device = new PhysicalMidiDevice();
device.info = info;
device.midiManager = mm;
return device;
}
public PhysicalMidiDevice()
{
bluetoothAddress = "";
juceOpenedPorts = new Hashtable<MidiPortID, JuceMidiPort>();
handle = null;
}
public MidiDeviceInfo.PortInfo[] getPorts()
{
return info.getPorts();
}
public String getHumanReadableNameForPort (MidiDeviceInfo.PortInfo port, int portIndexToUseInName)
{
String portName = port.getName();
if (portName.equals (""))
portName = ((port.getType() == MidiDeviceInfo.PortInfo.TYPE_OUTPUT) ? "Out " : "In ")
+ Integer.toString (portIndexToUseInName);
return getHumanReadableDeviceName() + " " + portName;
}
public String getHumanReadableNameForPort (int portType, int androidPortID, int portIndexToUseInName)
{
MidiDeviceInfo.PortInfo[] ports = info.getPorts();
for (MidiDeviceInfo.PortInfo port : ports)
{
if (port.getType() == portType)
{
if (port.getPortNumber() == androidPortID)
return getHumanReadableNameForPort (port, portIndexToUseInName);
}
}
return "Unknown";
}
public String getHumanReadableDeviceName()
{
Bundle bundle = info.getProperties();
return bundle.getString (MidiDeviceInfo.PROPERTY_NAME , "Unknown device");
}
public void checkIfDeviceCanBeClosed()
{
if (juceOpenedPorts.size() == 0)
{
// never close bluetooth LE devices, otherwise they unpair and we have
// no idea how many ports they have.
// Only remove bluetooth devices when we specifically unpair
if (bluetoothAddress.equals (""))
{
try
{
handle.close();
handle = null;
}
catch (IOException e)
{
Log.d ("JUCE", "PhysicalMidiDevice::checkIfDeviceCanBeClosed: IOException = " + e.toString());
}
}
}
}
public void removePort (int portIdx, boolean isInput)
{
MidiPortID portID = new MidiPortID (portIdx, isInput);
JuceMidiPort port = juceOpenedPorts.get (portID);
if (port != null)
{
juceOpenedPorts.remove (portID);
checkIfDeviceCanBeClosed();
return;
}
// tried to remove a port that was never added
assert false;
}
public JuceMidiPort openPort (int portIdx, boolean isInput, long host)
{
open();
if (handle == null)
{
Log.d ("JUCE", "PhysicalMidiDevice::openPort: handle = null, device not open");
return null;
}
// make sure that the port is not already open
if (findPortForIdx (portIdx, isInput) != null)
{
Log.d ("JUCE", "PhysicalMidiDevice::openInputPort: port already open, not opening again!");
return null;
}
JuceMidiPort retval = null;
if (isInput)
{
MidiOutputPort androidPort = handle.openOutputPort (portIdx);
if (androidPort == null)
{
Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openOutputPort (portIdx = "
+ Integer.toString (portIdx) + ") failed!");
return null;
}
retval = new JuceMidiInputPort (this, host, androidPort);
}
else
{
MidiInputPort androidPort = handle.openInputPort (portIdx);
if (androidPort == null)
{
Log.d ("JUCE", "PhysicalMidiDevice::openPort: MidiDevice::openInputPort (portIdx = "
+ Integer.toString (portIdx) + ") failed!");
return null;
}
retval = new JuceMidiOutputPort (this, androidPort);
}
juceOpenedPorts.put (new MidiPortID (portIdx, isInput), retval);
return retval;
}
private JuceMidiPort findPortForIdx (int idx, boolean isInput)
{
return juceOpenedPorts.get (new MidiPortID (idx, isInput));
}
// opens the device
private synchronized void open()
{
if (handle != null)
return;
Object waitForCreation = new Object();
MidiDeviceThread thread = new MidiDeviceThread (waitForCreation);
thread.start();
synchronized(waitForCreation)
{
while (thread.handler == null)
{
try
{
waitForCreation.wait();
}
catch (InterruptedException e)
{
Log.d ("JUCE", "wait was interrupted but we don't care");
}
}
}
Object waitForDevice = new Object();
MidiDeviceOpenCallback openCallback = new MidiDeviceOpenCallback (waitForDevice);
synchronized (waitForDevice)
{
midiManager.openDevice (info, openCallback, thread.handler);
while (openCallback.isWaiting)
{
try
{
waitForDevice.wait();
}
catch (InterruptedException e)
{
Log.d ("JUCE", "wait was interrupted but we don't care");
}
}
}
handle = openCallback.theDevice;
}
private MidiDeviceInfo info;
private Hashtable<MidiPortID, JuceMidiPort> juceOpenedPorts;
public MidiDevice handle;
public String bluetoothAddress;
private MidiManager midiManager;
}
//==============================================================================
public class MidiDeviceManager extends MidiManager.DeviceCallback
{
public class MidiPortPath
{
public PhysicalMidiDevice midiDevice;
public int androidMidiPortID;
public int portIndexToUseInName;
}
public class JuceDeviceList
{
public ArrayList<MidiPortPath> inPorts = new ArrayList<MidiPortPath>();
public ArrayList<MidiPortPath> outPorts = new ArrayList<MidiPortPath>();
}
// We need to keep a thread local copy of the devices
// which we returned the last time
// getJuceAndroidMidiIn/OutputDevices() was called
private final ThreadLocal<JuceDeviceList> lastDevicesReturned =
new ThreadLocal<JuceDeviceList>()
{
@Override protected JuceDeviceList initialValue()
{
return new JuceDeviceList();
}
};
public MidiDeviceManager()
{
physicalMidiDevices = new ArrayList<PhysicalMidiDevice>();
manager = (MidiManager) getSystemService (MIDI_SERVICE);
if (manager == null)
{
Log.d ("JUCE", "MidiDeviceManager error: could not get MidiManager system service");
return;
}
manager.registerDeviceCallback (this, null);
MidiDeviceInfo[] foundDevices = manager.getDevices();
for (MidiDeviceInfo info : foundDevices)
physicalMidiDevices.add (PhysicalMidiDevice.fromMidiDeviceInfo (info, manager));
}
// specifically add a device to the list
public void addDeviceToList (PhysicalMidiDevice device)
{
physicalMidiDevices.add (device);
}
public void unpairBluetoothDevice (String address)
{
for (int i = 0; i < physicalMidiDevices.size(); ++i)
{
PhysicalMidiDevice device = physicalMidiDevices.get(i);
if (device.bluetoothAddress.equals (address))
{
physicalMidiDevices.remove (i);
device.unpair();
return;
}
}
}
public boolean isBluetoothDevicePaired (String address)
{
for (int i = 0; i < physicalMidiDevices.size(); ++i)
{
PhysicalMidiDevice device = physicalMidiDevices.get(i);
if (device.bluetoothAddress.equals (address))
return true;
}
return false;
}
public String[] getJuceAndroidMidiInputDevices()
{
return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_INPUT);
}
public String[] getJuceAndroidMidiOutputDevices()
{
return getJuceAndroidMidiDevices (MidiDeviceInfo.PortInfo.TYPE_OUTPUT);
}
private String[] getJuceAndroidMidiDevices (int portType)
{
ArrayList<MidiPortPath> listOfReturnedDevices = new ArrayList<MidiPortPath>();
List<String> deviceNames = new ArrayList<String>();
for (PhysicalMidiDevice physicalMidiDevice : physicalMidiDevices)
{
int portIdx = 0;
MidiDeviceInfo.PortInfo[] ports = physicalMidiDevice.getPorts();
for (MidiDeviceInfo.PortInfo port : ports)
{
if (port.getType() == portType)
{
MidiPortPath path = new MidiPortPath();
path.midiDevice = physicalMidiDevice;
path.androidMidiPortID = port.getPortNumber();
path.portIndexToUseInName = ++portIdx;
listOfReturnedDevices.add (path);
deviceNames.add (physicalMidiDevice.getHumanReadableNameForPort (port,
path.portIndexToUseInName));
}
}
}
String[] deviceNamesArray = new String[deviceNames.size()];
if (portType == MidiDeviceInfo.PortInfo.TYPE_INPUT)
{
lastDevicesReturned.get().inPorts.clear();
lastDevicesReturned.get().inPorts.addAll (listOfReturnedDevices);
}
else
{
lastDevicesReturned.get().outPorts.clear();
lastDevicesReturned.get().outPorts.addAll (listOfReturnedDevices);
}
return deviceNames.toArray(deviceNamesArray);
}
public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host)
{
ArrayList<MidiPortPath> lastDevices = lastDevicesReturned.get().inPorts;
if (index >= lastDevices.size() || index < 0)
return null;
MidiPortPath path = lastDevices.get (index);
return path.midiDevice.openPort (path.androidMidiPortID, true, host);
}
public JuceMidiPort openMidiOutputPortWithJuceIndex (int index)
{
ArrayList<MidiPortPath> lastDevices = lastDevicesReturned.get().outPorts;
if (index >= lastDevices.size() || index < 0)
return null;
MidiPortPath path = lastDevices.get (index);
return path.midiDevice.openPort (path.androidMidiPortID, false, 0);
}
public String getInputPortNameForJuceIndex (int index)
{
ArrayList<MidiPortPath> lastDevices = lastDevicesReturned.get().inPorts;
if (index >= lastDevices.size() || index < 0)
return "";
MidiPortPath path = lastDevices.get (index);
return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_INPUT,
path.androidMidiPortID,
path.portIndexToUseInName);
}
public String getOutputPortNameForJuceIndex (int index)
{
ArrayList<MidiPortPath> lastDevices = lastDevicesReturned.get().outPorts;
if (index >= lastDevices.size() || index < 0)
return "";
MidiPortPath path = lastDevices.get (index);
return path.midiDevice.getHumanReadableNameForPort (MidiDeviceInfo.PortInfo.TYPE_OUTPUT,
path.androidMidiPortID,
path.portIndexToUseInName);
}
public void onDeviceAdded (MidiDeviceInfo info)
{
PhysicalMidiDevice device = PhysicalMidiDevice.fromMidiDeviceInfo (info, manager);
// Do not add bluetooth devices as they are already added by the native bluetooth dialog
if (info.getType() != MidiDeviceInfo.TYPE_BLUETOOTH)
physicalMidiDevices.add (device);
}
public void onDeviceRemoved (MidiDeviceInfo info)
{
for (int i = 0; i < physicalMidiDevices.size(); ++i)
{
if (physicalMidiDevices.get(i).info.getId() == info.getId())
{
physicalMidiDevices.remove (i);
return;
}
}
// Don't assert here as this may be called again after a bluetooth device is unpaired
}
public void onDeviceStatusChanged (MidiDeviceStatus status)
{
}
private ArrayList<PhysicalMidiDevice> physicalMidiDevices;
private MidiManager manager;
}
public MidiDeviceManager getAndroidMidiDeviceManager()
{
if (getSystemService (MIDI_SERVICE) == null)
return null;
synchronized (JuceAppActivity.class)
{
if (midiDeviceManager == null)
midiDeviceManager = new MidiDeviceManager();
}
return midiDeviceManager;
}
public BluetoothManager getAndroidBluetoothManager()
{
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
if (adapter == null)
return null;
if (adapter.getBluetoothLeScanner() == null)
return null;
synchronized (JuceAppActivity.class)
{
if (bluetoothManager == null)
bluetoothManager = new BluetoothManager();
}
return bluetoothManager;
}

View file

@ -0,0 +1,81 @@
//==============================================================================
public class BluetoothManager
{
BluetoothManager()
{
}
public String[] getMidiBluetoothAddresses()
{
String[] bluetoothAddresses = new String[0];
return bluetoothAddresses;
}
public String getHumanReadableStringForBluetoothAddress (String address)
{
return address;
}
public boolean isBluetoothDevicePaired (String address)
{
return false;
}
public boolean pairBluetoothMidiDevice(String address)
{
return false;
}
public void unpairBluetoothMidiDevice (String address)
{
}
}
//==============================================================================
public class MidiDeviceManager
{
public MidiDeviceManager()
{
}
public String[] getJuceAndroidMidiInputDevices()
{
return new String[0];
}
public String[] getJuceAndroidMidiOutputDevices()
{
return new String[0];
}
public JuceMidiPort openMidiInputPortWithJuceIndex (int index, long host)
{
return null;
}
public JuceMidiPort openMidiOutputPortWithJuceIndex (int index)
{
return null;
}
public String getInputPortNameForJuceIndex (int index)
{
return "";
}
public String getOutputPortNameForJuceIndex (int index)
{
return "";
}
}
public MidiDeviceManager getAndroidMidiDeviceManager()
{
return null;
}
public BluetoothManager getAndroidBluetoothManager()
{
return null;
}

View file

@ -30,15 +30,20 @@ import android.content.DialogInterface;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Looper;
import android.os.Handler;
import android.os.Build;
import android.os.Process;
import android.os.ParcelUuid;
import android.view.*;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.graphics.*;
import android.opengl.*;
import android.text.ClipboardManager;
import android.text.InputType;
import android.util.DisplayMetrics;
@ -53,12 +58,13 @@ import java.util.TimerTask;
import java.io.*;
import java.net.URL;
import java.net.HttpURLConnection;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.media.AudioManager;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
$$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the introjucer!
//==============================================================================
public class JuceAppActivity extends Activity
{
@ -68,6 +74,58 @@ public class JuceAppActivity extends Activity
System.loadLibrary ("juce_jni");
}
//==============================================================================
public static class MidiPortID extends Object
{
public MidiPortID (int index, boolean direction)
{
androidIndex = index;
isInput = direction;
}
public int androidIndex;
public boolean isInput;
@Override
public int hashCode()
{
Integer i = new Integer (androidIndex);
return i.hashCode() * (isInput ? -1 : 1);
}
@Override
public boolean equals (Object obj)
{
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
MidiPortID other = (MidiPortID) obj;
return (androidIndex == other.androidIndex && isInput == other.isInput);
}
}
public interface JuceMidiPort
{
boolean isInputPort();
// start, stop does nothing on an output port
void start();
void stop();
void close();
MidiPortID getPortId();
// send will do nothing on an input port
void sendMidi (byte[] msg, int offset, int count);
}
//==============================================================================
$$JuceAndroidMidiCode$$ // If you get an error here, you need to re-save your project with the introjucer!
//==============================================================================
@Override
public void onCreate (Bundle savedInstanceState)
{
@ -92,9 +150,6 @@ public class JuceAppActivity extends Activity
@Override
protected void onPause()
{
if (viewHolder != null)
viewHolder.onPause();
suspendApp();
super.onPause();
}
@ -103,10 +158,6 @@ public class JuceAppActivity extends Activity
protected void onResume()
{
super.onResume();
if (viewHolder != null)
viewHolder.onResume();
resumeApp();
}
@ -149,6 +200,8 @@ public class JuceAppActivity extends Activity
//==============================================================================
private ViewHolder viewHolder;
private MidiDeviceManager midiDeviceManager = null;
private BluetoothManager bluetoothManager = null;
private boolean isScreenSaverEnabled;
private java.util.Timer keepAliveTimer;
@ -167,7 +220,7 @@ public class JuceAppActivity extends Activity
group.removeView (view);
}
public final void deleteOpenGLView (OpenGLView view)
public final void deleteNativeSurfaceView (NativeSurfaceView view)
{
ViewGroup group = (ViewGroup) (view.getParent());
@ -195,28 +248,6 @@ public class JuceAppActivity extends Activity
}
}
public final void onPause()
{
for (int i = getChildCount(); --i >= 0;)
{
View v = getChildAt (i);
if (v instanceof ComponentPeerView)
((ComponentPeerView) v).onPause();
}
}
public final void onResume()
{
for (int i = getChildCount(); --i >= 0;)
{
View v = getChildAt (i);
if (v instanceof ComponentPeerView)
((ComponentPeerView) v).onResume();
}
}
private final int getDPI()
{
DisplayMetrics metrics = new DisplayMetrics();
@ -586,70 +617,83 @@ public class JuceAppActivity extends Activity
{
return true; //xxx needs to check overlapping views
}
public final void onPause()
{
for (int i = getChildCount(); --i >= 0;)
{
View v = getChildAt (i);
if (v instanceof OpenGLView)
((OpenGLView) v).onPause();
}
}
public final void onResume()
{
for (int i = getChildCount(); --i >= 0;)
{
View v = getChildAt (i);
if (v instanceof OpenGLView)
((OpenGLView) v).onResume();
}
}
public OpenGLView createGLView()
{
OpenGLView glView = new OpenGLView (getContext());
addView (glView);
return glView;
}
}
//==============================================================================
public final class OpenGLView extends GLSurfaceView
implements GLSurfaceView.Renderer
public static class NativeSurfaceView extends SurfaceView
implements SurfaceHolder.Callback
{
OpenGLView (Context context)
private long nativeContext = 0;
NativeSurfaceView (Context context, long nativeContextPtr)
{
super (context);
setEGLContextClientVersion (2);
setRenderer (this);
setRenderMode (RENDERMODE_WHEN_DIRTY);
nativeContext = nativeContextPtr;
}
public Surface getNativeSurface()
{
Surface retval = null;
SurfaceHolder holder = getHolder();
if (holder != null)
retval = holder.getSurface();
return retval;
}
//==============================================================================
@Override
public void surfaceChanged (SurfaceHolder holder, int format, int width, int height)
{
surfaceChangedNative (nativeContext, holder, format, width, height);
}
@Override
public void onSurfaceCreated (GL10 unused, EGLConfig config)
public void surfaceCreated (SurfaceHolder holder)
{
contextCreated();
surfaceCreatedNative (nativeContext, holder);
}
@Override
public void onSurfaceChanged (GL10 unused, int width, int height)
public void surfaceDestroyed (SurfaceHolder holder)
{
contextChangedSize();
surfaceDestroyedNative (nativeContext, holder);
}
@Override
public void onDrawFrame (GL10 unused)
protected void dispatchDraw (Canvas canvas)
{
render();
super.dispatchDraw (canvas);
dispatchDrawNative (nativeContext, canvas);
}
private native void contextCreated();
private native void contextChangedSize();
private native void render();
//==============================================================================
@Override
protected void onAttachedToWindow ()
{
super.onAttachedToWindow();
getHolder().addCallback (this);
}
@Override
protected void onDetachedFromWindow ()
{
super.onDetachedFromWindow();
getHolder().removeCallback (this);
}
//==============================================================================
private native void dispatchDrawNative (long nativeContextPtr, Canvas canvas);
private native void surfaceCreatedNative (long nativeContextptr, SurfaceHolder holder);
private native void surfaceDestroyedNative (long nativeContextptr, SurfaceHolder holder);
private native void surfaceChangedNative (long nativeContextptr, SurfaceHolder holder,
int format, int width, int height);
}
public NativeSurfaceView createNativeSurfaceView(long nativeSurfacePtr)
{
return new NativeSurfaceView (this, nativeSurfacePtr);
}
//==============================================================================
@ -984,4 +1028,70 @@ public class JuceAppActivity extends Activity
return null;
}
public final int getAndroidSDKVersion()
{
return android.os.Build.VERSION.SDK_INT;
}
public final String audioManagerGetProperty (String property)
{
Object obj = getSystemService (AUDIO_SERVICE);
if (obj == null)
return null;
java.lang.reflect.Method method;
try {
method = obj.getClass().getMethod ("getProperty", String.class);
} catch (SecurityException e) {
return null;
} catch (NoSuchMethodException e) {
return null;
}
if (method == null)
return null;
try {
return (String) method.invoke (obj, property);
} catch (java.lang.IllegalArgumentException e) {
} catch (java.lang.IllegalAccessException e) {
} catch (java.lang.reflect.InvocationTargetException e) {
}
return null;
}
public final int setCurrentThreadPriority (int priority)
{
android.os.Process.setThreadPriority (android.os.Process.myTid(), priority);
return android.os.Process.getThreadPriority (android.os.Process.myTid());
}
public final boolean hasSystemFeature (String property)
{
return getPackageManager().hasSystemFeature (property);
}
private static class JuceThread extends Thread
{
public JuceThread (long host)
{
_this = host;
}
public void run()
{
runThread(_this);
}
private native void runThread (long host);
private long _this;
}
public final Thread createNewThread(long host)
{
return new JuceThread(host);
}
}

View file

@ -36,6 +36,10 @@
//==============================================================================
extern JNIEnv* getEnv() noexcept;
// You should rarely need to use this function. Only if you expect callbacks
// on a java thread which you did not create yourself.
extern void setEnv (JNIEnv* env) noexcept;
//==============================================================================
class GlobalRef
{
@ -236,6 +240,8 @@ private:
#define JUCE_JNI_CALLBACK(className, methodName, returnType, params) \
extern "C" __attribute__ ((visibility("default"))) JUCE_ARM_SOFT_FLOAT_ABI returnType JUCE_JOIN_MACRO (JUCE_JOIN_MACRO (Java_, className), _ ## methodName) params
//==============================================================================
class AndroidSystem
{
@ -253,142 +259,11 @@ public:
extern AndroidSystem android;
//==============================================================================
class ThreadLocalJNIEnvHolder
{
public:
ThreadLocalJNIEnvHolder() noexcept
: jvm (nullptr)
{
zeromem (threads, sizeof (threads));
zeromem (envs, sizeof (envs));
}
void initialise (JNIEnv* env)
{
// NB: the DLL can be left loaded by the JVM, so the same static
// objects can end up being reused by subsequent runs of the app
zeromem (threads, sizeof (threads));
zeromem (envs, sizeof (envs));
env->GetJavaVM (&jvm);
addEnv (env);
}
JNIEnv* attach() noexcept
{
if (android.activity != nullptr)
{
if (JNIEnv* env = attachToCurrentThread())
{
SpinLock::ScopedLockType sl (addRemoveLock);
return addEnv (env);
}
jassertfalse;
}
return nullptr;
}
void detach() noexcept
{
if (android.activity != nullptr)
{
jvm->DetachCurrentThread();
removeCurrentThreadFromCache();
}
}
void removeCurrentThreadFromCache()
{
const pthread_t thisThread = pthread_self();
SpinLock::ScopedLockType sl (addRemoveLock);
for (int i = 0; i < maxThreads; ++i)
{
if (threads[i] == thisThread)
{
threads[i] = 0;
envs[i] = nullptr;
}
}
}
JNIEnv* getOrAttach() noexcept
{
if (JNIEnv* env = get())
return env;
SpinLock::ScopedLockType sl (addRemoveLock);
if (JNIEnv* env = get())
return env;
if (JNIEnv* env = attachToCurrentThread())
return addEnv (env);
return nullptr;
}
private:
JavaVM* jvm;
enum { maxThreads = 32 };
pthread_t threads [maxThreads];
JNIEnv* envs [maxThreads];
SpinLock addRemoveLock;
JNIEnv* addEnv (JNIEnv* env) noexcept
{
const pthread_t thisThread = pthread_self();
for (int i = 0; i < maxThreads; ++i)
{
if (threads[i] == 0)
{
envs[i] = env;
threads[i] = thisThread;
return env;
}
}
jassertfalse; // too many threads!
return nullptr;
}
JNIEnv* get() const noexcept
{
const pthread_t thisThread = pthread_self();
for (int i = 0; i < maxThreads; ++i)
if (threads[i] == thisThread)
return envs[i];
return nullptr;
}
JNIEnv* attachToCurrentThread()
{
JNIEnv* env = nullptr;
jvm->AttachCurrentThread (&env, nullptr);
return env;
}
};
extern ThreadLocalJNIEnvHolder threadLocalJNIEnvHolder;
struct AndroidThreadScope
{
AndroidThreadScope() { threadLocalJNIEnvHolder.attach(); }
~AndroidThreadScope() { threadLocalJNIEnvHolder.detach(); }
};
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \
METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \
METHOD (deleteOpenGLView, "deleteOpenGLView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$OpenGLView;)V") \
METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(J)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \
METHOD (postMessage, "postMessage", "(J)V") \
METHOD (finish, "finish", "()V") \
METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \
@ -405,7 +280,14 @@ struct AndroidThreadScope
METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \
METHOD (getTypeFaceFromByteArray,"getTypeFaceFromByteArray","([B)Landroid/graphics/Typeface;") \
METHOD (setScreenSaver, "setScreenSaver", "(Z)V") \
METHOD (getScreenSaver, "getScreenSaver", "()Z")
METHOD (getScreenSaver, "getScreenSaver", "()Z") \
METHOD (getAndroidMidiDeviceManager, "getAndroidMidiDeviceManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$MidiDeviceManager;") \
METHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager;") \
METHOD (getAndroidSDKVersion, "getAndroidSDKVersion", "()I") \
METHOD (audioManagerGetProperty, "audioManagerGetProperty", "(Ljava/lang/String;)Ljava/lang/String;") \
METHOD (setCurrentThreadPriority, "setCurrentThreadPriority", "(I)I") \
METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \
METHOD (createNewThread, "createNewThread", "(J)Ljava/lang/Thread;") \
DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH);
#undef JNI_CLASS_MEMBERS
@ -435,6 +317,19 @@ DECLARE_JNI_CLASS (Paint, "android/graphics/Paint");
DECLARE_JNI_CLASS (Matrix, "android/graphics/Matrix");
#undef JNI_CLASS_MEMBERS
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (start, "start", "()V") \
METHOD (stop, "stop", "()V") \
METHOD (setName, "setName", "(Ljava/lang/String;)V") \
METHOD (getName, "getName", "()Ljava/lang/String;") \
METHOD (getId, "getId", "()J") \
STATICMETHOD (currentThread, "currentThread", "()Ljava/lang/Thread;") \
METHOD (setPriority, "setPriority", "(I)V") \
DECLARE_JNI_CLASS (JuceThread, "java/lang/Thread");
#undef JNI_CLASS_MEMBERS
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (constructor, "<init>", "(IIII)V") \

View file

@ -98,24 +98,19 @@ jfieldID JNIClassBase::resolveStaticField (JNIEnv* env, const char* fieldName, c
}
//==============================================================================
ThreadLocalJNIEnvHolder threadLocalJNIEnvHolder;
#if JUCE_DEBUG
static bool systemInitialised = false;
#endif
ThreadLocalValue<JNIEnv*> androidJNIEnv;
JNIEnv* getEnv() noexcept
{
#if JUCE_DEBUG
if (! systemInitialised)
{
DBG ("*** Call to getEnv() when system not initialised");
jassertfalse;
std::exit (EXIT_FAILURE);
}
#endif
JNIEnv* env = androidJNIEnv.get();
jassert (env != nullptr);
return threadLocalJNIEnvHolder.getOrAttach();
return env;
}
void setEnv (JNIEnv* env) noexcept
{
androidJNIEnv.get() = env;
}
extern "C" jint JNI_OnLoad (JavaVM*, void*)
@ -134,11 +129,6 @@ void AndroidSystem::initialise (JNIEnv* env, jobject act, jstring file, jstring
dpi = 160;
JNIClassBase::initialiseAllClasses (env);
threadLocalJNIEnvHolder.initialise (env);
#if JUCE_DEBUG
systemInitialised = true;
#endif
activity = GlobalRef (act);
appFile = juceString (env, file);
appDataDir = juceString (env, dataDir);
@ -148,10 +138,6 @@ void AndroidSystem::shutdown (JNIEnv* env)
{
activity.clear();
#if JUCE_DEBUG
systemInitialised = false;
#endif
JNIClassBase::releaseAllClasses (env);
}
@ -253,7 +239,7 @@ String SystemStats::getLogonName()
if (struct passwd* const pw = getpwuid (getuid()))
return CharPointer_UTF8 (pw->pw_name);
return String::empty;
return String();
}
String SystemStats::getFullUserName()
@ -267,7 +253,7 @@ String SystemStats::getComputerName()
if (gethostname (name, sizeof (name) - 1) == 0)
return name;
return String::empty;
return String();
}

View file

@ -74,3 +74,235 @@ JUCE_API bool JUCE_CALLTYPE Process::isRunningUnderDebugger()
JUCE_API void JUCE_CALLTYPE Process::raisePrivilege() {}
JUCE_API void JUCE_CALLTYPE Process::lowerPrivilege() {}
struct AndroidThreadData
{
AndroidThreadData (Thread* thread) noexcept
: owner (thread), tId (0)
{
}
Thread* owner;
Thread::ThreadID tId;
WaitableEvent eventSet, eventGet;
};
void JUCE_API juce_threadEntryPoint (void*);
extern "C" void* threadEntryProc (void*);
extern "C" void* threadEntryProc (void* userData)
{
ScopedPointer<AndroidThreadData> priv (reinterpret_cast<AndroidThreadData*> (userData));
priv->tId = (Thread::ThreadID) pthread_self();
priv->eventSet.signal();
priv->eventGet.wait (-1);
juce_threadEntryPoint (priv->owner);
return nullptr;
}
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024JuceThread), runThread,
void, (JNIEnv* env, jobject device, jlong host))
{
// 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);
if (Thread* thread = reinterpret_cast<Thread*> (host))
threadEntryProc (thread);
}
void Thread::launchThread()
{
threadHandle = 0;
ScopedPointer<AndroidThreadData> threadPrivateData = new AndroidThreadData (this);
jobject juceNewThread = android.activity.callObjectMethod (JuceAppActivity.createNewThread, (jlong) threadPrivateData.get());
if (jobject juceThread = getEnv()->NewGlobalRef (juceNewThread))
{
AndroidThreadData* priv = threadPrivateData.release();
threadHandle = (void*) juceThread;
getEnv()->CallVoidMethod (juceThread, JuceThread.start);
priv->eventSet.wait (-1);
threadId = priv->tId;
priv->eventGet.signal();
}
}
void Thread::closeThreadHandle()
{
if (threadHandle != 0)
{
jobject juceThread = reinterpret_cast<jobject> (threadHandle);
getEnv()->DeleteGlobalRef (juceThread);
threadHandle = 0;
}
threadId = 0;
}
void Thread::killThread()
{
if (threadHandle != 0)
{
jobject juceThread = reinterpret_cast<jobject> (threadHandle);
getEnv()->CallVoidMethod (juceThread, JuceThread.stop);
}
}
void JUCE_CALLTYPE Thread::setCurrentThreadName (const String& name)
{
LocalRef<jobject> juceThread (getEnv()->CallStaticObjectMethod (JuceThread, JuceThread.currentThread));
if (jobject t = juceThread.get())
getEnv()->CallVoidMethod (t, JuceThread.setName, javaString (name).get());
}
bool Thread::setThreadPriority (void* handle, int priority)
{
if (handle == nullptr)
{
LocalRef<jobject> juceThread (getEnv()->CallStaticObjectMethod (JuceThread, JuceThread.currentThread));
if (jobject t = juceThread.get())
return setThreadPriority (t, priority);
return false;
}
jobject juceThread = reinterpret_cast<jobject> (handle);
const int minPriority = 1;
const int maxPriority = 10;
jint javaPriority = ((maxPriority - minPriority) * priority) / 10 + minPriority;
getEnv()->CallVoidMethod (juceThread, JuceThread.setPriority, javaPriority);
return true;
}
//==============================================================================
struct HighResolutionTimer::Pimpl
{
struct HighResolutionThread : public Thread
{
HighResolutionThread (HighResolutionTimer::Pimpl& parent)
: Thread ("High Resolution Timer"), pimpl (parent)
{
startThread();
}
void run() override
{
pimpl.timerThread();
}
private:
HighResolutionTimer::Pimpl& pimpl;
};
//==============================================================================
Pimpl (HighResolutionTimer& t) : owner (t) {}
~Pimpl()
{
stop();
}
void start (int newPeriod)
{
if (periodMs != newPeriod)
{
if (thread.get() == nullptr
|| thread->getThreadId() != Thread::getCurrentThreadId()
|| thread->threadShouldExit())
{
stop();
periodMs = newPeriod;
thread = new HighResolutionThread (*this);
}
else
{
periodMs = newPeriod;
}
}
}
void stop()
{
if (thread.get() != nullptr)
{
thread->signalThreadShouldExit();
if (thread->getThreadId() != Thread::getCurrentThreadId())
{
thread->waitForThreadToExit (-1);
thread = nullptr;
}
}
}
HighResolutionTimer& owner;
int volatile periodMs;
private:
ScopedPointer<Thread> thread;
void timerThread()
{
jassert (thread.get() != nullptr);
int lastPeriod = periodMs;
Clock clock (lastPeriod);
while (! thread->threadShouldExit())
{
clock.wait();
owner.hiResTimerCallback();
if (lastPeriod != periodMs)
{
lastPeriod = periodMs;
clock = Clock (lastPeriod);
}
}
periodMs = 0;
}
struct Clock
{
Clock (double millis) noexcept : delta ((uint64) (millis * 1000000))
{
}
void wait() noexcept
{
struct timespec t;
t.tv_sec = (time_t) (delta / 1000000000);
t.tv_nsec = (long) (delta % 1000000000);
nanosleep (&t, nullptr);
}
uint64 delta;
};
static bool setThreadToRealtime (pthread_t thread, uint64 periodMs)
{
ignoreUnused (periodMs);
struct sched_param param;
param.sched_priority = sched_get_priority_max (SCHED_RR);
return pthread_setschedparam (thread, SCHED_RR, &param) == 0;
}
JUCE_DECLARE_NON_COPYABLE (Pimpl)
};

View file

@ -841,6 +841,7 @@ void InterProcessLock::exit()
}
//==============================================================================
#if ! JUCE_ANDROID
void JUCE_API juce_threadEntryPoint (void*);
extern "C" void* threadEntryProc (void*);
@ -848,10 +849,6 @@ extern "C" void* threadEntryProc (void* userData)
{
JUCE_AUTORELEASEPOOL
{
#if JUCE_ANDROID
const AndroidThreadScope androidEnv;
#endif
juce_threadEntryPoint (userData);
}
@ -925,6 +922,7 @@ bool Thread::setThreadPriority (void* handle, int priority)
param.sched_priority = ((maxPriority - minPriority) * priority) / 10 + minPriority;
return pthread_setschedparam ((pthread_t) handle, policy, &param) == 0;
}
#endif
Thread::ThreadID JUCE_CALLTYPE Thread::getCurrentThreadId()
{
@ -1143,6 +1141,7 @@ bool ChildProcess::start (const StringArray& args, int streamFlags)
}
//==============================================================================
#if ! JUCE_ANDROID
struct HighResolutionTimer::Pimpl
{
Pimpl (HighResolutionTimer& t) : owner (t), thread (0), shouldStop (false)
@ -1249,20 +1248,6 @@ private:
uint64_t time, delta;
#elif JUCE_ANDROID
Clock (double millis) noexcept : delta ((uint64) (millis * 1000000))
{
}
void wait() noexcept
{
struct timespec t;
t.tv_sec = (time_t) (delta / 1000000000);
t.tv_nsec = (long) (delta % 1000000000);
nanosleep (&t, nullptr);
}
uint64 delta;
#else
Clock (double millis) noexcept : delta ((uint64) (millis * 1000000))
{
@ -1311,3 +1296,5 @@ private:
JUCE_DECLARE_NON_COPYABLE (Pimpl)
};
#endif

View file

@ -149,6 +149,29 @@
#endif
//==============================================================================
#if ! DOXYGEN
#define JUCE_JOIN_MACRO_HELPER(a, b) a ## b
#define JUCE_STRINGIFY_MACRO_HELPER(a) #a
#endif
/** A good old-fashioned C macro concatenation helper.
This combines two items (which may themselves be macros) into a single string,
avoiding the pitfalls of the ## macro operator.
*/
#define JUCE_JOIN_MACRO(item1, item2) JUCE_JOIN_MACRO_HELPER (item1, item2)
/** A handy C macro for stringifying any symbol, rather than just a macro parameter. */
#define JUCE_STRINGIFY(item) JUCE_STRINGIFY_MACRO_HELPER (item)
//==============================================================================
#ifdef JUCE_COMPILER_SUPPORTS_STATIC_ASSERT
/** A compile-time assertion macro.
If the expression parameter is false, the macro will cause a compile error. (The actual error
message that the compiler generates may be completely bizarre and seem to have no relation to
the place where you put the static_assert though!)
*/
#define static_jassert(expression) static_assert (expression);
#else
#ifndef DOXYGEN
namespace juce
{
@ -163,6 +186,7 @@ namespace juce
the place where you put the static_assert though!)
*/
#define static_jassert(expression) juce::JuceStaticAssert<expression>::dummy();
#endif
/** This is a shorthand macro for declaring stubs for a class's copy constructor and operator=.
@ -207,24 +231,6 @@ namespace juce
static void* operator new (size_t) JUCE_DELETED_FUNCTION; \
static void operator delete (void*) JUCE_DELETED_FUNCTION;
//==============================================================================
#if ! DOXYGEN
#define JUCE_JOIN_MACRO_HELPER(a, b) a ## b
#define JUCE_STRINGIFY_MACRO_HELPER(a) #a
#endif
/** A good old-fashioned C macro concatenation helper.
This combines two items (which may themselves be macros) into a single string,
avoiding the pitfalls of the ## macro operator.
*/
#define JUCE_JOIN_MACRO(item1, item2) JUCE_JOIN_MACRO_HELPER (item1, item2)
/** A handy C macro for stringifying any symbol, rather than just a macro parameter.
*/
#define JUCE_STRINGIFY(item) JUCE_STRINGIFY_MACRO_HELPER (item)
//==============================================================================
#if JUCE_MSVC && ! defined (DOXYGEN)
#define JUCE_WARNING_HELPER(file, line, mess) message(file "(" JUCE_STRINGIFY (line) ") : Warning: " #mess)

View file

@ -44,6 +44,8 @@ bool MessageManager::postMessageToSystemQueue (MessageManager::MessageBase* cons
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, deliverMessage, void, (JNIEnv* env, jobject activity, jlong value))
{
setEnv (env);
JUCE_TRY
{
MessageManager::MessageBase* const message = (MessageManager::MessageBase*) (pointer_sized_uint) value;

View file

@ -33,6 +33,8 @@ namespace juce
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, launchApp, void, (JNIEnv* env, jobject activity,
jstring appFile, jstring appDataDir))
{
setEnv (env);
android.initialise (env, activity, appFile, appDataDir);
DBG (SystemStats::getJUCEVersion());
@ -56,18 +58,24 @@ JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, launchApp, void, (JNIEnv* en
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, suspendApp, void, (JNIEnv* env, jobject activity))
{
setEnv (env);
if (JUCEApplicationBase* const app = JUCEApplicationBase::getInstance())
app->suspended();
}
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, resumeApp, void, (JNIEnv* env, jobject activity))
{
setEnv (env);
if (JUCEApplicationBase* const app = JUCEApplicationBase::getInstance())
app->resumed();
}
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, quitApp, void, (JNIEnv* env, jobject activity))
{
setEnv (env);
JUCEApplicationBase::appWillTerminateByForce();
android.shutdown (env);
@ -98,7 +106,6 @@ DECLARE_JNI_CLASS (CanvasMinimal, "android/graphics/Canvas");
METHOD (invalidate, "invalidate", "(IIII)V") \
METHOD (containsPoint, "containsPoint", "(II)Z") \
METHOD (showKeyboard, "showKeyboard", "(Ljava/lang/String;)V") \
METHOD (createGLView, "createGLView", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$OpenGLView;") \
DECLARE_JNI_CLASS (ComponentPeerView, JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView");
#undef JNI_CLASS_MEMBERS
@ -582,6 +589,7 @@ int64 AndroidComponentPeer::touchesDown = 0;
#define JUCE_VIEW_CALLBACK(returnType, javaMethodName, params, juceMethodInvocation) \
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024ComponentPeerView), javaMethodName, returnType, params) \
{ \
setEnv (env); \
if (AndroidComponentPeer* peer = (AndroidComponentPeer*) (pointer_sized_uint) host) \
peer->juceMethodInvocation; \
}
@ -601,12 +609,6 @@ ComponentPeer* Component::createNewPeer (int styleFlags, void*)
return new AndroidComponentPeer (*this, styleFlags);
}
jobject createOpenGLView (ComponentPeer* peer)
{
jobject parentView = static_cast<jobject> (peer->getNativeHandle());
return getEnv()->CallObjectMethod (parentView, ComponentPeerView.createGLView);
}
//==============================================================================
bool Desktop::canUseSemiTransparentWindows() noexcept
{
@ -700,6 +702,8 @@ int JUCE_CALLTYPE NativeMessageBox::showYesNoCancelBox (AlertWindow::AlertIconTy
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, alertDismissed, void, (JNIEnv* env, jobject activity,
jlong callbackAsLong, jint result))
{
setEnv (env);
if (ModalComponentManager::Callback* callback = (ModalComponentManager::Callback*) callbackAsLong)
{
callback->modalStateFinished (result);
@ -748,6 +752,8 @@ JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, setScreenSize, void, (JNIEnv
jint screenWidth, jint screenHeight,
jint dpi))
{
setEnv (env);
android.screenWidth = screenWidth;
android.screenHeight = screenHeight;
android.dpi = dpi;

View file

@ -34,10 +34,6 @@
#define JUCE_OPENGL_ES 1
#endif
#if ! JUCE_ANDROID
#define JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD 1
#endif
#if JUCE_WINDOWS
#ifndef APIENTRY
#define APIENTRY __stdcall
@ -75,7 +71,10 @@
#include <OpenGL/glext.h>
#endif
#elif JUCE_ANDROID
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <GLES2/gl2.h>
#include <EGL/egl.h>
#endif
#if GL_ES_VERSION_3_0

View file

@ -22,14 +22,21 @@
==============================================================================
*/
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (getParent, "getParent", "()Landroid/view/ViewParent;") \
METHOD (layout, "layout", "(IIII)V" ) \
METHOD (requestRender, "requestRender", "()V") \
METHOD (getNativeSurface, "getNativeSurface", "()Landroid/view/Surface;") \
DECLARE_JNI_CLASS (OpenGLView, JUCE_ANDROID_ACTIVITY_CLASSPATH "$OpenGLView");
DECLARE_JNI_CLASS (NativeSurfaceView, JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView")
#undef JNI_CLASS_MEMBERS
extern jobject createOpenGLView (ComponentPeer*);
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (addView, "addView", "(Landroid/view/View;)V") \
METHOD (removeView, "removeView", "(Landroid/view/View;)V") \
DECLARE_JNI_CLASS (AndroidViewGroup, "android/view/ViewGroup")
#undef JNI_CLASS_MEMBERS
//==============================================================================
class OpenGLContext::NativeContext
@ -41,150 +48,259 @@ public:
bool /*useMultisampling*/,
OpenGLVersion)
: component (comp),
isInsideGLCallback (false)
hasInitialised (false),
juceContext (nullptr), surface (EGL_NO_SURFACE), context (EGL_NO_CONTEXT)
{
{
const ScopedLock sl (contextListLock);
glView = GlobalRef (createOpenGLView (component.getPeer()));
contextList.add (this);
}
JNIEnv* env = getEnv();
// Do we have a native peer that we can attach to?
if (component.getPeer()->getNativeHandle() == nullptr)
return;
// Initialise the EGL display
if (! initEGLDisplay())
return;
// create a native surface view
surfaceView = GlobalRef (env->CallObjectMethod (android.activity.get(),
JuceAppActivity.createNativeSurfaceView,
reinterpret_cast<jlong> (this)));
if (surfaceView.get() == nullptr)
return;
// add the view to the view hierachy
// after this the nativecontext can receive callbacks
env->CallVoidMethod ((jobject) component.getPeer()->getNativeHandle(),
AndroidViewGroup.addView, surfaceView.get());
// initialise the geometry of the view
Rectangle<int> bounds = component.getTopLevelComponent()
->getLocalArea (&component, component.getLocalBounds());
bounds *= component.getDesktopScaleFactor();
updateWindowPosition (component.getTopLevelComponent()
->getLocalArea (&component, component.getLocalBounds()));
updateWindowPosition (bounds);
hasInitialised = true;
}
~NativeContext()
{
JNIEnv* env = getEnv();
if (jobject viewParent = env->CallObjectMethod (surfaceView.get(), NativeSurfaceView.getParent))
env->CallVoidMethod (viewParent, AndroidViewGroup.removeView, surfaceView.get());
}
//==============================================================================
void initialiseOnRenderThread (OpenGLContext& aContext)
{
const ScopedLock sl (contextListLock);
contextList.removeFirstMatchingValue (this);
jassert (hasInitialised);
// has the context already attached?
jassert (surface == EGL_NO_SURFACE && context == EGL_NO_CONTEXT);
JNIEnv* env = getEnv();
// get a pointer to the native window
ANativeWindow* window = nullptr;
if (jobject jSurface = env->CallObjectMethod (surfaceView.get(), NativeSurfaceView.getNativeSurface))
window = ANativeWindow_fromSurface (env, jSurface);
jassert (window != nullptr);
// create the surface
surface = eglCreateWindowSurface(display, config, window, 0);
jassert (surface != EGL_NO_SURFACE);
ANativeWindow_release (window);
// create the OpenGL context
EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
context = eglCreateContext(display, config, EGL_NO_CONTEXT, contextAttribs);
jassert (context != EGL_NO_CONTEXT);
juceContext = &aContext;
}
android.activity.callVoidMethod (JuceAppActivity.deleteOpenGLView, glView.get());
glView.clear();
void shutdownOnRenderThread()
{
jassert (hasInitialised);
// is there a context available to detach?
jassert (surface != EGL_NO_SURFACE && context != EGL_NO_CONTEXT);
eglDestroyContext (display, context);
context = EGL_NO_CONTEXT;
eglDestroySurface (display, surface);
surface = EGL_NO_SURFACE;
}
void initialiseOnRenderThread (OpenGLContext&) {}
void shutdownOnRenderThread() {}
//==============================================================================
bool makeActive() const noexcept
{
if (! hasInitialised)
return false;
bool makeActive() const noexcept { return isInsideGLCallback; }
bool isActive() const noexcept { return isInsideGLCallback; }
static void deactivateCurrentContext() {}
if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT)
return false;
void swapBuffers() const noexcept {}
if (! eglMakeCurrent (display, surface, surface, context))
return false;
return true;
}
bool isActive() const noexcept { return eglGetCurrentContext() == context; }
static void deactivateCurrentContext()
{
eglMakeCurrent (display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
//==============================================================================
void swapBuffers() const noexcept { eglSwapBuffers (display, surface); }
bool setSwapInterval (const int) { return false; }
int getSwapInterval() const { return 0; }
bool createdOk() const noexcept { return getRawContext() != nullptr; }
void* getRawContext() const noexcept { return glView.get(); }
//==============================================================================
bool createdOk() const noexcept { return hasInitialised; }
void* getRawContext() const noexcept { return surfaceView.get(); }
GLuint getFrameBufferID() const noexcept { return 0; }
//==============================================================================
void updateWindowPosition (const Rectangle<int>& bounds)
{
if (lastBounds != bounds)
{
lastBounds = bounds;
JNIEnv* env = getEnv();
lastBounds = bounds;
Rectangle<int> r = bounds * Desktop::getInstance().getDisplays().getMainDisplay().scale;
glView.callVoidMethod (OpenGLView.layout,
r.getX(), r.getY(),
r.getRight(), r.getBottom());
env->CallVoidMethod (surfaceView.get(), NativeSurfaceView.layout,
(jint) r.getX(), (jint) r.getY(), (jint) r.getRight(), (jint) r.getBottom());
}
}
void triggerRepaint()
{
glView.callVoidMethod (OpenGLView.requestRender);
}
//==============================================================================
void contextCreatedCallback();
void contextChangedSize() {}
void renderCallback();
// Android Surface Callbacks:
void dispatchDraw (jobject canvas)
{
ignoreUnused (canvas);
if (juceContext != nullptr)
juceContext->triggerRepaint();
}
void surfaceChanged (jobject holder, int format, int width, int height)
{
ignoreUnused (holder, format, width, height);
}
void surfaceCreated (jobject holder);
void surfaceDestroyed (jobject holder);
//==============================================================================
static NativeContext* findContextFor (JNIEnv* env, jobject glView)
{
const ScopedLock sl (contextListLock);
for (int i = contextList.size(); --i >= 0;)
{
NativeContext* const c = contextList.getUnchecked(i);
if (env->IsSameObject (c->glView.get(), glView))
return c;
}
return nullptr;
}
static NativeContext* getActiveContext() noexcept
{
const ScopedLock sl (contextListLock);
for (int i = contextList.size(); --i >= 0;)
{
NativeContext* const c = contextList.getUnchecked(i);
if (c->isInsideGLCallback)
return c;
}
return nullptr;
}
struct Locker { Locker (NativeContext&) {} };
Component& component;
private:
GlobalRef glView;
Rectangle<int> lastBounds;
bool isInsideGLCallback;
//==============================================================================
bool initEGLDisplay()
{
// already initialised?
if (display != EGL_NO_DISPLAY)
return true;
typedef Array<NativeContext*> ContextArray;
static CriticalSection contextListLock;
static ContextArray contextList;
const EGLint attribs[] =
{
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_ALPHA_SIZE, 0,
EGL_DEPTH_SIZE, 16,
EGL_NONE
};
EGLint numConfigs;
if ((display = eglGetDisplay (EGL_DEFAULT_DISPLAY)) == EGL_NO_DISPLAY)
{
jassertfalse;
return false;
}
if (! eglInitialize (display, 0, 0))
{
jassertfalse;
return false;
}
if (! eglChooseConfig (display, attribs, &config, 1, &numConfigs))
{
eglTerminate (display);
jassertfalse;
return false;
}
return true;
}
//==============================================================================
bool hasInitialised, hasBeenAddedToViewHierachy;
GlobalRef surfaceView;
Rectangle<int> lastBounds;
OpenGLContext* juceContext;
EGLSurface surface;
EGLContext context;
static EGLDisplay display;
static EGLConfig config;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeContext)
};
CriticalSection OpenGLContext::NativeContext::contextListLock;
OpenGLContext::NativeContext::ContextArray OpenGLContext::NativeContext::contextList;
//==============================================================================
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), dispatchDrawNative,
void, (JNIEnv* env, jobject nativeView, jlong host, jobject canvas))
{
ignoreUnused (nativeView);
setEnv (env);
reinterpret_cast<OpenGLContext::NativeContext*> (host)->dispatchDraw (canvas);
}
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceChangedNative,
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder, jint format, jint width, jint height))
{
ignoreUnused (nativeView);
setEnv (env);
reinterpret_cast<OpenGLContext::NativeContext*> (host)->surfaceChanged (holder, format, width, height);
}
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceCreatedNative,
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder))
{
ignoreUnused (nativeView);
setEnv (env);
reinterpret_cast<OpenGLContext::NativeContext*> (host)->surfaceCreated (holder);
}
JUCE_JNI_CALLBACK (JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024NativeSurfaceView), surfaceDestroyedNative,
void, (JNIEnv* env, jobject nativeView, jlong host, jobject holder))
{
ignoreUnused (nativeView);
setEnv (env);
reinterpret_cast<OpenGLContext::NativeContext*> (host)->surfaceDestroyed (holder);
}
//==============================================================================
bool OpenGLHelpers::isContextActive()
{
return OpenGLContext::NativeContext::getActiveContext() != nullptr;
}
//==============================================================================
#define GL_VIEW_CLASS_NAME JUCE_JOIN_MACRO (JUCE_ANDROID_ACTIVITY_CLASSNAME, _00024OpenGLView)
JUCE_JNI_CALLBACK (GL_VIEW_CLASS_NAME, contextCreated, void, (JNIEnv* env, jobject view))
{
threadLocalJNIEnvHolder.removeCurrentThreadFromCache();
threadLocalJNIEnvHolder.getOrAttach();
if (OpenGLContext::NativeContext* const context = OpenGLContext::NativeContext::findContextFor (env, view))
context->contextCreatedCallback();
else
jassertfalse;
}
JUCE_JNI_CALLBACK (GL_VIEW_CLASS_NAME, contextChangedSize, void, (JNIEnv* env, jobject view))
{
if (OpenGLContext::NativeContext* const context = OpenGLContext::NativeContext::findContextFor (env, view))
context->contextChangedSize();
}
JUCE_JNI_CALLBACK (GL_VIEW_CLASS_NAME, render, void, (JNIEnv* env, jobject view))
{
if (OpenGLContext::NativeContext* const context = OpenGLContext::NativeContext::findContextFor (env, view))
context->renderCallback();
return eglGetCurrentContext() != EGL_NO_CONTEXT;
}

View file

@ -22,18 +22,53 @@
==============================================================================
*/
class OpenGLContext::CachedImage : public CachedComponentImage
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
, private Thread
#if JUCE_IOS
struct AppInactivityCallback // NB: this is a duplicate of an internal declaration in juce_core
{
virtual ~AppInactivityCallback() {}
virtual void appBecomingInactive() = 0;
};
extern Array<AppInactivityCallback*> appBecomingInactiveCallbacks;
// On iOS, all GL calls will crash when the app is running in the background, so
// this prevents them from happening (which some messy locking behaviour)
struct iOSBackgroundProcessCheck : public AppInactivityCallback
{
iOSBackgroundProcessCheck() { isBackgroundProcess(); appBecomingInactiveCallbacks.add (this); }
~iOSBackgroundProcessCheck() { appBecomingInactiveCallbacks.removeAllInstancesOf (this); }
bool isBackgroundProcess()
{
const bool b = Process::isForegroundProcess();
isForeground.set (b ? 1 : 0);
return ! b;
}
void appBecomingInactive() override
{
int counter = 2000;
while (--counter > 0 && isForeground.get() != 0)
Thread::sleep (1);
}
private:
Atomic<int> isForeground;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (iOSBackgroundProcessCheck)
};
#endif
//==============================================================================
class OpenGLContext::CachedImage : public CachedComponentImage,
private ThreadPoolJob
{
public:
CachedImage (OpenGLContext& c, Component& comp,
const OpenGLPixelFormat& pixFormat, void* contextToShare)
:
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
Thread ("OpenGL Rendering"),
#endif
: ThreadPoolJob ("OpenGL Rendering"),
context (c), component (comp),
scale (1.0),
#if JUCE_OPENGL3
@ -61,22 +96,43 @@ public:
stop();
}
//==============================================================================
void start()
{
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
if (nativeContext != nullptr)
startThread (6);
#endif
{
renderThread = new ThreadPool (1);
resume();
}
}
void stop()
{
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
stopThread (10000);
#endif
if (renderThread != nullptr)
{
pause();
renderThread = nullptr;
}
hasInitialised = false;
}
//==============================================================================
void pause()
{
if (renderThread != nullptr)
{
repaintEvent.signal();
renderThread->removeJob (this, true, -1);
}
}
void resume()
{
if (renderThread != nullptr)
renderThread->addJob (this, false);
}
//==============================================================================
void paint (Graphics&) override {}
@ -99,13 +155,7 @@ public:
void triggerRepaint()
{
needsUpdate = 1;
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
notify();
#else
if (nativeContext != nullptr)
nativeContext->triggerRepaint();
#endif
repaintEvent.signal();
}
//==============================================================================
@ -156,19 +206,9 @@ public:
{
// This avoids hogging the message thread when doing intensive rendering.
if (lastMMLockReleaseTime + 1 >= Time::getMillisecondCounter())
{
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
wait (2);
#else
Thread::sleep (2);
#endif
}
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
mmLock = new MessageManagerLock (this); // need to acquire this before locking the context.
#else
mmLock = new MessageManagerLock (Thread::getCurrentThread());
#endif
if (! mmLock->lockWasGained())
return false;
@ -359,43 +399,44 @@ public:
}
//==============================================================================
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
void run() override
JobStatus runJob() override
{
{
// Allow the message thread to finish setting-up the context before using it..
MessageManagerLock mml (this);
if (! mml.lockWasGained())
return;
return ThreadPoolJob::jobHasFinished;
}
initialiseOnThread();
hasInitialised = true;
while (! threadShouldExit())
while (! shouldExit())
{
#if JUCE_IOS
// NB: on iOS, all GL calls will crash when the app is running
// in the background..
if (! Process::isForegroundProcess())
if (backgroundProcessCheck.isBackgroundProcess())
{
wait (500);
repaintEvent.wait (300);
continue;
}
#endif
if (shouldExit())
break;
if (! renderFrame())
wait (5); // failed to render, so avoid a tight fail-loop.
else if (! context.continuousRepaint)
wait (-1);
repaintEvent.wait (5); // failed to render, so avoid a tight fail-loop.
else if (! context.continuousRepaint && ! shouldExit())
repaintEvent.wait (-1);
}
hasInitialised = false;
context.makeActive();
shutdownOnThread();
OpenGLContext::deactivateCurrentContext();
return ThreadPoolJob::jobHasFinished;
}
#endif
void initialiseOnThread()
{
@ -407,6 +448,12 @@ public:
context.makeActive();
nativeContext->initialiseOnRenderThread (context);
#if JUCE_ANDROID
// On android the context may be created in initialiseOnRenderThread
// and we therefore need to call makeActive again
context.makeActive();
#endif
#if JUCE_OPENGL3
if (OpenGLShaderProgram::getLanguageVersion() > 1.2)
{
@ -467,11 +514,17 @@ public:
StringArray associatedObjectNames;
ReferenceCountedArray<ReferenceCountedObject> associatedObjects;
WaitableEvent canPaintNowFlag, finishedPaintingFlag;
WaitableEvent canPaintNowFlag, finishedPaintingFlag, repaintEvent;
bool shadersAvailable, hasInitialised;
Atomic<int> needsUpdate;
uint32 lastMMLockReleaseTime;
ScopedPointer<ThreadPool> renderThread;
#if JUCE_IOS
iOSBackgroundProcessCheck backgroundProcessCheck;
#endif
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CachedImage)
};
@ -967,32 +1020,33 @@ void OpenGLContext::copyTexture (const Rectangle<int>& targetClipArea,
JUCE_CHECK_OPENGL_ERROR
}
//==============================================================================
#if JUCE_ANDROID
void OpenGLContext::NativeContext::contextCreatedCallback()
EGLDisplay OpenGLContext::NativeContext::display = EGL_NO_DISPLAY;
EGLDisplay OpenGLContext::NativeContext::config;
void OpenGLContext::NativeContext::surfaceCreated (jobject holder)
{
isInsideGLCallback = true;
ignoreUnused (holder);
if (CachedImage* const c = CachedImage::get (component))
c->initialiseOnThread();
else
jassertfalse;
if (juceContext != nullptr)
{
if (OpenGLContext::CachedImage* cachedImage = juceContext->getCachedImage())
cachedImage->resume();
isInsideGLCallback = false;
juceContext->triggerRepaint();
}
}
void OpenGLContext::NativeContext::renderCallback()
void OpenGLContext::NativeContext::surfaceDestroyed (jobject holder)
{
isInsideGLCallback = true;
ignoreUnused (holder);
// unlike the name suggets this will be called just before the
// surface is destroyed. We need to pause the render thread.
if (CachedImage* const c = CachedImage::get (component))
if (juceContext != nullptr)
{
if (c->context.continuousRepaint)
c->context.triggerRepaint();
c->renderFrame();
if (OpenGLContext::CachedImage* cachedImage = juceContext->getCachedImage())
cachedImage->pause();
}
isInsideGLCallback = false;
}
#endif