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:
parent
02041328dc
commit
89ba69ab29
45 changed files with 4088 additions and 669 deletions
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -520,6 +520,10 @@ MainAppWindow::MainAppWindow()
|
|||
taskbarIcon = new DemoTaskbarComponent();
|
||||
#endif
|
||||
|
||||
#if JUCE_ANDROID
|
||||
setOpenGLRenderingEngine();
|
||||
#endif
|
||||
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
|
|
|
|||
146
examples/MidiTest/MidiTest.jucer
Normal file
146
examples/MidiTest/MidiTest.jucer
Normal 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>
|
||||
120
examples/MidiTest/Source/Main.cpp
Normal file
120
examples/MidiTest/Source/Main.cpp
Normal 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)
|
||||
457
examples/MidiTest/Source/MainComponent.cpp
Normal file
457
examples/MidiTest/Source/MainComponent.cpp
Normal 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];
|
||||
}
|
||||
99
examples/MidiTest/Source/MainComponent.h
Normal file
99
examples/MidiTest/Source/MainComponent.h
Normal 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
|
||||
5
examples/MidiTest/Source/Resources/Bluetooth.svg
Normal file
5
examples/MidiTest/Source/Resources/Bluetooth.svg
Normal 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 |
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -635,8 +635,8 @@ bool ProjectExporter::ConstConfigIterator::next()
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
ProjectExporter::BuildConfiguration::BuildConfiguration (Project& p, const ValueTree& configNode)
|
||||
: config (configNode), project (p)
|
||||
ProjectExporter::BuildConfiguration::BuildConfiguration (Project& p, const ValueTree& configNode, const ProjectExporter& e)
|
||||
: config (configNode), project (p), exporter (e)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -686,14 +686,13 @@ void ProjectExporter::BuildConfiguration::addGCCOptimisationProperty (PropertyLi
|
|||
|
||||
void ProjectExporter::BuildConfiguration::createPropertyEditors (PropertyListBuilder& props)
|
||||
{
|
||||
props.add (new TextPropertyComponent (getNameValue(), "Name", 96, false),
|
||||
"The name of this configuration.");
|
||||
if (exporter.supportsUserDefinedConfigurations())
|
||||
props.add (new TextPropertyComponent (getNameValue(), "Name", 96, false),
|
||||
"The name of this configuration.");
|
||||
|
||||
props.add (new BooleanPropertyComponent (isDebugValue(), "Debug mode", "Debugging enabled"),
|
||||
"If enabled, this means that the configuration should be built with debug synbols.");
|
||||
|
||||
// addGCCOptimisationProperty (props);
|
||||
|
||||
props.add (new TextPropertyComponent (getTargetBinaryName(), "Binary name", 256, false),
|
||||
"The filename to use for the destination binary executable file. If you don't add a suffix to this name, "
|
||||
"a suitable platform-specific suffix will be added automatically.");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
MidiInput* MidiInput::openDevice (int index, MidiInputCallback* callback)
|
||||
void MidiInput::start()
|
||||
{
|
||||
return nullptr;
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,26 +511,27 @@ private:
|
|||
|
||||
while (numSamples > 0)
|
||||
{
|
||||
int16* const destBuffer = bufferList.waitForFreeBuffer (thread);
|
||||
|
||||
if (destBuffer == nullptr)
|
||||
break;
|
||||
|
||||
for (int i = 0; i < bufferList.numChannels; ++i)
|
||||
if (int16* const destBuffer = bufferList.waitForFreeBuffer (thread))
|
||||
{
|
||||
typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> DstSampleType;
|
||||
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SrcSampleType;
|
||||
for (int i = 0; i < bufferList.numChannels; ++i)
|
||||
{
|
||||
typedef AudioData::Pointer<AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> DstSampleType;
|
||||
typedef AudioData::Pointer<AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SrcSampleType;
|
||||
|
||||
DstSampleType dstData (destBuffer + i, bufferList.numChannels);
|
||||
SrcSampleType srcData (buffer.getReadPointer (i, offset));
|
||||
dstData.convertSamples (srcData, bufferList.numSamples);
|
||||
DstSampleType dstData (destBuffer + i, bufferList.numChannels);
|
||||
SrcSampleType srcData (buffer.getReadPointer (i, offset));
|
||||
dstData.convertSamples (srcData, bufferList.numSamples);
|
||||
}
|
||||
|
||||
enqueueBuffer (destBuffer);
|
||||
|
||||
numSamples -= bufferList.numSamples;
|
||||
offset += bufferList.numSamples;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
check ((*playerBufferQueue)->Enqueue (playerBufferQueue, destBuffer, bufferList.getBufferSizeBytes()));
|
||||
bufferList.bufferSent();
|
||||
|
||||
numSamples -= bufferList.numSamples;
|
||||
offset += bufferList.numSamples;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -428,10 +542,16 @@ private:
|
|||
|
||||
BufferList bufferList;
|
||||
|
||||
static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context)
|
||||
void enqueueBuffer (int16* buffer) noexcept
|
||||
{
|
||||
jassert (queue == static_cast <Player*> (context)->playerBufferQueue); (void) queue;
|
||||
static_cast <Player*> (context)->bufferList.bufferReturned();
|
||||
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();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Player)
|
||||
|
|
@ -440,13 +560,11 @@ private:
|
|||
//==================================================================================================
|
||||
struct Recorder
|
||||
{
|
||||
Recorder (int numChannels, int sampleRate, Engine& engine)
|
||||
Recorder (int numChannels, int sampleRate, Engine& engine, const int numBuffers, const int numSamples)
|
||||
: recorderObject (nullptr), recorderRecord (nullptr),
|
||||
recorderBufferQueue (nullptr), configObject (nullptr),
|
||||
bufferList (numChannels)
|
||||
bufferList (numChannels, numBuffers, numSamples)
|
||||
{
|
||||
jassert (numChannels == 1); // STEREO doesn't always work!!
|
||||
|
||||
SLDataFormat_PCM pcmFormat =
|
||||
{
|
||||
SL_DATAFORMAT_PCM,
|
||||
|
|
@ -461,7 +579,8 @@ private:
|
|||
SLDataLocator_IODevice ioDevice = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, nullptr };
|
||||
SLDataSource audioSrc = { &ioDevice, nullptr };
|
||||
|
||||
SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, bufferList.numBuffers };
|
||||
SLDataLocator_AndroidSimpleBufferQueue bufferQueue = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,
|
||||
static_cast<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,25 +636,27 @@ private:
|
|||
|
||||
while (numSamples > 0)
|
||||
{
|
||||
int16* const srcBuffer = bufferList.waitForFreeBuffer (thread);
|
||||
|
||||
if (srcBuffer == nullptr)
|
||||
break;
|
||||
|
||||
for (int i = 0; i < bufferList.numChannels; ++i)
|
||||
if (int16* const srcBuffer = bufferList.waitForFreeBuffer (thread))
|
||||
{
|
||||
typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DstSampleType;
|
||||
typedef AudioData::Pointer <AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::Const> SrcSampleType;
|
||||
for (int i = 0; i < bufferList.numChannels; ++i)
|
||||
{
|
||||
typedef AudioData::Pointer<AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DstSampleType;
|
||||
typedef AudioData::Pointer<AudioData::Int16, AudioData::LittleEndian, AudioData::Interleaved, AudioData::Const> SrcSampleType;
|
||||
|
||||
DstSampleType dstData (buffer.getWritePointer (i, offset));
|
||||
SrcSampleType srcData (srcBuffer + i, bufferList.numChannels);
|
||||
dstData.convertSamples (srcData, bufferList.numSamples);
|
||||
DstSampleType dstData (buffer.getWritePointer (i, offset));
|
||||
SrcSampleType srcData (srcBuffer + i, bufferList.numChannels);
|
||||
dstData.convertSamples (srcData, bufferList.numSamples);
|
||||
}
|
||||
|
||||
enqueueBuffer (srcBuffer);
|
||||
|
||||
numSamples -= bufferList.numSamples;
|
||||
offset += bufferList.numSamples;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
enqueueBuffer (srcBuffer);
|
||||
|
||||
numSamples -= bufferList.numSamples;
|
||||
offset += bufferList.numSamples;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -558,16 +677,16 @@ private:
|
|||
|
||||
BufferList bufferList;
|
||||
|
||||
void enqueueBuffer (int16* buffer)
|
||||
void enqueueBuffer (int16* buffer) noexcept
|
||||
{
|
||||
check ((*recorderBufferQueue)->Enqueue (recorderBufferQueue, buffer, bufferList.getBufferSizeBytes()));
|
||||
bufferList.bufferSent();
|
||||
}
|
||||
|
||||
static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context)
|
||||
static void staticCallback (SLAndroidSimpleBufferQueueItf queue, void* context) noexcept
|
||||
{
|
||||
jassert (queue == static_cast <Recorder*> (context)->recorderBufferQueue); (void) queue;
|
||||
static_cast <Recorder*> (context)->bufferList.bufferReturned();
|
||||
jassert (queue == static_cast<Recorder*> (context)->recorderBufferQueue); (void) queue;
|
||||
static_cast<Recorder*> (context)->bufferList.bufferReturned();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Recorder)
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
835
modules/juce_core/native/java/AndroidMidi.java
Normal file
835
modules/juce_core/native/java/AndroidMidi.java
Normal 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;
|
||||
}
|
||||
81
modules/juce_core/native/java/AndroidMidiFallback.java
Normal file
81
modules/juce_core/native/java/AndroidMidiFallback.java
Normal 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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") \
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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, ¶m) == 0;
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (Pimpl)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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, ¶m) == 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
|
||||
|
|
|
|||
|
|
@ -149,20 +149,44 @@
|
|||
#endif
|
||||
|
||||
//==============================================================================
|
||||
#ifndef DOXYGEN
|
||||
namespace juce
|
||||
{
|
||||
template <bool b> struct JuceStaticAssert;
|
||||
template <> struct JuceStaticAssert<true> { static void dummy() {} };
|
||||
}
|
||||
#if ! DOXYGEN
|
||||
#define JUCE_JOIN_MACRO_HELPER(a, b) a ## b
|
||||
#define JUCE_STRINGIFY_MACRO_HELPER(a) #a
|
||||
#endif
|
||||
|
||||
/** 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!)
|
||||
/** 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 static_jassert(expression) juce::JuceStaticAssert<expression>::dummy();
|
||||
#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
|
||||
{
|
||||
template <bool b> struct JuceStaticAssert;
|
||||
template <> struct JuceStaticAssert<true> { static void dummy() {} };
|
||||
}
|
||||
#endif
|
||||
|
||||
/** 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) 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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,14 +22,21 @@
|
|||
==============================================================================
|
||||
*/
|
||||
|
||||
//==============================================================================
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
|
||||
METHOD (layout, "layout", "(IIII)V") \
|
||||
METHOD (requestRender, "requestRender", "()V") \
|
||||
METHOD (getParent, "getParent", "()Landroid/view/ViewParent;") \
|
||||
METHOD (layout, "layout", "(IIII)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()
|
||||
{
|
||||
{
|
||||
const ScopedLock sl (contextListLock);
|
||||
contextList.removeFirstMatchingValue (this);
|
||||
}
|
||||
JNIEnv* env = getEnv();
|
||||
|
||||
android.activity.callVoidMethod (JuceAppActivity.deleteOpenGLView, glView.get());
|
||||
glView.clear();
|
||||
if (jobject viewParent = env->CallObjectMethod (surfaceView.get(), NativeSurfaceView.getParent))
|
||||
env->CallVoidMethod (viewParent, AndroidViewGroup.removeView, surfaceView.get());
|
||||
}
|
||||
|
||||
void initialiseOnRenderThread (OpenGLContext&) {}
|
||||
void shutdownOnRenderThread() {}
|
||||
//==============================================================================
|
||||
void initialiseOnRenderThread (OpenGLContext& aContext)
|
||||
{
|
||||
jassert (hasInitialised);
|
||||
|
||||
bool makeActive() const noexcept { return isInsideGLCallback; }
|
||||
bool isActive() const noexcept { return isInsideGLCallback; }
|
||||
static void deactivateCurrentContext() {}
|
||||
// has the context already attached?
|
||||
jassert (surface == EGL_NO_SURFACE && context == EGL_NO_CONTEXT);
|
||||
|
||||
void swapBuffers() const noexcept {}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool makeActive() const noexcept
|
||||
{
|
||||
if (! hasInitialised)
|
||||
return false;
|
||||
|
||||
if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT)
|
||||
return false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,18 +22,53 @@
|
|||
==============================================================================
|
||||
*/
|
||||
|
||||
class OpenGLContext::CachedImage : public CachedComponentImage
|
||||
#if JUCE_OPENGL_CREATE_JUCE_RENDER_THREAD
|
||||
, private Thread
|
||||
#endif
|
||||
#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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue