diff --git a/Documents/discoDSP/OB-Xd/Themes/Ilkka Rosma Dark/coords.xml b/Documents/discoDSP/OB-Xd/Themes/Ilkka Rosma Dark/coords.xml
index 61738dc..cf02ced 100644
--- a/Documents/discoDSP/OB-Xd/Themes/Ilkka Rosma Dark/coords.xml
+++ b/Documents/discoDSP/OB-Xd/Themes/Ilkka Rosma Dark/coords.xml
@@ -6,8 +6,8 @@
-
-
+
+
@@ -27,15 +27,15 @@
-
+
-
+
-
+
@@ -52,9 +52,9 @@
-
-
-
+
+
+
@@ -82,7 +82,7 @@
-
+
@@ -103,14 +103,14 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/Documents/discoDSP/OB-Xd/Themes/Rin Elyran Classic SEM/coords.xml b/Documents/discoDSP/OB-Xd/Themes/Rin Elyran Classic SEM/coords.xml
index d755bc3..2212aee 100755
--- a/Documents/discoDSP/OB-Xd/Themes/Rin Elyran Classic SEM/coords.xml
+++ b/Documents/discoDSP/OB-Xd/Themes/Rin Elyran Classic SEM/coords.xml
@@ -6,8 +6,8 @@
-
-
+
+
@@ -27,15 +27,15 @@
-
+
-
+
-
+
@@ -46,9 +46,9 @@
-
-
-
+
+
+
@@ -77,7 +77,7 @@
-
+
@@ -105,13 +105,13 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/Documents/discoDSP/OB-Xd/Themes/discoDSP Blue/coords.xml b/Documents/discoDSP/OB-Xd/Themes/discoDSP Blue/coords.xml
index 7d79607..2301fe5 100755
--- a/Documents/discoDSP/OB-Xd/Themes/discoDSP Blue/coords.xml
+++ b/Documents/discoDSP/OB-Xd/Themes/discoDSP Blue/coords.xml
@@ -11,8 +11,8 @@
-
-
+
+
@@ -28,7 +28,7 @@
-
+
@@ -45,9 +45,9 @@
-
+
-
+
@@ -58,9 +58,9 @@
-
-
-
+
+
+
@@ -97,18 +97,18 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Documents/discoDSP/OB-Xd/Themes/discoDSP Classic/coords.xml b/Documents/discoDSP/OB-Xd/Themes/discoDSP Classic/coords.xml
index b007c76..286e0be 100755
--- a/Documents/discoDSP/OB-Xd/Themes/discoDSP Classic/coords.xml
+++ b/Documents/discoDSP/OB-Xd/Themes/discoDSP Classic/coords.xml
@@ -6,8 +6,8 @@
-
-
+
+
@@ -27,15 +27,15 @@
-
+
-
+
-
+
@@ -46,9 +46,9 @@
-
-
-
+
+
+
@@ -77,7 +77,7 @@
-
+
@@ -105,13 +105,13 @@
-
-
-
-
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/Documents/discoDSP/OB-Xd/Themes/discoDSP Grey/coords.xml b/Documents/discoDSP/OB-Xd/Themes/discoDSP Grey/coords.xml
index 7d79607..2301fe5 100755
--- a/Documents/discoDSP/OB-Xd/Themes/discoDSP Grey/coords.xml
+++ b/Documents/discoDSP/OB-Xd/Themes/discoDSP Grey/coords.xml
@@ -11,8 +11,8 @@
-
-
+
+
@@ -28,7 +28,7 @@
-
+
@@ -45,9 +45,9 @@
-
+
-
+
@@ -58,9 +58,9 @@
-
-
-
+
+
+
@@ -97,18 +97,18 @@
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h b/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h
index 716c570..bbcd089 100644
--- a/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h
+++ b/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h
@@ -113,7 +113,7 @@ public:
startTimer (500);
}
- virtual ~StandalonePluginHolder() override
+ ~StandalonePluginHolder() override
{
stopTimer();
@@ -128,13 +128,7 @@ public:
processor->disableNonMainBuses();
processor->setRateAndBufferSizeDetails (44100, 512);
- int inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns
- : processor->getMainBusNumInputChannels());
-
- int outChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts
- : processor->getMainBusNumOutputChannels());
-
- processorHasPotentialFeedbackLoop = (inChannels > 0 && outChannels > 0);
+ processorHasPotentialFeedbackLoop = (getNumInputChannels() > 0 && getNumOutputChannels() > 0);
}
virtual void deletePlugin()
@@ -143,6 +137,24 @@ public:
processor = nullptr;
}
+ int getNumInputChannels() const
+ {
+ if (processor == nullptr)
+ return 0;
+
+ return (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns
+ : processor->getMainBusNumInputChannels());
+ }
+
+ int getNumOutputChannels() const
+ {
+ if (processor == nullptr)
+ return 0;
+
+ return (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts
+ : processor->getMainBusNumOutputChannels());
+ }
+
static String getFilePatterns (const String& fileSuffix)
{
if (fileSuffix.isEmpty())
@@ -179,11 +191,18 @@ public:
/** Pops up a dialog letting the user save the processor's state to a file. */
void askUserToSaveState (const String& fileSuffix = String())
{
- #if JUCE_MODAL_LOOPS_PERMITTED
- FileChooser fc (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix));
+ stateFileChooser = std::make_unique (TRANS("Save current state"),
+ getLastFile(),
+ getFilePatterns (fileSuffix));
+ auto flags = FileBrowserComponent::saveMode
+ | FileBrowserComponent::canSelectFiles
+ | FileBrowserComponent::warnAboutOverwriting;
- if (fc.browseForFileToSave (true))
+ stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc)
{
+ if (fc.getResult() == File{})
+ return;
+
setLastFile (fc);
MemoryBlock data;
@@ -193,20 +212,23 @@ public:
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
TRANS("Error whilst saving"),
TRANS("Couldn't write to the specified file!"));
- }
- #else
- ignoreUnused (fileSuffix);
- #endif
+ });
}
/** Pops up a dialog letting the user re-load the processor's state from a file. */
void askUserToLoadState (const String& fileSuffix = String())
{
- #if JUCE_MODAL_LOOPS_PERMITTED
- FileChooser fc (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix));
+ stateFileChooser = std::make_unique (TRANS("Load a saved state"),
+ getLastFile(),
+ getFilePatterns (fileSuffix));
+ auto flags = FileBrowserComponent::openMode
+ | FileBrowserComponent::canSelectFiles;
- if (fc.browseForFileToOpen())
+ stateFileChooser->launchAsync (flags, [this] (const FileChooser& fc)
{
+ if (fc.getResult() == File{})
+ return;
+
setLastFile (fc);
MemoryBlock data;
@@ -217,10 +239,7 @@ public:
AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
TRANS("Error whilst loading"),
TRANS("Couldn't read from the specified file!"));
- }
- #else
- ignoreUnused (fileSuffix);
- #endif
+ });
}
//==============================================================================
@@ -264,15 +283,11 @@ public:
if (auto* bus = processor->getBus (false, 0))
maxNumOutputs = jmax (0, bus->getDefaultLayout().size());
- o.content.setOwned (new SettingsComponent (*this, deviceManager, maxNumInputs, maxNumOutputs));
-
- LookAndFeel_V4 *lfv4 = dynamic_cast(&o.content->getLookAndFeel());
-
- if (lfv4) {
- lfv4->setColour(ResizableWindow::backgroundColourId, Colour::fromHSV (0.0f, 0.0f, 0.1f, 1.0f));
- }
-
- o.content->setSize (500, 400);
+ auto content = std::make_unique (*this, deviceManager, maxNumInputs, maxNumOutputs);
+ content->setSize (500, 550);
+ content->setToRecommendedSize();
+
+ o.content.setOwned (content.release());
o.dialogTitle = TRANS("Audio/MIDI Settings");
o.dialogBackgroundColour = o.content->getLookAndFeel().findColour (ResizableWindow::backgroundColourId);
@@ -312,18 +327,17 @@ public:
#endif
}
- auto totalInChannels = processor->getMainBusNumInputChannels();
- auto totalOutChannels = processor->getMainBusNumOutputChannels();
+ auto inputChannels = getNumInputChannels();
+ auto outputChannels = getNumOutputChannels();
- if (channelConfiguration.size() > 0)
+ if (inputChannels == 0 && outputChannels == 0 && processor->isMidiEffect())
{
- auto defaultConfig = channelConfiguration.getReference (0);
- totalInChannels = defaultConfig.numIns;
- totalOutChannels = defaultConfig.numOuts;
+ // add a dummy output channel for MIDI effect plug-ins so they can receive audio callbacks
+ outputChannels = 1;
}
- deviceManager.initialise (enableAudioInput ? totalInChannels : 0,
- totalOutChannels,
+ deviceManager.initialise (enableAudioInput ? inputChannels : 0,
+ outputChannels,
savedState.get(),
true,
preferredDefaultDeviceName,
@@ -403,7 +417,93 @@ public:
std::unique_ptr options;
Array lastMidiDevices;
+ std::unique_ptr stateFileChooser;
+
private:
+ /* This class can be used to ensure that audio callbacks use buffers with a
+ predictable maximum size.
+
+ On some platforms (such as iOS 10), the expected buffer size reported in
+ audioDeviceAboutToStart may be smaller than the blocks passed to
+ audioDeviceIOCallback. This can lead to out-of-bounds reads if the render
+ callback depends on additional buffers which were initialised using the
+ smaller size.
+
+ As a workaround, this class will ensure that the render callback will
+ only ever be called with a block with a length less than or equal to the
+ expected block size.
+ */
+ class CallbackMaxSizeEnforcer : public AudioIODeviceCallback
+ {
+ public:
+ explicit CallbackMaxSizeEnforcer (AudioIODeviceCallback& callbackIn)
+ : inner (callbackIn) {}
+
+ void audioDeviceAboutToStart (AudioIODevice* device) override
+ {
+ maximumSize = device->getCurrentBufferSizeSamples();
+ storedInputChannels .resize ((size_t) device->getActiveInputChannels() .countNumberOfSetBits());
+ storedOutputChannels.resize ((size_t) device->getActiveOutputChannels().countNumberOfSetBits());
+
+ inner.audioDeviceAboutToStart (device);
+ }
+
+ void audioDeviceIOCallback (const float** inputChannelData,
+ int numInputChannels,
+ float** outputChannelData,
+ int numOutputChannels,
+ int numSamples) override
+ {
+ jassertquiet ((int) storedInputChannels.size() == numInputChannels);
+ jassertquiet ((int) storedOutputChannels.size() == numOutputChannels);
+
+ int position = 0;
+
+ while (position < numSamples)
+ {
+ const auto blockLength = jmin (maximumSize, numSamples - position);
+
+ initChannelPointers (inputChannelData, storedInputChannels, position);
+ initChannelPointers (outputChannelData, storedOutputChannels, position);
+
+ inner.audioDeviceIOCallback (storedInputChannels.data(),
+ (int) storedInputChannels.size(),
+ storedOutputChannels.data(),
+ (int) storedOutputChannels.size(),
+ blockLength);
+
+ position += blockLength;
+ }
+ }
+
+ void audioDeviceStopped() override
+ {
+ inner.audioDeviceStopped();
+ }
+
+ private:
+ struct GetChannelWithOffset
+ {
+ int offset;
+
+ template
+ auto operator() (Ptr ptr) const noexcept -> Ptr { return ptr + offset; }
+ };
+
+ template
+ void initChannelPointers (Ptr&& source, Vector&& target, int offset)
+ {
+ std::transform (source, source + target.size(), target.begin(), GetChannelWithOffset { offset });
+ }
+
+ AudioIODeviceCallback& inner;
+ int maximumSize = 0;
+ std::vector storedInputChannels;
+ std::vector storedOutputChannels;
+ };
+
+ CallbackMaxSizeEnforcer maxSizeEnforcer { *this };
+
//==============================================================================
class SettingsComponent : public Component
{
@@ -414,7 +514,7 @@ private:
int maxAudioOutputChannels)
: owner (pluginHolder),
deviceSelector (deviceManagerToUse,
- 0, 0, // maxAudioInputChannels, // force zero to fully disable input selection
+ 0, maxAudioInputChannels,
0, maxAudioOutputChannels,
true,
(pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()),
@@ -436,17 +536,6 @@ private:
shouldMuteLabel.attachToComponent (&shouldMuteButton, true);
}
-
- for (int i =0; i < deviceSelector.getNumChildComponents(); i ++){
-
- LookAndFeel_V4 *lfv4 = dynamic_cast(&deviceSelector.getChildComponent(i) ->getLookAndFeel());
- if (lfv4) {
- lfv4->setColour(ComboBox::ColourIds::backgroundColourId, Colour::fromHSV (0.0f, 0.0f, 0.2f, 1.0f));
- lfv4->setColour(ListBox::ColourIds::backgroundColourId, Colour::fromHSV (0.0f, 0.0f, 0.2f, 1.0f));
- lfv4->setColour(TextButton::ColourIds::buttonColourId, Colour::fromHSV (0.0f, 0.0f, 0.1f, 1.0f));
-
- }
- }
}
void paint (Graphics& g) override
@@ -456,6 +545,8 @@ private:
void resized() override
{
+ const ScopedValueSetter scope (isResizing, true);
+
auto r = getLocalBounds();
if (owner.getProcessorHasPotentialFeedbackLoop())
@@ -473,12 +564,34 @@ private:
deviceSelector.setBounds (r);
}
+ void childBoundsChanged (Component* childComp) override
+ {
+ if (! isResizing && childComp == &deviceSelector)
+ setToRecommendedSize();
+ }
+
+ void setToRecommendedSize()
+ {
+ const auto extraHeight = [&]
+ {
+ if (! owner.getProcessorHasPotentialFeedbackLoop())
+ return 0;
+
+ const auto itemHeight = deviceSelector.getItemHeight();
+ const auto separatorHeight = (itemHeight >> 1);
+ return itemHeight + separatorHeight;
+ }();
+
+ setSize (getWidth(), deviceSelector.getHeight() + extraHeight);
+ }
+
private:
//==============================================================================
StandalonePluginHolder& owner;
AudioDeviceSelectorComponent deviceSelector;
Label shouldMuteLabel;
ToggleButton shouldMuteButton;
+ bool isResizing = false;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsComponent)
@@ -522,7 +635,7 @@ private:
const String& preferredDefaultDeviceName,
const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions)
{
- deviceManager.addAudioCallback (this);
+ deviceManager.addAudioCallback (&maxSizeEnforcer);
deviceManager.addMidiInputDeviceCallback ({}, &player);
reloadAudioDeviceState (enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions);
@@ -533,7 +646,7 @@ private:
saveAudioDeviceState();
deviceManager.removeMidiInputDeviceCallback ({}, &player);
- deviceManager.removeAudioCallback (this);
+ deviceManager.removeAudioCallback (&maxSizeEnforcer);
}
void timerCallback() override
@@ -568,8 +681,9 @@ private:
@tags{Audio}
*/
class StandaloneFilterWindow : public DocumentWindow,
- public Button::Listener,
+ private Button::Listener,
public MenuBarModel
+
{
public:
//==============================================================================
@@ -588,46 +702,46 @@ public:
const String& preferredDefaultDeviceName = String(),
const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr,
const Array& constrainToConfiguration = {},
- #if JUCE_ANDROID || JUCE_IOS
- bool autoOpenMidiDevices = true
- #else
- bool autoOpenMidiDevices = false
- #endif
- )
+#if JUCE_ANDROID || JUCE_IOS
+ bool autoOpenMidiDevices = true
+#else
+ bool autoOpenMidiDevices = true
+#endif
+ )
#if JUCE_MAC
- : DocumentWindow ("", backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton),
+ : DocumentWindow("", backgroundColour, DocumentWindow::minimiseButton | DocumentWindow::closeButton),
#endif
#if ! JUCE_MAC
- : DocumentWindow (title, Colour::fromHSV (0.0f, 0.0f, 0.1f, 0.5f), DocumentWindow::minimiseButton | DocumentWindow::closeButton),
+ : DocumentWindow(title, juce::Colours::black, DocumentWindow::minimiseButton | DocumentWindow::closeButton),
#endif
-
+
menuBar(this)
- #if ! JUCE_MAC
- , optionsButton ("Settings")
- #endif
+#if ! JUCE_MAC
+ , optionsButton("Settings")
+#endif
{
- #if JUCE_IOS || JUCE_ANDROID
- setTitleBarHeight (0);
- #else
-
- LookAndFeel_V4 *lfv4 = dynamic_cast(&getLookAndFeel());
-if (lfv4) {
-lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widgetBackground, Colour::fromHSV (0.0f, 0.0f, 0.1f, 1.0f));
-}
-
- setTitleBarButtonsRequired (DocumentWindow::minimiseButton | DocumentWindow::closeButton, false);
-
- #if JUCE_MAC
+#if JUCE_IOS || JUCE_ANDROID
+ setTitleBarHeight(0);
+#else
+
+ LookAndFeel_V4* lfv4 = dynamic_cast(&getLookAndFeel());
+ if (lfv4) {
+ lfv4->getCurrentColourScheme().setUIColour(LookAndFeel_V4::ColourScheme::widgetBackground, Colour::fromHSV(0.0f, 0.0f, 0.1f, 1.0f));
+ }
+
+ setTitleBarButtonsRequired(DocumentWindow::minimiseButton | DocumentWindow::closeButton, false);
+
+#if JUCE_MAC
setUsingNativeTitleBar(true);
- menu.addItem (1, TRANS("Audio/MIDI Settings..."));
- MenuBarModel::setMacMainMenu (this, &menu);
- #else
- Component::addAndMakeVisible (optionsButton);
- optionsButton.addListener (this);
- optionsButton.setTriggeredOnMouseDown (true);
- #endif
- #endif
+ menu.addItem(1, TRANS("Audio/MIDI Settings..."));
+ MenuBarModel::setMacMainMenu(this, &menu);
+#else
+ Component::addAndMakeVisible(optionsButton);
+ optionsButton.addListener(this);
+ optionsButton.setTriggeredOnMouseDown(true);
+#endif
+#endif
pluginHolder.reset (new StandalonePluginHolder (settingsToUse, takeOwnershipOfSettings,
preferredDefaultDeviceName, preferredSetupOptions,
@@ -635,32 +749,55 @@ lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widget
#if JUCE_IOS || JUCE_ANDROID
setFullScreen (true);
- setContentOwned (new MainContentComponent (*this), false);
+ updateContent();
#else
- setContentOwned (new MainContentComponent (*this), true);
+ updateContent();
- if (auto* props = pluginHolder->settings.get())
+ const auto windowScreenBounds = [this]() -> Rectangle
{
- const int x = props->getIntValue ("windowX", -100);
- const int y = props->getIntValue ("windowY", -100);
+ const auto width = getWidth();
+ const auto height = getHeight();
- if (x != -100 && y != -100)
- setBoundsConstrained ({ x, y, getWidth(), getHeight() });
- else
- centreWithSize (getWidth(), getHeight());
- }
- else
- {
- centreWithSize (getWidth(), getHeight());
- }
+ const auto& displays = Desktop::getInstance().getDisplays();
+
+ if (auto* props = pluginHolder->settings.get())
+ {
+ constexpr int defaultValue = -100;
+
+ const auto x = props->getIntValue ("windowX", defaultValue);
+ const auto y = props->getIntValue ("windowY", defaultValue);
+
+ if (x != defaultValue && y != defaultValue)
+ {
+ const auto screenLimits = displays.getDisplayForRect ({ x, y, width, height })->userArea;
+
+ return { jlimit (screenLimits.getX(), jmax (screenLimits.getX(), screenLimits.getRight() - width), x),
+ jlimit (screenLimits.getY(), jmax (screenLimits.getY(), screenLimits.getBottom() - height), y),
+ width, height };
+ }
+ }
+
+ const auto displayArea = displays.getPrimaryDisplay()->userArea;
+
+ return { displayArea.getCentreX() - width / 2,
+ displayArea.getCentreY() - height / 2,
+ width, height };
+ }();
+
+ setBoundsConstrained (windowScreenBounds);
+
+ if (auto* processor = getAudioProcessor())
+ if (auto* editor = processor->getActiveEditor())
+ setResizable (editor->isResizable(), false);
#endif
}
~StandaloneFilterWindow() override
{
- #if JUCE_MAC
- setMacMainMenu(nullptr);
- #endif
+ #if JUCE_MAC
+ MenuBarModel::setMacMainMenu(nullptr);
+ #endif
+
#if (! JUCE_IOS) && (! JUCE_ANDROID)
if (auto* props = pluginHolder->settings.get())
{
@@ -689,7 +826,7 @@ lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widget
props->removeValue ("filterState");
pluginHolder->createPlugin();
- setContentOwned (new MainContentComponent (*this), true);
+ updateContent();
pluginHolder->startPlaying();
}
@@ -703,31 +840,31 @@ lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widget
StringArray getMenuBarNames() override
{
-// StringArray menuBarNames;
-// menuBarNames.add("Options");
-// return menuBarNames;
- const char *menuNames[] = { 0 };
-
- return StringArray (menuNames);
+ // StringArray menuBarNames;
+ // menuBarNames.add("Options");
+ // return menuBarNames;
+ const char* menuNames[] = { 0 };
+
+ return StringArray(menuNames);
}
- PopupMenu getMenuForIndex (int topLevelMenuIndex, const String& menuName) override
+ PopupMenu getMenuForIndex(int topLevelMenuIndex, const String& menuName) override
{
PopupMenu m;
-// m.addItem (1, TRANS("Audio Settings..."));
-// m.addSeparator();
+ // m.addItem (1, TRANS("Audio Settings..."));
+ // m.addSeparator();
return m;
}
- void menuItemSelected (int menuItemID, int topLevelMenuIndex) override
+ void menuItemSelected(int menuItemID, int topLevelMenuIndex) override
{
handleMenuResult(menuItemID);
}
- void menuBarActivated (bool isActive) override {};
+ void menuBarActivated(bool isActive) override {};
+
-
void handleMenuResult (int result)
{
@@ -750,9 +887,7 @@ lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widget
void resized() override
{
DocumentWindow::resized();
- #if ! JUCE_MAC
optionsButton.setBounds (8, 6, 60, getTitleBarHeight() - 8);
- #endif
}
virtual StandalonePluginHolder* getPluginHolder() { return pluginHolder.get(); }
@@ -762,20 +897,23 @@ lfv4->getCurrentColourScheme().setUIColour (LookAndFeel_V4::ColourScheme::widget
PopupMenu menu;
private:
+ void updateContent()
+ {
+ auto* content = new MainContentComponent (*this);
+ decoratorConstrainer.setMainContentComponent (content);
+
+ #if JUCE_IOS || JUCE_ANDROID
+ constexpr auto resizeAutomatically = false;
+ #else
+ constexpr auto resizeAutomatically = true;
+ #endif
+
+ setContentOwned (content, resizeAutomatically);
+ }
+
void buttonClicked (Button*) override
{
pluginHolder->showAudioSettingsDialog();
-
- //PopupMenu m;
- //m.addItem (1, TRANS("Audio/MIDI Settings..."));
- //m.addSeparator();
- //m.addItem (2, TRANS("Save current state..."));
- //m.addItem (3, TRANS("Load a saved state..."));
- //m.addSeparator();
- //m.addItem (4, TRANS("Reset to default state"));
-
- //m.showMenuAsync (PopupMenu::Options(),
- // ModalCallbackFunction::forComponent (menuCallback, this));
}
//==============================================================================
@@ -790,7 +928,7 @@ private:
editor (owner.getAudioProcessor()->hasEditor() ? owner.getAudioProcessor()->createEditorIfNeeded()
: new GenericAudioProcessorEditor (*owner.getAudioProcessor()))
{
- Value& inputMutedValue = owner.pluginHolder->getMuteInputValue();
+ inputMutedValue.referTo (owner.pluginHolder->getMuteInputValue());
if (editor != nullptr)
{
@@ -829,9 +967,31 @@ private:
notification.setBounds (r.removeFromTop (NotificationArea::height));
if (editor != nullptr)
- editor->setBounds (editor->getLocalArea (this, r.toFloat())
- .withPosition (r.getTopLeft().toFloat().transformedBy (editor->getTransform().inverted()))
- .toNearestInt());
+ {
+ const auto newPos = r.getTopLeft().toFloat().transformedBy (editor->getTransform().inverted());
+
+ if (preventResizingEditor)
+ editor->setTopLeftPosition (newPos.roundToInt());
+ else
+ editor->setBoundsConstrained (editor->getLocalArea (this, r.toFloat()).withPosition (newPos).toNearestInt());
+ }
+ }
+
+ ComponentBoundsConstrainer* getEditorConstrainer() const
+ {
+ if (auto* e = editor.get())
+ return e->getConstrainer();
+
+ return nullptr;
+ }
+
+ BorderSize computeBorder() const
+ {
+ const auto outer = owner.getContentComponentBorder();
+ return { outer.getTop() + (shouldShowNotification ? NotificationArea::height : 0),
+ outer.getLeft(),
+ outer.getBottom(),
+ outer.getRight() };
}
private:
@@ -846,7 +1006,7 @@ private:
#if JUCE_IOS || JUCE_ANDROID
settingsButton ("Unmute Input")
#else
- settingsButton ("Settings")
+ settingsButton ("Settings...")
#endif
{
setOpaque (true);
@@ -893,10 +1053,9 @@ private:
#else
if (editor != nullptr)
{
- auto rect = getSizeToContainEditor();
-
- setSize (rect.getWidth(),
- rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0));
+ const int extraHeight = shouldShowNotification ? NotificationArea::height : 0;
+ const auto rect = getSizeToContainEditor();
+ setSize (rect.getWidth(), rect.getHeight() + extraHeight);
}
#endif
}
@@ -914,6 +1073,8 @@ private:
//==============================================================================
void componentMovedOrResized (Component&, bool, bool) override
{
+ const ScopedValueSetter scope (preventResizingEditor, true);
+
if (editor != nullptr)
{
auto rect = getSizeToContainEditor();
@@ -935,15 +1096,87 @@ private:
StandaloneFilterWindow& owner;
NotificationArea notification;
std::unique_ptr editor;
+ Value inputMutedValue;
bool shouldShowNotification = false;
+ bool preventResizingEditor = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};
+ /* This custom constrainer checks with the AudioProcessorEditor (which might itself be
+ constrained) to ensure that any size we choose for the standalone window will be suitable
+ for the editor too.
+
+ Without this constrainer, attempting to resize the standalone window may set bounds on the
+ peer that are unsupported by the inner editor. In this scenario, the peer will be set to a
+ 'bad' size, then the inner editor will be resized. The editor will check the new bounds with
+ its own constrainer, and may set itself to a more suitable size. After that, the resizable
+ window will see that its content component has changed size, and set the bounds of the peer
+ accordingly. The end result is that the peer is resized twice in a row to different sizes,
+ which can appear glitchy/flickery to the user.
+ */
+ struct DecoratorConstrainer : public ComponentBoundsConstrainer
+ {
+ void checkBounds (Rectangle& bounds,
+ const Rectangle& previousBounds,
+ const Rectangle& limits,
+ bool isStretchingTop,
+ bool isStretchingLeft,
+ bool isStretchingBottom,
+ bool isStretchingRight) override
+ {
+ auto* decorated = contentComponent != nullptr ? contentComponent->getEditorConstrainer()
+ : nullptr;
+
+ if (decorated != nullptr)
+ {
+ const auto border = contentComponent->computeBorder();
+ const auto requestedBounds = bounds;
+
+ border.subtractFrom (bounds);
+ decorated->checkBounds (bounds,
+ border.subtractedFrom (previousBounds),
+ limits,
+ isStretchingTop,
+ isStretchingLeft,
+ isStretchingBottom,
+ isStretchingRight);
+ border.addTo (bounds);
+ bounds = bounds.withPosition (requestedBounds.getPosition());
+
+ if (isStretchingTop && ! isStretchingBottom)
+ bounds = bounds.withBottomY (previousBounds.getBottom());
+
+ if (! isStretchingTop && isStretchingBottom)
+ bounds = bounds.withY (previousBounds.getY());
+
+ if (isStretchingLeft && ! isStretchingRight)
+ bounds = bounds.withRightX (previousBounds.getRight());
+
+ if (! isStretchingLeft && isStretchingRight)
+ bounds = bounds.withX (previousBounds.getX());
+ }
+ else
+ {
+ ComponentBoundsConstrainer::checkBounds (bounds,
+ previousBounds,
+ limits,
+ isStretchingTop,
+ isStretchingLeft,
+ isStretchingBottom,
+ isStretchingRight);
+ }
+ }
+
+ void setMainContentComponent (MainContentComponent* in) { contentComponent = in; }
+
+ private:
+ MainContentComponent* contentComponent = nullptr;
+ };
+
//==============================================================================
- #if ! JUCE_MAC
TextButton optionsButton;
- #endif
+ DecoratorConstrainer decoratorConstrainer;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandaloneFilterWindow)
};
diff --git a/OB-Xd.jucer b/OB-Xd.jucer
index 2df16f6..8c8e6d0 100644
--- a/OB-Xd.jucer
+++ b/OB-Xd.jucer
@@ -1,6 +1,6 @@
-
+ pluginAAXCategory="2048" pluginVSTCategory="kPlugCategSynth"
+ displaySplashScreen="1">
@@ -91,13 +92,13 @@
+ aaxFolder="Modules/aax" extraDefs="JUCE_MODAL_LOOPS_PERMITTED=1">
+ enablePluginBinaryCopyStep="0" macOSDeploymentTarget="10.9"/>
@@ -115,8 +116,8 @@
-
+
+ aaxFolder="Modules/aax" extraDefs="JUCE_MODAL_LOOPS_PERMITTED=1">
@@ -166,20 +167,20 @@
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index 7751f60..e9e8732 100644
--- a/README.md
+++ b/README.md
@@ -15,4 +15,4 @@ Latest binaries can be downloaded at https://www.discodsp.com/obxd/
# Building
-Source code can be compiled with [JUCE 6.0.5](https://github.com/juce-framework/JUCE/releases/tag/6.0.5) and VST3 SDK.
+Source code can be compiled with [JUCE 6.1.6](https://github.com/juce-framework/JUCE/releases/tag/6.1.6) and VST3 SDK.
diff --git a/Source/Gui/Knob.h b/Source/Gui/Knob.h
index fbced57..f9c4835 100644
--- a/Source/Gui/Knob.h
+++ b/Source/Gui/Knob.h
@@ -61,8 +61,8 @@ public:
h2 = fh;
w2 = kni.getWidth();
numFr = kni.getHeight() / h2;
- setPopupDisplayEnabled(true, true, getParentComponent());
setLookAndFeel(&lookAndFeel);
+ setVelocityModeParameters(1.0, 1, 0.0, true, ModifierKeys::ctrlModifier);
}
~Knob() override
@@ -105,6 +105,17 @@ public:
setValue(shiftDragCallback(getValue()), sendNotificationAsync);
}
}
+ if (event.mods.isAltDown())
+ {
+ if (altDragCallback)
+ {
+ setValue(altDragCallback(getValue()), sendNotificationAsync);
+ }
+ }
+ if (alternativeValueMapCallback)
+ {
+ setValue(alternativeValueMapCallback(getValue()), sendNotificationAsync);
+ }
}
// Source: https://git.iem.at/audioplugins/IEMPluginSuite/-/blob/master/resources/customComponents/ReverseSlider.h
@@ -161,7 +172,7 @@ public:
resetActionMessage = identifier;
}
- std::function shiftDragCallback;
+ std::function shiftDragCallback, altDragCallback, alternativeValueMapCallback;
private:
Image kni;
int fh, numFr;
diff --git a/Source/PluginEditor.cpp b/Source/PluginEditor.cpp
index 95c5981..9db7c3d 100755
--- a/Source/PluginEditor.cpp
+++ b/Source/PluginEditor.cpp
@@ -117,10 +117,20 @@ void ObxdAudioProcessorEditor::resized() {
int d = child->getIntAttribute("d");
int w = child->getIntAttribute("w");
int h = child->getIntAttribute("h");
+ bool tooltipEnabled = child->getBoolAttribute("tooltip", false);
DBG(" COmponent : " << name);
if (mappingComps[name] != nullptr){
- if (dynamic_cast(mappingComps[name])){
- mappingComps[name]->setBounds(transformBounds(x, y, d,d));
+ if (auto* knob = dynamic_cast(mappingComps[name])){
+ knob->setBounds(transformBounds(x, y, d,d));
+ const auto tooltipBehavior = processor.getTooltipBehavior();
+ if (tooltipBehavior == Tooltip::Disable ||
+ (tooltipBehavior == Tooltip::StandardDisplay && !tooltipEnabled))
+ {
+ knob->setPopupDisplayEnabled(false, false, nullptr);
+ } else
+ {
+ knob->setPopupDisplayEnabled(true, true, knob->getParentComponent());
+ }
}
else if (dynamic_cast(mappingComps[name])){
mappingComps[name]->setBounds(transformBounds(x, y, w, h));
@@ -218,6 +228,11 @@ void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
if (value < 0.875) return 0.75;
return 1.0;
};
+ osc1PitchKnob->altDragCallback = [](double value)
+ {
+ const auto semitoneValue = (int)jmap(value, -24.0, 24.0);
+ return jmap((double)semitoneValue, -24.0, 24.0, 0.0, 1.0);
+ };
mappingComps["osc1PitchKnob"] = osc1PitchKnob;
}
if (name == "pulseWidthKnob"){
@@ -234,6 +249,11 @@ void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
if (value < 0.875) return 0.75;
return 1.0;
};
+ osc2PitchKnob->altDragCallback = [](double value)
+ {
+ const auto semitoneValue = (int)jmap(value, -24.0, 24.0);
+ return jmap((double)semitoneValue, -24.0, 24.0, 0.0, 1.0);
+ };
mappingComps["osc2PitchKnob"] = osc2PitchKnob;
}
@@ -374,6 +394,27 @@ void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
if (name == "pitchQuantButton"){
pitchQuantButton = addButton (x, y, w, h, ownerFilter, OSCQuantize, "Step");
+ pitchQuantButton->onStateChange = [&]
+ {
+ const auto isButtonOn = pitchQuantButton->getToggleState();
+ const auto configureOscKnob = [&](const String& parameter)
+ {
+ if (auto* knob = dynamic_cast(mappingComps[parameter]))
+ {
+ if (isButtonOn)
+ knob->alternativeValueMapCallback = [](double value)
+ {
+ const auto semitoneValue = (int)jmap(value, -24.0, 24.0);
+ return jmap((double)semitoneValue, -24.0, 24.0, 0.0, 1.0);
+ };
+ else
+ knob->alternativeValueMapCallback = nullptr;
+ }
+ };
+ configureOscKnob("osc1PitchKnob");
+ configureOscKnob("osc2PitchKnob");
+
+ };
mappingComps["pitchQuantButton"] = pitchQuantButton;
}
@@ -511,17 +552,17 @@ void ObxdAudioProcessorEditor::loadSkin (ObxdAudioProcessor& ownerFilter)
}
if (name == "voiceSwitch"){
- voiceSwitch = addList (x, y, w, h, ownerFilter, VOICE_COUNT, "VoiceCount", "voices");
+ voiceSwitch.reset(addList (x, y, w, h, ownerFilter, VOICE_COUNT, "VoiceCount", "voices"));
voiceSwitch->setLookAndFeel(&this->getLookAndFeel());
- mappingComps["voiceSwitch"] = voiceSwitch;
+ mappingComps["voiceSwitch"] = voiceSwitch.get();
}
if (name == "legatoSwitch"){
- legatoSwitch = addList (x, y, w, h, ownerFilter, LEGATOMODE, "Legato", "legato");
+ legatoSwitch.reset(addList (x, y, w, h, ownerFilter, LEGATOMODE, "Legato", "legato"));
legatoSwitch->setLookAndFeel(&this->getLookAndFeel());
- mappingComps["legatoSwitch"] = legatoSwitch;
+ mappingComps["legatoSwitch"] = legatoSwitch.get();
}
@@ -729,7 +770,7 @@ Knob* ObxdAudioProcessorEditor::addKnob (int x, int y, int d, ObxdAudioProcessor
knob->setRange (0, 1);
knob->setBounds (x, y, d+(d/6), d+(d/6));
knob->setTextBoxIsEditable (false);
- knob->setDoubleClickReturnValue (true, defval);
+ knob->setDoubleClickReturnValue (true, defval, ModifierKeys::noModifiers);
knob->setValue (filter.getPluginState().getParameter (filter.getEngineParameterId (parameter))->getValue());
addAndMakeVisible (knob);
@@ -951,6 +992,24 @@ void ObxdAudioProcessorEditor::createMenu ()
scaleMenu.addItem(menuScaleNum+2, "2x", true, getScaleFactor() == 2.0f);
menu->addSubMenu("GUI Size", scaleMenu, true);
+ PopupMenu tooltipMenu;
+ tooltipMenu.addItem("Disabled", true, processor.getTooltipBehavior() == Tooltip::Disable, [&]
+ {
+ processor.setTooltipBehavior(Tooltip::Disable);
+ resized();
+ });
+ tooltipMenu.addItem("Standard Display", true, processor.getTooltipBehavior() == Tooltip::StandardDisplay, [&]
+ {
+ processor.setTooltipBehavior(Tooltip::StandardDisplay);
+ resized();
+ });
+ tooltipMenu.addItem("Full Display", true, processor.getTooltipBehavior() == Tooltip::FullDisplay, [&]
+ {
+ processor.setTooltipBehavior(Tooltip::FullDisplay);
+ resized();
+ });
+ menu->addSubMenu("Tooltips", tooltipMenu, true);
+
#ifdef LINUX
menu->addItem(1, String("Release ") + String(JucePlugin_VersionString).dropLastCharacters(2), false);
#endif
diff --git a/Source/PluginEditor.h b/Source/PluginEditor.h
index 5efb1dd..2ea0fc9 100644
--- a/Source/PluginEditor.h
+++ b/Source/PluginEditor.h
@@ -272,8 +272,7 @@ private:
*midiLearnButton=nullptr,
*midiUnlearnButton=nullptr;
- ButtonList *voiceSwitch = nullptr,
- *legatoSwitch = nullptr;
+ std::unique_ptr voiceSwitch, legatoSwitch;
File skinFolder;
diff --git a/Source/PluginProcessor.cpp b/Source/PluginProcessor.cpp
index 02a0f61..50115bf 100644
--- a/Source/PluginProcessor.cpp
+++ b/Source/PluginProcessor.cpp
@@ -74,6 +74,7 @@ ObxdAudioProcessor::ObxdAudioProcessor()
config = std::unique_ptr (new PropertiesFile (getDocumentFolder().getChildFile ("Skin.xml"), options));
showPresetBar = config->getBoolValue("presetnavigation");
gui_size = config->getIntValue("gui_size", 1);
+ tooltipBehavior = static_cast(config->getIntValue("tooltip", 1));
currentSkin = config->containsKey("skin") ? config->getValue("skin") : "Ilkka Rosma Dark";
currentBank = "000 - FMR OB-Xa Patch Book";
@@ -824,6 +825,19 @@ void ObxdAudioProcessor::setGuiSize(const int gui_size) {
config->setValue("gui_size", gui_size);
config->setNeedsToBeSaved(true);
}
+
+Tooltip ObxdAudioProcessor::getTooltipBehavior() const
+{
+ return tooltipBehavior;
+}
+
+void ObxdAudioProcessor::setTooltipBehavior(const Tooltip tooltip)
+{
+ this->tooltipBehavior = tooltip;
+ config->setValue("tooltip", static_cast(tooltip));
+ config->setNeedsToBeSaved(true);
+}
+
//==============================================================================
String ObxdAudioProcessor::getEngineParameterId (size_t index)
{
@@ -988,8 +1002,8 @@ String ObxdAudioProcessor::getTrueParameterValueFromNormalizedRange(size_t index
}
// case XMOD: return "Xmod";
// case OSC2HS: return "Osc2HardSync";
- // case OSC1P: return String{ getPitch(value * 48), 2 };
- // case OSC2P: return String{ getPitch(value * 48), 2 };
+ case OSC1P: return String{ (float(value * 4) - 2) * 12.f, 1 } + " Semitones";
+ case OSC2P: return String{ (float(value * 4) - 2) * 12.f, 1 } + " Semitones";
// case PORTAMENTO: return "Portamento";
// case UNISON: return "Unison";
// case FLT_KF: return "FilterKeyFollow";
@@ -1017,7 +1031,7 @@ String ObxdAudioProcessor::getTrueParameterValueFromNormalizedRange(size_t index
break;
}
- return String{ value, 2 };
+ return String{ static_cast(jmap(value, 0.f, 127.f)) };
}
int ObxdAudioProcessor::getParameterIndexFromId (String paramId)
diff --git a/Source/PluginProcessor.h b/Source/PluginProcessor.h
index 0656600..127dbf8 100644
--- a/Source/PluginProcessor.h
+++ b/Source/PluginProcessor.h
@@ -110,6 +110,12 @@ static inline float fxbSwapFloat (const float x) noexcept
#endif
}
+enum class Tooltip
+{
+ Disable = 0,
+ StandardDisplay,
+ FullDisplay
+};
//==============================================================================
/**
*/
@@ -196,6 +202,8 @@ public:
File getCurrentSkinFolder() const;
void setCurrentSkinFolder(const String& folderName);
void setGuiSize(const int gui_size);
+ Tooltip getTooltipBehavior() const;
+ void setTooltipBehavior(const Tooltip tooltip);
//==============================================================================
static String getEngineParameterId (size_t);
static String getTrueParameterValueFromNormalizedRange(size_t, float normalizedValue);
@@ -251,6 +259,7 @@ public:
private:
Array bankFiles;
Array skinFiles;
+ Tooltip tooltipBehavior;
std::unique_ptr config;
InterProcessLock configLock;