/* ============================================================================== 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 (x + haloRadius, y, width - (haloRadius * 2), height); auto backgroundBar = r.withSizeKeepingCentre(r.getWidth(), 2); sliderPos = (sliderPos - minSliderPos) / static_cast (width); auto knobPos = static_cast (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 (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 (std::ceil (maxDurationOfRecording * lastSampleRate))); samplesRecorded = 0; synth.setCurrentPlaybackSampleRate (lastSampleRate); reverb.setSampleRate (lastSampleRate); } void processBlock (AudioBuffer& 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 ""; } //============================================================================== 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 (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 (data, static_cast (dataSize), false); loadNewSample (std::move (soundBuffer), format); } void loadNewSample (std::unique_ptr soundBuffer, const char* format) { std::unique_ptr 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 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 (mb.getSize()), "wav"); } //============================================================================== static constexpr int maxNumVoices = 5; static constexpr double maxDurationOfRecording = 1.0; //============================================================================== AudioFormatManager formatManager; int samplesRecorded; double lastSampleRate; AudioBuffer currentRecording; Reverb reverb; Synthesiser synth; SynthesiserSound::Ptr sound; AudioParameterBool* isRecordingParam; AudioParameterFloat* roomSizeParam; int currentProgram; //============================================================================== JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AUv3SynthProcessor) };