build(deps): vendor JUCE 7.0.12
This commit is contained in:
+2349
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,477 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: AUv3SynthPlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: AUv3 synthesiser audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: AUv3SynthProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn
|
||||
extraPluginFormats: AUv3
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
|
||||
//==============================================================================
|
||||
class MaterialLookAndFeel final : public LookAndFeel_V4
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MaterialLookAndFeel()
|
||||
{
|
||||
setColour (ResizableWindow::backgroundColourId, windowBackgroundColour);
|
||||
setColour (TextButton::buttonOnColourId, brightButtonColour);
|
||||
setColour (TextButton::buttonColourId, disabledButtonColour);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void drawButtonBackground (Graphics& g,
|
||||
Button& button,
|
||||
const Colour& /*backgroundColour*/,
|
||||
bool /*isMouseOverButton*/,
|
||||
bool isButtonDown) override
|
||||
{
|
||||
auto buttonRect = button.getLocalBounds().toFloat();
|
||||
|
||||
if (isButtonDown)
|
||||
g.setColour (brightButtonColour.withAlpha (0.7f));
|
||||
else if (! button.isEnabled())
|
||||
g.setColour (disabledButtonColour);
|
||||
else
|
||||
g.setColour (brightButtonColour);
|
||||
|
||||
g.fillRoundedRectangle (buttonRect, 5.0f);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void drawButtonText (Graphics& g, TextButton& button, bool isMouseOverButton, bool isButtonDown) override
|
||||
{
|
||||
ignoreUnused (isMouseOverButton, isButtonDown);
|
||||
|
||||
Font font (getTextButtonFont (button, button.getHeight()));
|
||||
g.setFont (font);
|
||||
|
||||
if (button.isEnabled())
|
||||
g.setColour (Colours::white);
|
||||
else
|
||||
g.setColour (backgroundColour);
|
||||
|
||||
g.drawFittedText (button.getButtonText(), 0, 0,
|
||||
button.getWidth(),
|
||||
button.getHeight(),
|
||||
Justification::centred, 2);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void drawLinearSlider (Graphics& g, int x, int y, int width, int height,
|
||||
float sliderPos, float minSliderPos, float maxSliderPos,
|
||||
Slider::SliderStyle style, Slider& slider) override
|
||||
{
|
||||
ignoreUnused (style, minSliderPos, maxSliderPos);
|
||||
|
||||
auto r = Rectangle<int> (x + haloRadius, y, width - (haloRadius * 2), height);
|
||||
auto backgroundBar = r.withSizeKeepingCentre (r.getWidth(), 2);
|
||||
|
||||
sliderPos = (sliderPos - minSliderPos) / static_cast<float> (width);
|
||||
|
||||
auto knobPos = static_cast<int> (sliderPos * (float) r.getWidth());
|
||||
|
||||
g.setColour (sliderActivePart);
|
||||
g.fillRect (backgroundBar.removeFromLeft (knobPos));
|
||||
|
||||
g.setColour (sliderInactivePart);
|
||||
g.fillRect (backgroundBar);
|
||||
|
||||
if (slider.isMouseOverOrDragging())
|
||||
{
|
||||
auto haloBounds = r.withTrimmedLeft (knobPos - haloRadius)
|
||||
.withWidth (haloRadius * 2)
|
||||
.withSizeKeepingCentre (haloRadius * 2, haloRadius * 2);
|
||||
|
||||
g.setColour (sliderActivePart.withAlpha (0.5f));
|
||||
g.fillEllipse (haloBounds.toFloat());
|
||||
}
|
||||
|
||||
auto knobRadius = slider.isMouseOverOrDragging() ? knobActiveRadius : knobInActiveRadius;
|
||||
auto knobBounds = r.withTrimmedLeft (knobPos - knobRadius)
|
||||
.withWidth (knobRadius * 2)
|
||||
.withSizeKeepingCentre (knobRadius * 2, knobRadius * 2);
|
||||
|
||||
g.setColour (sliderActivePart);
|
||||
g.fillEllipse (knobBounds.toFloat());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Font getTextButtonFont (TextButton& button, int buttonHeight) override
|
||||
{
|
||||
return LookAndFeel_V3::getTextButtonFont (button, buttonHeight).withHeight (buttonFontSize);
|
||||
}
|
||||
|
||||
Font getLabelFont (Label& label) override
|
||||
{
|
||||
return LookAndFeel_V3::getLabelFont (label).withHeight (labelFontSize);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
enum
|
||||
{
|
||||
labelFontSize = 12,
|
||||
buttonFontSize = 15
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
enum
|
||||
{
|
||||
knobActiveRadius = 12,
|
||||
knobInActiveRadius = 8,
|
||||
haloRadius = 18
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
const Colour windowBackgroundColour = Colour (0xff262328);
|
||||
const Colour backgroundColour = Colour (0xff4d4d4d);
|
||||
const Colour brightButtonColour = Colour (0xff80cbc4);
|
||||
const Colour disabledButtonColour = Colour (0xffe4e4e4);
|
||||
const Colour sliderInactivePart = Colour (0xff545d62);
|
||||
const Colour sliderActivePart = Colour (0xff80cbc4);
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AUv3SynthEditor final : public AudioProcessorEditor,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
AUv3SynthEditor (AudioProcessor& processorIn)
|
||||
: AudioProcessorEditor (processorIn),
|
||||
roomSizeSlider (Slider::LinearHorizontal, Slider::NoTextBox)
|
||||
{
|
||||
setLookAndFeel (&materialLookAndFeel);
|
||||
|
||||
roomSizeSlider.setValue (getParameterValue ("roomSize"), NotificationType::dontSendNotification);
|
||||
|
||||
recordButton.onClick = [this] { startRecording(); };
|
||||
addAndMakeVisible (recordButton);
|
||||
|
||||
roomSizeSlider.onValueChange = [this] { setParameterValue ("roomSize", (float) roomSizeSlider.getValue()); };
|
||||
roomSizeSlider.setRange (0.0, 1.0);
|
||||
addAndMakeVisible (roomSizeSlider);
|
||||
|
||||
if (auto fileStream = createAssetInputStream ("proaudio.path"))
|
||||
{
|
||||
Path proAudioPath;
|
||||
proAudioPath.loadPathFromStream (*fileStream);
|
||||
proAudioIcon.setPath (proAudioPath);
|
||||
addAndMakeVisible (proAudioIcon);
|
||||
|
||||
auto proAudioIconColour = findColour (TextButton::buttonOnColourId);
|
||||
proAudioIcon.setFill (FillType (proAudioIconColour));
|
||||
}
|
||||
|
||||
setSize (600, 400);
|
||||
startTimer (100);
|
||||
}
|
||||
|
||||
~AUv3SynthEditor() override
|
||||
{
|
||||
setLookAndFeel (nullptr);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (findColour (ResizableWindow::backgroundColourId));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto r = getLocalBounds();
|
||||
|
||||
auto guiElementAreaHeight = r.getHeight() / 3;
|
||||
|
||||
proAudioIcon.setTransformToFit (r.removeFromLeft (proportionOfWidth (0.25))
|
||||
.withSizeKeepingCentre (guiElementAreaHeight, guiElementAreaHeight)
|
||||
.toFloat(),
|
||||
RectanglePlacement::fillDestination);
|
||||
|
||||
auto margin = guiElementAreaHeight / 4;
|
||||
r.reduce (margin, margin);
|
||||
|
||||
auto buttonHeight = guiElementAreaHeight - margin;
|
||||
|
||||
recordButton .setBounds (r.removeFromTop (guiElementAreaHeight).withSizeKeepingCentre (r.getWidth(), buttonHeight));
|
||||
roomSizeSlider.setBounds (r.removeFromTop (guiElementAreaHeight).withSizeKeepingCentre (r.getWidth(), buttonHeight));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void startRecording()
|
||||
{
|
||||
recordButton.setEnabled (false);
|
||||
setParameterValue ("isRecording", 1.0f);
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void timerCallback() override
|
||||
{
|
||||
auto isRecordingNow = (getParameterValue ("isRecording") >= 0.5f);
|
||||
|
||||
recordButton.setEnabled (! isRecordingNow);
|
||||
roomSizeSlider.setValue (getParameterValue ("roomSize"), NotificationType::dontSendNotification);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorParameter* getParameter (const String& paramId)
|
||||
{
|
||||
if (auto* audioProcessor = getAudioProcessor())
|
||||
{
|
||||
auto& params = audioProcessor->getParameters();
|
||||
|
||||
for (auto p : params)
|
||||
{
|
||||
if (auto* param = dynamic_cast<AudioProcessorParameterWithID*> (p))
|
||||
{
|
||||
if (param->paramID == paramId)
|
||||
return param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
float getParameterValue (const String& paramId)
|
||||
{
|
||||
if (auto* param = getParameter (paramId))
|
||||
return param->getValue();
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
void setParameterValue (const String& paramId, float value)
|
||||
{
|
||||
if (auto* param = getParameter (paramId))
|
||||
param->setValueNotifyingHost (value);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MaterialLookAndFeel materialLookAndFeel;
|
||||
|
||||
//==============================================================================
|
||||
TextButton recordButton { "Record" };
|
||||
Slider roomSizeSlider;
|
||||
DrawablePath proAudioIcon;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUv3SynthEditor)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AUv3SynthProcessor final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
AUv3SynthProcessor()
|
||||
: AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true)),
|
||||
currentRecording (1, 1), currentProgram (0)
|
||||
{
|
||||
// initialize parameters
|
||||
addParameter (isRecordingParam = new AudioParameterBool ({ "isRecording", 1 }, "Is Recording", false));
|
||||
addParameter (roomSizeParam = new AudioParameterFloat ({ "roomSize", 1 }, "Room Size", 0.0f, 1.0f, 0.5f));
|
||||
|
||||
formatManager.registerBasicFormats();
|
||||
|
||||
for (auto i = 0; i < maxNumVoices; ++i)
|
||||
synth.addVoice (new SamplerVoice());
|
||||
|
||||
loadNewSample (createAssetInputStream ("singing.ogg"), "ogg");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
||||
{
|
||||
return (layouts.getMainOutputChannels() <= 2);
|
||||
}
|
||||
|
||||
void prepareToPlay (double sampleRate, int estimatedMaxSizeOfBuffer) override
|
||||
{
|
||||
ignoreUnused (estimatedMaxSizeOfBuffer);
|
||||
|
||||
lastSampleRate = sampleRate;
|
||||
|
||||
currentRecording.setSize (1, static_cast<int> (std::ceil (maxDurationOfRecording * lastSampleRate)));
|
||||
samplesRecorded = 0;
|
||||
|
||||
synth.setCurrentPlaybackSampleRate (lastSampleRate);
|
||||
reverb.setSampleRate (lastSampleRate);
|
||||
}
|
||||
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
|
||||
{
|
||||
Reverb::Parameters reverbParameters;
|
||||
reverbParameters.roomSize = roomSizeParam->get();
|
||||
|
||||
reverb.setParameters (reverbParameters);
|
||||
synth.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples());
|
||||
|
||||
if (getMainBusNumOutputChannels() == 1)
|
||||
reverb.processMono (buffer.getWritePointer (0), buffer.getNumSamples());
|
||||
else if (getMainBusNumOutputChannels() == 2)
|
||||
reverb.processStereo (buffer.getWritePointer (0), buffer.getWritePointer (1), buffer.getNumSamples());
|
||||
}
|
||||
|
||||
using AudioProcessor::processBlock;
|
||||
|
||||
//==============================================================================
|
||||
void releaseResources() override { currentRecording.setSize (1, 1); }
|
||||
|
||||
//==============================================================================
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new AUv3SynthEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "AUv3 Synth"; }
|
||||
int getNumPrograms() override { return 4; }
|
||||
int getCurrentProgram() override { return currentProgram; }
|
||||
void setCurrentProgram (int index) override { currentProgram = index; }
|
||||
|
||||
const String getProgramName (int index) override
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case 0: return "Piano";
|
||||
case 1: return "Singing";
|
||||
case 2: return "Pinched Balloon";
|
||||
case 3: return "Gazeebo";
|
||||
default: break;
|
||||
}
|
||||
|
||||
return "<Unknown>";
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void changeProgramName (int /*index*/, const String& /*name*/) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
MemoryOutputStream stream (destData, true);
|
||||
|
||||
stream.writeFloat (*isRecordingParam);
|
||||
stream.writeFloat (*roomSizeParam);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
MemoryInputStream stream (data, static_cast<size_t> (sizeInBytes), false);
|
||||
|
||||
isRecordingParam->setValueNotifyingHost (stream.readFloat());
|
||||
roomSizeParam->setValueNotifyingHost (stream.readFloat());
|
||||
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void loadNewSampleBinary (const void* data, int dataSize, const char* format)
|
||||
{
|
||||
auto soundBuffer = std::make_unique<MemoryInputStream> (data, static_cast<std::size_t> (dataSize), false);
|
||||
loadNewSample (std::move (soundBuffer), format);
|
||||
}
|
||||
|
||||
void loadNewSample (std::unique_ptr<InputStream> soundBuffer, const char* format)
|
||||
{
|
||||
std::unique_ptr<AudioFormatReader> formatReader (formatManager.findFormatForFileExtension (format)->createReaderFor (soundBuffer.release(), true));
|
||||
|
||||
BigInteger midiNotes;
|
||||
midiNotes.setRange (0, 126, true);
|
||||
SynthesiserSound::Ptr newSound = new SamplerSound ("Voice", *formatReader, midiNotes, 0x40, 0.0, 0.0, 10.0);
|
||||
synth.removeSound (0);
|
||||
sound = newSound;
|
||||
synth.addSound (sound);
|
||||
}
|
||||
|
||||
void swapSamples()
|
||||
{
|
||||
MemoryBlock mb;
|
||||
auto* stream = new MemoryOutputStream (mb, true);
|
||||
|
||||
{
|
||||
std::unique_ptr<AudioFormatWriter> writer (formatManager.findFormatForFileExtension ("wav")->createWriterFor (stream, lastSampleRate, 1, 16,
|
||||
StringPairArray(), 0));
|
||||
writer->writeFromAudioSampleBuffer (currentRecording, 0, currentRecording.getNumSamples());
|
||||
writer->flush();
|
||||
stream->flush();
|
||||
}
|
||||
|
||||
loadNewSampleBinary (mb.getData(), static_cast<int> (mb.getSize()), "wav");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static constexpr int maxNumVoices = 5;
|
||||
static constexpr double maxDurationOfRecording = 1.0;
|
||||
|
||||
//==============================================================================
|
||||
AudioFormatManager formatManager;
|
||||
|
||||
int samplesRecorded;
|
||||
double lastSampleRate;
|
||||
AudioBuffer<float> currentRecording;
|
||||
|
||||
Reverb reverb;
|
||||
Synthesiser synth;
|
||||
SynthesiserSound::Ptr sound;
|
||||
|
||||
AudioParameterBool* isRecordingParam;
|
||||
AudioParameterFloat* roomSizeParam;
|
||||
|
||||
int currentProgram;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUv3SynthProcessor)
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: ArpeggiatorPlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Arpeggiator audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: Arpeggiator
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginWantsMidiIn, pluginProducesMidiOut, pluginIsMidiEffectPlugin
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class Arpeggiator final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
|
||||
//==============================================================================
|
||||
Arpeggiator()
|
||||
: AudioProcessor (BusesProperties()) // add no audio buses at all
|
||||
{
|
||||
addParameter (speed = new AudioParameterFloat ({ "speed", 1 }, "Arpeggiator Speed", 0.0, 1.0, 0.5));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (double sampleRate, int samplesPerBlock) override
|
||||
{
|
||||
ignoreUnused (samplesPerBlock);
|
||||
|
||||
notes.clear();
|
||||
currentNote = 0;
|
||||
lastNoteValue = -1;
|
||||
time = 0;
|
||||
rate = static_cast<float> (sampleRate);
|
||||
}
|
||||
|
||||
void releaseResources() override {}
|
||||
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midi) override
|
||||
{
|
||||
// A pure MIDI plugin shouldn't be provided any audio data
|
||||
jassert (buffer.getNumChannels() == 0);
|
||||
|
||||
// however we use the buffer to get timing information
|
||||
auto numSamples = buffer.getNumSamples();
|
||||
|
||||
// get note duration
|
||||
auto noteDuration = static_cast<int> (std::ceil (rate * 0.25f * (0.1f + (1.0f - (*speed)))));
|
||||
|
||||
for (const auto metadata : midi)
|
||||
{
|
||||
const auto msg = metadata.getMessage();
|
||||
if (msg.isNoteOn()) notes.add (msg.getNoteNumber());
|
||||
else if (msg.isNoteOff()) notes.removeValue (msg.getNoteNumber());
|
||||
}
|
||||
|
||||
midi.clear();
|
||||
|
||||
if ((time + numSamples) >= noteDuration)
|
||||
{
|
||||
auto offset = jmax (0, jmin ((int) (noteDuration - time), numSamples - 1));
|
||||
|
||||
if (lastNoteValue > 0)
|
||||
{
|
||||
midi.addEvent (MidiMessage::noteOff (1, lastNoteValue), offset);
|
||||
lastNoteValue = -1;
|
||||
}
|
||||
|
||||
if (notes.size() > 0)
|
||||
{
|
||||
currentNote = (currentNote + 1) % notes.size();
|
||||
lastNoteValue = notes[currentNote];
|
||||
midi.addEvent (MidiMessage::noteOn (1, lastNoteValue, (uint8) 127), offset);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
time = (time + numSamples) % noteDuration;
|
||||
}
|
||||
|
||||
using AudioProcessor::processBlock;
|
||||
|
||||
//==============================================================================
|
||||
bool isMidiEffect() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new GenericAudioProcessorEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "Arpeggiator"; }
|
||||
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return true; }
|
||||
double getTailLengthSeconds() const override { return 0; }
|
||||
|
||||
//==============================================================================
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
MemoryOutputStream (destData, true).writeFloat (*speed);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
speed->setValueNotifyingHost (MemoryInputStream (data, static_cast<size_t> (sizeInBytes), false).readFloat());
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioParameterFloat* speed;
|
||||
int currentNote, lastNoteValue;
|
||||
int time;
|
||||
float rate;
|
||||
SortedSet<int> notes;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Arpeggiator)
|
||||
};
|
||||
+665
@@ -0,0 +1,665 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: AudioPluginDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Synthesiser audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2017, vs2022, linux_make, xcode_iphone, androidstudio
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: JuceDemoPluginAudioProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn, pluginProducesMidiOut,
|
||||
pluginEditorRequiresKeys
|
||||
extraPluginFormats: AUv3
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** A demo synth sound that's just a basic sine wave.. */
|
||||
class SineWaveSound final : public SynthesiserSound
|
||||
{
|
||||
public:
|
||||
SineWaveSound() {}
|
||||
|
||||
bool appliesToNote (int /*midiNoteNumber*/) override { return true; }
|
||||
bool appliesToChannel (int /*midiChannel*/) override { return true; }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** A simple demo synth voice that just plays a sine wave.. */
|
||||
class SineWaveVoice final : public SynthesiserVoice
|
||||
{
|
||||
public:
|
||||
SineWaveVoice() {}
|
||||
|
||||
bool canPlaySound (SynthesiserSound* sound) override
|
||||
{
|
||||
return dynamic_cast<SineWaveSound*> (sound) != nullptr;
|
||||
}
|
||||
|
||||
void startNote (int midiNoteNumber, float velocity,
|
||||
SynthesiserSound* /*sound*/,
|
||||
int /*currentPitchWheelPosition*/) override
|
||||
{
|
||||
currentAngle = 0.0;
|
||||
level = velocity * 0.15;
|
||||
tailOff = 0.0;
|
||||
|
||||
auto cyclesPerSecond = MidiMessage::getMidiNoteInHertz (midiNoteNumber);
|
||||
auto cyclesPerSample = cyclesPerSecond / getSampleRate();
|
||||
|
||||
angleDelta = cyclesPerSample * MathConstants<double>::twoPi;
|
||||
}
|
||||
|
||||
void stopNote (float /*velocity*/, bool allowTailOff) override
|
||||
{
|
||||
if (allowTailOff)
|
||||
{
|
||||
// start a tail-off by setting this flag. The render callback will pick up on
|
||||
// this and do a fade out, calling clearCurrentNote() when it's finished.
|
||||
|
||||
if (approximatelyEqual (tailOff, 0.0)) // we only need to begin a tail-off if it's not already doing so - the
|
||||
// stopNote method could be called more than once.
|
||||
tailOff = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're being told to stop playing immediately, so reset everything..
|
||||
|
||||
clearCurrentNote();
|
||||
angleDelta = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void pitchWheelMoved (int /*newValue*/) override
|
||||
{
|
||||
// not implemented for the purposes of this demo!
|
||||
}
|
||||
|
||||
void controllerMoved (int /*controllerNumber*/, int /*newValue*/) override
|
||||
{
|
||||
// not implemented for the purposes of this demo!
|
||||
}
|
||||
|
||||
void renderNextBlock (AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
|
||||
{
|
||||
if (! approximatelyEqual (angleDelta, 0.0))
|
||||
{
|
||||
if (tailOff > 0.0)
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = (float) (sin (currentAngle) * level * tailOff);
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
currentAngle += angleDelta;
|
||||
++startSample;
|
||||
|
||||
tailOff *= 0.99;
|
||||
|
||||
if (tailOff <= 0.005)
|
||||
{
|
||||
// tells the synth that this voice has stopped
|
||||
clearCurrentNote();
|
||||
|
||||
angleDelta = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = (float) (sin (currentAngle) * level);
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
currentAngle += angleDelta;
|
||||
++startSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using SynthesiserVoice::renderNextBlock;
|
||||
|
||||
private:
|
||||
double currentAngle = 0.0;
|
||||
double angleDelta = 0.0;
|
||||
double level = 0.0;
|
||||
double tailOff = 0.0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** As the name suggest, this class does the actual audio processing. */
|
||||
class JuceDemoPluginAudioProcessor final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
JuceDemoPluginAudioProcessor()
|
||||
: AudioProcessor (getBusesProperties()),
|
||||
state (*this, nullptr, "state",
|
||||
{ std::make_unique<AudioParameterFloat> (ParameterID { "gain", 1 }, "Gain", NormalisableRange<float> (0.0f, 1.0f), 0.9f),
|
||||
std::make_unique<AudioParameterFloat> (ParameterID { "delay", 1 }, "Delay Feedback", NormalisableRange<float> (0.0f, 1.0f), 0.5f) })
|
||||
{
|
||||
// Add a sub-tree to store the state of our UI
|
||||
state.state.addChild ({ "uiState", { { "width", 400 }, { "height", 200 } }, {} }, -1, nullptr);
|
||||
|
||||
initialiseSynth();
|
||||
}
|
||||
|
||||
~JuceDemoPluginAudioProcessor() override = default;
|
||||
|
||||
//==============================================================================
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
||||
{
|
||||
// Only mono/stereo and input/output must have same layout
|
||||
const auto& mainOutput = layouts.getMainOutputChannelSet();
|
||||
const auto& mainInput = layouts.getMainInputChannelSet();
|
||||
|
||||
// input and output layout must either be the same or the input must be disabled altogether
|
||||
if (! mainInput.isDisabled() && mainInput != mainOutput)
|
||||
return false;
|
||||
|
||||
// only allow stereo and mono
|
||||
if (mainOutput.size() > 2)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void prepareToPlay (double newSampleRate, int /*samplesPerBlock*/) override
|
||||
{
|
||||
// Use this method as the place to do any pre-playback
|
||||
// initialisation that you need..
|
||||
synth.setCurrentPlaybackSampleRate (newSampleRate);
|
||||
keyboardState.reset();
|
||||
|
||||
if (isUsingDoublePrecision())
|
||||
{
|
||||
delayBufferDouble.setSize (2, 12000);
|
||||
delayBufferFloat .setSize (1, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
delayBufferFloat .setSize (2, 12000);
|
||||
delayBufferDouble.setSize (1, 1);
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
void releaseResources() override
|
||||
{
|
||||
// When playback stops, you can use this as an opportunity to free up any
|
||||
// spare memory, etc.
|
||||
keyboardState.reset();
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
// Use this method as the place to clear any delay lines, buffers, etc, as it
|
||||
// means there's been a break in the audio's continuity.
|
||||
delayBufferFloat .clear();
|
||||
delayBufferDouble.clear();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
|
||||
{
|
||||
jassert (! isUsingDoublePrecision());
|
||||
process (buffer, midiMessages, delayBufferFloat);
|
||||
}
|
||||
|
||||
void processBlock (AudioBuffer<double>& buffer, MidiBuffer& midiMessages) override
|
||||
{
|
||||
jassert (isUsingDoublePrecision());
|
||||
process (buffer, midiMessages, delayBufferDouble);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
AudioProcessorEditor* createEditor() override
|
||||
{
|
||||
return new JuceDemoPluginAudioProcessorEditor (*this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "AudioPluginDemo"; }
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return true; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
//==============================================================================
|
||||
int getNumPrograms() override { return 0; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
// Store an xml representation of our state.
|
||||
if (auto xmlState = state.copyState().createXml())
|
||||
copyXmlToBinary (*xmlState, destData);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
// Restore our plug-in's state from the xml representation stored in the above
|
||||
// method.
|
||||
if (auto xmlState = getXmlFromBinary (data, sizeInBytes))
|
||||
state.replaceState (ValueTree::fromXml (*xmlState));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void updateTrackProperties (const TrackProperties& properties) override
|
||||
{
|
||||
{
|
||||
const ScopedLock sl (trackPropertiesLock);
|
||||
trackProperties = properties;
|
||||
}
|
||||
|
||||
MessageManager::callAsync ([this]
|
||||
{
|
||||
if (auto* editor = dynamic_cast<JuceDemoPluginAudioProcessorEditor*> (getActiveEditor()))
|
||||
editor->updateTrackProperties();
|
||||
});
|
||||
}
|
||||
|
||||
TrackProperties getTrackProperties() const
|
||||
{
|
||||
const ScopedLock sl (trackPropertiesLock);
|
||||
return trackProperties;
|
||||
}
|
||||
|
||||
class SpinLockedPosInfo
|
||||
{
|
||||
public:
|
||||
// Wait-free, but setting new info may fail if the main thread is currently
|
||||
// calling `get`. This is unlikely to matter in practice because
|
||||
// we'll be calling `set` much more frequently than `get`.
|
||||
void set (const AudioPlayHead::PositionInfo& newInfo)
|
||||
{
|
||||
const juce::SpinLock::ScopedTryLockType lock (mutex);
|
||||
|
||||
if (lock.isLocked())
|
||||
info = newInfo;
|
||||
}
|
||||
|
||||
AudioPlayHead::PositionInfo get() const noexcept
|
||||
{
|
||||
const juce::SpinLock::ScopedLockType lock (mutex);
|
||||
return info;
|
||||
}
|
||||
|
||||
private:
|
||||
juce::SpinLock mutex;
|
||||
AudioPlayHead::PositionInfo info;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// These properties are public so that our editor component can access them
|
||||
// A bit of a hacky way to do it, but it's only a demo! Obviously in your own
|
||||
// code you'll do this much more neatly..
|
||||
|
||||
// this is kept up to date with the midi messages that arrive, and the UI component
|
||||
// registers with it so it can represent the incoming messages
|
||||
MidiKeyboardState keyboardState;
|
||||
|
||||
// this keeps a copy of the last set of time info that was acquired during an audio
|
||||
// callback - the UI component will read this and display it.
|
||||
SpinLockedPosInfo lastPosInfo;
|
||||
|
||||
// Our plug-in's current state
|
||||
AudioProcessorValueTreeState state;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
/** This is the editor component that our filter will display. */
|
||||
class JuceDemoPluginAudioProcessorEditor final : public AudioProcessorEditor,
|
||||
private Timer,
|
||||
private Value::Listener
|
||||
{
|
||||
public:
|
||||
JuceDemoPluginAudioProcessorEditor (JuceDemoPluginAudioProcessor& owner)
|
||||
: AudioProcessorEditor (owner),
|
||||
midiKeyboard (owner.keyboardState, MidiKeyboardComponent::horizontalKeyboard),
|
||||
gainAttachment (owner.state, "gain", gainSlider),
|
||||
delayAttachment (owner.state, "delay", delaySlider)
|
||||
{
|
||||
// add some sliders..
|
||||
addAndMakeVisible (gainSlider);
|
||||
gainSlider.setSliderStyle (Slider::Rotary);
|
||||
|
||||
addAndMakeVisible (delaySlider);
|
||||
delaySlider.setSliderStyle (Slider::Rotary);
|
||||
|
||||
// add some labels for the sliders..
|
||||
gainLabel.attachToComponent (&gainSlider, false);
|
||||
gainLabel.setFont (Font (11.0f));
|
||||
|
||||
delayLabel.attachToComponent (&delaySlider, false);
|
||||
delayLabel.setFont (Font (11.0f));
|
||||
|
||||
// add the midi keyboard component..
|
||||
addAndMakeVisible (midiKeyboard);
|
||||
|
||||
// add a label that will display the current timecode and status..
|
||||
addAndMakeVisible (timecodeDisplayLabel);
|
||||
timecodeDisplayLabel.setFont (Font (Font::getDefaultMonospacedFontName(), 15.0f, Font::plain));
|
||||
|
||||
// set resize limits for this plug-in
|
||||
setResizeLimits (400, 200, 1024, 700);
|
||||
setResizable (true, owner.wrapperType != wrapperType_AudioUnitv3);
|
||||
|
||||
lastUIWidth .referTo (owner.state.state.getChildWithName ("uiState").getPropertyAsValue ("width", nullptr));
|
||||
lastUIHeight.referTo (owner.state.state.getChildWithName ("uiState").getPropertyAsValue ("height", nullptr));
|
||||
|
||||
// set our component's initial size to be the last one that was stored in the filter's settings
|
||||
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
||||
|
||||
lastUIWidth. addListener (this);
|
||||
lastUIHeight.addListener (this);
|
||||
|
||||
updateTrackProperties();
|
||||
|
||||
// start a timer which will keep our timecode display updated
|
||||
startTimerHz (30);
|
||||
}
|
||||
|
||||
~JuceDemoPluginAudioProcessorEditor() override {}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setColour (backgroundColour);
|
||||
g.fillAll();
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
// This lays out our child components...
|
||||
|
||||
auto r = getLocalBounds().reduced (8);
|
||||
|
||||
timecodeDisplayLabel.setBounds (r.removeFromTop (26));
|
||||
midiKeyboard .setBounds (r.removeFromBottom (70));
|
||||
|
||||
r.removeFromTop (20);
|
||||
auto sliderArea = r.removeFromTop (60);
|
||||
gainSlider.setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth() / 2)));
|
||||
delaySlider.setBounds (sliderArea.removeFromLeft (jmin (180, sliderArea.getWidth())));
|
||||
|
||||
lastUIWidth = getWidth();
|
||||
lastUIHeight = getHeight();
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
updateTimecodeDisplay (getProcessor().lastPosInfo.get());
|
||||
}
|
||||
|
||||
void hostMIDIControllerIsAvailable (bool controllerIsAvailable) override
|
||||
{
|
||||
midiKeyboard.setVisible (! controllerIsAvailable);
|
||||
}
|
||||
|
||||
int getControlParameterIndex (Component& control) override
|
||||
{
|
||||
if (&control == &gainSlider)
|
||||
return 0;
|
||||
|
||||
if (&control == &delaySlider)
|
||||
return 1;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void updateTrackProperties()
|
||||
{
|
||||
auto trackColour = getProcessor().getTrackProperties().colour;
|
||||
auto& lf = getLookAndFeel();
|
||||
|
||||
backgroundColour = (trackColour == Colour() ? lf.findColour (ResizableWindow::backgroundColourId)
|
||||
: trackColour.withAlpha (1.0f).withBrightness (0.266f));
|
||||
repaint();
|
||||
}
|
||||
|
||||
private:
|
||||
MidiKeyboardComponent midiKeyboard;
|
||||
|
||||
Label timecodeDisplayLabel,
|
||||
gainLabel { {}, "Throughput level:" },
|
||||
delayLabel { {}, "Delay:" };
|
||||
|
||||
Slider gainSlider, delaySlider;
|
||||
AudioProcessorValueTreeState::SliderAttachment gainAttachment, delayAttachment;
|
||||
Colour backgroundColour;
|
||||
|
||||
// these are used to persist the UI's size - the values are stored along with the
|
||||
// filter's other parameters, and the UI component will update them when it gets
|
||||
// resized.
|
||||
Value lastUIWidth, lastUIHeight;
|
||||
|
||||
//==============================================================================
|
||||
JuceDemoPluginAudioProcessor& getProcessor() const
|
||||
{
|
||||
return static_cast<JuceDemoPluginAudioProcessor&> (processor);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// quick-and-dirty function to format a timecode string
|
||||
static String timeToTimecodeString (double seconds)
|
||||
{
|
||||
auto millisecs = roundToInt (seconds * 1000.0);
|
||||
auto absMillisecs = std::abs (millisecs);
|
||||
|
||||
return String::formatted ("%02d:%02d:%02d.%03d",
|
||||
millisecs / 3600000,
|
||||
(absMillisecs / 60000) % 60,
|
||||
(absMillisecs / 1000) % 60,
|
||||
absMillisecs % 1000);
|
||||
}
|
||||
|
||||
// quick-and-dirty function to format a bars/beats string
|
||||
static String quarterNotePositionToBarsBeatsString (double quarterNotes, AudioPlayHead::TimeSignature sig)
|
||||
{
|
||||
if (sig.numerator == 0 || sig.denominator == 0)
|
||||
return "1|1|000";
|
||||
|
||||
auto quarterNotesPerBar = (sig.numerator * 4 / sig.denominator);
|
||||
auto beats = (fmod (quarterNotes, quarterNotesPerBar) / quarterNotesPerBar) * sig.numerator;
|
||||
|
||||
auto bar = ((int) quarterNotes) / quarterNotesPerBar + 1;
|
||||
auto beat = ((int) beats) + 1;
|
||||
auto ticks = ((int) (fmod (beats, 1.0) * 960.0 + 0.5));
|
||||
|
||||
return String::formatted ("%d|%d|%03d", bar, beat, ticks);
|
||||
}
|
||||
|
||||
// Updates the text in our position label.
|
||||
void updateTimecodeDisplay (const AudioPlayHead::PositionInfo& pos)
|
||||
{
|
||||
MemoryOutputStream displayText;
|
||||
|
||||
const auto sig = pos.getTimeSignature().orFallback (AudioPlayHead::TimeSignature{});
|
||||
|
||||
displayText << "[" << SystemStats::getJUCEVersion() << "] "
|
||||
<< String (pos.getBpm().orFallback (120.0), 2) << " bpm, "
|
||||
<< sig.numerator << '/' << sig.denominator
|
||||
<< " - " << timeToTimecodeString (pos.getTimeInSeconds().orFallback (0.0))
|
||||
<< " - " << quarterNotePositionToBarsBeatsString (pos.getPpqPosition().orFallback (0.0), sig);
|
||||
|
||||
if (pos.getIsRecording())
|
||||
displayText << " (recording)";
|
||||
else if (pos.getIsPlaying())
|
||||
displayText << " (playing)";
|
||||
|
||||
timecodeDisplayLabel.setText (displayText.toString(), dontSendNotification);
|
||||
}
|
||||
|
||||
// called when the stored window size changes
|
||||
void valueChanged (Value&) override
|
||||
{
|
||||
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
template <typename FloatType>
|
||||
void process (AudioBuffer<FloatType>& buffer, MidiBuffer& midiMessages, AudioBuffer<FloatType>& delayBuffer)
|
||||
{
|
||||
auto gainParamValue = state.getParameter ("gain") ->getValue();
|
||||
auto delayParamValue = state.getParameter ("delay")->getValue();
|
||||
auto numSamples = buffer.getNumSamples();
|
||||
|
||||
// In case we have more outputs than inputs, we'll clear any output
|
||||
// channels that didn't contain input data, (because these aren't
|
||||
// guaranteed to be empty - they may contain garbage).
|
||||
for (auto i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
|
||||
buffer.clear (i, 0, numSamples);
|
||||
|
||||
// Now pass any incoming midi messages to our keyboard state object, and let it
|
||||
// add messages to the buffer if the user is clicking on the on-screen keys
|
||||
keyboardState.processNextMidiBuffer (midiMessages, 0, numSamples, true);
|
||||
|
||||
// and now get our synth to process these midi events and generate its output.
|
||||
synth.renderNextBlock (buffer, midiMessages, 0, numSamples);
|
||||
|
||||
// Apply our delay effect to the new output..
|
||||
applyDelay (buffer, delayBuffer, delayParamValue);
|
||||
|
||||
// Apply our gain change to the outgoing data..
|
||||
applyGain (buffer, delayBuffer, gainParamValue);
|
||||
|
||||
// Now ask the host for the current time so we can store it to be displayed later...
|
||||
updateCurrentTimeInfoFromHost();
|
||||
}
|
||||
|
||||
template <typename FloatType>
|
||||
void applyGain (AudioBuffer<FloatType>& buffer, AudioBuffer<FloatType>& delayBuffer, float gainLevel)
|
||||
{
|
||||
ignoreUnused (delayBuffer);
|
||||
|
||||
for (auto channel = 0; channel < getTotalNumOutputChannels(); ++channel)
|
||||
buffer.applyGain (channel, 0, buffer.getNumSamples(), gainLevel);
|
||||
}
|
||||
|
||||
template <typename FloatType>
|
||||
void applyDelay (AudioBuffer<FloatType>& buffer, AudioBuffer<FloatType>& delayBuffer, float delayLevel)
|
||||
{
|
||||
auto numSamples = buffer.getNumSamples();
|
||||
|
||||
auto delayPos = 0;
|
||||
|
||||
for (auto channel = 0; channel < getTotalNumOutputChannels(); ++channel)
|
||||
{
|
||||
auto channelData = buffer.getWritePointer (channel);
|
||||
auto delayData = delayBuffer.getWritePointer (jmin (channel, delayBuffer.getNumChannels() - 1));
|
||||
delayPos = delayPosition;
|
||||
|
||||
for (auto i = 0; i < numSamples; ++i)
|
||||
{
|
||||
auto in = channelData[i];
|
||||
channelData[i] += delayData[delayPos];
|
||||
delayData[delayPos] = (delayData[delayPos] + in) * delayLevel;
|
||||
|
||||
if (++delayPos >= delayBuffer.getNumSamples())
|
||||
delayPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
delayPosition = delayPos;
|
||||
}
|
||||
|
||||
AudioBuffer<float> delayBufferFloat;
|
||||
AudioBuffer<double> delayBufferDouble;
|
||||
|
||||
int delayPosition = 0;
|
||||
|
||||
Synthesiser synth;
|
||||
|
||||
CriticalSection trackPropertiesLock;
|
||||
TrackProperties trackProperties;
|
||||
|
||||
void initialiseSynth()
|
||||
{
|
||||
auto numVoices = 8;
|
||||
|
||||
// Add some voices...
|
||||
for (auto i = 0; i < numVoices; ++i)
|
||||
synth.addVoice (new SineWaveVoice());
|
||||
|
||||
// ..and give the synth a sound to play
|
||||
synth.addSound (new SineWaveSound());
|
||||
}
|
||||
|
||||
void updateCurrentTimeInfoFromHost()
|
||||
{
|
||||
const auto newInfo = [&]
|
||||
{
|
||||
if (auto* ph = getPlayHead())
|
||||
if (auto result = ph->getPosition())
|
||||
return *result;
|
||||
|
||||
// If the host fails to provide the current time, we'll just use default values
|
||||
return AudioPlayHead::PositionInfo{};
|
||||
}();
|
||||
|
||||
lastPosInfo.set (newInfo);
|
||||
}
|
||||
|
||||
static BusesProperties getBusesProperties()
|
||||
{
|
||||
return BusesProperties().withInput ("Input", AudioChannelSet::stereo(), true)
|
||||
.withOutput ("Output", AudioChannelSet::stereo(), true);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (JuceDemoPluginAudioProcessor)
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
# ==============================================================================
|
||||
#
|
||||
# This file is part of the JUCE library.
|
||||
# Copyright (c) 2022 - Raw Material Software Limited
|
||||
#
|
||||
# JUCE is an open source library subject to commercial or open-source
|
||||
# licensing.
|
||||
#
|
||||
# By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
# Agreement and JUCE Privacy Policy.
|
||||
#
|
||||
# End User License Agreement: www.juce.com/juce-7-licence
|
||||
# Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
#
|
||||
# Or: You may also use this code under the terms of the GPL v3 (see
|
||||
# www.gnu.org/licenses).
|
||||
#
|
||||
# JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
# DISCLAIMED.
|
||||
#
|
||||
# ==============================================================================
|
||||
|
||||
_juce_add_pips()
|
||||
+2099
File diff suppressed because it is too large
Load Diff
+122
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: GainPlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Gain audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: GainProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class GainProcessor final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
|
||||
//==============================================================================
|
||||
GainProcessor()
|
||||
: AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo())
|
||||
.withOutput ("Output", AudioChannelSet::stereo()))
|
||||
{
|
||||
addParameter (gain = new AudioParameterFloat ({ "gain", 1 }, "Gain", 0.0f, 1.0f, 0.5f));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (double, int) override {}
|
||||
void releaseResources() override {}
|
||||
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
|
||||
{
|
||||
buffer.applyGain (*gain);
|
||||
}
|
||||
|
||||
void processBlock (AudioBuffer<double>& buffer, MidiBuffer&) override
|
||||
{
|
||||
buffer.applyGain ((float) *gain);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new GenericAudioProcessorEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "Gain PlugIn"; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0; }
|
||||
|
||||
//==============================================================================
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
MemoryOutputStream (destData, true).writeFloat (*gain);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
gain->setValueNotifyingHost (MemoryInputStream (data, static_cast<size_t> (sizeInBytes), false).readFloat());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
||||
{
|
||||
const auto& mainInLayout = layouts.getChannelSet (true, 0);
|
||||
const auto& mainOutLayout = layouts.getChannelSet (false, 0);
|
||||
|
||||
return (mainInLayout == mainOutLayout && (! mainInLayout.isDisabled()));
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioParameterFloat* gain;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GainProcessor)
|
||||
};
|
||||
+584
@@ -0,0 +1,584 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: HostPluginDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Plugin that can host other plugins
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022, linux_make
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
JUCE_PLUGINHOST_LV2=1
|
||||
JUCE_PLUGINHOST_VST3=1
|
||||
JUCE_PLUGINHOST_VST=0
|
||||
JUCE_PLUGINHOST_AU=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: HostAudioProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn, pluginProducesMidiOut,
|
||||
pluginEditorRequiresKeys
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
//==============================================================================
|
||||
enum class EditorStyle { thisWindow, newWindow };
|
||||
|
||||
class HostAudioProcessorImpl : public AudioProcessor,
|
||||
private ChangeListener
|
||||
{
|
||||
public:
|
||||
HostAudioProcessorImpl()
|
||||
: AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo(), true)
|
||||
.withOutput ("Output", AudioChannelSet::stereo(), true))
|
||||
{
|
||||
appProperties.setStorageParameters ([&]
|
||||
{
|
||||
PropertiesFile::Options opt;
|
||||
opt.applicationName = getName();
|
||||
opt.commonToAllUsers = false;
|
||||
opt.doNotSave = false;
|
||||
opt.filenameSuffix = ".props";
|
||||
opt.ignoreCaseOfKeyNames = false;
|
||||
opt.storageFormat = PropertiesFile::StorageFormat::storeAsXML;
|
||||
opt.osxLibrarySubFolder = "Application Support";
|
||||
return opt;
|
||||
}());
|
||||
|
||||
pluginFormatManager.addDefaultFormats();
|
||||
|
||||
if (auto savedPluginList = appProperties.getUserSettings()->getXmlValue ("pluginList"))
|
||||
pluginList.recreateFromXml (*savedPluginList);
|
||||
|
||||
MessageManagerLock lock;
|
||||
pluginList.addChangeListener (this);
|
||||
}
|
||||
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const final
|
||||
{
|
||||
const auto& mainOutput = layouts.getMainOutputChannelSet();
|
||||
const auto& mainInput = layouts.getMainInputChannelSet();
|
||||
|
||||
if (! mainInput.isDisabled() && mainInput != mainOutput)
|
||||
return false;
|
||||
|
||||
if (mainOutput.size() > 2)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void prepareToPlay (double sr, int bs) final
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
active = true;
|
||||
|
||||
if (inner != nullptr)
|
||||
{
|
||||
inner->setRateAndBufferSizeDetails (sr, bs);
|
||||
inner->prepareToPlay (sr, bs);
|
||||
}
|
||||
}
|
||||
|
||||
void releaseResources() final
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
active = false;
|
||||
|
||||
if (inner != nullptr)
|
||||
inner->releaseResources();
|
||||
}
|
||||
|
||||
void reset() final
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
if (inner != nullptr)
|
||||
inner->reset();
|
||||
}
|
||||
|
||||
// In this example, we don't actually pass any audio through the inner processor.
|
||||
// In a 'real' plugin, we'd need to add some synchronisation to ensure that the inner
|
||||
// plugin instance was never modified (deleted, replaced etc.) during a call to processBlock.
|
||||
void processBlock (AudioBuffer<float>&, MidiBuffer&) final
|
||||
{
|
||||
jassert (! isUsingDoublePrecision());
|
||||
}
|
||||
|
||||
void processBlock (AudioBuffer<double>&, MidiBuffer&) final
|
||||
{
|
||||
jassert (isUsingDoublePrecision());
|
||||
}
|
||||
|
||||
bool hasEditor() const override { return false; }
|
||||
AudioProcessorEditor* createEditor() override { return nullptr; }
|
||||
|
||||
const String getName() const final { return "HostPluginDemo"; }
|
||||
bool acceptsMidi() const final { return true; }
|
||||
bool producesMidi() const final { return true; }
|
||||
double getTailLengthSeconds() const final { return 0.0; }
|
||||
|
||||
int getNumPrograms() final { return 0; }
|
||||
int getCurrentProgram() final { return 0; }
|
||||
void setCurrentProgram (int) final {}
|
||||
const String getProgramName (int) final { return "None"; }
|
||||
void changeProgramName (int, const String&) final {}
|
||||
|
||||
void getStateInformation (MemoryBlock& destData) final
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
XmlElement xml ("state");
|
||||
|
||||
if (inner != nullptr)
|
||||
{
|
||||
xml.setAttribute (editorStyleTag, (int) editorStyle);
|
||||
xml.addChildElement (inner->getPluginDescription().createXml().release());
|
||||
xml.addChildElement ([this]
|
||||
{
|
||||
MemoryBlock innerState;
|
||||
inner->getStateInformation (innerState);
|
||||
|
||||
auto stateNode = std::make_unique<XmlElement> (innerStateTag);
|
||||
stateNode->addTextElement (innerState.toBase64Encoding());
|
||||
return stateNode.release();
|
||||
}());
|
||||
}
|
||||
|
||||
const auto text = xml.toString();
|
||||
destData.replaceAll (text.toRawUTF8(), text.getNumBytesAsUTF8());
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) final
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
auto xml = XmlDocument::parse (String (CharPointer_UTF8 (static_cast<const char*> (data)), (size_t) sizeInBytes));
|
||||
|
||||
if (auto* pluginNode = xml->getChildByName ("PLUGIN"))
|
||||
{
|
||||
PluginDescription pd;
|
||||
pd.loadFromXml (*pluginNode);
|
||||
|
||||
MemoryBlock innerState;
|
||||
innerState.fromBase64Encoding (xml->getChildElementAllSubText (innerStateTag, {}));
|
||||
|
||||
setNewPlugin (pd,
|
||||
(EditorStyle) xml->getIntAttribute (editorStyleTag, 0),
|
||||
innerState);
|
||||
}
|
||||
}
|
||||
|
||||
void setNewPlugin (const PluginDescription& pd, EditorStyle where, const MemoryBlock& mb = {})
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
const auto callback = [this, where, mb] (std::unique_ptr<AudioPluginInstance> instance, const String& error)
|
||||
{
|
||||
if (error.isNotEmpty())
|
||||
{
|
||||
auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
|
||||
"Plugin Load Failed",
|
||||
error);
|
||||
messageBox = AlertWindow::showScopedAsync (options, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
inner = std::move (instance);
|
||||
editorStyle = where;
|
||||
|
||||
if (inner != nullptr && ! mb.isEmpty())
|
||||
inner->setStateInformation (mb.getData(), (int) mb.getSize());
|
||||
|
||||
// In a 'real' plugin, we'd also need to set the bus configuration of the inner plugin.
|
||||
// One possibility would be to match the bus configuration of the wrapper plugin, but
|
||||
// the inner plugin isn't guaranteed to support the same layout. Alternatively, we
|
||||
// could try to apply a reasonably similar layout, and maintain a mapping between the
|
||||
// inner/outer channel layouts.
|
||||
//
|
||||
// In any case, it is essential that the inner plugin is told about the bus
|
||||
// configuration that will be used. The AudioBuffer passed to the inner plugin must also
|
||||
// exactly match this layout.
|
||||
|
||||
if (active)
|
||||
{
|
||||
inner->setRateAndBufferSizeDetails (getSampleRate(), getBlockSize());
|
||||
inner->prepareToPlay (getSampleRate(), getBlockSize());
|
||||
}
|
||||
|
||||
NullCheckedInvocation::invoke (pluginChanged);
|
||||
};
|
||||
|
||||
pluginFormatManager.createPluginInstanceAsync (pd, getSampleRate(), getBlockSize(), callback);
|
||||
}
|
||||
|
||||
void clearPlugin()
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
|
||||
inner = nullptr;
|
||||
NullCheckedInvocation::invoke (pluginChanged);
|
||||
}
|
||||
|
||||
bool isPluginLoaded() const
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
return inner != nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioProcessorEditor> createInnerEditor() const
|
||||
{
|
||||
const ScopedLock sl (innerMutex);
|
||||
return rawToUniquePtr (inner->hasEditor() ? inner->createEditorIfNeeded() : nullptr);
|
||||
}
|
||||
|
||||
EditorStyle getEditorStyle() const noexcept { return editorStyle; }
|
||||
|
||||
ApplicationProperties appProperties;
|
||||
AudioPluginFormatManager pluginFormatManager;
|
||||
KnownPluginList pluginList;
|
||||
std::function<void()> pluginChanged;
|
||||
|
||||
private:
|
||||
CriticalSection innerMutex;
|
||||
std::unique_ptr<AudioPluginInstance> inner;
|
||||
EditorStyle editorStyle = EditorStyle{};
|
||||
bool active = false;
|
||||
ScopedMessageBox messageBox;
|
||||
|
||||
static constexpr const char* innerStateTag = "inner_state";
|
||||
static constexpr const char* editorStyleTag = "editor_style";
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster* source) final
|
||||
{
|
||||
if (source != &pluginList)
|
||||
return;
|
||||
|
||||
if (auto savedPluginList = pluginList.createXml())
|
||||
{
|
||||
appProperties.getUserSettings()->setValue ("pluginList", savedPluginList.get());
|
||||
appProperties.saveIfNeeded();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
constexpr auto margin = 10;
|
||||
|
||||
static void doLayout (Component* main, Component& bottom, int bottomHeight, Rectangle<int> bounds)
|
||||
{
|
||||
Grid grid;
|
||||
grid.setGap (Grid::Px { margin });
|
||||
grid.templateColumns = { Grid::TrackInfo { Grid::Fr { 1 } } };
|
||||
grid.templateRows = { Grid::TrackInfo { Grid::Fr { 1 } },
|
||||
Grid::TrackInfo { Grid::Px { bottomHeight }} };
|
||||
grid.items = { GridItem { main }, GridItem { bottom }.withMargin ({ 0, margin, margin, margin }) };
|
||||
grid.performLayout (bounds);
|
||||
}
|
||||
|
||||
class PluginLoaderComponent final : public Component
|
||||
{
|
||||
public:
|
||||
template <typename Callback>
|
||||
PluginLoaderComponent (AudioPluginFormatManager& manager,
|
||||
KnownPluginList& list,
|
||||
Callback&& callback)
|
||||
: pluginListComponent (manager, list, {}, {})
|
||||
{
|
||||
pluginListComponent.getTableListBox().setMultipleSelectionEnabled (false);
|
||||
|
||||
addAndMakeVisible (pluginListComponent);
|
||||
addAndMakeVisible (buttons);
|
||||
|
||||
const auto getCallback = [this, &list, cb = std::forward<Callback> (callback)] (EditorStyle style)
|
||||
{
|
||||
return [this, &list, cb, style]
|
||||
{
|
||||
const auto index = pluginListComponent.getTableListBox().getSelectedRow();
|
||||
const auto& types = list.getTypes();
|
||||
|
||||
if (isPositiveAndBelow (index, types.size()))
|
||||
NullCheckedInvocation::invoke (cb, types.getReference (index), style);
|
||||
};
|
||||
};
|
||||
|
||||
buttons.thisWindowButton.onClick = getCallback (EditorStyle::thisWindow);
|
||||
buttons.newWindowButton .onClick = getCallback (EditorStyle::newWindow);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
doLayout (&pluginListComponent, buttons, 80, getLocalBounds());
|
||||
}
|
||||
|
||||
private:
|
||||
struct Buttons final : public Component
|
||||
{
|
||||
Buttons()
|
||||
{
|
||||
label.setJustificationType (Justification::centred);
|
||||
|
||||
addAndMakeVisible (label);
|
||||
addAndMakeVisible (thisWindowButton);
|
||||
addAndMakeVisible (newWindowButton);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
Grid vertical;
|
||||
vertical.autoFlow = Grid::AutoFlow::row;
|
||||
vertical.setGap (Grid::Px { margin });
|
||||
vertical.autoRows = vertical.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
|
||||
vertical.items.insertMultiple (0, GridItem{}, 2);
|
||||
vertical.performLayout (getLocalBounds());
|
||||
|
||||
label.setBounds (vertical.items[0].currentBounds.toNearestInt());
|
||||
|
||||
Grid grid;
|
||||
grid.autoFlow = Grid::AutoFlow::column;
|
||||
grid.setGap (Grid::Px { margin });
|
||||
grid.autoRows = grid.autoColumns = Grid::TrackInfo { Grid::Fr { 1 } };
|
||||
grid.items = { GridItem { thisWindowButton },
|
||||
GridItem { newWindowButton } };
|
||||
|
||||
grid.performLayout (vertical.items[1].currentBounds.toNearestInt());
|
||||
}
|
||||
|
||||
Label label { "", "Select a plugin from the list, then display it using the buttons below." };
|
||||
TextButton thisWindowButton { "Open In This Window" };
|
||||
TextButton newWindowButton { "Open In New Window" };
|
||||
};
|
||||
|
||||
PluginListComponent pluginListComponent;
|
||||
Buttons buttons;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class PluginEditorComponent final : public Component
|
||||
{
|
||||
public:
|
||||
template <typename Callback>
|
||||
PluginEditorComponent (std::unique_ptr<AudioProcessorEditor> editorIn, Callback&& onClose)
|
||||
: editor (std::move (editorIn))
|
||||
{
|
||||
addAndMakeVisible (editor.get());
|
||||
addAndMakeVisible (closeButton);
|
||||
|
||||
childBoundsChanged (editor.get());
|
||||
|
||||
closeButton.onClick = std::forward<Callback> (onClose);
|
||||
}
|
||||
|
||||
void setScaleFactor (float scale)
|
||||
{
|
||||
if (editor != nullptr)
|
||||
editor->setScaleFactor (scale);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
doLayout (editor.get(), closeButton, buttonHeight, getLocalBounds());
|
||||
}
|
||||
|
||||
void childBoundsChanged (Component* child) override
|
||||
{
|
||||
if (child != editor.get())
|
||||
return;
|
||||
|
||||
const auto size = editor != nullptr ? editor->getLocalBounds()
|
||||
: Rectangle<int>();
|
||||
|
||||
setSize (size.getWidth(), margin + buttonHeight + size.getHeight());
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr auto buttonHeight = 40;
|
||||
|
||||
std::unique_ptr<AudioProcessorEditor> editor;
|
||||
TextButton closeButton { "Close Plugin" };
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ScaledDocumentWindow final : public DocumentWindow
|
||||
{
|
||||
public:
|
||||
ScaledDocumentWindow (Colour bg, float scale)
|
||||
: DocumentWindow ("Editor", bg, 0), desktopScale (scale) {}
|
||||
|
||||
float getDesktopScaleFactor() const override { return Desktop::getInstance().getGlobalScaleFactor() * desktopScale; }
|
||||
|
||||
private:
|
||||
float desktopScale = 1.0f;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class HostAudioProcessorEditor final : public AudioProcessorEditor
|
||||
{
|
||||
public:
|
||||
explicit HostAudioProcessorEditor (HostAudioProcessorImpl& owner)
|
||||
: AudioProcessorEditor (owner),
|
||||
hostProcessor (owner),
|
||||
loader (owner.pluginFormatManager,
|
||||
owner.pluginList,
|
||||
[&owner] (const PluginDescription& pd,
|
||||
EditorStyle editorStyle)
|
||||
{
|
||||
owner.setNewPlugin (pd, editorStyle);
|
||||
}),
|
||||
scopedCallback (owner.pluginChanged, [this] { pluginChanged(); })
|
||||
{
|
||||
setSize (500, 500);
|
||||
setResizable (false, false);
|
||||
addAndMakeVisible (closeButton);
|
||||
addAndMakeVisible (loader);
|
||||
|
||||
hostProcessor.pluginChanged();
|
||||
|
||||
closeButton.onClick = [this] { clearPlugin(); };
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker());
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
closeButton.setBounds (getLocalBounds().withSizeKeepingCentre (200, buttonHeight));
|
||||
loader.setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
void childBoundsChanged (Component* child) override
|
||||
{
|
||||
if (child != editor.get())
|
||||
return;
|
||||
|
||||
const auto size = editor != nullptr ? editor->getLocalBounds()
|
||||
: Rectangle<int>();
|
||||
|
||||
setSize (size.getWidth(), size.getHeight());
|
||||
}
|
||||
|
||||
void setScaleFactor (float scale) override
|
||||
{
|
||||
currentScaleFactor = scale;
|
||||
AudioProcessorEditor::setScaleFactor (scale);
|
||||
|
||||
[[maybe_unused]] const auto posted = MessageManager::callAsync ([ref = SafePointer<HostAudioProcessorEditor> (this), scale]
|
||||
{
|
||||
if (auto* r = ref.getComponent())
|
||||
if (auto* e = r->currentEditorComponent)
|
||||
e->setScaleFactor (scale);
|
||||
});
|
||||
|
||||
jassert (posted);
|
||||
}
|
||||
|
||||
private:
|
||||
void pluginChanged()
|
||||
{
|
||||
loader.setVisible (! hostProcessor.isPluginLoaded());
|
||||
closeButton.setVisible (hostProcessor.isPluginLoaded());
|
||||
|
||||
if (hostProcessor.isPluginLoaded())
|
||||
{
|
||||
auto editorComponent = std::make_unique<PluginEditorComponent> (hostProcessor.createInnerEditor(), [this]
|
||||
{
|
||||
[[maybe_unused]] const auto posted = MessageManager::callAsync ([this] { clearPlugin(); });
|
||||
jassert (posted);
|
||||
});
|
||||
|
||||
editorComponent->setScaleFactor (currentScaleFactor);
|
||||
currentEditorComponent = editorComponent.get();
|
||||
|
||||
editor = [&]() -> std::unique_ptr<Component>
|
||||
{
|
||||
switch (hostProcessor.getEditorStyle())
|
||||
{
|
||||
case EditorStyle::thisWindow:
|
||||
addAndMakeVisible (editorComponent.get());
|
||||
setSize (editorComponent->getWidth(), editorComponent->getHeight());
|
||||
return std::move (editorComponent);
|
||||
|
||||
case EditorStyle::newWindow:
|
||||
const auto bg = getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker();
|
||||
auto window = std::make_unique<ScaledDocumentWindow> (bg, currentScaleFactor);
|
||||
window->setAlwaysOnTop (true);
|
||||
window->setContentOwned (editorComponent.release(), true);
|
||||
window->centreAroundComponent (this, window->getWidth(), window->getHeight());
|
||||
window->setVisible (true);
|
||||
return window;
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return nullptr;
|
||||
}();
|
||||
}
|
||||
else
|
||||
{
|
||||
editor = nullptr;
|
||||
setSize (500, 500);
|
||||
}
|
||||
}
|
||||
|
||||
void clearPlugin()
|
||||
{
|
||||
currentEditorComponent = nullptr;
|
||||
editor = nullptr;
|
||||
hostProcessor.clearPlugin();
|
||||
}
|
||||
|
||||
static constexpr auto buttonHeight = 30;
|
||||
|
||||
HostAudioProcessorImpl& hostProcessor;
|
||||
PluginLoaderComponent loader;
|
||||
std::unique_ptr<Component> editor;
|
||||
PluginEditorComponent* currentEditorComponent = nullptr;
|
||||
ScopedValueSetter<std::function<void()>> scopedCallback;
|
||||
TextButton closeButton { "Close Plugin" };
|
||||
float currentScaleFactor = 1.0f;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class HostAudioProcessor final : public HostAudioProcessorImpl
|
||||
{
|
||||
public:
|
||||
bool hasEditor() const override { return true; }
|
||||
AudioProcessorEditor* createEditor() override { return new HostAudioProcessorEditor (*this); }
|
||||
};
|
||||
@@ -0,0 +1,351 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: MIDILogger
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Logs incoming MIDI messages.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022, linux_make
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: MidiLoggerPluginDemoProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginWantsMidiIn, pluginProducesMidiOut, pluginIsMidiEffectPlugin
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iterator>
|
||||
|
||||
class MidiQueue
|
||||
{
|
||||
public:
|
||||
void push (const MidiBuffer& buffer)
|
||||
{
|
||||
for (const auto metadata : buffer)
|
||||
fifo.write (1).forEach ([&] (int dest) { messages[(size_t) dest] = metadata.getMessage(); });
|
||||
}
|
||||
|
||||
template <typename OutputIt>
|
||||
void pop (OutputIt out)
|
||||
{
|
||||
fifo.read (fifo.getNumReady()).forEach ([&] (int source) { *out++ = messages[(size_t) source]; });
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr auto queueSize = 1 << 14;
|
||||
AbstractFifo fifo { queueSize };
|
||||
std::vector<MidiMessage> messages = std::vector<MidiMessage> (queueSize);
|
||||
};
|
||||
|
||||
// Stores the last N messages. Safe to access from the message thread only.
|
||||
class MidiListModel
|
||||
{
|
||||
public:
|
||||
template <typename It>
|
||||
void addMessages (It begin, It end)
|
||||
{
|
||||
if (begin == end)
|
||||
return;
|
||||
|
||||
const auto numNewMessages = (int) std::distance (begin, end);
|
||||
const auto numToAdd = juce::jmin (numToStore, numNewMessages);
|
||||
const auto numToRemove = jmax (0, (int) messages.size() + numToAdd - numToStore);
|
||||
messages.erase (messages.begin(), std::next (messages.begin(), numToRemove));
|
||||
messages.insert (messages.end(), std::prev (end, numToAdd), end);
|
||||
|
||||
NullCheckedInvocation::invoke (onChange);
|
||||
}
|
||||
|
||||
void clear()
|
||||
{
|
||||
messages.clear();
|
||||
|
||||
NullCheckedInvocation::invoke (onChange);
|
||||
}
|
||||
|
||||
const MidiMessage& operator[] (size_t ind) const { return messages[ind]; }
|
||||
|
||||
size_t size() const { return messages.size(); }
|
||||
|
||||
std::function<void()> onChange;
|
||||
|
||||
private:
|
||||
static constexpr auto numToStore = 1000;
|
||||
std::vector<MidiMessage> messages;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MidiTable final : public Component,
|
||||
private TableListBoxModel
|
||||
{
|
||||
public:
|
||||
MidiTable (MidiListModel& m)
|
||||
: messages (m)
|
||||
{
|
||||
addAndMakeVisible (table);
|
||||
|
||||
table.setModel (this);
|
||||
table.setClickingTogglesRowSelection (false);
|
||||
table.setHeader ([&]
|
||||
{
|
||||
auto header = std::make_unique<TableHeaderComponent>();
|
||||
header->addColumn ("Message", messageColumn, 200, 30, -1, TableHeaderComponent::notSortable);
|
||||
header->addColumn ("Time", timeColumn, 100, 30, -1, TableHeaderComponent::notSortable);
|
||||
header->addColumn ("Channel", channelColumn, 100, 30, -1, TableHeaderComponent::notSortable);
|
||||
header->addColumn ("Data", dataColumn, 200, 30, -1, TableHeaderComponent::notSortable);
|
||||
return header;
|
||||
}());
|
||||
|
||||
messages.onChange = [&] { table.updateContent(); };
|
||||
}
|
||||
|
||||
~MidiTable() override { messages.onChange = nullptr; }
|
||||
|
||||
void resized() override { table.setBounds (getLocalBounds()); }
|
||||
|
||||
private:
|
||||
enum
|
||||
{
|
||||
messageColumn = 1,
|
||||
timeColumn,
|
||||
channelColumn,
|
||||
dataColumn
|
||||
};
|
||||
|
||||
int getNumRows() override { return (int) messages.size(); }
|
||||
|
||||
void paintRowBackground (Graphics&, int, int, int, bool) override {}
|
||||
void paintCell (Graphics&, int, int, int, int, bool) override {}
|
||||
|
||||
Component* refreshComponentForCell (int rowNumber,
|
||||
int columnId,
|
||||
bool,
|
||||
Component* existingComponentToUpdate) override
|
||||
{
|
||||
delete existingComponentToUpdate;
|
||||
|
||||
const auto index = (int) messages.size() - 1 - rowNumber;
|
||||
const auto message = messages[(size_t) index];
|
||||
|
||||
return new Label ({}, [&]
|
||||
{
|
||||
switch (columnId)
|
||||
{
|
||||
case messageColumn: return getEventString (message);
|
||||
case timeColumn: return String (message.getTimeStamp());
|
||||
case channelColumn: return String (message.getChannel());
|
||||
case dataColumn: return getDataString (message);
|
||||
default: break;
|
||||
}
|
||||
|
||||
jassertfalse;
|
||||
return String();
|
||||
}());
|
||||
}
|
||||
|
||||
static String getEventString (const MidiMessage& m)
|
||||
{
|
||||
if (m.isNoteOn()) return "Note on";
|
||||
if (m.isNoteOff()) return "Note off";
|
||||
if (m.isProgramChange()) return "Program change";
|
||||
if (m.isPitchWheel()) return "Pitch wheel";
|
||||
if (m.isAftertouch()) return "Aftertouch";
|
||||
if (m.isChannelPressure()) return "Channel pressure";
|
||||
if (m.isAllNotesOff()) return "All notes off";
|
||||
if (m.isAllSoundOff()) return "All sound off";
|
||||
if (m.isMetaEvent()) return "Meta event";
|
||||
|
||||
if (m.isController())
|
||||
{
|
||||
const auto* name = MidiMessage::getControllerName (m.getControllerNumber());
|
||||
return "Controller " + (name == nullptr ? String (m.getControllerNumber()) : String (name));
|
||||
}
|
||||
|
||||
return String::toHexString (m.getRawData(), m.getRawDataSize());
|
||||
}
|
||||
|
||||
static String getDataString (const MidiMessage& m)
|
||||
{
|
||||
if (m.isNoteOn()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
|
||||
if (m.isNoteOff()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + " Velocity " + String (m.getVelocity());
|
||||
if (m.isProgramChange()) return String (m.getProgramChangeNumber());
|
||||
if (m.isPitchWheel()) return String (m.getPitchWheelValue());
|
||||
if (m.isAftertouch()) return MidiMessage::getMidiNoteName (m.getNoteNumber(), true, true, 3) + ": " + String (m.getAfterTouchValue());
|
||||
if (m.isChannelPressure()) return String (m.getChannelPressureValue());
|
||||
if (m.isController()) return String (m.getControllerValue());
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
MidiListModel& messages;
|
||||
TableListBox table;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MidiLoggerPluginDemoProcessor final : public AudioProcessor,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
MidiLoggerPluginDemoProcessor()
|
||||
: AudioProcessor (getBusesLayout())
|
||||
{
|
||||
state.addChild ({ "uiState", { { "width", 600 }, { "height", 300 } }, {} }, -1, nullptr);
|
||||
startTimerHz (60);
|
||||
}
|
||||
|
||||
~MidiLoggerPluginDemoProcessor() override { stopTimer(); }
|
||||
|
||||
void processBlock (AudioBuffer<float>& audio, MidiBuffer& midi) override { process (audio, midi); }
|
||||
void processBlock (AudioBuffer<double>& audio, MidiBuffer& midi) override { process (audio, midi); }
|
||||
|
||||
bool isBusesLayoutSupported (const BusesLayout&) const override { return true; }
|
||||
bool isMidiEffect() const override { return true; }
|
||||
bool hasEditor() const override { return true; }
|
||||
AudioProcessorEditor* createEditor() override { return new Editor (*this); }
|
||||
|
||||
const String getName() const override { return "MIDI Logger"; }
|
||||
bool acceptsMidi() const override { return true; }
|
||||
bool producesMidi() const override { return true; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
int getNumPrograms() override { return 0; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
void prepareToPlay (double, int) override {}
|
||||
void releaseResources() override {}
|
||||
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
if (auto xmlState = state.createXml())
|
||||
copyXmlToBinary (*xmlState, destData);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int size) override
|
||||
{
|
||||
if (auto xmlState = getXmlFromBinary (data, size))
|
||||
state = ValueTree::fromXml (*xmlState);
|
||||
}
|
||||
|
||||
private:
|
||||
class Editor final : public AudioProcessorEditor,
|
||||
private Value::Listener
|
||||
{
|
||||
public:
|
||||
explicit Editor (MidiLoggerPluginDemoProcessor& ownerIn)
|
||||
: AudioProcessorEditor (ownerIn),
|
||||
owner (ownerIn),
|
||||
table (owner.model)
|
||||
{
|
||||
addAndMakeVisible (table);
|
||||
addAndMakeVisible (clearButton);
|
||||
|
||||
setResizable (true, true);
|
||||
lastUIWidth .referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("width", nullptr));
|
||||
lastUIHeight.referTo (owner.state.getChildWithName ("uiState").getPropertyAsValue ("height", nullptr));
|
||||
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
||||
|
||||
lastUIWidth. addListener (this);
|
||||
lastUIHeight.addListener (this);
|
||||
|
||||
clearButton.onClick = [&] { owner.model.clear(); };
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto bounds = getLocalBounds();
|
||||
|
||||
clearButton.setBounds (bounds.removeFromBottom (30).withSizeKeepingCentre (50, 24));
|
||||
table.setBounds (bounds);
|
||||
|
||||
lastUIWidth = getWidth();
|
||||
lastUIHeight = getHeight();
|
||||
}
|
||||
|
||||
private:
|
||||
void valueChanged (Value&) override
|
||||
{
|
||||
setSize (lastUIWidth.getValue(), lastUIHeight.getValue());
|
||||
}
|
||||
|
||||
MidiLoggerPluginDemoProcessor& owner;
|
||||
|
||||
MidiTable table;
|
||||
TextButton clearButton { "Clear" };
|
||||
|
||||
Value lastUIWidth, lastUIHeight;
|
||||
};
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
std::vector<MidiMessage> messages;
|
||||
queue.pop (std::back_inserter (messages));
|
||||
model.addMessages (messages.begin(), messages.end());
|
||||
}
|
||||
|
||||
template <typename Element>
|
||||
void process (AudioBuffer<Element>& audio, MidiBuffer& midi)
|
||||
{
|
||||
audio.clear();
|
||||
queue.push (midi);
|
||||
}
|
||||
|
||||
static BusesProperties getBusesLayout()
|
||||
{
|
||||
// Live and Cakewalk don't like to load midi-only plugins, so we add an audio output there.
|
||||
const PluginHostType host;
|
||||
return host.isAbletonLive() || host.isSonar()
|
||||
? BusesProperties().withOutput ("out", AudioChannelSet::stereo())
|
||||
: BusesProperties();
|
||||
}
|
||||
|
||||
ValueTree state { "state" };
|
||||
MidiQueue queue;
|
||||
MidiListModel model; // The data to show in the UI. We keep it around in the processor so that
|
||||
// the view is persistent even when the plugin UI is closed and reopened.
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiLoggerPluginDemoProcessor)
|
||||
};
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: MultiOutSynthPlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Multi-out synthesiser audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: MultiOutSynth
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
|
||||
//==============================================================================
|
||||
class MultiOutSynth final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
enum
|
||||
{
|
||||
maxMidiChannel = 16,
|
||||
maxNumberOfVoices = 5
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
MultiOutSynth()
|
||||
: AudioProcessor (BusesProperties()
|
||||
.withOutput ("Output #1", AudioChannelSet::stereo(), true)
|
||||
.withOutput ("Output #2", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #3", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #4", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #5", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #6", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #7", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #8", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #9", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #10", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #11", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #12", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #13", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #14", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #15", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output #16", AudioChannelSet::stereo(), false))
|
||||
{
|
||||
// initialize other stuff (not related to buses)
|
||||
formatManager.registerBasicFormats();
|
||||
|
||||
for (int midiChannel = 0; midiChannel < maxMidiChannel; ++midiChannel)
|
||||
{
|
||||
synth.add (new Synthesiser());
|
||||
|
||||
for (int i = 0; i < maxNumberOfVoices; ++i)
|
||||
synth[midiChannel]->addVoice (new SamplerVoice());
|
||||
}
|
||||
|
||||
loadNewSample (createAssetInputStream ("singing.ogg"), "ogg");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool canAddBus (bool isInput) const override { return ! isInput; }
|
||||
bool canRemoveBus (bool isInput) const override { return ! isInput; }
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (double newSampleRate, int samplesPerBlock) override
|
||||
{
|
||||
ignoreUnused (samplesPerBlock);
|
||||
|
||||
for (auto* s : synth)
|
||||
s->setCurrentPlaybackSampleRate (newSampleRate);
|
||||
}
|
||||
|
||||
void releaseResources() override {}
|
||||
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiBuffer) override
|
||||
{
|
||||
auto busCount = getBusCount (false);
|
||||
|
||||
for (auto busNr = 0; busNr < busCount; ++busNr)
|
||||
{
|
||||
if (synth.size() <= busNr)
|
||||
continue;
|
||||
|
||||
auto midiChannelBuffer = filterMidiMessagesForChannel (midiBuffer, busNr + 1);
|
||||
auto audioBusBuffer = getBusBuffer (buffer, false, busNr);
|
||||
|
||||
// Voices add to the contents of the buffer. Make sure the buffer is clear before
|
||||
// rendering, just in case the host left old data in the buffer.
|
||||
audioBusBuffer.clear();
|
||||
|
||||
synth [busNr]->renderNextBlock (audioBusBuffer, midiChannelBuffer, 0, audioBusBuffer.getNumSamples());
|
||||
}
|
||||
}
|
||||
|
||||
using AudioProcessor::processBlock;
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new GenericAudioProcessorEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "Multi Out Synth PlugIn"; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0; }
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
bool isBusesLayoutSupported (const BusesLayout& layout) const override
|
||||
{
|
||||
const auto& outputs = layout.outputBuses;
|
||||
|
||||
return layout.inputBuses.isEmpty()
|
||||
&& 1 <= outputs.size()
|
||||
&& std::all_of (outputs.begin(), outputs.end(), [] (const auto& bus)
|
||||
{
|
||||
return bus.isDisabled() || bus == AudioChannelSet::stereo();
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock&) override {}
|
||||
void setStateInformation (const void*, int) override {}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
static MidiBuffer filterMidiMessagesForChannel (const MidiBuffer& input, int channel)
|
||||
{
|
||||
MidiBuffer output;
|
||||
|
||||
for (const auto metadata : input)
|
||||
{
|
||||
const auto message = metadata.getMessage();
|
||||
|
||||
if (message.getChannel() == channel)
|
||||
output.addEvent (message, metadata.samplePosition);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
void loadNewSample (std::unique_ptr<InputStream> soundBuffer, const char* format)
|
||||
{
|
||||
std::unique_ptr<AudioFormatReader> formatReader (formatManager.findFormatForFileExtension (format)->createReaderFor (soundBuffer.release(), true));
|
||||
|
||||
BigInteger midiNotes;
|
||||
midiNotes.setRange (0, 126, true);
|
||||
SynthesiserSound::Ptr newSound = new SamplerSound ("Voice", *formatReader, midiNotes, 0x40, 0.0, 0.0, 10.0);
|
||||
|
||||
for (auto* s : synth)
|
||||
s->removeSound (0);
|
||||
|
||||
sound = newSound;
|
||||
|
||||
for (auto* s : synth)
|
||||
s->addSound (sound);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
AudioFormatManager formatManager;
|
||||
OwnedArray<Synthesiser> synth;
|
||||
SynthesiserSound::Ptr sound;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MultiOutSynth)
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: NoiseGatePlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Noise gate audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: NoiseGate
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class NoiseGate final : public AudioProcessor
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
NoiseGate()
|
||||
: AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo())
|
||||
.withOutput ("Output", AudioChannelSet::stereo())
|
||||
.withInput ("Sidechain", AudioChannelSet::stereo()))
|
||||
{
|
||||
addParameter (threshold = new AudioParameterFloat ({ "threshold", 1 }, "Threshold", 0.0f, 1.0f, 0.5f));
|
||||
addParameter (alpha = new AudioParameterFloat ({ "alpha", 1 }, "Alpha", 0.0f, 1.0f, 0.8f));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
||||
{
|
||||
// the sidechain can take any layout, the main bus needs to be the same on the input and output
|
||||
return layouts.getMainInputChannelSet() == layouts.getMainOutputChannelSet()
|
||||
&& ! layouts.getMainInputChannelSet().isDisabled();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (double, int) override { lowPassCoeff = 0.0f; sampleCountDown = 0; }
|
||||
void releaseResources() override {}
|
||||
|
||||
void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
|
||||
{
|
||||
auto mainInputOutput = getBusBuffer (buffer, true, 0);
|
||||
auto sideChainInput = getBusBuffer (buffer, true, 1);
|
||||
|
||||
auto alphaCopy = alpha->get();
|
||||
auto thresholdCopy = threshold->get();
|
||||
|
||||
for (int j = 0; j < buffer.getNumSamples(); ++j)
|
||||
{
|
||||
auto mixedSamples = 0.0f;
|
||||
|
||||
for (int i = 0; i < sideChainInput.getNumChannels(); ++i)
|
||||
mixedSamples += sideChainInput.getReadPointer (i)[j];
|
||||
|
||||
mixedSamples /= static_cast<float> (sideChainInput.getNumChannels());
|
||||
lowPassCoeff = (alphaCopy * lowPassCoeff) + ((1.0f - alphaCopy) * mixedSamples);
|
||||
|
||||
if (lowPassCoeff >= thresholdCopy)
|
||||
sampleCountDown = (int) getSampleRate();
|
||||
|
||||
// very in-effective way of doing this
|
||||
for (int i = 0; i < mainInputOutput.getNumChannels(); ++i)
|
||||
*mainInputOutput.getWritePointer (i, j) = sampleCountDown > 0 ? *mainInputOutput.getReadPointer (i, j) : 0.0f;
|
||||
|
||||
if (sampleCountDown > 0)
|
||||
--sampleCountDown;
|
||||
}
|
||||
}
|
||||
|
||||
using AudioProcessor::processBlock;
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new GenericAudioProcessorEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
const String getName() const override { return "NoiseGate"; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
bool isVST2() const noexcept { return (wrapperType == wrapperType_VST); }
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
MemoryOutputStream stream (destData, true);
|
||||
|
||||
stream.writeFloat (*threshold);
|
||||
stream.writeFloat (*alpha);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
MemoryInputStream stream (data, static_cast<size_t> (sizeInBytes), false);
|
||||
|
||||
threshold->setValueNotifyingHost (stream.readFloat());
|
||||
alpha->setValueNotifyingHost (stream.readFloat());
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
AudioParameterFloat* threshold;
|
||||
AudioParameterFloat* alpha;
|
||||
int sampleCountDown;
|
||||
|
||||
float lowPassCoeff;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NoiseGate)
|
||||
};
|
||||
@@ -0,0 +1,447 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: ReaperEmbeddedViewDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: An audio plugin which embeds a secondary view in VST2 and
|
||||
VST3 formats in REAPER
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022, linux_make
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: ReaperEmbeddedViewDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
/* This demo shows how to use the VST2ClientExtensions and VST3ClientExtensions
|
||||
classes to provide extended functionality in compatible VST/VST3 hosts.
|
||||
|
||||
If this project is built as a VST or VST3 plugin and loaded in REAPER
|
||||
6.29 or higher, it will provide an embedded level meter in the track
|
||||
control panel. To enable the embedded view, right-click on the plugin
|
||||
and select "Show embedded UI in TCP".
|
||||
|
||||
The plugin's editor also include a button which can be used to toggle
|
||||
all inserts on and off.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wshadow-field-in-constructor",
|
||||
"-Wnon-virtual-dtor")
|
||||
|
||||
#include <pluginterfaces/base/ftypes.h>
|
||||
#include <pluginterfaces/base/funknown.h>
|
||||
#include <pluginterfaces/vst/ivsthostapplication.h>
|
||||
#include <pluginterfaces/vst2.x/aeffect.h>
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
namespace reaper
|
||||
{
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wzero-as-null-pointer-constant",
|
||||
"-Wunused-parameter",
|
||||
"-Wnon-virtual-dtor")
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4100)
|
||||
|
||||
using namespace Steinberg;
|
||||
using INT_PTR = pointer_sized_int;
|
||||
using uint32 = Steinberg::uint32;
|
||||
|
||||
#include "extern/reaper_plugin_fx_embed.h"
|
||||
#include "extern/reaper_vst3_interfaces.h"
|
||||
|
||||
//==============================================================================
|
||||
/* These should live in a file which is guaranteed to be compiled only once
|
||||
(i.e. a .cpp file, normally). This demo is a bit special, because we know
|
||||
that this header will only be included in a single translation unit.
|
||||
*/
|
||||
DEF_CLASS_IID (IReaperHostApplication)
|
||||
DEF_CLASS_IID (IReaperUIEmbedInterface)
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_MSVC
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct EmbeddedViewListener
|
||||
{
|
||||
virtual ~EmbeddedViewListener() = default;
|
||||
virtual Steinberg::TPtrInt handledEmbeddedUIMessage (int msg,
|
||||
Steinberg::TPtrInt parm2,
|
||||
Steinberg::TPtrInt parm3) = 0;
|
||||
|
||||
virtual void setGlobalBypassFunction (void (*) (int)) = 0;
|
||||
};
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnon-virtual-dtor")
|
||||
|
||||
//==============================================================================
|
||||
class EmbeddedUI final : public reaper::IReaperUIEmbedInterface
|
||||
{
|
||||
public:
|
||||
explicit EmbeddedUI (EmbeddedViewListener& demo) : listener (demo) {}
|
||||
|
||||
Steinberg::TPtrInt embed_message (int msg,
|
||||
Steinberg::TPtrInt parm2,
|
||||
Steinberg::TPtrInt parm3) override
|
||||
{
|
||||
return listener.handledEmbeddedUIMessage (msg, parm2, parm3);
|
||||
}
|
||||
|
||||
Steinberg::uint32 PLUGIN_API addRef() override { return ++refCount; }
|
||||
Steinberg::uint32 PLUGIN_API release() override { return --refCount; }
|
||||
|
||||
Steinberg::tresult PLUGIN_API queryInterface (const Steinberg::TUID tuid, void** obj) override
|
||||
{
|
||||
if (std::memcmp (tuid, iid, sizeof (Steinberg::TUID)) == 0)
|
||||
{
|
||||
++refCount;
|
||||
*obj = this;
|
||||
return Steinberg::kResultOk;
|
||||
}
|
||||
|
||||
*obj = nullptr;
|
||||
return Steinberg::kNoInterface;
|
||||
}
|
||||
|
||||
private:
|
||||
EmbeddedViewListener& listener;
|
||||
std::atomic<Steinberg::uint32> refCount { 1 };
|
||||
};
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
class VST2Extensions final : public VST2ClientExtensions
|
||||
{
|
||||
public:
|
||||
explicit VST2Extensions (EmbeddedViewListener& l)
|
||||
: listener (l) {}
|
||||
|
||||
pointer_sized_int handleVstPluginCanDo (int32, pointer_sized_int, void* ptr, float) override
|
||||
{
|
||||
if (auto* str = static_cast<const char*> (ptr))
|
||||
for (auto* key : { "hasCockosEmbeddedUI", "hasCockosExtensions" })
|
||||
if (strcmp (str, key) == 0)
|
||||
return (pointer_sized_int) 0xbeef0000;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
pointer_sized_int handleVstManufacturerSpecific (int32 index,
|
||||
pointer_sized_int value,
|
||||
void* ptr,
|
||||
float opt) override
|
||||
{
|
||||
// The docstring at the top of reaper_plugin_fx_embed.h specifies
|
||||
// that the index will always be effEditDraw, which is now deprecated.
|
||||
if (index != __effEditDrawDeprecated)
|
||||
return 0;
|
||||
|
||||
return (pointer_sized_int) listener.handledEmbeddedUIMessage ((int) opt,
|
||||
(Steinberg::TPtrInt) value,
|
||||
(Steinberg::TPtrInt) ptr);
|
||||
}
|
||||
|
||||
void handleVstHostCallbackAvailable (std::function<VstHostCallbackType>&& hostcb) override
|
||||
{
|
||||
char functionName[] = "BypassFxAllTracks";
|
||||
listener.setGlobalBypassFunction (reinterpret_cast<void (*) (int)> (hostcb ((int32_t) 0xdeadbeef, (int32_t) 0xdeadf00d, 0, functionName, 0.0)));
|
||||
}
|
||||
|
||||
private:
|
||||
EmbeddedViewListener& listener;
|
||||
};
|
||||
|
||||
class VST3Extensions final : public VST3ClientExtensions
|
||||
{
|
||||
public:
|
||||
explicit VST3Extensions (EmbeddedViewListener& l)
|
||||
: listener (l) {}
|
||||
|
||||
int32_t queryIEditController (const Steinberg::TUID tuid, void** obj) override
|
||||
{
|
||||
if (embeddedUi.queryInterface (tuid, obj) == Steinberg::kResultOk)
|
||||
return Steinberg::kResultOk;
|
||||
|
||||
*obj = nullptr;
|
||||
return Steinberg::kNoInterface;
|
||||
}
|
||||
|
||||
void setIHostApplication (Steinberg::FUnknown* ptr) override
|
||||
{
|
||||
if (ptr == nullptr)
|
||||
return;
|
||||
|
||||
void* objPtr = nullptr;
|
||||
|
||||
if (ptr->queryInterface (reaper::IReaperHostApplication::iid, &objPtr) == Steinberg::kResultOk)
|
||||
{
|
||||
if (void* fnPtr = static_cast<reaper::IReaperHostApplication*> (objPtr)->getReaperApi ("BypassFxAllTracks"))
|
||||
listener.setGlobalBypassFunction (reinterpret_cast<void (*) (int)> (fnPtr));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
EmbeddedViewListener& listener;
|
||||
EmbeddedUI embeddedUi { listener };
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class Editor final : public AudioProcessorEditor
|
||||
{
|
||||
public:
|
||||
explicit Editor (AudioProcessor& proc,
|
||||
AudioParameterFloat& param,
|
||||
void (*globalBypass) (int))
|
||||
: AudioProcessorEditor (proc), attachment (param, slider)
|
||||
{
|
||||
addAndMakeVisible (slider);
|
||||
addAndMakeVisible (bypassButton);
|
||||
|
||||
// Clicking will bypass *everything*
|
||||
bypassButton.onClick = [globalBypass] { NullCheckedInvocation::invoke (globalBypass, -1); };
|
||||
|
||||
setSize (300, 80);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto b = getLocalBounds();
|
||||
slider.setBounds (b.removeFromTop (40));
|
||||
bypassButton.setBounds (b);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::darkgrey);
|
||||
}
|
||||
|
||||
private:
|
||||
Slider slider;
|
||||
TextButton bypassButton { "global bypass" };
|
||||
SliderParameterAttachment attachment;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ReaperEmbeddedViewDemo final : public AudioProcessor,
|
||||
private EmbeddedViewListener,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
ReaperEmbeddedViewDemo()
|
||||
{
|
||||
addParameter (gain = new AudioParameterFloat ({ "gain", 1 }, "Gain", 0.0f, 1.0f, 0.5f));
|
||||
startTimerHz (60);
|
||||
}
|
||||
|
||||
void prepareToPlay (double, int) override {}
|
||||
void reset() override {}
|
||||
|
||||
void releaseResources() override {}
|
||||
|
||||
void processBlock (AudioBuffer<float>& audio, MidiBuffer&) override { processBlockImpl (audio); }
|
||||
void processBlock (AudioBuffer<double>& audio, MidiBuffer&) override { processBlockImpl (audio); }
|
||||
|
||||
//==============================================================================
|
||||
AudioProcessorEditor* createEditor() override { return new Editor (*this, *gain, globalBypassFn); }
|
||||
bool hasEditor() const override { return true; }
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "ReaperEmbeddedViewDemo"; }
|
||||
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
bool isMidiEffect() const override { return false; }
|
||||
|
||||
double getTailLengthSeconds() const override { return 0.0; }
|
||||
|
||||
//==============================================================================
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock& destData) override
|
||||
{
|
||||
MemoryOutputStream (destData, true).writeFloat (*gain);
|
||||
}
|
||||
|
||||
void setStateInformation (const void* data, int sizeInBytes) override
|
||||
{
|
||||
gain->setValueNotifyingHost (MemoryInputStream (data,
|
||||
static_cast<size_t> (sizeInBytes),
|
||||
false).readFloat());
|
||||
}
|
||||
|
||||
VST2ClientExtensions* getVST2ClientExtensions() override { return &vst2Extensions; }
|
||||
VST3ClientExtensions* getVST3ClientExtensions() override { return &vst3Extensions; }
|
||||
|
||||
private:
|
||||
template <typename Float>
|
||||
void processBlockImpl (AudioBuffer<Float>& audio)
|
||||
{
|
||||
audio.applyGain (*gain);
|
||||
|
||||
const auto minMax = audio.findMinMax (0, 0, audio.getNumSamples());
|
||||
const auto newMax = (float) std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd()));
|
||||
|
||||
auto loaded = storedLevel.load();
|
||||
while (loaded < newMax && ! storedLevel.compare_exchange_weak (loaded, newMax)) {}
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
levelToDraw = std::max (levelToDraw * 0.95f, storedLevel.exchange (0.0f));
|
||||
}
|
||||
|
||||
Steinberg::TPtrInt getSizeInfo (reaper::REAPER_FXEMBED_SizeHints* sizeHints)
|
||||
{
|
||||
if (sizeHints == nullptr)
|
||||
return 0;
|
||||
|
||||
sizeHints->preferred_aspect = 1 << 16;
|
||||
sizeHints->minimum_aspect = 1 << 16;
|
||||
sizeHints->min_height = sizeHints->min_width = 50;
|
||||
sizeHints->max_height = sizeHints->max_width = 1000;
|
||||
return 1;
|
||||
}
|
||||
|
||||
Steinberg::TPtrInt doPaint (reaper::REAPER_FXEMBED_IBitmap* bitmap,
|
||||
reaper::REAPER_FXEMBED_DrawInfo* drawInfo)
|
||||
{
|
||||
if (bitmap == nullptr || drawInfo == nullptr || bitmap->getWidth() <= 0 || bitmap->getHeight() <= 0)
|
||||
return 0;
|
||||
|
||||
Image img (juce::Image::PixelFormat::ARGB, bitmap->getWidth(), bitmap->getHeight(), true);
|
||||
Graphics g (img);
|
||||
|
||||
g.fillAll (Colours::black);
|
||||
|
||||
const auto bounds = g.getClipBounds();
|
||||
const auto corner = 3.0f;
|
||||
|
||||
g.setColour (Colours::darkgrey);
|
||||
g.fillRoundedRectangle (bounds.withSizeKeepingCentre (20, bounds.getHeight() - 6).toFloat(),
|
||||
corner);
|
||||
|
||||
const auto minDb = -50.0f;
|
||||
const auto maxDb = 6.0f;
|
||||
const auto levelInDb = Decibels::gainToDecibels (levelToDraw, minDb);
|
||||
const auto fractionOfHeight = jmap (levelInDb, minDb, maxDb, 0.0f, 1.0f);
|
||||
const auto trackBounds = bounds.withSizeKeepingCentre (16, bounds.getHeight() - 10).toFloat();
|
||||
|
||||
g.setColour (Colours::black);
|
||||
const auto zeroDbIndicatorY = trackBounds.proportionOfHeight (jmap (0.0f,
|
||||
minDb,
|
||||
maxDb,
|
||||
0.0f,
|
||||
1.0f));
|
||||
g.drawHorizontalLine ((int) (trackBounds.getBottom() - zeroDbIndicatorY),
|
||||
trackBounds.getX(),
|
||||
trackBounds.getRight());
|
||||
|
||||
g.setGradientFill (ColourGradient (Colours::darkgreen,
|
||||
{ 0.0f, (float) bounds.getHeight() },
|
||||
Colours::darkred,
|
||||
{ 0.0f, 0.0f },
|
||||
false));
|
||||
|
||||
g.fillRoundedRectangle (trackBounds.withHeight (trackBounds.proportionOfHeight (fractionOfHeight))
|
||||
.withBottomY (trackBounds.getBottom()),
|
||||
corner);
|
||||
|
||||
Image::BitmapData imgData { img, Image::BitmapData::readOnly };
|
||||
const auto pixelsWidth = imgData.pixelStride * imgData.width;
|
||||
|
||||
auto* px = bitmap->getBits();
|
||||
const auto rowSpan = bitmap->getRowSpan();
|
||||
const auto numRows = bitmap->getHeight();
|
||||
|
||||
for (int y = 0; y < numRows; ++y)
|
||||
std::memcpy (px + (y * rowSpan), imgData.getLinePointer (y), (size_t) pixelsWidth);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
Steinberg::TPtrInt handledEmbeddedUIMessage (int msg,
|
||||
Steinberg::TPtrInt parm2,
|
||||
Steinberg::TPtrInt parm3) override
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case REAPER_FXEMBED_WM_IS_SUPPORTED:
|
||||
return 1;
|
||||
|
||||
case REAPER_FXEMBED_WM_PAINT:
|
||||
return doPaint (reinterpret_cast<reaper::REAPER_FXEMBED_IBitmap*> (parm2),
|
||||
reinterpret_cast<reaper::REAPER_FXEMBED_DrawInfo*> (parm3));
|
||||
|
||||
case REAPER_FXEMBED_WM_GETMINMAXINFO:
|
||||
return getSizeInfo (reinterpret_cast<reaper::REAPER_FXEMBED_SizeHints*> (parm3));
|
||||
|
||||
// Implementing mouse behaviour is left as an exercise for the reaper, I mean reader
|
||||
case REAPER_FXEMBED_WM_CREATE: break;
|
||||
case REAPER_FXEMBED_WM_DESTROY: break;
|
||||
case REAPER_FXEMBED_WM_SETCURSOR: break;
|
||||
case REAPER_FXEMBED_WM_MOUSEMOVE: break;
|
||||
case REAPER_FXEMBED_WM_LBUTTONDOWN: break;
|
||||
case REAPER_FXEMBED_WM_LBUTTONUP: break;
|
||||
case REAPER_FXEMBED_WM_LBUTTONDBLCLK: break;
|
||||
case REAPER_FXEMBED_WM_RBUTTONDOWN: break;
|
||||
case REAPER_FXEMBED_WM_RBUTTONUP: break;
|
||||
case REAPER_FXEMBED_WM_RBUTTONDBLCLK: break;
|
||||
case REAPER_FXEMBED_WM_MOUSEWHEEL: break;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setGlobalBypassFunction (void (*fn) (int)) override { globalBypassFn = fn; }
|
||||
|
||||
AudioParameterFloat* gain = nullptr;
|
||||
void (*globalBypassFn) (int) = nullptr;
|
||||
|
||||
std::atomic<float> storedLevel { 0.0f };
|
||||
float levelToDraw = 0.0f;
|
||||
|
||||
VST2Extensions vst2Extensions { *this };
|
||||
VST3Extensions vst3Extensions { *this };
|
||||
};
|
||||
+2605
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE examples.
|
||||
Copyright (c) 2022 - Raw Material Software Limited
|
||||
|
||||
The code included in this file is provided under the terms of the ISC license
|
||||
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
|
||||
To use, copy, modify, and/or distribute this software for any purpose with or
|
||||
without fee is hereby granted provided that the above copyright notice and
|
||||
this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
|
||||
WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
|
||||
PURPOSE, ARE DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this PIP. A PIP is a short snippet
|
||||
of code that can be read by the Projucer and used to generate a JUCE project.
|
||||
|
||||
BEGIN_JUCE_PIP_METADATA
|
||||
|
||||
name: SurroundPlugin
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Surround audio plugin.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_plugin_client, juce_audio_processors,
|
||||
juce_audio_utils, juce_core, juce_data_structures,
|
||||
juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022, linux_make
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: AudioProcessor
|
||||
mainClass: SurroundProcessor
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class ProcessorWithLevels : public AudioProcessor,
|
||||
private AsyncUpdater,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
ProcessorWithLevels()
|
||||
: AudioProcessor (BusesProperties().withInput ("Input", AudioChannelSet::stereo())
|
||||
.withInput ("Aux", AudioChannelSet::stereo(), false)
|
||||
.withOutput ("Output", AudioChannelSet::stereo())
|
||||
.withOutput ("Aux", AudioChannelSet::stereo(), false))
|
||||
{
|
||||
startTimerHz (60);
|
||||
applyBusLayouts (getBusesLayout());
|
||||
}
|
||||
|
||||
~ProcessorWithLevels() override
|
||||
{
|
||||
stopTimer();
|
||||
cancelPendingUpdate();
|
||||
}
|
||||
|
||||
void prepareToPlay (double, int) override
|
||||
{
|
||||
samplesToPlay = (int) getSampleRate();
|
||||
reset();
|
||||
}
|
||||
|
||||
void processBlock (AudioBuffer<float>& audio, MidiBuffer&) override { processAudio (audio); }
|
||||
void processBlock (AudioBuffer<double>& audio, MidiBuffer&) override { processAudio (audio); }
|
||||
|
||||
void releaseResources() override { reset(); }
|
||||
|
||||
float getLevel (int bus, int channel) const
|
||||
{
|
||||
return readableLevels[(size_t) getChannelIndexInProcessBlockBuffer (true, bus, channel)];
|
||||
}
|
||||
|
||||
bool isBusesLayoutSupported (const BusesLayout& layouts) const override
|
||||
{
|
||||
const auto isSetValid = [] (const AudioChannelSet& set)
|
||||
{
|
||||
return ! set.isDisabled()
|
||||
&& ! (set.isDiscreteLayout() && set.getChannelIndexForType (AudioChannelSet::discreteChannel0) == -1);
|
||||
};
|
||||
|
||||
return isSetValid (layouts.getMainOutputChannelSet())
|
||||
&& isSetValid (layouts.getMainInputChannelSet());
|
||||
}
|
||||
|
||||
void reset() override
|
||||
{
|
||||
channelClicked = 0;
|
||||
samplesPlayed = samplesToPlay;
|
||||
}
|
||||
|
||||
bool applyBusLayouts (const BusesLayout& layouts) final
|
||||
{
|
||||
// Some very badly-behaved hosts will call this during processing!
|
||||
const SpinLock::ScopedLockType lock (levelMutex);
|
||||
|
||||
const auto result = AudioProcessor::applyBusLayouts (layouts);
|
||||
|
||||
size_t numInputChannels = 0;
|
||||
|
||||
for (auto i = 0; i < getBusCount (true); ++i)
|
||||
numInputChannels += (size_t) getBus (true, i)->getLastEnabledLayout().size();
|
||||
|
||||
incomingLevels = readableLevels = std::vector<float> (numInputChannels, 0.0f);
|
||||
|
||||
triggerAsyncUpdate();
|
||||
return result;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const String getName() const override { return "Surround PlugIn"; }
|
||||
bool acceptsMidi() const override { return false; }
|
||||
bool producesMidi() const override { return false; }
|
||||
double getTailLengthSeconds() const override { return 0; }
|
||||
|
||||
//==============================================================================
|
||||
int getNumPrograms() override { return 1; }
|
||||
int getCurrentProgram() override { return 0; }
|
||||
void setCurrentProgram (int) override {}
|
||||
const String getProgramName (int) override { return "None"; }
|
||||
void changeProgramName (int, const String&) override {}
|
||||
|
||||
//==============================================================================
|
||||
void getStateInformation (MemoryBlock&) override {}
|
||||
void setStateInformation (const void*, int) override {}
|
||||
|
||||
void channelButtonClicked (int bus, int channelIndex)
|
||||
{
|
||||
channelClicked = getChannelIndexInProcessBlockBuffer (false, bus, channelIndex);
|
||||
samplesPlayed = 0;
|
||||
}
|
||||
|
||||
std::function<void()> updateEditor;
|
||||
|
||||
private:
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
NullCheckedInvocation::invoke (updateEditor);
|
||||
}
|
||||
|
||||
template <typename Float>
|
||||
void processAudio (AudioBuffer<Float>& audio)
|
||||
{
|
||||
{
|
||||
SpinLock::ScopedTryLockType lock (levelMutex);
|
||||
|
||||
if (lock.isLocked())
|
||||
{
|
||||
const auto numInputChannels = (size_t) getTotalNumInputChannels();
|
||||
|
||||
for (size_t i = 0; i < numInputChannels; ++i)
|
||||
{
|
||||
const auto minMax = audio.findMinMax ((int) i, 0, audio.getNumSamples());
|
||||
const auto newMax = (float) std::max (std::abs (minMax.getStart()), std::abs (minMax.getEnd()));
|
||||
|
||||
auto& toUpdate = incomingLevels[i];
|
||||
toUpdate = jmax (toUpdate, newMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audio.clear (0, audio.getNumSamples());
|
||||
|
||||
auto fillSamples = jmin (samplesToPlay - samplesPlayed, audio.getNumSamples());
|
||||
|
||||
if (isPositiveAndBelow (channelClicked, audio.getNumChannels()))
|
||||
{
|
||||
auto* channelBuffer = audio.getWritePointer (channelClicked);
|
||||
auto freq = (float) (440.0 / getSampleRate());
|
||||
|
||||
for (auto i = 0; i < fillSamples; ++i)
|
||||
channelBuffer[i] += std::sin (MathConstants<float>::twoPi * freq * (float) samplesPlayed++);
|
||||
}
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
const SpinLock::ScopedLockType lock (levelMutex);
|
||||
|
||||
for (size_t i = 0; i < readableLevels.size(); ++i)
|
||||
readableLevels[i] = std::max (readableLevels[i] * 0.95f, std::exchange (incomingLevels[i], 0.0f));
|
||||
}
|
||||
|
||||
SpinLock levelMutex;
|
||||
std::vector<float> incomingLevels;
|
||||
std::vector<float> readableLevels;
|
||||
|
||||
int channelClicked;
|
||||
int samplesPlayed;
|
||||
int samplesToPlay;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
const Colour textColour = Colours::white.withAlpha (0.8f);
|
||||
|
||||
inline void drawBackground (Component& comp, Graphics& g)
|
||||
{
|
||||
g.setColour (comp.getLookAndFeel().findColour (ResizableWindow::backgroundColourId).darker (0.8f));
|
||||
g.fillRoundedRectangle (comp.getLocalBounds().toFloat(), 4.0f);
|
||||
}
|
||||
|
||||
inline void configureLabel (Label& label, const AudioProcessor::Bus* layout)
|
||||
{
|
||||
const auto text = layout != nullptr
|
||||
? (layout->getName() + ": " + layout->getCurrentLayout().getDescription())
|
||||
: "";
|
||||
label.setText (text, dontSendNotification);
|
||||
label.setJustificationType (Justification::centred);
|
||||
label.setColour (Label::textColourId, textColour);
|
||||
}
|
||||
|
||||
class InputBusViewer final : public Component,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
InputBusViewer (ProcessorWithLevels& proc, int busNumber)
|
||||
: processor (proc),
|
||||
bus (busNumber)
|
||||
{
|
||||
configureLabel (layoutName, processor.getBus (true, bus));
|
||||
addAndMakeVisible (layoutName);
|
||||
|
||||
startTimerHz (60);
|
||||
}
|
||||
|
||||
~InputBusViewer() override
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
drawBackground (*this, g);
|
||||
|
||||
auto* layout = processor.getBus (true, bus);
|
||||
|
||||
if (layout == nullptr)
|
||||
return;
|
||||
|
||||
const auto channelSet = layout->getCurrentLayout();
|
||||
const auto numChannels = channelSet.size();
|
||||
|
||||
Grid grid;
|
||||
|
||||
grid.autoFlow = Grid::AutoFlow::column;
|
||||
grid.autoColumns = grid.autoRows = Grid::TrackInfo (Grid::Fr (1));
|
||||
grid.items.insertMultiple (0, GridItem(), numChannels);
|
||||
grid.performLayout (getLocalBounds());
|
||||
|
||||
const auto minDb = -50.0f;
|
||||
const auto maxDb = 6.0f;
|
||||
|
||||
for (auto i = 0; i < numChannels; ++i)
|
||||
{
|
||||
g.setColour (Colours::orange.darker());
|
||||
|
||||
const auto levelInDb = Decibels::gainToDecibels (processor.getLevel (bus, i), minDb);
|
||||
const auto fractionOfHeight = jmap (levelInDb, minDb, maxDb, 0.0f, 1.0f);
|
||||
const auto bounds = grid.items[i].currentBounds;
|
||||
const auto trackBounds = bounds.withSizeKeepingCentre (16, bounds.getHeight() - 10).toFloat();
|
||||
g.fillRect (trackBounds.withHeight (trackBounds.proportionOfHeight (fractionOfHeight)).withBottomY (trackBounds.getBottom()));
|
||||
|
||||
g.setColour (textColour);
|
||||
|
||||
g.drawText (channelSet.getAbbreviatedChannelTypeName (channelSet.getTypeOfChannel (i)),
|
||||
bounds,
|
||||
Justification::centredBottom);
|
||||
}
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
layoutName.setBounds (getLocalBounds().removeFromTop (20));
|
||||
}
|
||||
|
||||
int getNumChannels() const
|
||||
{
|
||||
if (auto* b = processor.getBus (true, bus))
|
||||
return b->getCurrentLayout().size();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
void timerCallback() override { repaint(); }
|
||||
|
||||
ProcessorWithLevels& processor;
|
||||
int bus = 0;
|
||||
Label layoutName;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class OutputBusViewer final : public Component
|
||||
{
|
||||
public:
|
||||
OutputBusViewer (ProcessorWithLevels& proc, int busNumber)
|
||||
: processor (proc),
|
||||
bus (busNumber)
|
||||
{
|
||||
auto* layout = processor.getBus (false, bus);
|
||||
|
||||
configureLabel (layoutName, layout);
|
||||
addAndMakeVisible (layoutName);
|
||||
|
||||
if (layout == nullptr)
|
||||
return;
|
||||
|
||||
const auto& channelSet = layout->getCurrentLayout();
|
||||
|
||||
const auto numChannels = channelSet.size();
|
||||
|
||||
for (auto i = 0; i < numChannels; ++i)
|
||||
{
|
||||
const auto channelName = channelSet.getAbbreviatedChannelTypeName (channelSet.getTypeOfChannel (i));
|
||||
|
||||
channelButtons.emplace_back (channelName, channelName);
|
||||
|
||||
auto& newButton = channelButtons.back();
|
||||
newButton.onClick = [&p = processor, b = bus, i] { p.channelButtonClicked (b, i); };
|
||||
addAndMakeVisible (newButton);
|
||||
}
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
drawBackground (*this, g);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto b = getLocalBounds();
|
||||
|
||||
layoutName.setBounds (b.removeFromBottom (20));
|
||||
|
||||
Grid grid;
|
||||
grid.autoFlow = Grid::AutoFlow::column;
|
||||
grid.autoColumns = grid.autoRows = Grid::TrackInfo (Grid::Fr (1));
|
||||
|
||||
for (auto& channelButton : channelButtons)
|
||||
grid.items.add (GridItem (channelButton));
|
||||
|
||||
grid.performLayout (b.reduced (2));
|
||||
}
|
||||
|
||||
int getNumChannels() const
|
||||
{
|
||||
if (auto* b = processor.getBus (false, bus))
|
||||
return b->getCurrentLayout().size();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
ProcessorWithLevels& processor;
|
||||
int bus = 0;
|
||||
Label layoutName;
|
||||
std::list<TextButton> channelButtons;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class SurroundEditor final : public AudioProcessorEditor
|
||||
{
|
||||
public:
|
||||
explicit SurroundEditor (ProcessorWithLevels& parent)
|
||||
: AudioProcessorEditor (parent),
|
||||
customProcessor (parent),
|
||||
scopedUpdateEditor (customProcessor.updateEditor, [this] { updateGUI(); })
|
||||
{
|
||||
updateGUI();
|
||||
setResizable (true, true);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto r = getLocalBounds();
|
||||
doLayout (inputViewers, r.removeFromTop (proportionOfHeight (0.5f)));
|
||||
doLayout (outputViewers, r);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
|
||||
}
|
||||
|
||||
private:
|
||||
template <typename Range>
|
||||
void doLayout (Range& range, Rectangle<int> bounds) const
|
||||
{
|
||||
FlexBox fb;
|
||||
|
||||
for (auto& viewer : range)
|
||||
{
|
||||
if (viewer.getNumChannels() != 0)
|
||||
{
|
||||
fb.items.add (FlexItem (viewer)
|
||||
.withFlex ((float) viewer.getNumChannels())
|
||||
.withMargin (4.0f));
|
||||
}
|
||||
}
|
||||
|
||||
fb.performLayout (bounds);
|
||||
}
|
||||
|
||||
void updateGUI()
|
||||
{
|
||||
inputViewers.clear();
|
||||
outputViewers.clear();
|
||||
|
||||
const auto inputBuses = getAudioProcessor()->getBusCount (true);
|
||||
|
||||
for (auto i = 0; i < inputBuses; ++i)
|
||||
{
|
||||
inputViewers.emplace_back (customProcessor, i);
|
||||
addAndMakeVisible (inputViewers.back());
|
||||
}
|
||||
|
||||
const auto outputBuses = getAudioProcessor()->getBusCount (false);
|
||||
|
||||
for (auto i = 0; i < outputBuses; ++i)
|
||||
{
|
||||
outputViewers.emplace_back (customProcessor, i);
|
||||
addAndMakeVisible (outputViewers.back());
|
||||
}
|
||||
|
||||
const auto channels = jmax (processor.getTotalNumInputChannels(),
|
||||
processor.getTotalNumOutputChannels());
|
||||
setSize (jmax (150, channels * 40), 200);
|
||||
|
||||
resized();
|
||||
}
|
||||
|
||||
ProcessorWithLevels& customProcessor;
|
||||
ScopedValueSetter<std::function<void()>> scopedUpdateEditor;
|
||||
std::list<InputBusViewer> inputViewers;
|
||||
std::list<OutputBusViewer> outputViewers;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct SurroundProcessor final : public ProcessorWithLevels
|
||||
{
|
||||
AudioProcessorEditor* createEditor() override { return new SurroundEditor (*this); }
|
||||
bool hasEditor() const override { return true; }
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
The files in this folder were copied from the [reaper-sdk
|
||||
repository](https://github.com/justinfrankel/reaper-sdk) on Github. At the time
|
||||
of writing, these files were distributed under the following license:
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
@@ -0,0 +1,140 @@
|
||||
#ifndef _REAPER_PLUGIN_FX_EMBED_H_
|
||||
#define _REAPER_PLUGIN_FX_EMBED_H_
|
||||
|
||||
|
||||
/*
|
||||
* to support via VST2: canDo("hasCockosEmbeddedUI") should return 0xbeef0000
|
||||
* dispatcher will be called with opcode=effVendorSpecific, index=effEditDraw, value=parm2, ptr=(void*)(INT_PTR)parm3, opt=message (REAPER_FXEMBED_WM_*)
|
||||
*
|
||||
* to support via VST3: IController should support IReaperUIEmbedInterface, see reaper_vst3_interfaces.h
|
||||
*
|
||||
* to support via LV2: todo
|
||||
*/
|
||||
|
||||
// these alias to win32's WM_*
|
||||
|
||||
|
||||
#define REAPER_FXEMBED_WM_IS_SUPPORTED 0x0000
|
||||
/* return 1 if embedding is supported and available
|
||||
* return -1 if embedding is supported and unavailable
|
||||
* return 0 if embedding is not supported
|
||||
*/
|
||||
|
||||
#define REAPER_FXEMBED_WM_CREATE 0x0001 // called when embedding begins (return value ignored)
|
||||
#define REAPER_FXEMBED_WM_DESTROY 0x0002 // called when embedding ends (return value ignored)
|
||||
|
||||
|
||||
|
||||
typedef struct REAPER_FXEMBED_DrawInfo // alias of REAPER_inline_positioninfo
|
||||
{
|
||||
int context; // 0=unknown (v6.23 and earlier), 1=TCP, 2=MCP
|
||||
int dpi; // 0=unknown (v6.23 and earlier), otherwise 24.8 fixed point (256=100%)
|
||||
int mousewheel_amt; // for REAPER_FXEMBED_WM_MOUSEWHEEL, 120 = step, typically
|
||||
double _res2;
|
||||
|
||||
int width, height;
|
||||
int mouse_x, mouse_y;
|
||||
|
||||
int flags; // REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL etc
|
||||
int _res3;
|
||||
|
||||
void *spare[6];
|
||||
} REAPER_FXEMBED_DrawInfo;
|
||||
|
||||
#define REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL 1
|
||||
#define REAPER_FXEMBED_DRAWINFO_FLAG_LBUTTON_CAPTURED 0x10000
|
||||
#define REAPER_FXEMBED_DRAWINFO_FLAG_RBUTTON_CAPTURED 0x20000
|
||||
|
||||
#define REAPER_FXEMBED_WM_PAINT 0x000F
|
||||
/*
|
||||
* draw embedded UI.
|
||||
* parm2: REAPER_FXEMBED_IBitmap * to draw into. note
|
||||
* parm3: REAPER_FXEMBED_DrawInfo *
|
||||
*
|
||||
* if flags has REAPER_FXEMBED_DRAWINFO_FLAG_PAINT_OPTIONAL set, update is optional. if no change since last draw, return 0.
|
||||
* if flags has REAPER_FXEMBED_DRAWINFO_FLAG_LBUTTON_CAPTURED set, left mouse button is down and captured
|
||||
* if flags has REAPER_FXEMBED_DRAWINFO_FLAG_RBUTTON_CAPTURED set, right mouse button is down and captured
|
||||
*
|
||||
* HiDPI:
|
||||
* if REAPER_FXEMBED_IBitmap::Extended(REAPER_FXEMBED_EXT_GET_ADVISORY_SCALING,NULL) returns nonzero, then it is a 24.8 scalefactor for UI drawing
|
||||
*
|
||||
* return 1 if drawing occurred, 0 otherwise.
|
||||
*
|
||||
*/
|
||||
|
||||
#define REAPER_FXEMBED_WM_SETCURSOR 0x0020 // parm3: REAPER_FXEMBED_DrawInfo*. set mouse cursor and return REAPER_FXEMBED_RETNOTIFY_HANDLED, or return 0.
|
||||
|
||||
#define REAPER_FXEMBED_WM_GETMINMAXINFO 0x0024
|
||||
/*
|
||||
* get size hints. parm3 = (REAPER_FXEMBED_SizeHints*). return 1 if supported
|
||||
* note that these are just hints, the actual size may vary
|
||||
*/
|
||||
typedef struct REAPER_FXEMBED_SizeHints { // alias to MINMAXINFO
|
||||
int preferred_aspect; // 16.16 fixed point (65536 = 1:1, 32768 = 1:2, etc)
|
||||
int minimum_aspect; // 16.16 fixed point
|
||||
|
||||
int _res1, _res2, _res3, _res4;
|
||||
|
||||
int min_width, min_height;
|
||||
int max_width, max_height;
|
||||
} REAPER_FXEMBED_SizeHints;
|
||||
|
||||
/*
|
||||
* mouse messages
|
||||
* parm3 = (REAPER_FXEMBED_DrawInfo*)
|
||||
* capture is automatically set on mouse down, released on mouse up
|
||||
* when not captured, will always receive a mousemove when exiting the window
|
||||
*/
|
||||
|
||||
#define REAPER_FXEMBED_WM_MOUSEMOVE 0x0200
|
||||
#define REAPER_FXEMBED_WM_LBUTTONDOWN 0x0201
|
||||
#define REAPER_FXEMBED_WM_LBUTTONUP 0x0202
|
||||
#define REAPER_FXEMBED_WM_LBUTTONDBLCLK 0x0203
|
||||
#define REAPER_FXEMBED_WM_RBUTTONDOWN 0x0204
|
||||
#define REAPER_FXEMBED_WM_RBUTTONUP 0x0205
|
||||
#define REAPER_FXEMBED_WM_RBUTTONDBLCLK 0x0206
|
||||
#define REAPER_FXEMBED_WM_MOUSEWHEEL 0x020A
|
||||
|
||||
|
||||
/* REAPER_FXEMBED_WM_SETCURSOR should return REAPER_FXEMBED_RETNOTIFY_HANDLED if a cursor was set
|
||||
*/
|
||||
#define REAPER_FXEMBED_RETNOTIFY_HANDLED 0x0000001
|
||||
|
||||
/* if the mouse messages return with REAPER_FXEMBED_RETNOTIFY_INVALIDATE set, a non-optional
|
||||
* redraw is initiated (generally sooner than the next timer-based redraw)
|
||||
*/
|
||||
#define REAPER_FXEMBED_RETNOTIFY_INVALIDATE 0x1000000
|
||||
|
||||
/*
|
||||
* bitmap interface
|
||||
* this is an alias of LICE_IBitmap etc from WDL/lice/lice.h
|
||||
*
|
||||
*/
|
||||
#define REAPER_FXEMBED_RGBA(r,g,b,a) (((b)&0xff)|(((g)&0xff)<<8)|(((r)&0xff)<<16)|(((a)&0xff)<<24))
|
||||
#define REAPER_FXEMBED_GETB(v) ((v)&0xff)
|
||||
#define REAPER_FXEMBED_GETG(v) (((v)>>8)&0xff)
|
||||
#define REAPER_FXEMBED_GETR(v) (((v)>>16)&0xff)
|
||||
#define REAPER_FXEMBED_GETA(v) (((v)>>24)&0xff)
|
||||
|
||||
#ifdef __cplusplus
|
||||
class REAPER_FXEMBED_IBitmap // alias of LICE_IBitmap
|
||||
{
|
||||
public:
|
||||
virtual ~REAPER_FXEMBED_IBitmap() { }
|
||||
|
||||
virtual unsigned int *getBits()=0;
|
||||
virtual int getWidth()=0;
|
||||
virtual int getHeight()=0;
|
||||
virtual int getRowSpan()=0; // includes any off-bitmap data. this is in sizeof(unsigned int) units, not bytes.
|
||||
virtual bool isFlipped() { return false; }
|
||||
virtual bool resize(int w, int h)=0;
|
||||
|
||||
virtual void *getDC() { return 0; } // do not use
|
||||
|
||||
virtual INT_PTR Extended(int id, void* data) { return 0; }
|
||||
};
|
||||
#endif
|
||||
|
||||
#define REAPER_FXEMBED_EXT_GET_ADVISORY_SCALING 0x2003 // data ignored, returns .8 fixed point. returns 0 if unscaled
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef _REAPER_VST3_INTERFACES_H_
|
||||
#define _REAPER_VST3_INTERFACES_H_
|
||||
|
||||
class IReaperHostApplication : public FUnknown // available from IHostApplication in REAPER v5.02+
|
||||
{
|
||||
public:
|
||||
// Gets a REAPER Extension API function by name, returns NULL is failed
|
||||
virtual void* PLUGIN_API getReaperApi(CStringA funcname) = 0;
|
||||
|
||||
virtual void* PLUGIN_API getReaperParent(uint32 w) = 0; // get parent track(=1), take(=2), project(=3), fxdsp(=4), trackchan(=5)
|
||||
|
||||
// Multi-purpose function, returns NULL if unsupported
|
||||
virtual void* PLUGIN_API reaperExtended(uint32 call, void *parm1, void *parm2, void *parm3) = 0;
|
||||
|
||||
static const FUID iid;
|
||||
};
|
||||
|
||||
DECLARE_CLASS_IID (IReaperHostApplication, 0x79655E36, 0x77EE4267, 0xA573FEF7, 0x4912C27C)
|
||||
|
||||
class IReaperUIEmbedInterface : public FUnknown // supported by REAPER v6.24+, queried from plug-in IController
|
||||
{
|
||||
public:
|
||||
// note: VST2 uses CanDo "hasCockosEmbeddedUI"==0xbeef0000, then opcode=effVendorSpecific, index=effEditDraw, opt=(float)msg, value=parm2, ptr=parm3
|
||||
// see reaper_plugin_fx_embed.h
|
||||
virtual Steinberg::TPtrInt embed_message(int msg, Steinberg::TPtrInt parm2, Steinberg::TPtrInt parm3) = 0;
|
||||
|
||||
static const FUID iid;
|
||||
};
|
||||
|
||||
DECLARE_CLASS_IID (IReaperUIEmbedInterface, 0x049bf9e7, 0xbc74ead0, 0xc4101e86, 0x7f725981)
|
||||
#endif
|
||||
Reference in New Issue
Block a user