diff --git a/examples/Assets/ADSRComponent.h b/examples/Assets/ADSRComponent.h new file mode 100644 index 0000000000..a8763c5864 --- /dev/null +++ b/examples/Assets/ADSRComponent.h @@ -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 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; +}; diff --git a/examples/Audio/AudioWorkgroupDemo.h b/examples/Audio/AudioWorkgroupDemo.h new file mode 100644 index 0000000000..a44c24a97e --- /dev/null +++ b/examples/Audio/AudioWorkgroupDemo.h @@ -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; + + 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 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; + 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 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 jobQueue; + AbstractFifo jobQueueFifo { numJobs }; + std::atomic lastJobCount = 0; + +private: + JUCE_DECLARE_NON_COPYABLE (AudioWorkerThread) + JUCE_DECLARE_NON_MOVEABLE (AudioWorkerThread) +}; + +template +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 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, 128> voices; + std::vector activeVoices; + + template + using ThreadValue = SharedThreadValue; + + SpinLock paramLock; + ThreadValue envelope { paramLock, { 0.f, 0.3f, 1.f, 0.3f } }; + ThreadValue 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