mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
AudioWorkgroup: Add demo app
This commit is contained in:
parent
82e1c7483e
commit
3624346e90
5 changed files with 1220 additions and 3 deletions
186
examples/Assets/ADSRComponent.h
Normal file
186
examples/Assets/ADSRComponent.h
Normal file
|
|
@ -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;
|
||||
};
|
||||
659
examples/Audio/AudioWorkgroupDemo.h
Normal file
659
examples/Audio/AudioWorkgroupDemo.h
Normal file
|
|
@ -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/AudioSettingsDemo.h"
|
||||
#include "../../../Audio/AudioSynthesiserDemo.h"
|
||||
#include "../../../Audio/AudioWorkgroupDemo.h"
|
||||
#include "../../../Audio/MidiDemo.h"
|
||||
#include "../../../Audio/MPEDemo.h"
|
||||
#include "../../../Audio/PluckedStringsDemo.h"
|
||||
|
|
@ -75,6 +76,7 @@ void registerDemos_One() noexcept
|
|||
REGISTER_DEMO (AudioRecordingDemo, Audio, false)
|
||||
REGISTER_DEMO (AudioSettingsDemo, Audio, false)
|
||||
REGISTER_DEMO (AudioSynthesiserDemo, Audio, false)
|
||||
REGISTER_DEMO (AudioWorkgroupDemo, Audio, false)
|
||||
REGISTER_DEMO (MidiDemo, Audio, false)
|
||||
REGISTER_DEMO (MPEDemo, Audio, false)
|
||||
REGISTER_DEMO (PluckedStringsDemo, Audio, false)
|
||||
|
|
@ -86,9 +88,9 @@ void registerDemos_One() noexcept
|
|||
REGISTER_DEMO (IIRFilterDemo, DSP, false)
|
||||
REGISTER_DEMO (OscillatorDemo, 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 (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;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue