1
0
Fork 0
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:
Oliver James 2023-10-16 11:09:44 +01:00 committed by reuk
parent 82e1c7483e
commit 3624346e90
No known key found for this signature in database
GPG key ID: FCB43929F012EE5C
5 changed files with 1220 additions and 3 deletions

View 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;
};

View 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)
};

View file

@ -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;
};

View file

@ -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)

View file

@ -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;
};