478 lines
18 KiB
C
478 lines
18 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE examples.
|
||
|
Copyright (c) 2020 - 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 : 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,
|
||
|
const 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 : 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 : public AudioProcessor
|
||
|
{
|
||
|
public:
|
||
|
AUv3SynthProcessor ()
|
||
|
: AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true)),
|
||
|
currentRecording (1, 1), currentProgram (0)
|
||
|
{
|
||
|
// initialize parameters
|
||
|
addParameter (isRecordingParam = new AudioParameterBool ("isRecording", "Is Recording", false));
|
||
|
addParameter (roomSizeParam = new AudioParameterFloat ("roomSize", "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)
|
||
|
};
|