|
- /*
- ==============================================================================
-
- 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)
- };
|