| @@ -0,0 +1,186 @@ | |||||
| /* | |||||
| ============================================================================== | |||||
| 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. | |||||
| ============================================================================== | |||||
| */ | |||||
| #pragma once | |||||
| class ADSRComponent final : public Component | |||||
| { | |||||
| public: | |||||
| ADSRComponent() | |||||
| : envelope { *this } | |||||
| { | |||||
| for (Slider* slider : { &adsrAttack, &adsrDecay, &adsrSustain, &adsrRelease }) | |||||
| { | |||||
| if (slider == &adsrSustain) | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| const auto val = (int) jmap (value, 0.0, 1.0, 0.0, 100.0); | |||||
| text << String::formatted (": %d%%", val); | |||||
| return text; | |||||
| }; | |||||
| } | |||||
| else | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| text << ": " << ((value < 0.4f) ? String::formatted ("%dms", (int) std::round (value * 1000)) | |||||
| : String::formatted ("%0.2lf Sec", value)); | |||||
| return text; | |||||
| }; | |||||
| slider->setSkewFactor (0.3); | |||||
| } | |||||
| slider->setRange (0, 1); | |||||
| slider->setTextBoxStyle (Slider::TextBoxBelow, true, 300, 25); | |||||
| slider->onValueChange = [this] | |||||
| { | |||||
| NullCheckedInvocation::invoke (onChange); | |||||
| repaint(); | |||||
| }; | |||||
| addAndMakeVisible (slider); | |||||
| } | |||||
| adsrAttack.setName ("Attack"); | |||||
| adsrDecay.setName ("Decay"); | |||||
| adsrSustain.setName ("Sustain"); | |||||
| adsrRelease.setName ("Release"); | |||||
| adsrAttack.setValue (0.1, dontSendNotification); | |||||
| adsrDecay.setValue (0.3, dontSendNotification); | |||||
| adsrSustain.setValue (0.3, dontSendNotification); | |||||
| adsrRelease.setValue (0.2, dontSendNotification); | |||||
| addAndMakeVisible (envelope); | |||||
| } | |||||
| std::function<void()> onChange; | |||||
| ADSR::Parameters getParameters() const | |||||
| { | |||||
| return | |||||
| { | |||||
| (float) adsrAttack.getValue(), | |||||
| (float) adsrDecay.getValue(), | |||||
| (float) adsrSustain.getValue(), | |||||
| (float) adsrRelease.getValue(), | |||||
| }; | |||||
| } | |||||
| void resized() final | |||||
| { | |||||
| auto bounds = getLocalBounds(); | |||||
| const auto knobWidth = bounds.getWidth() / 4; | |||||
| auto knobBounds = bounds.removeFromBottom (bounds.getHeight() / 2); | |||||
| { | |||||
| adsrAttack.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrDecay.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrSustain.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrRelease.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| } | |||||
| envelope.setBounds (bounds); | |||||
| } | |||||
| Slider adsrAttack { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrDecay { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrSustain { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrRelease { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| private: | |||||
| class Envelope final : public Component | |||||
| { | |||||
| public: | |||||
| Envelope (ADSRComponent& adsr) : parent { adsr } {} | |||||
| void paint (Graphics& g) final | |||||
| { | |||||
| const auto env = parent.getParameters(); | |||||
| // sustain isn't a length but we use a fixed value here to give | |||||
| // sustain some visual width in the envelope | |||||
| constexpr auto sustainLength = 0.1; | |||||
| const auto adsrLength = env.attack | |||||
| + env.decay | |||||
| + sustainLength | |||||
| + env.release; | |||||
| auto bounds = getLocalBounds().toFloat(); | |||||
| const auto attackWidth = bounds.proportionOfWidth (env.attack / adsrLength); | |||||
| const auto decayWidth = bounds.proportionOfWidth (env.decay / adsrLength); | |||||
| const auto sustainWidth = bounds.proportionOfWidth (sustainLength / adsrLength); | |||||
| const auto releaseWidth = bounds.proportionOfWidth (env.release / adsrLength); | |||||
| const auto sustainHeight = bounds.proportionOfHeight (1 - env.sustain); | |||||
| const auto attackBounds = bounds.removeFromLeft (attackWidth); | |||||
| const auto decayBounds = bounds.removeFromLeft (decayWidth); | |||||
| const auto sustainBounds = bounds.removeFromLeft (sustainWidth); | |||||
| const auto releaseBounds = bounds.removeFromLeft (releaseWidth); | |||||
| g.setColour (Colours::black.withAlpha (0.1f)); | |||||
| g.fillRect (bounds); | |||||
| const auto alpha = 0.4f; | |||||
| g.setColour (Colour (246, 98, 92).withAlpha (alpha)); | |||||
| g.fillRect (attackBounds); | |||||
| g.setColour (Colour (242, 187, 60).withAlpha (alpha)); | |||||
| g.fillRect (decayBounds); | |||||
| g.setColour (Colour (109, 234, 166).withAlpha (alpha)); | |||||
| g.fillRect (sustainBounds); | |||||
| g.setColour (Colour (131, 61, 183).withAlpha (alpha)); | |||||
| g.fillRect (releaseBounds); | |||||
| Path envelopePath; | |||||
| envelopePath.startNewSubPath (attackBounds.getBottomLeft()); | |||||
| envelopePath.lineTo (decayBounds.getTopLeft()); | |||||
| envelopePath.lineTo (sustainBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getBottomRight()); | |||||
| const auto lineThickness = 4.0f; | |||||
| g.setColour (Colours::white); | |||||
| g.strokePath (envelopePath, PathStrokeType { lineThickness }); | |||||
| } | |||||
| private: | |||||
| ADSRComponent& parent; | |||||
| }; | |||||
| Envelope envelope; | |||||
| }; | |||||
| @@ -0,0 +1,659 @@ | |||||
| /* | |||||
| ============================================================================== | |||||
| 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: AudioWorkgroupDemo | |||||
| version: 1.0.0 | |||||
| vendor: JUCE | |||||
| website: http://juce.com | |||||
| description: Simple audio workgroup demo application. | |||||
| dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats, | |||||
| 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: Component | |||||
| mainClass: AudioWorkgroupDemo | |||||
| useLocalCopy: 1 | |||||
| END_JUCE_PIP_METADATA | |||||
| *******************************************************************************/ | |||||
| #pragma once | |||||
| #include "../Assets/DemoUtilities.h" | |||||
| #include "../Assets/AudioLiveScrollingDisplay.h" | |||||
| #include "../Assets/ADSRComponent.h" | |||||
| constexpr auto NumWorkerThreads = 4; | |||||
| //============================================================================== | |||||
| class ThreadBarrier : public ReferenceCountedObject | |||||
| { | |||||
| public: | |||||
| using Ptr = ReferenceCountedObjectPtr<ThreadBarrier>; | |||||
| static Ptr make (int numThreadsToSynchronise) | |||||
| { | |||||
| return { new ThreadBarrier { numThreadsToSynchronise } }; | |||||
| } | |||||
| void arriveAndWait() | |||||
| { | |||||
| std::unique_lock lk { mutex }; | |||||
| [[maybe_unused]] const auto c = ++blockCount; | |||||
| // You've tried to synchronise too many threads!! | |||||
| jassert (c <= threadCount); | |||||
| if (blockCount == threadCount) | |||||
| { | |||||
| blockCount = 0; | |||||
| cv.notify_all(); | |||||
| return; | |||||
| } | |||||
| cv.wait (lk, [this] { return blockCount == 0; }); | |||||
| } | |||||
| private: | |||||
| std::mutex mutex; | |||||
| std::condition_variable cv; | |||||
| int blockCount{}; | |||||
| const int threadCount{}; | |||||
| explicit ThreadBarrier (int numThreadsToSynchronise) | |||||
| : threadCount (numThreadsToSynchronise) {} | |||||
| JUCE_DECLARE_NON_COPYABLE (ThreadBarrier) | |||||
| JUCE_DECLARE_NON_MOVEABLE (ThreadBarrier) | |||||
| }; | |||||
| struct Voice | |||||
| { | |||||
| struct Oscillator | |||||
| { | |||||
| float getNextSample() | |||||
| { | |||||
| const auto s = (2.f * phase - 1.f); | |||||
| phase += delta; | |||||
| if (phase >= 1.f) | |||||
| phase -= 1.f; | |||||
| return s; | |||||
| } | |||||
| float delta = 0; | |||||
| float phase = 0; | |||||
| }; | |||||
| Voice (int numSamples, double newSampleRate) | |||||
| : sampleRate (newSampleRate), | |||||
| workBuffer (2, numSamples) | |||||
| { | |||||
| } | |||||
| bool isActive() const { return adsr.isActive(); } | |||||
| void startNote (int midiNoteNumber, float detuneAmount, ADSR::Parameters env) | |||||
| { | |||||
| constexpr float superSawDetuneValues[] = { -1.f, -0.8f, -0.6f, 0.f, 0.5f, 0.7f, 1.f }; | |||||
| const auto freq = 440.f * std::pow (2.f, ((float) midiNoteNumber - 69.f) / 12.f); | |||||
| for (size_t i = 0; i < 7; i++) | |||||
| { | |||||
| auto& osc = oscillators[i]; | |||||
| const auto detune = superSawDetuneValues[i] * detuneAmount; | |||||
| osc.delta = (freq + detune) / (float) sampleRate; | |||||
| osc.phase = wobbleGenerator.nextFloat(); | |||||
| } | |||||
| currentNote = midiNoteNumber; | |||||
| adsr.setParameters (env); | |||||
| adsr.setSampleRate (sampleRate); | |||||
| adsr.noteOn(); | |||||
| } | |||||
| void stopNote() | |||||
| { | |||||
| adsr.noteOff(); | |||||
| } | |||||
| void run() | |||||
| { | |||||
| workBuffer.clear(); | |||||
| constexpr auto oscillatorCount = 7; | |||||
| constexpr float superSawPanValues[] = { -1.f, -0.7f, -0.3f, 0.f, 0.3f, 0.7f, 1.f }; | |||||
| constexpr auto spread = 0.8f; | |||||
| constexpr auto mix = 1 / 7.f; | |||||
| auto* l = workBuffer.getWritePointer (0); | |||||
| auto* r = workBuffer.getWritePointer (1); | |||||
| for (int i = 0; i < workBuffer.getNumSamples(); i++) | |||||
| { | |||||
| const auto a = adsr.getNextSample(); | |||||
| float left = 0; | |||||
| float right = 0; | |||||
| for (size_t o = 0; o < oscillatorCount; o++) | |||||
| { | |||||
| auto& osc = oscillators[o]; | |||||
| const auto s = a * osc.getNextSample(); | |||||
| left += s * (1.f - (superSawPanValues[o] * spread)); | |||||
| right += s * (1.f + (superSawPanValues[o] * spread)); | |||||
| } | |||||
| l[i] += left * mix; | |||||
| r[i] += right * mix; | |||||
| } | |||||
| workBuffer.applyGain (0.25f); | |||||
| } | |||||
| const AudioSampleBuffer& getWorkBuffer() const { return workBuffer; } | |||||
| ADSR adsr; | |||||
| double sampleRate; | |||||
| std::array<Oscillator, 7> oscillators; | |||||
| int currentNote = 0; | |||||
| Random wobbleGenerator; | |||||
| private: | |||||
| AudioSampleBuffer workBuffer; | |||||
| JUCE_DECLARE_NON_COPYABLE (Voice) | |||||
| JUCE_DECLARE_NON_MOVEABLE (Voice) | |||||
| }; | |||||
| struct AudioWorkerThreadOptions | |||||
| { | |||||
| int numChannels; | |||||
| int numSamples; | |||||
| double sampleRate; | |||||
| AudioWorkgroup workgroup; | |||||
| ThreadBarrier::Ptr completionBarrier; | |||||
| }; | |||||
| class AudioWorkerThread final : private Thread | |||||
| { | |||||
| public: | |||||
| using Ptr = std::unique_ptr<AudioWorkerThread>; | |||||
| using Options = AudioWorkerThreadOptions; | |||||
| explicit AudioWorkerThread (const Options& workerOptions) | |||||
| : Thread ("AudioWorkerThread"), | |||||
| options (workerOptions) | |||||
| { | |||||
| jassert (options.completionBarrier != nullptr); | |||||
| #if defined (JUCE_MAC) | |||||
| jassert (options.workgroup); | |||||
| #endif | |||||
| startRealtimeThread (RealtimeOptions{}.withApproximateAudioProcessingTime (options.numSamples, options.sampleRate)); | |||||
| } | |||||
| ~AudioWorkerThread() override { stop(); } | |||||
| using Thread::notify; | |||||
| using Thread::signalThreadShouldExit; | |||||
| using Thread::isThreadRunning; | |||||
| int getJobCount() const { return lastJobCount; } | |||||
| int queueAudioJobs (Span<Voice*> jobs) | |||||
| { | |||||
| size_t spanIndex = 0; | |||||
| const auto write = jobQueueFifo.write ((int) jobs.size()); | |||||
| write.forEach ([&, jobs] (int dstIndex) | |||||
| { | |||||
| jobQueue[(size_t) dstIndex] = jobs[spanIndex++]; | |||||
| }); | |||||
| return write.blockSize1 + write.blockSize2; | |||||
| } | |||||
| private: | |||||
| void stop() | |||||
| { | |||||
| signalThreadShouldExit(); | |||||
| stopThread (-1); | |||||
| } | |||||
| void run() override | |||||
| { | |||||
| WorkgroupToken token; | |||||
| options.workgroup.join (token); | |||||
| while (wait (-1) && ! threadShouldExit()) | |||||
| { | |||||
| const auto numReady = jobQueueFifo.getNumReady(); | |||||
| lastJobCount = numReady; | |||||
| if (numReady > 0) | |||||
| { | |||||
| jobQueueFifo.read (jobQueueFifo.getNumReady()) | |||||
| .forEach ([this] (int srcIndex) | |||||
| { | |||||
| jobQueue[(size_t) srcIndex]->run(); | |||||
| }); | |||||
| } | |||||
| // Wait for all our threads to get to this point. | |||||
| options.completionBarrier->arriveAndWait(); | |||||
| } | |||||
| } | |||||
| static constexpr auto numJobs = 128; | |||||
| Options options; | |||||
| std::array<Voice*, numJobs> jobQueue; | |||||
| AbstractFifo jobQueueFifo { numJobs }; | |||||
| std::atomic<int> lastJobCount = 0; | |||||
| private: | |||||
| JUCE_DECLARE_NON_COPYABLE (AudioWorkerThread) | |||||
| JUCE_DECLARE_NON_MOVEABLE (AudioWorkerThread) | |||||
| }; | |||||
| template <typename ValueType, typename LockType> | |||||
| struct SharedThreadValue | |||||
| { | |||||
| SharedThreadValue (LockType& lockRef, ValueType initialValue = {}) | |||||
| : lock (lockRef), | |||||
| preSyncValue (initialValue), | |||||
| postSyncValue (initialValue) | |||||
| { | |||||
| } | |||||
| void set (const ValueType& newValue) | |||||
| { | |||||
| const typename LockType::ScopedLockType sl { lock }; | |||||
| preSyncValue = newValue; | |||||
| } | |||||
| ValueType get() const | |||||
| { | |||||
| { | |||||
| const typename LockType::ScopedTryLockType sl { lock, true }; | |||||
| if (sl.isLocked()) | |||||
| postSyncValue = preSyncValue; | |||||
| } | |||||
| return postSyncValue; | |||||
| } | |||||
| private: | |||||
| LockType& lock; | |||||
| ValueType preSyncValue{}; | |||||
| mutable ValueType postSyncValue{}; | |||||
| JUCE_DECLARE_NON_COPYABLE (SharedThreadValue) | |||||
| JUCE_DECLARE_NON_MOVEABLE (SharedThreadValue) | |||||
| }; | |||||
| //============================================================================== | |||||
| class SuperSynth | |||||
| { | |||||
| public: | |||||
| SuperSynth() = default; | |||||
| void setEnvelope (ADSR::Parameters params) | |||||
| { | |||||
| envelope.set (params); | |||||
| } | |||||
| void setThickness (float newThickness) | |||||
| { | |||||
| thickness.set (newThickness); | |||||
| } | |||||
| void prepareToPlay (int numSamples, double sampleRate) | |||||
| { | |||||
| activeVoices.reserve (128); | |||||
| for (auto& voice : voices) | |||||
| voice.reset (new Voice { numSamples, sampleRate }); | |||||
| } | |||||
| void process (ThreadBarrier::Ptr barrier, Span<AudioWorkerThread*> workers, | |||||
| AudioSampleBuffer& buffer, MidiBuffer& midiBuffer) | |||||
| { | |||||
| const auto blockThickness = thickness.get(); | |||||
| const auto blockEnvelope = envelope.get(); | |||||
| // We're not trying to be sample accurate.. handle the on/off events in a single block. | |||||
| for (auto event : midiBuffer) | |||||
| { | |||||
| const auto message = event.getMessage(); | |||||
| if (message.isNoteOn()) | |||||
| { | |||||
| for (auto& voice : voices) | |||||
| { | |||||
| if (! voice->isActive()) | |||||
| { | |||||
| voice->startNote (message.getNoteNumber(), blockThickness, blockEnvelope); | |||||
| break; | |||||
| } | |||||
| } | |||||
| continue; | |||||
| } | |||||
| if (message.isNoteOff()) | |||||
| { | |||||
| for (auto& voice : voices) | |||||
| { | |||||
| if (voice->currentNote == message.getNoteNumber()) | |||||
| voice->stopNote(); | |||||
| } | |||||
| continue; | |||||
| } | |||||
| } | |||||
| // Queue up all active voices | |||||
| for (auto& voice : voices) | |||||
| if (voice->isActive()) | |||||
| activeVoices.push_back (voice.get()); | |||||
| constexpr auto jobsPerThread = 1; | |||||
| // Try and split the voices evenly just for demonstration purposes. | |||||
| // You could also do some of the work on this thread instead of waiting. | |||||
| for (int i = 0; i < (int) activeVoices.size();) | |||||
| { | |||||
| for (auto worker : workers) | |||||
| { | |||||
| if (i >= (int) activeVoices.size()) | |||||
| break; | |||||
| const auto jobCount = jmin (jobsPerThread, (int) activeVoices.size() - i); | |||||
| i += worker->queueAudioJobs ({ activeVoices.data() + i, (size_t) jobCount }); | |||||
| } | |||||
| } | |||||
| // kick off the work. | |||||
| for (auto& worker : workers) | |||||
| worker->notify(); | |||||
| // Wait for our jobs to complete. | |||||
| barrier->arriveAndWait(); | |||||
| // mix the jobs into the main audio thread buffer. | |||||
| for (auto* voice : activeVoices) | |||||
| { | |||||
| buffer.addFrom (0, 0, voice->getWorkBuffer(), 0, 0, buffer.getNumSamples()); | |||||
| buffer.addFrom (1, 0, voice->getWorkBuffer(), 1, 0, buffer.getNumSamples()); | |||||
| } | |||||
| // Abuse std::vector not reallocating on clear. | |||||
| activeVoices.clear(); | |||||
| } | |||||
| private: | |||||
| std::array<std::unique_ptr<Voice>, 128> voices; | |||||
| std::vector<Voice*> activeVoices; | |||||
| template <typename T> | |||||
| using ThreadValue = SharedThreadValue<T, SpinLock>; | |||||
| SpinLock paramLock; | |||||
| ThreadValue<ADSR::Parameters> envelope { paramLock, { 0.f, 0.3f, 1.f, 0.3f } }; | |||||
| ThreadValue<float> thickness { paramLock, 1.f }; | |||||
| JUCE_DECLARE_NON_COPYABLE (SuperSynth) | |||||
| JUCE_DECLARE_NON_MOVEABLE (SuperSynth) | |||||
| }; | |||||
| //============================================================================== | |||||
| class AudioWorkgroupDemo : public Component, | |||||
| private Timer, | |||||
| private AudioSource, | |||||
| private MidiInputCallback | |||||
| { | |||||
| public: | |||||
| AudioWorkgroupDemo() | |||||
| { | |||||
| addAndMakeVisible (keyboardComponent); | |||||
| addAndMakeVisible (liveAudioDisplayComp); | |||||
| addAndMakeVisible (envelopeComponent); | |||||
| addAndMakeVisible (keyboardComponent); | |||||
| addAndMakeVisible (thicknessSlider); | |||||
| addAndMakeVisible (voiceCountLabel); | |||||
| std::generate (threadLabels.begin(), threadLabels.end(), &std::make_unique<Label>); | |||||
| for (auto& label : threadLabels) | |||||
| { | |||||
| addAndMakeVisible (*label); | |||||
| label->setEditable (false); | |||||
| } | |||||
| thicknessSlider.textFromValueFunction = [] (double) { return "Phatness"; }; | |||||
| thicknessSlider.onValueChange = [this] { synthesizer.setThickness ((float) thicknessSlider.getValue()); }; | |||||
| thicknessSlider.setRange (0.5, 15, 0.1); | |||||
| thicknessSlider.setValue (7, dontSendNotification); | |||||
| thicknessSlider.setTextBoxIsEditable (false); | |||||
| envelopeComponent.onChange = [this] { synthesizer.setEnvelope (envelopeComponent.getParameters()); }; | |||||
| voiceCountLabel.setEditable (false); | |||||
| audioSourcePlayer.setSource (this); | |||||
| #ifndef JUCE_DEMO_RUNNER | |||||
| audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr); | |||||
| #endif | |||||
| audioDeviceManager.addAudioCallback (&audioSourcePlayer); | |||||
| audioDeviceManager.addMidiInputDeviceCallback ({}, this); | |||||
| setOpaque (true); | |||||
| setSize (640, 480); | |||||
| startTimerHz (10); | |||||
| } | |||||
| ~AudioWorkgroupDemo() override | |||||
| { | |||||
| audioSourcePlayer.setSource (nullptr); | |||||
| audioDeviceManager.removeMidiInputDeviceCallback ({}, this); | |||||
| audioDeviceManager.removeAudioCallback (&audioSourcePlayer); | |||||
| } | |||||
| //============================================================================== | |||||
| void paint (Graphics& g) override | |||||
| { | |||||
| g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground)); | |||||
| } | |||||
| void resized() override | |||||
| { | |||||
| auto bounds = getLocalBounds(); | |||||
| liveAudioDisplayComp.setBounds (bounds.removeFromTop (60)); | |||||
| keyboardComponent.setBounds (bounds.removeFromBottom (150)); | |||||
| envelopeComponent.setBounds (bounds.removeFromBottom (150)); | |||||
| thicknessSlider.setBounds (bounds.removeFromTop (30)); | |||||
| voiceCountLabel.setBounds (bounds.removeFromTop (30)); | |||||
| const auto maxLabelWidth = bounds.getWidth() / 4; | |||||
| auto currentBounds = bounds.removeFromLeft (maxLabelWidth); | |||||
| for (auto& l : threadLabels) | |||||
| { | |||||
| if (currentBounds.getHeight() < 30) | |||||
| currentBounds = bounds.removeFromLeft (maxLabelWidth); | |||||
| l->setBounds (currentBounds.removeFromTop (30)); | |||||
| } | |||||
| } | |||||
| void timerCallback() override | |||||
| { | |||||
| String text; | |||||
| int totalVoices = 0; | |||||
| { | |||||
| const SpinLock::ScopedLockType sl { threadArrayUiLock }; | |||||
| for (size_t i = 0; i < NumWorkerThreads; i++) | |||||
| { | |||||
| const auto& thread = workerThreads[i]; | |||||
| auto& label = threadLabels[i]; | |||||
| if (thread != nullptr) | |||||
| { | |||||
| const auto count = thread->getJobCount(); | |||||
| text = "Thread "; | |||||
| text << (int) i << ": " << count << " jobs"; | |||||
| label->setText (text, dontSendNotification); | |||||
| totalVoices += count; | |||||
| } | |||||
| } | |||||
| } | |||||
| text = {}; | |||||
| text << "Voices: " << totalVoices << " (" << totalVoices * 7 << " oscs)"; | |||||
| voiceCountLabel.setText (text, dontSendNotification); | |||||
| } | |||||
| //============================================================================== | |||||
| void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override | |||||
| { | |||||
| completionBarrier = ThreadBarrier::make ((int) NumWorkerThreads + 1); | |||||
| const auto numChannels = 2; | |||||
| const auto workerOptions = AudioWorkerThreadOptions | |||||
| { | |||||
| numChannels, | |||||
| samplesPerBlockExpected, | |||||
| sampleRate, | |||||
| audioDeviceManager.getDeviceAudioWorkgroup(), | |||||
| completionBarrier, | |||||
| }; | |||||
| { | |||||
| const SpinLock::ScopedLockType sl { threadArrayUiLock }; | |||||
| for (auto& worker : workerThreads) | |||||
| worker.reset (new AudioWorkerThread { workerOptions }); | |||||
| } | |||||
| synthesizer.prepareToPlay (samplesPerBlockExpected, sampleRate); | |||||
| liveAudioDisplayComp.audioDeviceAboutToStart (audioDeviceManager.getCurrentAudioDevice()); | |||||
| waveformBuffer.setSize (1, samplesPerBlockExpected); | |||||
| } | |||||
| void releaseResources() override | |||||
| { | |||||
| { | |||||
| const SpinLock::ScopedLockType sl { threadArrayUiLock }; | |||||
| for (auto& thread : workerThreads) | |||||
| thread.reset(); | |||||
| } | |||||
| liveAudioDisplayComp.audioDeviceStopped(); | |||||
| } | |||||
| void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override | |||||
| { | |||||
| midiBuffer.clear(); | |||||
| bufferToFill.clearActiveBufferRegion(); | |||||
| keyboardState.processNextMidiBuffer (midiBuffer, bufferToFill.startSample, bufferToFill.numSamples, true); | |||||
| AudioWorkerThread* workers[NumWorkerThreads]{}; | |||||
| std::transform (workerThreads.begin(), workerThreads.end(), workers, | |||||
| [] (auto& worker) { return worker.get(); }); | |||||
| synthesizer.process (completionBarrier, Span { workers }, *bufferToFill.buffer, midiBuffer); | |||||
| // LiveAudioScrollingDisplay applies a 10x gain to the input signal, we need to reduce the gain on our signal. | |||||
| waveformBuffer.copyFrom (0, 0, | |||||
| bufferToFill.buffer->getReadPointer (0), | |||||
| bufferToFill.numSamples, | |||||
| 1 / 10.f); | |||||
| liveAudioDisplayComp.audioDeviceIOCallbackWithContext (waveformBuffer.getArrayOfReadPointers(), 1, | |||||
| nullptr, 0, bufferToFill.numSamples, {}); | |||||
| } | |||||
| void handleIncomingMidiMessage (MidiInput*, const MidiMessage& message) override | |||||
| { | |||||
| if (message.isNoteOn()) | |||||
| keyboardState.noteOn (message.getChannel(), message.getNoteNumber(), 1); | |||||
| else if (message.isNoteOff()) | |||||
| keyboardState.noteOff (message.getChannel(), message.getNoteNumber(), 1); | |||||
| } | |||||
| private: | |||||
| // if this PIP is running inside the demo runner, we'll use the shared device manager instead | |||||
| #ifndef JUCE_DEMO_RUNNER | |||||
| AudioDeviceManager audioDeviceManager; | |||||
| #else | |||||
| AudioDeviceManager& audioDeviceManager { getSharedAudioDeviceManager (0, 2) }; | |||||
| #endif | |||||
| MidiBuffer midiBuffer; | |||||
| MidiKeyboardState keyboardState; | |||||
| AudioSourcePlayer audioSourcePlayer; | |||||
| SuperSynth synthesizer; | |||||
| AudioSampleBuffer waveformBuffer; | |||||
| MidiKeyboardComponent keyboardComponent { keyboardState, MidiKeyboardComponent::horizontalKeyboard }; | |||||
| LiveScrollingAudioDisplay liveAudioDisplayComp; | |||||
| ADSRComponent envelopeComponent; | |||||
| Slider thicknessSlider { Slider::SliderStyle::LinearHorizontal, Slider::TextBoxLeft }; | |||||
| Label voiceCountLabel; | |||||
| SpinLock threadArrayUiLock; | |||||
| ThreadBarrier::Ptr completionBarrier; | |||||
| std::array<std::unique_ptr<Label>, NumWorkerThreads> threadLabels; | |||||
| std::array<AudioWorkerThread::Ptr, NumWorkerThreads> workerThreads; | |||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioWorkgroupDemo) | |||||
| }; | |||||
| @@ -0,0 +1,185 @@ | |||||
| /* | |||||
| ============================================================================== | |||||
| 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. | |||||
| ============================================================================== | |||||
| */ | |||||
| #pragma once | |||||
| class ADSRComponent final : public Component | |||||
| { | |||||
| public: | |||||
| ADSRComponent() : envelope { *this } | |||||
| { | |||||
| for (Slider* slider : { &adsrAttack, &adsrDecay, &adsrSustain, &adsrRelease }) | |||||
| { | |||||
| if (slider == &adsrSustain) | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| const auto val = (int) jmap (value, 0.0, 1.0, 0.0, 100.0); | |||||
| text << String::formatted (": %d%%", val); | |||||
| return text; | |||||
| }; | |||||
| } | |||||
| else | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| text << ": " << ((value < 0.4f) ? String::formatted ("%dms", (int) std::round (value * 1000)) | |||||
| : String::formatted ("%0.2lf Sec", value)); | |||||
| return text; | |||||
| }; | |||||
| slider->setSkewFactor (0.3); | |||||
| } | |||||
| slider->setRange (0, 1); | |||||
| slider->setTextBoxStyle (Slider::TextBoxBelow, true, 300, 25); | |||||
| slider->onValueChange = [this] | |||||
| { | |||||
| NullCheckedInvocation::invoke (onChange); | |||||
| repaint(); | |||||
| }; | |||||
| addAndMakeVisible (slider); | |||||
| } | |||||
| adsrAttack.setName ("Attack"); | |||||
| adsrDecay.setName ("Decay"); | |||||
| adsrSustain.setName ("Sustain"); | |||||
| adsrRelease.setName ("Release"); | |||||
| adsrAttack.setValue (0.1, dontSendNotification); | |||||
| adsrDecay.setValue (0.3, dontSendNotification); | |||||
| adsrSustain.setValue (0.3, dontSendNotification); | |||||
| adsrRelease.setValue (0.2, dontSendNotification); | |||||
| addAndMakeVisible (envelope); | |||||
| } | |||||
| std::function<void()> onChange; | |||||
| ADSR::Parameters getParameters() const | |||||
| { | |||||
| return | |||||
| { | |||||
| (float) adsrAttack.getValue(), | |||||
| (float) adsrDecay.getValue(), | |||||
| (float) adsrSustain.getValue(), | |||||
| (float) adsrRelease.getValue(), | |||||
| }; | |||||
| } | |||||
| void resized() final | |||||
| { | |||||
| auto bounds = getLocalBounds(); | |||||
| const auto knobWidth = bounds.getWidth() / 4; | |||||
| auto knobBounds = bounds.removeFromBottom (bounds.getHeight() / 2); | |||||
| { | |||||
| adsrAttack.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrDecay.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrSustain.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrRelease.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| } | |||||
| envelope.setBounds (bounds); | |||||
| } | |||||
| Slider adsrAttack { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrDecay { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrSustain { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrRelease { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| private: | |||||
| class Envelope final : public Component | |||||
| { | |||||
| public: | |||||
| Envelope (ADSRComponent& adsr) : parent { adsr } {} | |||||
| void paint (Graphics& g) final | |||||
| { | |||||
| const auto env = parent.getParameters(); | |||||
| // sustain isn't a length but we use a fixed value here to give | |||||
| // sustain some visual width in the envelope | |||||
| constexpr auto sustainLength = 0.1; | |||||
| const auto adsrLength = env.attack | |||||
| + env.decay | |||||
| + sustainLength | |||||
| + env.release; | |||||
| auto bounds = getLocalBounds().toFloat(); | |||||
| const auto attackWidth = bounds.proportionOfWidth (env.attack / adsrLength); | |||||
| const auto decayWidth = bounds.proportionOfWidth (env.decay / adsrLength); | |||||
| const auto sustainWidth = bounds.proportionOfWidth (sustainLength / adsrLength); | |||||
| const auto releaseWidth = bounds.proportionOfWidth (env.release / adsrLength); | |||||
| const auto sustainHeight = bounds.proportionOfHeight (1 - env.sustain); | |||||
| const auto attackBounds = bounds.removeFromLeft (attackWidth); | |||||
| const auto decayBounds = bounds.removeFromLeft (decayWidth); | |||||
| const auto sustainBounds = bounds.removeFromLeft (sustainWidth); | |||||
| const auto releaseBounds = bounds.removeFromLeft (releaseWidth); | |||||
| g.setColour (Colours::black.withAlpha (0.1f)); | |||||
| g.fillRect (bounds); | |||||
| const auto alpha = 0.4f; | |||||
| g.setColour (Colour (246, 98, 92).withAlpha (alpha)); | |||||
| g.fillRect (attackBounds); | |||||
| g.setColour (Colour (242, 187, 60).withAlpha (alpha)); | |||||
| g.fillRect (decayBounds); | |||||
| g.setColour (Colour (109, 234, 166).withAlpha (alpha)); | |||||
| g.fillRect (sustainBounds); | |||||
| g.setColour (Colour (131, 61, 183).withAlpha (alpha)); | |||||
| g.fillRect (releaseBounds); | |||||
| Path envelopePath; | |||||
| envelopePath.startNewSubPath (attackBounds.getBottomLeft()); | |||||
| envelopePath.lineTo (decayBounds.getTopLeft()); | |||||
| envelopePath.lineTo (sustainBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getBottomRight()); | |||||
| const auto lineThickness = 4.0f; | |||||
| g.setColour (Colours::white); | |||||
| g.strokePath (envelopePath, PathStrokeType { lineThickness }); | |||||
| } | |||||
| private: | |||||
| ADSRComponent& parent; | |||||
| }; | |||||
| Envelope envelope; | |||||
| }; | |||||
| @@ -37,6 +37,7 @@ | |||||
| #include "../../../Audio/AudioRecordingDemo.h" | #include "../../../Audio/AudioRecordingDemo.h" | ||||
| #include "../../../Audio/AudioSettingsDemo.h" | #include "../../../Audio/AudioSettingsDemo.h" | ||||
| #include "../../../Audio/AudioSynthesiserDemo.h" | #include "../../../Audio/AudioSynthesiserDemo.h" | ||||
| #include "../../../Audio/AudioWorkgroupDemo.h" | |||||
| #include "../../../Audio/MidiDemo.h" | #include "../../../Audio/MidiDemo.h" | ||||
| #include "../../../Audio/MPEDemo.h" | #include "../../../Audio/MPEDemo.h" | ||||
| #include "../../../Audio/PluckedStringsDemo.h" | #include "../../../Audio/PluckedStringsDemo.h" | ||||
| @@ -75,6 +76,7 @@ void registerDemos_One() noexcept | |||||
| REGISTER_DEMO (AudioRecordingDemo, Audio, false) | REGISTER_DEMO (AudioRecordingDemo, Audio, false) | ||||
| REGISTER_DEMO (AudioSettingsDemo, Audio, false) | REGISTER_DEMO (AudioSettingsDemo, Audio, false) | ||||
| REGISTER_DEMO (AudioSynthesiserDemo, Audio, false) | REGISTER_DEMO (AudioSynthesiserDemo, Audio, false) | ||||
| REGISTER_DEMO (AudioWorkgroupDemo, Audio, false) | |||||
| REGISTER_DEMO (MidiDemo, Audio, false) | REGISTER_DEMO (MidiDemo, Audio, false) | ||||
| REGISTER_DEMO (MPEDemo, Audio, false) | REGISTER_DEMO (MPEDemo, Audio, false) | ||||
| REGISTER_DEMO (PluckedStringsDemo, Audio, false) | REGISTER_DEMO (PluckedStringsDemo, Audio, false) | ||||
| @@ -86,9 +88,9 @@ void registerDemos_One() noexcept | |||||
| REGISTER_DEMO (IIRFilterDemo, DSP, false) | REGISTER_DEMO (IIRFilterDemo, DSP, false) | ||||
| REGISTER_DEMO (OscillatorDemo, DSP, false) | REGISTER_DEMO (OscillatorDemo, DSP, false) | ||||
| REGISTER_DEMO (OverdriveDemo, DSP, false) | REGISTER_DEMO (OverdriveDemo, DSP, false) | ||||
| #if JUCE_USE_SIMD | |||||
| REGISTER_DEMO (SIMDRegisterDemo, DSP, false) | |||||
| #endif | |||||
| #if JUCE_USE_SIMD | |||||
| REGISTER_DEMO (SIMDRegisterDemo, DSP, false) | |||||
| #endif | |||||
| REGISTER_DEMO (StateVariableFilterDemo, DSP, false) | REGISTER_DEMO (StateVariableFilterDemo, DSP, false) | ||||
| REGISTER_DEMO (WaveShaperTanhDemo, DSP, false) | REGISTER_DEMO (WaveShaperTanhDemo, DSP, false) | ||||
| @@ -0,0 +1,185 @@ | |||||
| /* | |||||
| ============================================================================== | |||||
| 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. | |||||
| ============================================================================== | |||||
| */ | |||||
| #pragma once | |||||
| class ADSRComponent final : public Component | |||||
| { | |||||
| public: | |||||
| ADSRComponent() : envelope { *this } | |||||
| { | |||||
| for (Slider* slider : { &adsrAttack, &adsrDecay, &adsrSustain, &adsrRelease }) | |||||
| { | |||||
| if (slider == &adsrSustain) | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| const auto val = (int) jmap (value, 0.0, 1.0, 0.0, 100.0); | |||||
| text << String::formatted (": %d%%", val); | |||||
| return text; | |||||
| }; | |||||
| } | |||||
| else | |||||
| { | |||||
| slider->textFromValueFunction = [slider] (double value) | |||||
| { | |||||
| String text; | |||||
| text << slider->getName(); | |||||
| text << ": " << ((value < 0.4f) ? String::formatted ("%dms", (int) std::round (value * 1000)) | |||||
| : String::formatted ("%0.2lf Sec", value)); | |||||
| return text; | |||||
| }; | |||||
| slider->setSkewFactor (0.3); | |||||
| } | |||||
| slider->setRange (0, 1); | |||||
| slider->setTextBoxStyle (Slider::TextBoxBelow, true, 300, 25); | |||||
| slider->onValueChange = [this] | |||||
| { | |||||
| NullCheckedInvocation::invoke (onChange); | |||||
| repaint(); | |||||
| }; | |||||
| addAndMakeVisible (slider); | |||||
| } | |||||
| adsrAttack.setName ("Attack"); | |||||
| adsrDecay.setName ("Decay"); | |||||
| adsrSustain.setName ("Sustain"); | |||||
| adsrRelease.setName ("Release"); | |||||
| adsrAttack.setValue (0.1, dontSendNotification); | |||||
| adsrDecay.setValue (0.3, dontSendNotification); | |||||
| adsrSustain.setValue (0.3, dontSendNotification); | |||||
| adsrRelease.setValue (0.2, dontSendNotification); | |||||
| addAndMakeVisible (envelope); | |||||
| } | |||||
| std::function<void()> onChange; | |||||
| ADSR::Parameters getParameters() const | |||||
| { | |||||
| return | |||||
| { | |||||
| (float) adsrAttack.getValue(), | |||||
| (float) adsrDecay.getValue(), | |||||
| (float) adsrSustain.getValue(), | |||||
| (float) adsrRelease.getValue(), | |||||
| }; | |||||
| } | |||||
| void resized() final | |||||
| { | |||||
| auto bounds = getLocalBounds(); | |||||
| const auto knobWidth = bounds.getWidth() / 4; | |||||
| auto knobBounds = bounds.removeFromBottom (bounds.getHeight() / 2); | |||||
| { | |||||
| adsrAttack.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrDecay.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrSustain.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| adsrRelease.setBounds (knobBounds.removeFromLeft (knobWidth)); | |||||
| } | |||||
| envelope.setBounds (bounds); | |||||
| } | |||||
| Slider adsrAttack { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrDecay { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrSustain { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| Slider adsrRelease { Slider::RotaryVerticalDrag, Slider::TextBoxBelow }; | |||||
| private: | |||||
| class Envelope final : public Component | |||||
| { | |||||
| public: | |||||
| Envelope (ADSRComponent& adsr) : parent { adsr } {} | |||||
| void paint (Graphics& g) final | |||||
| { | |||||
| const auto env = parent.getParameters(); | |||||
| // sustain isn't a length but we use a fixed value here to give | |||||
| // sustain some visual width in the envelope | |||||
| constexpr auto sustainLength = 0.1; | |||||
| const auto adsrLength = env.attack | |||||
| + env.decay | |||||
| + sustainLength | |||||
| + env.release; | |||||
| auto bounds = getLocalBounds().toFloat(); | |||||
| const auto attackWidth = bounds.proportionOfWidth (env.attack / adsrLength); | |||||
| const auto decayWidth = bounds.proportionOfWidth (env.decay / adsrLength); | |||||
| const auto sustainWidth = bounds.proportionOfWidth (sustainLength / adsrLength); | |||||
| const auto releaseWidth = bounds.proportionOfWidth (env.release / adsrLength); | |||||
| const auto sustainHeight = bounds.proportionOfHeight (1 - env.sustain); | |||||
| const auto attackBounds = bounds.removeFromLeft (attackWidth); | |||||
| const auto decayBounds = bounds.removeFromLeft (decayWidth); | |||||
| const auto sustainBounds = bounds.removeFromLeft (sustainWidth); | |||||
| const auto releaseBounds = bounds.removeFromLeft (releaseWidth); | |||||
| g.setColour (Colours::black.withAlpha (0.1f)); | |||||
| g.fillRect (bounds); | |||||
| const auto alpha = 0.4f; | |||||
| g.setColour (Colour (246, 98, 92).withAlpha (alpha)); | |||||
| g.fillRect (attackBounds); | |||||
| g.setColour (Colour (242, 187, 60).withAlpha (alpha)); | |||||
| g.fillRect (decayBounds); | |||||
| g.setColour (Colour (109, 234, 166).withAlpha (alpha)); | |||||
| g.fillRect (sustainBounds); | |||||
| g.setColour (Colour (131, 61, 183).withAlpha (alpha)); | |||||
| g.fillRect (releaseBounds); | |||||
| Path envelopePath; | |||||
| envelopePath.startNewSubPath (attackBounds.getBottomLeft()); | |||||
| envelopePath.lineTo (decayBounds.getTopLeft()); | |||||
| envelopePath.lineTo (sustainBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getX(), sustainHeight); | |||||
| envelopePath.lineTo (releaseBounds.getBottomRight()); | |||||
| const auto lineThickness = 4.0f; | |||||
| g.setColour (Colours::white); | |||||
| g.strokePath (envelopePath, PathStrokeType { lineThickness }); | |||||
| } | |||||
| private: | |||||
| ADSRComponent& parent; | |||||
| }; | |||||
| Envelope envelope; | |||||
| }; | |||||