build(deps): vendor JUCE 7.0.12
This commit is contained in:
+186
@@ -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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
/*******************************************************************************
|
||||
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: AudioAppDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Simple audio 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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AudioAppDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class AudioAppDemo final : public AudioAppComponent
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
AudioAppDemo()
|
||||
#ifdef JUCE_DEMO_RUNNER
|
||||
: AudioAppComponent (getSharedAudioDeviceManager (0, 2))
|
||||
#endif
|
||||
{
|
||||
setAudioChannels (0, 2);
|
||||
|
||||
setSize (800, 600);
|
||||
}
|
||||
|
||||
~AudioAppDemo() override
|
||||
{
|
||||
shutdownAudio();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (int samplesPerBlockExpected, double newSampleRate) override
|
||||
{
|
||||
sampleRate = newSampleRate;
|
||||
expectedSamplesPerBlock = samplesPerBlockExpected;
|
||||
}
|
||||
|
||||
/* This method generates the actual audio samples.
|
||||
In this example the buffer is filled with a sine wave whose frequency and
|
||||
amplitude are controlled by the mouse position.
|
||||
*/
|
||||
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
|
||||
{
|
||||
bufferToFill.clearActiveBufferRegion();
|
||||
auto originalPhase = phase;
|
||||
|
||||
for (auto chan = 0; chan < bufferToFill.buffer->getNumChannels(); ++chan)
|
||||
{
|
||||
phase = originalPhase;
|
||||
|
||||
auto* channelData = bufferToFill.buffer->getWritePointer (chan, bufferToFill.startSample);
|
||||
|
||||
for (auto i = 0; i < bufferToFill.numSamples ; ++i)
|
||||
{
|
||||
channelData[i] = amplitude * std::sin (phase);
|
||||
|
||||
// increment the phase step for the next sample
|
||||
phase = std::fmod (phase + phaseDelta, MathConstants<float>::twoPi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void releaseResources() override
|
||||
{
|
||||
// This gets automatically called when audio device parameters change
|
||||
// or device is restarted.
|
||||
}
|
||||
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
// (Our component is opaque, so we must completely fill the background with a solid colour)
|
||||
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
|
||||
|
||||
auto centreY = (float) getHeight() / 2.0f;
|
||||
auto radius = amplitude * 200.0f;
|
||||
|
||||
if (radius >= 0.0f)
|
||||
{
|
||||
// Draw an ellipse based on the mouse position and audio volume
|
||||
g.setColour (Colours::lightgreen);
|
||||
|
||||
g.fillEllipse (jmax (0.0f, lastMousePosition.x) - radius / 2.0f,
|
||||
jmax (0.0f, lastMousePosition.y) - radius / 2.0f,
|
||||
radius, radius);
|
||||
}
|
||||
|
||||
// Draw a representative sine wave.
|
||||
Path wavePath;
|
||||
wavePath.startNewSubPath (0, centreY);
|
||||
|
||||
for (auto x = 1.0f; x < (float) getWidth(); ++x)
|
||||
wavePath.lineTo (x, centreY + amplitude * (float) getHeight() * 2.0f
|
||||
* std::sin (x * frequency * 0.0001f));
|
||||
|
||||
g.setColour (getLookAndFeel().findColour (Slider::thumbColourId));
|
||||
g.strokePath (wavePath, PathStrokeType (2.0f));
|
||||
}
|
||||
|
||||
// Mouse handling..
|
||||
void mouseDown (const MouseEvent& e) override
|
||||
{
|
||||
mouseDrag (e);
|
||||
}
|
||||
|
||||
void mouseDrag (const MouseEvent& e) override
|
||||
{
|
||||
lastMousePosition = e.position;
|
||||
|
||||
frequency = (float) (getHeight() - e.y) * 10.0f;
|
||||
amplitude = jmin (0.9f, 0.2f * e.position.x / (float) getWidth());
|
||||
|
||||
phaseDelta = (float) (MathConstants<double>::twoPi * frequency / sampleRate);
|
||||
|
||||
repaint();
|
||||
}
|
||||
|
||||
void mouseUp (const MouseEvent&) override
|
||||
{
|
||||
amplitude = 0.0f;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
// This is called when the component is resized.
|
||||
// If you add any child components, this is where you should
|
||||
// update their positions.
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
float phase = 0.0f;
|
||||
float phaseDelta = 0.0f;
|
||||
float frequency = 5000.0f;
|
||||
float amplitude = 0.2f;
|
||||
|
||||
double sampleRate = 0.0;
|
||||
int expectedSamplesPerBlock = 0;
|
||||
Point<float> lastMousePosition;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioAppDemo)
|
||||
};
|
||||
+406
@@ -0,0 +1,406 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: AudioLatencyDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Tests the audio latency of a device.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AudioLatencyDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
#include "../Assets/AudioLiveScrollingDisplay.h"
|
||||
|
||||
//==============================================================================
|
||||
class LatencyTester final : public AudioIODeviceCallback,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
LatencyTester (TextEditor& editorBox)
|
||||
: resultsBox (editorBox)
|
||||
{}
|
||||
|
||||
//==============================================================================
|
||||
void beginTest()
|
||||
{
|
||||
resultsBox.moveCaretToEnd();
|
||||
resultsBox.insertTextAtCaret (newLine + newLine + "Starting test..." + newLine);
|
||||
resultsBox.moveCaretToEnd();
|
||||
|
||||
startTimer (50);
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
createTestSound();
|
||||
recordedSound.clear();
|
||||
playingSampleNum = recordedSampleNum = 0;
|
||||
testIsRunning = true;
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (testIsRunning && recordedSampleNum >= recordedSound.getNumSamples())
|
||||
{
|
||||
testIsRunning = false;
|
||||
stopTimer();
|
||||
|
||||
// Test has finished, so calculate the result..
|
||||
auto latencySamples = calculateLatencySamples();
|
||||
|
||||
resultsBox.moveCaretToEnd();
|
||||
resultsBox.insertTextAtCaret (getMessageDescribingResult (latencySamples));
|
||||
resultsBox.moveCaretToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
String getMessageDescribingResult (int latencySamples)
|
||||
{
|
||||
String message;
|
||||
|
||||
if (latencySamples >= 0)
|
||||
{
|
||||
message << newLine
|
||||
<< "Results:" << newLine
|
||||
<< latencySamples << " samples (" << String (latencySamples * 1000.0 / sampleRate, 1)
|
||||
<< " milliseconds)" << newLine
|
||||
<< "The audio device reports an input latency of "
|
||||
<< deviceInputLatency << " samples, output latency of "
|
||||
<< deviceOutputLatency << " samples." << newLine
|
||||
<< "So the corrected latency = "
|
||||
<< (latencySamples - deviceInputLatency - deviceOutputLatency)
|
||||
<< " samples (" << String ((latencySamples - deviceInputLatency - deviceOutputLatency) * 1000.0 / sampleRate, 2)
|
||||
<< " milliseconds)";
|
||||
}
|
||||
else
|
||||
{
|
||||
message << newLine
|
||||
<< "Couldn't detect the test signal!!" << newLine
|
||||
<< "Make sure there's no background noise that might be confusing it..";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void audioDeviceAboutToStart (AudioIODevice* device) override
|
||||
{
|
||||
testIsRunning = false;
|
||||
playingSampleNum = recordedSampleNum = 0;
|
||||
|
||||
sampleRate = device->getCurrentSampleRate();
|
||||
deviceInputLatency = device->getInputLatencyInSamples();
|
||||
deviceOutputLatency = device->getOutputLatencyInSamples();
|
||||
|
||||
recordedSound.setSize (1, (int) (0.9 * sampleRate));
|
||||
recordedSound.clear();
|
||||
}
|
||||
|
||||
void audioDeviceStopped() override {}
|
||||
|
||||
void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels,
|
||||
float* const* outputChannelData, int numOutputChannels,
|
||||
int numSamples, const AudioIODeviceCallbackContext& context) override
|
||||
{
|
||||
ignoreUnused (context);
|
||||
|
||||
const ScopedLock sl (lock);
|
||||
|
||||
if (testIsRunning)
|
||||
{
|
||||
auto* recordingBuffer = recordedSound.getWritePointer (0);
|
||||
auto* playBuffer = testSound.getReadPointer (0);
|
||||
|
||||
for (int i = 0; i < numSamples; ++i)
|
||||
{
|
||||
if (recordedSampleNum < recordedSound.getNumSamples())
|
||||
{
|
||||
auto inputSamp = 0.0f;
|
||||
|
||||
for (auto j = numInputChannels; --j >= 0;)
|
||||
if (inputChannelData[j] != nullptr)
|
||||
inputSamp += inputChannelData[j][i];
|
||||
|
||||
recordingBuffer[recordedSampleNum] = inputSamp;
|
||||
}
|
||||
|
||||
++recordedSampleNum;
|
||||
|
||||
auto outputSamp = (playingSampleNum < testSound.getNumSamples()) ? playBuffer[playingSampleNum] : 0.0f;
|
||||
|
||||
for (auto j = numOutputChannels; --j >= 0;)
|
||||
if (outputChannelData[j] != nullptr)
|
||||
outputChannelData[j][i] = outputSamp;
|
||||
|
||||
++playingSampleNum;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We need to clear the output buffers, in case they're full of junk..
|
||||
for (int i = 0; i < numOutputChannels; ++i)
|
||||
if (outputChannelData[i] != nullptr)
|
||||
zeromem (outputChannelData[i], (size_t) numSamples * sizeof (float));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
TextEditor& resultsBox;
|
||||
AudioBuffer<float> testSound, recordedSound;
|
||||
Array<int> spikePositions;
|
||||
CriticalSection lock;
|
||||
|
||||
int playingSampleNum = 0;
|
||||
int recordedSampleNum = -1;
|
||||
double sampleRate = 0.0;
|
||||
bool testIsRunning = false;
|
||||
int deviceInputLatency, deviceOutputLatency;
|
||||
|
||||
//==============================================================================
|
||||
// create a test sound which consists of a series of randomly-spaced audio spikes..
|
||||
void createTestSound()
|
||||
{
|
||||
auto length = ((int) sampleRate) / 4;
|
||||
testSound.setSize (1, length);
|
||||
testSound.clear();
|
||||
|
||||
Random rand;
|
||||
|
||||
for (int i = 0; i < length; ++i)
|
||||
testSound.setSample (0, i, (rand.nextFloat() - rand.nextFloat() + rand.nextFloat() - rand.nextFloat()) * 0.06f);
|
||||
|
||||
spikePositions.clear();
|
||||
|
||||
int spikePos = 0;
|
||||
int spikeDelta = 50;
|
||||
|
||||
while (spikePos < length - 1)
|
||||
{
|
||||
spikePositions.add (spikePos);
|
||||
|
||||
testSound.setSample (0, spikePos, 0.99f);
|
||||
testSound.setSample (0, spikePos + 1, -0.99f);
|
||||
|
||||
spikePos += spikeDelta;
|
||||
spikeDelta += spikeDelta / 6 + rand.nextInt (5);
|
||||
}
|
||||
}
|
||||
|
||||
// Searches a buffer for a set of spikes that matches those in the test sound
|
||||
int findOffsetOfSpikes (const AudioBuffer<float>& buffer) const
|
||||
{
|
||||
auto minSpikeLevel = 5.0f;
|
||||
auto smooth = 0.975;
|
||||
auto* s = buffer.getReadPointer (0);
|
||||
int spikeDriftAllowed = 5;
|
||||
|
||||
Array<int> spikesFound;
|
||||
spikesFound.ensureStorageAllocated (100);
|
||||
auto runningAverage = 0.0;
|
||||
int lastSpike = 0;
|
||||
|
||||
for (int i = 0; i < buffer.getNumSamples() - 10; ++i)
|
||||
{
|
||||
auto samp = std::abs (s[i]);
|
||||
|
||||
if (samp > runningAverage * minSpikeLevel && i > lastSpike + 20)
|
||||
{
|
||||
lastSpike = i;
|
||||
spikesFound.add (i);
|
||||
}
|
||||
|
||||
runningAverage = runningAverage * smooth + (1.0 - smooth) * samp;
|
||||
}
|
||||
|
||||
int bestMatch = -1;
|
||||
auto bestNumMatches = spikePositions.size() / 3; // the minimum number of matches required
|
||||
|
||||
if (spikesFound.size() < bestNumMatches)
|
||||
return -1;
|
||||
|
||||
for (int offsetToTest = 0; offsetToTest < buffer.getNumSamples() - 2048; ++offsetToTest)
|
||||
{
|
||||
int numMatchesHere = 0;
|
||||
int foundIndex = 0;
|
||||
|
||||
for (int refIndex = 0; refIndex < spikePositions.size(); ++refIndex)
|
||||
{
|
||||
auto referenceSpike = spikePositions.getUnchecked (refIndex) + offsetToTest;
|
||||
int spike = 0;
|
||||
|
||||
while ((spike = spikesFound.getUnchecked (foundIndex)) < referenceSpike - spikeDriftAllowed
|
||||
&& foundIndex < spikesFound.size() - 1)
|
||||
++foundIndex;
|
||||
|
||||
if (spike >= referenceSpike - spikeDriftAllowed && spike <= referenceSpike + spikeDriftAllowed)
|
||||
++numMatchesHere;
|
||||
}
|
||||
|
||||
if (numMatchesHere > bestNumMatches)
|
||||
{
|
||||
bestNumMatches = numMatchesHere;
|
||||
bestMatch = offsetToTest;
|
||||
|
||||
if (numMatchesHere == spikePositions.size())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
int calculateLatencySamples() const
|
||||
{
|
||||
// Detect the sound in both our test sound and the recording of it, and measure the difference
|
||||
// in their start times..
|
||||
auto referenceStart = findOffsetOfSpikes (testSound);
|
||||
jassert (referenceStart >= 0);
|
||||
|
||||
auto recordedStart = findOffsetOfSpikes (recordedSound);
|
||||
|
||||
return (recordedStart < 0) ? -1
|
||||
: (recordedStart - referenceStart);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatencyTester)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AudioLatencyDemo final : public Component
|
||||
{
|
||||
public:
|
||||
AudioLatencyDemo()
|
||||
{
|
||||
setOpaque (true);
|
||||
|
||||
liveAudioScroller.reset (new LiveScrollingAudioDisplay());
|
||||
addAndMakeVisible (liveAudioScroller.get());
|
||||
|
||||
addAndMakeVisible (resultsBox);
|
||||
resultsBox.setMultiLine (true);
|
||||
resultsBox.setReturnKeyStartsNewLine (true);
|
||||
resultsBox.setReadOnly (true);
|
||||
resultsBox.setScrollbarsShown (true);
|
||||
resultsBox.setCaretVisible (false);
|
||||
resultsBox.setPopupMenuEnabled (true);
|
||||
|
||||
resultsBox.setColour (TextEditor::outlineColourId, Colour (0x1c000000));
|
||||
resultsBox.setColour (TextEditor::shadowColourId, Colour (0x16000000));
|
||||
|
||||
resultsBox.setText ("Running this test measures the round-trip latency between the audio output and input "
|
||||
"devices you\'ve got selected.\n\n"
|
||||
"It\'ll play a sound, then try to measure the time at which the sound arrives "
|
||||
"back at the audio input. Obviously for this to work you need to have your "
|
||||
"microphone somewhere near your speakers...");
|
||||
|
||||
addAndMakeVisible (startTestButton);
|
||||
startTestButton.onClick = [this] { startTest(); };
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
RuntimePermissions::request (RuntimePermissions::recordAudio,
|
||||
[this] (bool granted)
|
||||
{
|
||||
int numInputChannels = granted ? 2 : 0;
|
||||
audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr);
|
||||
});
|
||||
#endif
|
||||
|
||||
audioDeviceManager.addAudioCallback (liveAudioScroller.get());
|
||||
|
||||
setSize (500, 500);
|
||||
}
|
||||
|
||||
~AudioLatencyDemo() override
|
||||
{
|
||||
audioDeviceManager.removeAudioCallback (liveAudioScroller.get());
|
||||
audioDeviceManager.removeAudioCallback (latencyTester .get());
|
||||
latencyTester .reset();
|
||||
liveAudioScroller.reset();
|
||||
}
|
||||
|
||||
void startTest()
|
||||
{
|
||||
if (latencyTester.get() == nullptr)
|
||||
{
|
||||
latencyTester.reset (new LatencyTester (resultsBox));
|
||||
audioDeviceManager.addAudioCallback (latencyTester.get());
|
||||
}
|
||||
|
||||
latencyTester->beginTest();
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (findColour (ResizableWindow::backgroundColourId));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto b = getLocalBounds().reduced (5);
|
||||
|
||||
if (liveAudioScroller.get() != nullptr)
|
||||
{
|
||||
liveAudioScroller->setBounds (b.removeFromTop (b.getHeight() / 5));
|
||||
b.removeFromTop (10);
|
||||
}
|
||||
|
||||
startTestButton.setBounds (b.removeFromBottom (b.getHeight() / 10));
|
||||
b.removeFromBottom (10);
|
||||
|
||||
resultsBox.setBounds (b);
|
||||
}
|
||||
|
||||
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 (1, 2) };
|
||||
#endif
|
||||
|
||||
std::unique_ptr<LatencyTester> latencyTester;
|
||||
std::unique_ptr<LiveScrollingAudioDisplay> liveAudioScroller;
|
||||
|
||||
TextButton startTestButton { "Test Latency" };
|
||||
TextEditor resultsBox;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioLatencyDemo)
|
||||
};
|
||||
+540
@@ -0,0 +1,540 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: AudioPlaybackDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Plays an audio file.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
type: Component
|
||||
mainClass: AudioPlaybackDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
|
||||
class DemoThumbnailComp final : public Component,
|
||||
public ChangeListener,
|
||||
public FileDragAndDropTarget,
|
||||
public ChangeBroadcaster,
|
||||
private ScrollBar::Listener,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
DemoThumbnailComp (AudioFormatManager& formatManager,
|
||||
AudioTransportSource& source,
|
||||
Slider& slider)
|
||||
: transportSource (source),
|
||||
zoomSlider (slider),
|
||||
thumbnail (512, formatManager, thumbnailCache)
|
||||
{
|
||||
thumbnail.addChangeListener (this);
|
||||
|
||||
addAndMakeVisible (scrollbar);
|
||||
scrollbar.setRangeLimits (visibleRange);
|
||||
scrollbar.setAutoHide (false);
|
||||
scrollbar.addListener (this);
|
||||
|
||||
currentPositionMarker.setFill (Colours::white.withAlpha (0.85f));
|
||||
addAndMakeVisible (currentPositionMarker);
|
||||
}
|
||||
|
||||
~DemoThumbnailComp() override
|
||||
{
|
||||
scrollbar.removeListener (this);
|
||||
thumbnail.removeChangeListener (this);
|
||||
}
|
||||
|
||||
void setURL (const URL& url)
|
||||
{
|
||||
if (auto inputSource = makeInputSource (url))
|
||||
{
|
||||
thumbnail.setSource (inputSource.release());
|
||||
|
||||
Range<double> newRange (0.0, thumbnail.getTotalLength());
|
||||
scrollbar.setRangeLimits (newRange);
|
||||
setRange (newRange);
|
||||
|
||||
startTimerHz (40);
|
||||
}
|
||||
}
|
||||
|
||||
URL getLastDroppedFile() const noexcept { return lastFileDropped; }
|
||||
|
||||
void setZoomFactor (double amount)
|
||||
{
|
||||
if (thumbnail.getTotalLength() > 0)
|
||||
{
|
||||
auto newScale = jmax (0.001, thumbnail.getTotalLength() * (1.0 - jlimit (0.0, 0.99, amount)));
|
||||
auto timeAtCentre = xToTime ((float) getWidth() / 2.0f);
|
||||
|
||||
setRange ({ timeAtCentre - newScale * 0.5, timeAtCentre + newScale * 0.5 });
|
||||
}
|
||||
}
|
||||
|
||||
void setRange (Range<double> newRange)
|
||||
{
|
||||
visibleRange = newRange;
|
||||
scrollbar.setCurrentRange (visibleRange);
|
||||
updateCursorPosition();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void setFollowsTransport (bool shouldFollow)
|
||||
{
|
||||
isFollowingTransport = shouldFollow;
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::darkgrey);
|
||||
g.setColour (Colours::lightblue);
|
||||
|
||||
if (thumbnail.getTotalLength() > 0.0)
|
||||
{
|
||||
auto thumbArea = getLocalBounds();
|
||||
|
||||
thumbArea.removeFromBottom (scrollbar.getHeight() + 4);
|
||||
thumbnail.drawChannels (g, thumbArea.reduced (2),
|
||||
visibleRange.getStart(), visibleRange.getEnd(), 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setFont (14.0f);
|
||||
g.drawFittedText ("(No audio file selected)", getLocalBounds(), Justification::centred, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
scrollbar.setBounds (getLocalBounds().removeFromBottom (14).reduced (2));
|
||||
}
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster*) override
|
||||
{
|
||||
// this method is called by the thumbnail when it has changed, so we should repaint it..
|
||||
repaint();
|
||||
}
|
||||
|
||||
bool isInterestedInFileDrag (const StringArray& /*files*/) override
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void filesDropped (const StringArray& files, int /*x*/, int /*y*/) override
|
||||
{
|
||||
lastFileDropped = URL (File (files[0]));
|
||||
sendChangeMessage();
|
||||
}
|
||||
|
||||
void mouseDown (const MouseEvent& e) override
|
||||
{
|
||||
mouseDrag (e);
|
||||
}
|
||||
|
||||
void mouseDrag (const MouseEvent& e) override
|
||||
{
|
||||
if (canMoveTransport())
|
||||
transportSource.setPosition (jmax (0.0, xToTime ((float) e.x)));
|
||||
}
|
||||
|
||||
void mouseUp (const MouseEvent&) override
|
||||
{
|
||||
transportSource.start();
|
||||
}
|
||||
|
||||
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& wheel) override
|
||||
{
|
||||
if (thumbnail.getTotalLength() > 0.0)
|
||||
{
|
||||
auto newStart = visibleRange.getStart() - wheel.deltaX * (visibleRange.getLength()) / 10.0;
|
||||
newStart = jlimit (0.0, jmax (0.0, thumbnail.getTotalLength() - (visibleRange.getLength())), newStart);
|
||||
|
||||
if (canMoveTransport())
|
||||
setRange ({ newStart, newStart + visibleRange.getLength() });
|
||||
|
||||
if (! approximatelyEqual (wheel.deltaY, 0.0f))
|
||||
zoomSlider.setValue (zoomSlider.getValue() - wheel.deltaY);
|
||||
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
AudioTransportSource& transportSource;
|
||||
Slider& zoomSlider;
|
||||
ScrollBar scrollbar { false };
|
||||
|
||||
AudioThumbnailCache thumbnailCache { 5 };
|
||||
AudioThumbnail thumbnail;
|
||||
Range<double> visibleRange;
|
||||
bool isFollowingTransport = false;
|
||||
URL lastFileDropped;
|
||||
|
||||
DrawableRectangle currentPositionMarker;
|
||||
|
||||
float timeToX (const double time) const
|
||||
{
|
||||
if (visibleRange.getLength() <= 0)
|
||||
return 0;
|
||||
|
||||
return (float) getWidth() * (float) ((time - visibleRange.getStart()) / visibleRange.getLength());
|
||||
}
|
||||
|
||||
double xToTime (const float x) const
|
||||
{
|
||||
return (x / (float) getWidth()) * (visibleRange.getLength()) + visibleRange.getStart();
|
||||
}
|
||||
|
||||
bool canMoveTransport() const noexcept
|
||||
{
|
||||
return ! (isFollowingTransport && transportSource.isPlaying());
|
||||
}
|
||||
|
||||
void scrollBarMoved (ScrollBar* scrollBarThatHasMoved, double newRangeStart) override
|
||||
{
|
||||
if (scrollBarThatHasMoved == &scrollbar)
|
||||
if (! (isFollowingTransport && transportSource.isPlaying()))
|
||||
setRange (visibleRange.movedToStartAt (newRangeStart));
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (canMoveTransport())
|
||||
updateCursorPosition();
|
||||
else
|
||||
setRange (visibleRange.movedToStartAt (transportSource.getCurrentPosition() - (visibleRange.getLength() / 2.0)));
|
||||
}
|
||||
|
||||
void updateCursorPosition()
|
||||
{
|
||||
currentPositionMarker.setVisible (transportSource.isPlaying() || isMouseButtonDown());
|
||||
|
||||
currentPositionMarker.setRectangle (Rectangle<float> (timeToX (transportSource.getCurrentPosition()) - 0.75f, 0,
|
||||
1.5f, (float) (getHeight() - scrollbar.getHeight())));
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AudioPlaybackDemo final : public Component,
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
private Button::Listener,
|
||||
#else
|
||||
private FileBrowserListener,
|
||||
#endif
|
||||
private ChangeListener
|
||||
{
|
||||
public:
|
||||
AudioPlaybackDemo()
|
||||
{
|
||||
addAndMakeVisible (zoomLabel);
|
||||
zoomLabel.setFont (Font (15.00f, Font::plain));
|
||||
zoomLabel.setJustificationType (Justification::centredRight);
|
||||
zoomLabel.setEditable (false, false, false);
|
||||
zoomLabel.setColour (TextEditor::textColourId, Colours::black);
|
||||
zoomLabel.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
|
||||
|
||||
addAndMakeVisible (followTransportButton);
|
||||
followTransportButton.onClick = [this] { updateFollowTransportState(); };
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
addAndMakeVisible (chooseFileButton);
|
||||
chooseFileButton.addListener (this);
|
||||
#else
|
||||
addAndMakeVisible (fileTreeComp);
|
||||
|
||||
directoryList.setDirectory (File::getSpecialLocation (File::userHomeDirectory), true, true);
|
||||
|
||||
fileTreeComp.setTitle ("Files");
|
||||
fileTreeComp.setColour (FileTreeComponent::backgroundColourId, Colours::lightgrey.withAlpha (0.6f));
|
||||
fileTreeComp.addListener (this);
|
||||
|
||||
addAndMakeVisible (explanation);
|
||||
explanation.setFont (Font (14.00f, Font::plain));
|
||||
explanation.setJustificationType (Justification::bottomRight);
|
||||
explanation.setEditable (false, false, false);
|
||||
explanation.setColour (TextEditor::textColourId, Colours::black);
|
||||
explanation.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
|
||||
#endif
|
||||
|
||||
addAndMakeVisible (zoomSlider);
|
||||
zoomSlider.setRange (0, 1, 0);
|
||||
zoomSlider.onValueChange = [this] { thumbnail->setZoomFactor (zoomSlider.getValue()); };
|
||||
zoomSlider.setSkewFactor (2);
|
||||
|
||||
thumbnail = std::make_unique<DemoThumbnailComp> (formatManager, transportSource, zoomSlider);
|
||||
addAndMakeVisible (thumbnail.get());
|
||||
thumbnail->addChangeListener (this);
|
||||
|
||||
addAndMakeVisible (startStopButton);
|
||||
startStopButton.setColour (TextButton::buttonColourId, Colour (0xff79ed7f));
|
||||
startStopButton.setColour (TextButton::textColourOffId, Colours::black);
|
||||
startStopButton.onClick = [this] { startOrStop(); };
|
||||
|
||||
// audio setup
|
||||
formatManager.registerBasicFormats();
|
||||
|
||||
thread.startThread (Thread::Priority::normal);
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
|
||||
#endif
|
||||
|
||||
audioDeviceManager.addAudioCallback (&audioSourcePlayer);
|
||||
audioSourcePlayer.setSource (&transportSource);
|
||||
|
||||
setOpaque (true);
|
||||
setSize (500, 500);
|
||||
}
|
||||
|
||||
~AudioPlaybackDemo() override
|
||||
{
|
||||
transportSource .setSource (nullptr);
|
||||
audioSourcePlayer.setSource (nullptr);
|
||||
|
||||
audioDeviceManager.removeAudioCallback (&audioSourcePlayer);
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
chooseFileButton.removeListener (this);
|
||||
#else
|
||||
fileTreeComp.removeListener (this);
|
||||
#endif
|
||||
|
||||
thumbnail->removeChangeListener (this);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto r = getLocalBounds().reduced (4);
|
||||
|
||||
auto controls = r.removeFromBottom (90);
|
||||
|
||||
auto controlRightBounds = controls.removeFromRight (controls.getWidth() / 3);
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
chooseFileButton.setBounds (controlRightBounds.reduced (10));
|
||||
#else
|
||||
explanation.setBounds (controlRightBounds);
|
||||
#endif
|
||||
|
||||
auto zoom = controls.removeFromTop (25);
|
||||
zoomLabel .setBounds (zoom.removeFromLeft (50));
|
||||
zoomSlider.setBounds (zoom);
|
||||
|
||||
followTransportButton.setBounds (controls.removeFromTop (25));
|
||||
startStopButton .setBounds (controls);
|
||||
|
||||
r.removeFromBottom (6);
|
||||
|
||||
#if JUCE_ANDROID || JUCE_IOS
|
||||
thumbnail->setBounds (r);
|
||||
#else
|
||||
thumbnail->setBounds (r.removeFromBottom (140));
|
||||
r.removeFromBottom (6);
|
||||
|
||||
fileTreeComp.setBounds (r);
|
||||
#endif
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
AudioFormatManager formatManager;
|
||||
TimeSliceThread thread { "audio file preview" };
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
std::unique_ptr<FileChooser> fileChooser;
|
||||
TextButton chooseFileButton {"Choose Audio File...", "Choose an audio file for playback"};
|
||||
#else
|
||||
DirectoryContentsList directoryList {nullptr, thread};
|
||||
FileTreeComponent fileTreeComp {directoryList};
|
||||
Label explanation { {}, "Select an audio file in the treeview above, and this page will display its waveform, and let you play it.." };
|
||||
#endif
|
||||
|
||||
URL currentAudioFile;
|
||||
AudioSourcePlayer audioSourcePlayer;
|
||||
AudioTransportSource transportSource;
|
||||
std::unique_ptr<AudioFormatReaderSource> currentAudioFileSource;
|
||||
|
||||
std::unique_ptr<DemoThumbnailComp> thumbnail;
|
||||
Label zoomLabel { {}, "zoom:" };
|
||||
Slider zoomSlider { Slider::LinearHorizontal, Slider::NoTextBox };
|
||||
ToggleButton followTransportButton { "Follow Transport" };
|
||||
TextButton startStopButton { "Play/Stop" };
|
||||
|
||||
//==============================================================================
|
||||
void showAudioResource (URL resource)
|
||||
{
|
||||
if (! loadURLIntoTransport (resource))
|
||||
{
|
||||
// Failed to load the audio file!
|
||||
jassertfalse;
|
||||
return;
|
||||
}
|
||||
|
||||
currentAudioFile = std::move (resource);
|
||||
zoomSlider.setValue (0, dontSendNotification);
|
||||
thumbnail->setURL (currentAudioFile);
|
||||
}
|
||||
|
||||
bool loadURLIntoTransport (const URL& audioURL)
|
||||
{
|
||||
// unload the previous file source and delete it..
|
||||
transportSource.stop();
|
||||
transportSource.setSource (nullptr);
|
||||
currentAudioFileSource.reset();
|
||||
|
||||
const auto source = makeInputSource (audioURL);
|
||||
|
||||
if (source == nullptr)
|
||||
return false;
|
||||
|
||||
auto stream = rawToUniquePtr (source->createInputStream());
|
||||
|
||||
if (stream == nullptr)
|
||||
return false;
|
||||
|
||||
auto reader = rawToUniquePtr (formatManager.createReaderFor (std::move (stream)));
|
||||
|
||||
if (reader == nullptr)
|
||||
return false;
|
||||
|
||||
currentAudioFileSource = std::make_unique<AudioFormatReaderSource> (reader.release(), true);
|
||||
|
||||
// ..and plug it into our transport source
|
||||
transportSource.setSource (currentAudioFileSource.get(),
|
||||
32768, // tells it to buffer this many samples ahead
|
||||
&thread, // this is the background thread to use for reading-ahead
|
||||
currentAudioFileSource->getAudioFormatReader()->sampleRate); // allows for sample rate correction
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void startOrStop()
|
||||
{
|
||||
if (transportSource.isPlaying())
|
||||
{
|
||||
transportSource.stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
transportSource.setPosition (0);
|
||||
transportSource.start();
|
||||
}
|
||||
}
|
||||
|
||||
void updateFollowTransportState()
|
||||
{
|
||||
thumbnail->setFollowsTransport (followTransportButton.getToggleState());
|
||||
}
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
void buttonClicked (Button* btn) override
|
||||
{
|
||||
if (btn == &chooseFileButton && fileChooser.get() == nullptr)
|
||||
{
|
||||
if (! RuntimePermissions::isGranted (RuntimePermissions::readExternalStorage))
|
||||
{
|
||||
SafePointer<AudioPlaybackDemo> safeThis (this);
|
||||
RuntimePermissions::request (RuntimePermissions::readExternalStorage,
|
||||
[safeThis] (bool granted) mutable
|
||||
{
|
||||
if (safeThis != nullptr && granted)
|
||||
safeThis->buttonClicked (&safeThis->chooseFileButton);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (FileChooser::isPlatformDialogAvailable())
|
||||
{
|
||||
fileChooser = std::make_unique<FileChooser> ("Select an audio file...", File(), "*.wav;*.flac;*.aif");
|
||||
|
||||
fileChooser->launchAsync (FileBrowserComponent::openMode | FileBrowserComponent::canSelectFiles,
|
||||
[this] (const FileChooser& fc) mutable
|
||||
{
|
||||
if (fc.getURLResults().size() > 0)
|
||||
{
|
||||
auto u = fc.getURLResult();
|
||||
|
||||
showAudioResource (std::move (u));
|
||||
}
|
||||
|
||||
fileChooser = nullptr;
|
||||
}, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
NativeMessageBox::showAsync (MessageBoxOptions()
|
||||
.withIconType (MessageBoxIconType::WarningIcon)
|
||||
.withTitle ("Enable Code Signing")
|
||||
.withMessage ("You need to enable code-signing for your iOS project and enable \"iCloud Documents\" "
|
||||
"permissions to be able to open audio files on your iDevice. See: "
|
||||
"https://forum.juce.com/t/native-ios-android-file-choosers"),
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
void selectionChanged() override
|
||||
{
|
||||
showAudioResource (URL (fileTreeComp.getSelectedFile()));
|
||||
}
|
||||
|
||||
void fileClicked (const File&, const MouseEvent&) override {}
|
||||
void fileDoubleClicked (const File&) override {}
|
||||
void browserRootChanged (const File&) override {}
|
||||
#endif
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster* source) override
|
||||
{
|
||||
if (source == thumbnail.get())
|
||||
showAudioResource (URL (thumbnail->getLastDroppedFile()));
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioPlaybackDemo)
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: AudioRecordingDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Records audio to a file.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AudioRecordingDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
#include "../Assets/AudioLiveScrollingDisplay.h"
|
||||
|
||||
//==============================================================================
|
||||
/** A simple class that acts as an AudioIODeviceCallback and writes the
|
||||
incoming audio data to a WAV file.
|
||||
*/
|
||||
class AudioRecorder final : public AudioIODeviceCallback
|
||||
{
|
||||
public:
|
||||
AudioRecorder (AudioThumbnail& thumbnailToUpdate)
|
||||
: thumbnail (thumbnailToUpdate)
|
||||
{
|
||||
backgroundThread.startThread();
|
||||
}
|
||||
|
||||
~AudioRecorder() override
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void startRecording (const File& file)
|
||||
{
|
||||
stop();
|
||||
|
||||
if (sampleRate > 0)
|
||||
{
|
||||
// Create an OutputStream to write to our destination file...
|
||||
file.deleteFile();
|
||||
|
||||
if (auto fileStream = std::unique_ptr<FileOutputStream> (file.createOutputStream()))
|
||||
{
|
||||
// Now create a WAV writer object that writes to our output stream...
|
||||
WavAudioFormat wavFormat;
|
||||
|
||||
if (auto writer = wavFormat.createWriterFor (fileStream.get(), sampleRate, 1, 16, {}, 0))
|
||||
{
|
||||
fileStream.release(); // (passes responsibility for deleting the stream to the writer object that is now using it)
|
||||
|
||||
// Now we'll create one of these helper objects which will act as a FIFO buffer, and will
|
||||
// write the data to disk on our background thread.
|
||||
threadedWriter.reset (new AudioFormatWriter::ThreadedWriter (writer, backgroundThread, 32768));
|
||||
|
||||
// Reset our recording thumbnail
|
||||
thumbnail.reset (writer->getNumChannels(), writer->getSampleRate());
|
||||
nextSampleNum = 0;
|
||||
|
||||
// And now, swap over our active writer pointer so that the audio callback will start using it..
|
||||
const ScopedLock sl (writerLock);
|
||||
activeWriter = threadedWriter.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
// First, clear this pointer to stop the audio callback from using our writer object..
|
||||
{
|
||||
const ScopedLock sl (writerLock);
|
||||
activeWriter = nullptr;
|
||||
}
|
||||
|
||||
// Now we can delete the writer object. It's done in this order because the deletion could
|
||||
// take a little time while remaining data gets flushed to disk, so it's best to avoid blocking
|
||||
// the audio callback while this happens.
|
||||
threadedWriter.reset();
|
||||
}
|
||||
|
||||
bool isRecording() const
|
||||
{
|
||||
return activeWriter.load() != nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void audioDeviceAboutToStart (AudioIODevice* device) override
|
||||
{
|
||||
sampleRate = device->getCurrentSampleRate();
|
||||
}
|
||||
|
||||
void audioDeviceStopped() override
|
||||
{
|
||||
sampleRate = 0;
|
||||
}
|
||||
|
||||
void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels,
|
||||
float* const* outputChannelData, int numOutputChannels,
|
||||
int numSamples, const AudioIODeviceCallbackContext& context) override
|
||||
{
|
||||
ignoreUnused (context);
|
||||
|
||||
const ScopedLock sl (writerLock);
|
||||
|
||||
if (activeWriter.load() != nullptr && numInputChannels >= thumbnail.getNumChannels())
|
||||
{
|
||||
activeWriter.load()->write (inputChannelData, numSamples);
|
||||
|
||||
// Create an AudioBuffer to wrap our incoming data, note that this does no allocations or copies, it simply references our input data
|
||||
AudioBuffer<float> buffer (const_cast<float**> (inputChannelData), thumbnail.getNumChannels(), numSamples);
|
||||
thumbnail.addBlock (nextSampleNum, buffer, 0, numSamples);
|
||||
nextSampleNum += numSamples;
|
||||
}
|
||||
|
||||
// We need to clear the output buffers, in case they're full of junk..
|
||||
for (int i = 0; i < numOutputChannels; ++i)
|
||||
if (outputChannelData[i] != nullptr)
|
||||
FloatVectorOperations::clear (outputChannelData[i], numSamples);
|
||||
}
|
||||
|
||||
private:
|
||||
AudioThumbnail& thumbnail;
|
||||
TimeSliceThread backgroundThread { "Audio Recorder Thread" }; // the thread that will write our audio data to disk
|
||||
std::unique_ptr<AudioFormatWriter::ThreadedWriter> threadedWriter; // the FIFO used to buffer the incoming data
|
||||
double sampleRate = 0.0;
|
||||
int64 nextSampleNum = 0;
|
||||
|
||||
CriticalSection writerLock;
|
||||
std::atomic<AudioFormatWriter::ThreadedWriter*> activeWriter { nullptr };
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class RecordingThumbnail final : public Component,
|
||||
private ChangeListener
|
||||
{
|
||||
public:
|
||||
RecordingThumbnail()
|
||||
{
|
||||
formatManager.registerBasicFormats();
|
||||
thumbnail.addChangeListener (this);
|
||||
}
|
||||
|
||||
~RecordingThumbnail() override
|
||||
{
|
||||
thumbnail.removeChangeListener (this);
|
||||
}
|
||||
|
||||
AudioThumbnail& getAudioThumbnail() { return thumbnail; }
|
||||
|
||||
void setDisplayFullThumbnail (bool displayFull)
|
||||
{
|
||||
displayFullThumb = displayFull;
|
||||
repaint();
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::darkgrey);
|
||||
g.setColour (Colours::lightgrey);
|
||||
|
||||
if (thumbnail.getTotalLength() > 0.0)
|
||||
{
|
||||
auto endTime = displayFullThumb ? thumbnail.getTotalLength()
|
||||
: jmax (30.0, thumbnail.getTotalLength());
|
||||
|
||||
auto thumbArea = getLocalBounds();
|
||||
thumbnail.drawChannels (g, thumbArea.reduced (2), 0.0, endTime, 1.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
g.setFont (14.0f);
|
||||
g.drawFittedText ("(No file recorded)", getLocalBounds(), Justification::centred, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
AudioFormatManager formatManager;
|
||||
AudioThumbnailCache thumbnailCache { 10 };
|
||||
AudioThumbnail thumbnail { 512, formatManager, thumbnailCache };
|
||||
|
||||
bool displayFullThumb = false;
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster* source) override
|
||||
{
|
||||
if (source == &thumbnail)
|
||||
repaint();
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (RecordingThumbnail)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AudioRecordingDemo final : public Component
|
||||
{
|
||||
public:
|
||||
AudioRecordingDemo()
|
||||
{
|
||||
setOpaque (true);
|
||||
addAndMakeVisible (liveAudioScroller);
|
||||
|
||||
addAndMakeVisible (explanationLabel);
|
||||
explanationLabel.setFont (Font (15.0f, Font::plain));
|
||||
explanationLabel.setJustificationType (Justification::topLeft);
|
||||
explanationLabel.setEditable (false, false, false);
|
||||
explanationLabel.setColour (TextEditor::textColourId, Colours::black);
|
||||
explanationLabel.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
|
||||
|
||||
addAndMakeVisible (recordButton);
|
||||
recordButton.setColour (TextButton::buttonColourId, Colour (0xffff5c5c));
|
||||
recordButton.setColour (TextButton::textColourOnId, Colours::black);
|
||||
|
||||
recordButton.onClick = [this]
|
||||
{
|
||||
if (recorder.isRecording())
|
||||
stopRecording();
|
||||
else
|
||||
startRecording();
|
||||
};
|
||||
|
||||
addAndMakeVisible (recordingThumbnail);
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
RuntimePermissions::request (RuntimePermissions::recordAudio,
|
||||
[this] (bool granted)
|
||||
{
|
||||
int numInputChannels = granted ? 2 : 0;
|
||||
audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr);
|
||||
});
|
||||
#endif
|
||||
|
||||
audioDeviceManager.addAudioCallback (&liveAudioScroller);
|
||||
audioDeviceManager.addAudioCallback (&recorder);
|
||||
|
||||
setSize (500, 500);
|
||||
}
|
||||
|
||||
~AudioRecordingDemo() override
|
||||
{
|
||||
audioDeviceManager.removeAudioCallback (&recorder);
|
||||
audioDeviceManager.removeAudioCallback (&liveAudioScroller);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto area = getLocalBounds();
|
||||
|
||||
liveAudioScroller .setBounds (area.removeFromTop (80).reduced (8));
|
||||
recordingThumbnail.setBounds (area.removeFromTop (80).reduced (8));
|
||||
recordButton .setBounds (area.removeFromTop (36).removeFromLeft (140).reduced (8));
|
||||
explanationLabel .setBounds (area.reduced (8));
|
||||
}
|
||||
|
||||
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 (1, 0) };
|
||||
#endif
|
||||
|
||||
LiveScrollingAudioDisplay liveAudioScroller;
|
||||
RecordingThumbnail recordingThumbnail;
|
||||
AudioRecorder recorder { recordingThumbnail.getAudioThumbnail() };
|
||||
|
||||
Label explanationLabel { {},
|
||||
"This page demonstrates how to record a wave file from the live audio input.\n\n"
|
||||
"After you are done with your recording you can choose where to save it." };
|
||||
TextButton recordButton { "Record" };
|
||||
File lastRecording;
|
||||
FileChooser chooser { "Output file...", File::getCurrentWorkingDirectory().getChildFile ("recording.wav"), "*.wav" };
|
||||
|
||||
void startRecording()
|
||||
{
|
||||
if (! RuntimePermissions::isGranted (RuntimePermissions::writeExternalStorage))
|
||||
{
|
||||
SafePointer<AudioRecordingDemo> safeThis (this);
|
||||
|
||||
RuntimePermissions::request (RuntimePermissions::writeExternalStorage,
|
||||
[safeThis] (bool granted) mutable
|
||||
{
|
||||
if (granted)
|
||||
safeThis->startRecording();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
#if (JUCE_ANDROID || JUCE_IOS)
|
||||
auto parentDir = File::getSpecialLocation (File::tempDirectory);
|
||||
#else
|
||||
auto parentDir = File::getSpecialLocation (File::userDocumentsDirectory);
|
||||
#endif
|
||||
|
||||
lastRecording = parentDir.getNonexistentChildFile ("JUCE Demo Audio Recording", ".wav");
|
||||
|
||||
recorder.startRecording (lastRecording);
|
||||
|
||||
recordButton.setButtonText ("Stop");
|
||||
recordingThumbnail.setDisplayFullThumbnail (false);
|
||||
}
|
||||
|
||||
void stopRecording()
|
||||
{
|
||||
recorder.stop();
|
||||
|
||||
chooser.launchAsync ( FileBrowserComponent::saveMode
|
||||
| FileBrowserComponent::canSelectFiles
|
||||
| FileBrowserComponent::warnAboutOverwriting,
|
||||
[this] (const FileChooser& c)
|
||||
{
|
||||
if (FileInputStream inputStream (lastRecording); inputStream.openedOk())
|
||||
if (const auto outputStream = makeOutputStream (c.getURLResult()))
|
||||
outputStream->writeFromInputStream (inputStream, -1);
|
||||
|
||||
recordButton.setButtonText ("Record");
|
||||
recordingThumbnail.setDisplayFullThumbnail (true);
|
||||
});
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioRecordingDemo)
|
||||
};
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: AudioSettingsDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Displays information about audio devices.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AudioSettingsDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
|
||||
//==============================================================================
|
||||
class AudioSettingsDemo final : public Component,
|
||||
public ChangeListener
|
||||
{
|
||||
public:
|
||||
AudioSettingsDemo()
|
||||
{
|
||||
setOpaque (true);
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
RuntimePermissions::request (RuntimePermissions::recordAudio,
|
||||
[this] (bool granted)
|
||||
{
|
||||
int numInputChannels = granted ? 2 : 0;
|
||||
audioDeviceManager.initialise (numInputChannels, 2, nullptr, true, {}, nullptr);
|
||||
});
|
||||
#endif
|
||||
|
||||
audioSetupComp.reset (new AudioDeviceSelectorComponent (audioDeviceManager,
|
||||
0, 256, 0, 256, true, true, true, false));
|
||||
addAndMakeVisible (audioSetupComp.get());
|
||||
|
||||
addAndMakeVisible (diagnosticsBox);
|
||||
diagnosticsBox.setMultiLine (true);
|
||||
diagnosticsBox.setReturnKeyStartsNewLine (true);
|
||||
diagnosticsBox.setReadOnly (true);
|
||||
diagnosticsBox.setScrollbarsShown (true);
|
||||
diagnosticsBox.setCaretVisible (false);
|
||||
diagnosticsBox.setPopupMenuEnabled (true);
|
||||
|
||||
audioDeviceManager.addChangeListener (this);
|
||||
|
||||
logMessage ("Audio device diagnostics:\n");
|
||||
dumpDeviceInfo();
|
||||
|
||||
setSize (500, 600);
|
||||
}
|
||||
|
||||
~AudioSettingsDemo() override
|
||||
{
|
||||
audioDeviceManager.removeChangeListener (this);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto r = getLocalBounds().reduced (4);
|
||||
audioSetupComp->setBounds (r.removeFromTop (proportionOfHeight (0.65f)));
|
||||
diagnosticsBox.setBounds (r);
|
||||
}
|
||||
|
||||
void dumpDeviceInfo()
|
||||
{
|
||||
logMessage ("--------------------------------------");
|
||||
logMessage ("Current audio device type: " + (audioDeviceManager.getCurrentDeviceTypeObject() != nullptr
|
||||
? audioDeviceManager.getCurrentDeviceTypeObject()->getTypeName()
|
||||
: "<none>"));
|
||||
|
||||
if (AudioIODevice* device = audioDeviceManager.getCurrentAudioDevice())
|
||||
{
|
||||
logMessage ("Current audio device: " + device->getName().quoted());
|
||||
logMessage ("Sample rate: " + String (device->getCurrentSampleRate()) + " Hz");
|
||||
logMessage ("Block size: " + String (device->getCurrentBufferSizeSamples()) + " samples");
|
||||
logMessage ("Output Latency: " + String (device->getOutputLatencyInSamples()) + " samples");
|
||||
logMessage ("Input Latency: " + String (device->getInputLatencyInSamples()) + " samples");
|
||||
logMessage ("Bit depth: " + String (device->getCurrentBitDepth()));
|
||||
logMessage ("Input channel names: " + device->getInputChannelNames().joinIntoString (", "));
|
||||
logMessage ("Active input channels: " + getListOfActiveBits (device->getActiveInputChannels()));
|
||||
logMessage ("Output channel names: " + device->getOutputChannelNames().joinIntoString (", "));
|
||||
logMessage ("Active output channels: " + getListOfActiveBits (device->getActiveOutputChannels()));
|
||||
}
|
||||
else
|
||||
{
|
||||
logMessage ("No audio device open");
|
||||
}
|
||||
}
|
||||
|
||||
void logMessage (const String& m)
|
||||
{
|
||||
diagnosticsBox.moveCaretToEnd();
|
||||
diagnosticsBox.insertTextAtCaret (m + newLine);
|
||||
}
|
||||
|
||||
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() };
|
||||
#endif
|
||||
|
||||
std::unique_ptr<AudioDeviceSelectorComponent> audioSetupComp;
|
||||
TextEditor diagnosticsBox;
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster*) override
|
||||
{
|
||||
dumpDeviceInfo();
|
||||
}
|
||||
|
||||
void lookAndFeelChanged() override
|
||||
{
|
||||
diagnosticsBox.applyFontToAllText (diagnosticsBox.getFont());
|
||||
}
|
||||
|
||||
static String getListOfActiveBits (const BigInteger& b)
|
||||
{
|
||||
StringArray bits;
|
||||
|
||||
for (int i = 0; i <= b.getHighestBit(); ++i)
|
||||
if (b[i])
|
||||
bits.add (String (i));
|
||||
|
||||
return bits.joinIntoString (", ");
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioSettingsDemo)
|
||||
};
|
||||
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: AudioSynthesiserDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Simple synthesiser 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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: AudioSynthesiserDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "../Assets/DemoUtilities.h"
|
||||
#include "../Assets/AudioLiveScrollingDisplay.h"
|
||||
|
||||
//==============================================================================
|
||||
/** Our demo synth sound is just a basic sine wave.. */
|
||||
struct SineWaveSound final : public SynthesiserSound
|
||||
{
|
||||
bool appliesToNote (int /*midiNoteNumber*/) override { return true; }
|
||||
bool appliesToChannel (int /*midiChannel*/) override { return true; }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Our demo synth voice just plays a sine wave.. */
|
||||
struct SineWaveVoice final : public SynthesiserVoice
|
||||
{
|
||||
bool canPlaySound (SynthesiserSound* sound) override
|
||||
{
|
||||
return dynamic_cast<SineWaveSound*> (sound) != nullptr;
|
||||
}
|
||||
|
||||
void startNote (int midiNoteNumber, float velocity,
|
||||
SynthesiserSound*, int /*currentPitchWheelPosition*/) override
|
||||
{
|
||||
currentAngle = 0.0;
|
||||
level = velocity * 0.15;
|
||||
tailOff = 0.0;
|
||||
|
||||
auto cyclesPerSecond = MidiMessage::getMidiNoteInHertz (midiNoteNumber);
|
||||
auto cyclesPerSample = cyclesPerSecond / getSampleRate();
|
||||
|
||||
angleDelta = cyclesPerSample * MathConstants<double>::twoPi;
|
||||
}
|
||||
|
||||
void stopNote (float /*velocity*/, bool allowTailOff) override
|
||||
{
|
||||
if (allowTailOff)
|
||||
{
|
||||
// start a tail-off by setting this flag. The render callback will pick up on
|
||||
// this and do a fade out, calling clearCurrentNote() when it's finished.
|
||||
|
||||
if (approximatelyEqual (tailOff, 0.0)) // we only need to begin a tail-off if it's not already doing so - the
|
||||
tailOff = 1.0; // stopNote method could be called more than once.
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're being told to stop playing immediately, so reset everything..
|
||||
clearCurrentNote();
|
||||
angleDelta = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void pitchWheelMoved (int /*newValue*/) override {}
|
||||
void controllerMoved (int /*controllerNumber*/, int /*newValue*/) override {}
|
||||
|
||||
void renderNextBlock (AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
|
||||
{
|
||||
if (! approximatelyEqual (angleDelta, 0.0))
|
||||
{
|
||||
if (tailOff > 0.0)
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = (float) (std::sin (currentAngle) * level * tailOff);
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
currentAngle += angleDelta;
|
||||
++startSample;
|
||||
|
||||
tailOff *= 0.99;
|
||||
|
||||
if (tailOff <= 0.005)
|
||||
{
|
||||
clearCurrentNote();
|
||||
|
||||
angleDelta = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = (float) (std::sin (currentAngle) * level);
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
currentAngle += angleDelta;
|
||||
++startSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using SynthesiserVoice::renderNextBlock;
|
||||
|
||||
private:
|
||||
double currentAngle = 0.0, angleDelta = 0.0, level = 0.0, tailOff = 0.0;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// This is an audio source that streams the output of our demo synth.
|
||||
struct SynthAudioSource final : public AudioSource
|
||||
{
|
||||
SynthAudioSource (MidiKeyboardState& keyState) : keyboardState (keyState)
|
||||
{
|
||||
// Add some voices to our synth, to play the sounds..
|
||||
for (auto i = 0; i < 4; ++i)
|
||||
{
|
||||
synth.addVoice (new SineWaveVoice()); // These voices will play our custom sine-wave sounds..
|
||||
synth.addVoice (new SamplerVoice()); // and these ones play the sampled sounds
|
||||
}
|
||||
|
||||
// ..and add a sound for them to play...
|
||||
setUsingSineWaveSound();
|
||||
}
|
||||
|
||||
void setUsingSineWaveSound()
|
||||
{
|
||||
synth.clearSounds();
|
||||
synth.addSound (new SineWaveSound());
|
||||
}
|
||||
|
||||
void setUsingSampledSound()
|
||||
{
|
||||
WavAudioFormat wavFormat;
|
||||
|
||||
std::unique_ptr<AudioFormatReader> audioReader (wavFormat.createReaderFor (createAssetInputStream ("cello.wav").release(), true));
|
||||
|
||||
BigInteger allNotes;
|
||||
allNotes.setRange (0, 128, true);
|
||||
|
||||
synth.clearSounds();
|
||||
synth.addSound (new SamplerSound ("demo sound",
|
||||
*audioReader,
|
||||
allNotes,
|
||||
74, // root midi note
|
||||
0.1, // attack time
|
||||
0.1, // release time
|
||||
10.0 // maximum sample length
|
||||
));
|
||||
}
|
||||
|
||||
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
|
||||
{
|
||||
midiCollector.reset (sampleRate);
|
||||
|
||||
synth.setCurrentPlaybackSampleRate (sampleRate);
|
||||
}
|
||||
|
||||
void releaseResources() override {}
|
||||
|
||||
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
|
||||
{
|
||||
// the synth always adds its output to the audio buffer, so we have to clear it
|
||||
// first..
|
||||
bufferToFill.clearActiveBufferRegion();
|
||||
|
||||
// fill a midi buffer with incoming messages from the midi input.
|
||||
MidiBuffer incomingMidi;
|
||||
midiCollector.removeNextBlockOfMessages (incomingMidi, bufferToFill.numSamples);
|
||||
|
||||
// pass these messages to the keyboard state so that it can update the component
|
||||
// to show on-screen which keys are being pressed on the physical midi keyboard.
|
||||
// This call will also add midi messages to the buffer which were generated by
|
||||
// the mouse-clicking on the on-screen keyboard.
|
||||
keyboardState.processNextMidiBuffer (incomingMidi, 0, bufferToFill.numSamples, true);
|
||||
|
||||
// and now get the synth to process the midi events and generate its output.
|
||||
synth.renderNextBlock (*bufferToFill.buffer, incomingMidi, 0, bufferToFill.numSamples);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// this collects real-time midi messages from the midi input device, and
|
||||
// turns them into blocks that we can process in our audio callback
|
||||
MidiMessageCollector midiCollector;
|
||||
|
||||
// this represents the state of which keys on our on-screen keyboard are held
|
||||
// down. When the mouse is clicked on the keyboard component, this object also
|
||||
// generates midi messages for this, which we can pass on to our synth.
|
||||
MidiKeyboardState& keyboardState;
|
||||
|
||||
// the synth itself!
|
||||
Synthesiser synth;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class Callback final : public AudioIODeviceCallback
|
||||
{
|
||||
public:
|
||||
Callback (AudioSourcePlayer& playerIn, LiveScrollingAudioDisplay& displayIn)
|
||||
: player (playerIn), display (displayIn) {}
|
||||
|
||||
void audioDeviceIOCallbackWithContext (const float* const* inputChannelData,
|
||||
int numInputChannels,
|
||||
float* const* outputChannelData,
|
||||
int numOutputChannels,
|
||||
int numSamples,
|
||||
const AudioIODeviceCallbackContext& context) override
|
||||
{
|
||||
player.audioDeviceIOCallbackWithContext (inputChannelData,
|
||||
numInputChannels,
|
||||
outputChannelData,
|
||||
numOutputChannels,
|
||||
numSamples,
|
||||
context);
|
||||
display.audioDeviceIOCallbackWithContext (outputChannelData,
|
||||
numOutputChannels,
|
||||
nullptr,
|
||||
0,
|
||||
numSamples,
|
||||
context);
|
||||
}
|
||||
|
||||
void audioDeviceAboutToStart (AudioIODevice* device) override
|
||||
{
|
||||
player.audioDeviceAboutToStart (device);
|
||||
display.audioDeviceAboutToStart (device);
|
||||
}
|
||||
|
||||
void audioDeviceStopped() override
|
||||
{
|
||||
player.audioDeviceStopped();
|
||||
display.audioDeviceStopped();
|
||||
}
|
||||
|
||||
private:
|
||||
AudioSourcePlayer& player;
|
||||
LiveScrollingAudioDisplay& display;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class AudioSynthesiserDemo final : public Component
|
||||
{
|
||||
public:
|
||||
AudioSynthesiserDemo()
|
||||
{
|
||||
addAndMakeVisible (keyboardComponent);
|
||||
|
||||
addAndMakeVisible (sineButton);
|
||||
sineButton.setRadioGroupId (321);
|
||||
sineButton.setToggleState (true, dontSendNotification);
|
||||
sineButton.onClick = [this] { synthAudioSource.setUsingSineWaveSound(); };
|
||||
|
||||
addAndMakeVisible (sampledButton);
|
||||
sampledButton.setRadioGroupId (321);
|
||||
sampledButton.onClick = [this] { synthAudioSource.setUsingSampledSound(); };
|
||||
|
||||
addAndMakeVisible (liveAudioDisplayComp);
|
||||
audioSourcePlayer.setSource (&synthAudioSource);
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
|
||||
#endif
|
||||
|
||||
audioDeviceManager.addAudioCallback (&callback);
|
||||
audioDeviceManager.addMidiInputDeviceCallback ({}, &(synthAudioSource.midiCollector));
|
||||
|
||||
setOpaque (true);
|
||||
setSize (640, 480);
|
||||
}
|
||||
|
||||
~AudioSynthesiserDemo() override
|
||||
{
|
||||
audioSourcePlayer.setSource (nullptr);
|
||||
audioDeviceManager.removeMidiInputDeviceCallback ({}, &(synthAudioSource.midiCollector));
|
||||
audioDeviceManager.removeAudioCallback (&callback);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (getUIColourIfAvailable (LookAndFeel_V4::ColourScheme::UIColour::windowBackground));
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
keyboardComponent .setBounds (8, 96, getWidth() - 16, 64);
|
||||
sineButton .setBounds (16, 176, 150, 24);
|
||||
sampledButton .setBounds (16, 200, 150, 24);
|
||||
liveAudioDisplayComp.setBounds (8, 8, getWidth() - 16, 64);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
MidiKeyboardState keyboardState;
|
||||
AudioSourcePlayer audioSourcePlayer;
|
||||
SynthAudioSource synthAudioSource { keyboardState };
|
||||
MidiKeyboardComponent keyboardComponent { keyboardState, MidiKeyboardComponent::horizontalKeyboard};
|
||||
|
||||
ToggleButton sineButton { "Use sine wave" };
|
||||
ToggleButton sampledButton { "Use sampled sound" };
|
||||
|
||||
LiveScrollingAudioDisplay liveAudioDisplayComp;
|
||||
|
||||
Callback callback { audioSourcePlayer, liveAudioDisplayComp };
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AudioSynthesiserDemo)
|
||||
};
|
||||
@@ -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)
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
# ==============================================================================
|
||||
#
|
||||
# This file is part of the JUCE library.
|
||||
# Copyright (c) 2022 - Raw Material Software Limited
|
||||
#
|
||||
# JUCE is an open source library subject to commercial or open-source
|
||||
# licensing.
|
||||
#
|
||||
# By using JUCE, you agree to the terms of both the JUCE 7 End-User License
|
||||
# Agreement and JUCE Privacy Policy.
|
||||
#
|
||||
# End User License Agreement: www.juce.com/juce-7-licence
|
||||
# Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
#
|
||||
# Or: You may also use this code under the terms of the GPL v3 (see
|
||||
# www.gnu.org/licenses).
|
||||
#
|
||||
# JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
# EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
# DISCLAIMED.
|
||||
#
|
||||
# ==============================================================================
|
||||
|
||||
_juce_add_pips()
|
||||
+5029
File diff suppressed because it is too large
Load Diff
+773
@@ -0,0 +1,773 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: MPEDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Simple MPE synthesiser 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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: MPEDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class ZoneColourPicker
|
||||
{
|
||||
public:
|
||||
ZoneColourPicker() {}
|
||||
|
||||
//==============================================================================
|
||||
Colour getColourForMidiChannel (int midiChannel) noexcept
|
||||
{
|
||||
if (legacyModeEnabled)
|
||||
return Colours::white;
|
||||
|
||||
if (zoneLayout.getLowerZone().isUsingChannelAsMemberChannel (midiChannel))
|
||||
return getColourForZone (true);
|
||||
|
||||
if (zoneLayout.getUpperZone().isUsingChannelAsMemberChannel (midiChannel))
|
||||
return getColourForZone (false);
|
||||
|
||||
return Colours::transparentBlack;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Colour getColourForZone (bool isLowerZone) const noexcept
|
||||
{
|
||||
if (legacyModeEnabled)
|
||||
return Colours::white;
|
||||
|
||||
if (isLowerZone)
|
||||
return Colours::blue;
|
||||
|
||||
return Colours::red;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void setZoneLayout (MPEZoneLayout layout) noexcept { zoneLayout = layout; }
|
||||
void setLegacyModeEnabled (bool shouldBeEnabled) noexcept { legacyModeEnabled = shouldBeEnabled; }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
MPEZoneLayout zoneLayout;
|
||||
bool legacyModeEnabled = false;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ZoneColourPicker)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MPESetupComponent final : public Component
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MPESetupComponent (MPEInstrument& instr)
|
||||
: instrument (instr)
|
||||
{
|
||||
addAndMakeVisible (isLowerZoneButton);
|
||||
isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);
|
||||
|
||||
initialiseComboBoxWithConsecutiveIntegers (memberChannels, memberChannelsLabel, 0, 16, defaultMemberChannels);
|
||||
initialiseComboBoxWithConsecutiveIntegers (masterPitchbendRange, masterPitchbendRangeLabel, 0, 96, defaultMasterPitchbendRange);
|
||||
initialiseComboBoxWithConsecutiveIntegers (notePitchbendRange, notePitchbendRangeLabel, 0, 96, defaultNotePitchbendRange);
|
||||
|
||||
initialiseComboBoxWithConsecutiveIntegers (legacyStartChannel, legacyStartChannelLabel, 1, 16, 1, false);
|
||||
initialiseComboBoxWithConsecutiveIntegers (legacyEndChannel, legacyEndChannelLabel, 1, 16, 16, false);
|
||||
initialiseComboBoxWithConsecutiveIntegers (legacyPitchbendRange, legacyPitchbendRangeLabel, 0, 96, 2, false);
|
||||
|
||||
addAndMakeVisible (setZoneButton);
|
||||
setZoneButton.onClick = [this] { setZoneButtonClicked(); };
|
||||
|
||||
addAndMakeVisible (clearAllZonesButton);
|
||||
clearAllZonesButton.onClick = [this] { clearAllZonesButtonClicked(); };
|
||||
|
||||
addAndMakeVisible (legacyModeEnabledToggle);
|
||||
legacyModeEnabledToggle.onClick = [this] { legacyModeEnabledToggleClicked(); };
|
||||
|
||||
addAndMakeVisible (voiceStealingEnabledToggle);
|
||||
voiceStealingEnabledToggle.onClick = [this] { voiceStealingEnabledToggleClicked(); };
|
||||
|
||||
initialiseComboBoxWithConsecutiveIntegers (numberOfVoices, numberOfVoicesLabel, 1, 20, 15);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void resized() override
|
||||
{
|
||||
Rectangle<int> r (proportionOfWidth (0.65f), 15, proportionOfWidth (0.25f), 3000);
|
||||
auto h = 24;
|
||||
auto hspace = 6;
|
||||
auto hbigspace = 18;
|
||||
|
||||
isLowerZoneButton.setBounds (r.removeFromTop (h));
|
||||
r.removeFromTop (hspace);
|
||||
memberChannels.setBounds (r.removeFromTop (h));
|
||||
r.removeFromTop (hspace);
|
||||
notePitchbendRange.setBounds (r.removeFromTop (h));
|
||||
r.removeFromTop (hspace);
|
||||
masterPitchbendRange.setBounds (r.removeFromTop (h));
|
||||
|
||||
legacyStartChannel .setBounds (isLowerZoneButton .getBounds());
|
||||
legacyEndChannel .setBounds (memberChannels .getBounds());
|
||||
legacyPitchbendRange.setBounds (notePitchbendRange.getBounds());
|
||||
|
||||
r.removeFromTop (hbigspace);
|
||||
|
||||
auto buttonLeft = proportionOfWidth (0.5f);
|
||||
|
||||
setZoneButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
|
||||
r.removeFromTop (hspace);
|
||||
clearAllZonesButton.setBounds (r.removeFromTop (h).withLeft (buttonLeft));
|
||||
|
||||
r.removeFromTop (hbigspace);
|
||||
|
||||
auto toggleLeft = proportionOfWidth (0.25f);
|
||||
|
||||
legacyModeEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
|
||||
r.removeFromTop (hspace);
|
||||
voiceStealingEnabledToggle.setBounds (r.removeFromTop (h).withLeft (toggleLeft));
|
||||
r.removeFromTop (hspace);
|
||||
numberOfVoices.setBounds (r.removeFromTop (h));
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isVoiceStealingEnabled() const { return voiceStealingEnabledToggle.getToggleState(); }
|
||||
int getNumVoices() const { return numberOfVoices.getText().getIntValue(); }
|
||||
|
||||
std::function<void()> onSynthParametersChange;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void initialiseComboBoxWithConsecutiveIntegers (ComboBox& comboBox, Label& labelToAttach,
|
||||
int firstValue, int numValues, int valueToSelect,
|
||||
bool makeVisible = true)
|
||||
{
|
||||
for (auto i = 0; i < numValues; ++i)
|
||||
comboBox.addItem (String (i + firstValue), i + 1);
|
||||
|
||||
comboBox.setSelectedId (valueToSelect - firstValue + 1);
|
||||
labelToAttach.attachToComponent (&comboBox, true);
|
||||
|
||||
if (makeVisible)
|
||||
addAndMakeVisible (comboBox);
|
||||
else
|
||||
addChildComponent (comboBox);
|
||||
|
||||
if (&comboBox == &numberOfVoices)
|
||||
comboBox.onChange = [this] { numberOfVoicesChanged(); };
|
||||
else if (&comboBox == &legacyPitchbendRange)
|
||||
comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModePitchbendRangeChanged(); };
|
||||
else if (&comboBox == &legacyStartChannel || &comboBox == &legacyEndChannel)
|
||||
comboBox.onChange = [this] { if (legacyModeEnabledToggle.getToggleState()) legacyModeChannelRangeChanged(); };
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void setZoneButtonClicked()
|
||||
{
|
||||
auto isLowerZone = isLowerZoneButton.getToggleState();
|
||||
auto numMemberChannels = memberChannels.getText().getIntValue();
|
||||
auto perNotePb = notePitchbendRange.getText().getIntValue();
|
||||
auto masterPb = masterPitchbendRange.getText().getIntValue();
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
if (isLowerZone)
|
||||
zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
|
||||
else
|
||||
zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);
|
||||
|
||||
instrument.setZoneLayout (zoneLayout);
|
||||
}
|
||||
|
||||
void clearAllZonesButtonClicked()
|
||||
{
|
||||
instrument.setZoneLayout ({});
|
||||
}
|
||||
|
||||
void legacyModeEnabledToggleClicked()
|
||||
{
|
||||
auto legacyModeEnabled = legacyModeEnabledToggle.getToggleState();
|
||||
|
||||
isLowerZoneButton .setVisible (! legacyModeEnabled);
|
||||
memberChannels .setVisible (! legacyModeEnabled);
|
||||
notePitchbendRange .setVisible (! legacyModeEnabled);
|
||||
masterPitchbendRange.setVisible (! legacyModeEnabled);
|
||||
setZoneButton .setVisible (! legacyModeEnabled);
|
||||
clearAllZonesButton .setVisible (! legacyModeEnabled);
|
||||
|
||||
legacyStartChannel .setVisible (legacyModeEnabled);
|
||||
legacyEndChannel .setVisible (legacyModeEnabled);
|
||||
legacyPitchbendRange.setVisible (legacyModeEnabled);
|
||||
|
||||
if (legacyModeEnabled)
|
||||
{
|
||||
if (areLegacyModeParametersValid())
|
||||
{
|
||||
instrument.enableLegacyMode();
|
||||
|
||||
instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
|
||||
instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
|
||||
}
|
||||
else
|
||||
{
|
||||
handleInvalidLegacyModeParameters();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
instrument.setZoneLayout ({ MPEZone (MPEZone::Type::lower, 15) });
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void legacyModePitchbendRangeChanged()
|
||||
{
|
||||
jassert (legacyModeEnabledToggle.getToggleState() == true);
|
||||
|
||||
instrument.setLegacyModePitchbendRange (getLegacyModePitchbendRange());
|
||||
}
|
||||
|
||||
void legacyModeChannelRangeChanged()
|
||||
{
|
||||
jassert (legacyModeEnabledToggle.getToggleState() == true);
|
||||
|
||||
if (areLegacyModeParametersValid())
|
||||
instrument.setLegacyModeChannelRange (getLegacyModeChannelRange());
|
||||
else
|
||||
handleInvalidLegacyModeParameters();
|
||||
}
|
||||
|
||||
bool areLegacyModeParametersValid() const
|
||||
{
|
||||
return legacyStartChannel.getText().getIntValue() <= legacyEndChannel.getText().getIntValue();
|
||||
}
|
||||
|
||||
void handleInvalidLegacyModeParameters()
|
||||
{
|
||||
auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
|
||||
"Invalid legacy mode channel layout",
|
||||
"Cannot set legacy mode start/end channel:\n"
|
||||
"The end channel must not be less than the start channel!",
|
||||
"Got it");
|
||||
messageBox = AlertWindow::showScopedAsync (options, nullptr);
|
||||
}
|
||||
|
||||
Range<int> getLegacyModeChannelRange() const
|
||||
{
|
||||
return { legacyStartChannel.getText().getIntValue(),
|
||||
legacyEndChannel.getText().getIntValue() + 1 };
|
||||
}
|
||||
|
||||
int getLegacyModePitchbendRange() const
|
||||
{
|
||||
return legacyPitchbendRange.getText().getIntValue();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void voiceStealingEnabledToggleClicked()
|
||||
{
|
||||
jassert (onSynthParametersChange != nullptr);
|
||||
onSynthParametersChange();
|
||||
}
|
||||
|
||||
void numberOfVoicesChanged()
|
||||
{
|
||||
jassert (onSynthParametersChange != nullptr);
|
||||
onSynthParametersChange();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
MPEInstrument& instrument;
|
||||
|
||||
ComboBox memberChannels, masterPitchbendRange, notePitchbendRange;
|
||||
|
||||
ToggleButton isLowerZoneButton { "Lower zone" };
|
||||
|
||||
Label memberChannelsLabel { {}, "Nr. of member channels:" };
|
||||
Label masterPitchbendRangeLabel { {}, "Master pitchbend range (semitones):" };
|
||||
Label notePitchbendRangeLabel { {}, "Note pitchbend range (semitones):" };
|
||||
|
||||
TextButton setZoneButton { "Set zone" };
|
||||
TextButton clearAllZonesButton { "Clear all zones" };
|
||||
|
||||
ComboBox legacyStartChannel, legacyEndChannel, legacyPitchbendRange;
|
||||
|
||||
Label legacyStartChannelLabel { {}, "First channel:" };
|
||||
Label legacyEndChannelLabel { {}, "Last channel:" };
|
||||
Label legacyPitchbendRangeLabel { {}, "Pitchbend range (semitones):"};
|
||||
|
||||
ToggleButton legacyModeEnabledToggle { "Enable Legacy Mode" };
|
||||
ToggleButton voiceStealingEnabledToggle { "Enable synth voice stealing" };
|
||||
|
||||
ComboBox numberOfVoices;
|
||||
Label numberOfVoicesLabel { {}, "Number of synth voices"};
|
||||
|
||||
ScopedMessageBox messageBox;
|
||||
|
||||
static constexpr int defaultMemberChannels = 15,
|
||||
defaultMasterPitchbendRange = 2,
|
||||
defaultNotePitchbendRange = 48;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESetupComponent)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class ZoneLayoutComponent final : public Component,
|
||||
private MPEInstrument::Listener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
ZoneLayoutComponent (MPEInstrument& instr, ZoneColourPicker& zoneColourPicker)
|
||||
: instrument (instr),
|
||||
colourPicker (zoneColourPicker)
|
||||
{
|
||||
instrument.addListener (this);
|
||||
}
|
||||
|
||||
~ZoneLayoutComponent() override
|
||||
{
|
||||
instrument.removeListener (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
paintBackground (g);
|
||||
|
||||
if (instrument.isLegacyModeEnabled())
|
||||
paintLegacyMode (g);
|
||||
else
|
||||
paintZones (g);
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void zoneLayoutChanged() override
|
||||
{
|
||||
repaint();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paintBackground (Graphics& g)
|
||||
{
|
||||
g.setColour (Colours::black);
|
||||
auto channelWidth = getChannelRectangleWidth();
|
||||
|
||||
for (auto i = 0; i < numMidiChannels; ++i)
|
||||
{
|
||||
auto x = float (i) * channelWidth;
|
||||
Rectangle<int> channelArea ((int) x, 0, (int) channelWidth, getHeight());
|
||||
|
||||
g.drawLine ({ x, 0.0f, x, float (getHeight()) });
|
||||
g.drawText (String (i + 1), channelArea.reduced (4, 4), Justification::topLeft, false);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paintZones (Graphics& g)
|
||||
{
|
||||
auto channelWidth = getChannelRectangleWidth();
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
Array<MPEZoneLayout::Zone> activeZones;
|
||||
if (zoneLayout.getLowerZone().isActive()) activeZones.add (zoneLayout.getLowerZone());
|
||||
if (zoneLayout.getUpperZone().isActive()) activeZones.add (zoneLayout.getUpperZone());
|
||||
|
||||
for (auto zone : activeZones)
|
||||
{
|
||||
auto zoneColour = colourPicker.getColourForZone (zone.isLowerZone());
|
||||
|
||||
auto xPos = zone.isLowerZone() ? 0 : zone.getLastMemberChannel() - 1;
|
||||
|
||||
Rectangle<int> zoneRect { int (channelWidth * (float) xPos), 20,
|
||||
int (channelWidth * (float) (zone.numMemberChannels + 1)), getHeight() - 20 };
|
||||
|
||||
g.setColour (zoneColour);
|
||||
g.drawRect (zoneRect, 3);
|
||||
|
||||
auto masterRect = zone.isLowerZone() ? zoneRect.removeFromLeft ((int) channelWidth) : zoneRect.removeFromRight ((int) channelWidth);
|
||||
|
||||
g.setColour (zoneColour.withAlpha (0.3f));
|
||||
g.fillRect (masterRect);
|
||||
|
||||
g.setColour (zoneColour.contrasting());
|
||||
g.drawText ("<>" + String (zone.masterPitchbendRange), masterRect.reduced (4), Justification::top, false);
|
||||
g.drawText ("<>" + String (zone.perNotePitchbendRange), masterRect.reduced (4), Justification::bottom, false);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paintLegacyMode (Graphics& g)
|
||||
{
|
||||
auto channelRange = instrument.getLegacyModeChannelRange();
|
||||
auto startChannel = channelRange.getStart() - 1;
|
||||
auto numChannels = channelRange.getEnd() - startChannel - 1;
|
||||
|
||||
Rectangle<int> zoneRect (int (getChannelRectangleWidth() * (float) startChannel), 0,
|
||||
int (getChannelRectangleWidth() * (float) numChannels), getHeight());
|
||||
|
||||
zoneRect.removeFromTop (20);
|
||||
|
||||
g.setColour (Colours::white);
|
||||
g.drawRect (zoneRect, 3);
|
||||
g.drawText ("LGCY", zoneRect.reduced (4, 4), Justification::topLeft, false);
|
||||
g.drawText ("<>" + String (instrument.getLegacyModePitchbendRange()), zoneRect.reduced (4, 4), Justification::bottomLeft, false);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
float getChannelRectangleWidth() const noexcept
|
||||
{
|
||||
return (float) getWidth() / (float) numMidiChannels;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static constexpr int numMidiChannels = 16;
|
||||
|
||||
MPEInstrument& instrument;
|
||||
ZoneColourPicker& colourPicker;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MPEDemoSynthVoice final : public MPESynthesiserVoice
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MPEDemoSynthVoice() {}
|
||||
|
||||
//==============================================================================
|
||||
void noteStarted() override
|
||||
{
|
||||
jassert (currentlyPlayingNote.isValid());
|
||||
jassert (currentlyPlayingNote.keyState == MPENote::keyDown
|
||||
|| currentlyPlayingNote.keyState == MPENote::keyDownAndSustained);
|
||||
|
||||
level .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||||
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||||
timbre .setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||||
|
||||
phase = 0.0;
|
||||
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||||
phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
|
||||
|
||||
tailOff = 0.0;
|
||||
}
|
||||
|
||||
void noteStopped (bool allowTailOff) override
|
||||
{
|
||||
jassert (currentlyPlayingNote.keyState == MPENote::off);
|
||||
|
||||
if (allowTailOff)
|
||||
{
|
||||
// start a tail-off by setting this flag. The render callback will pick up on
|
||||
// this and do a fade out, calling clearCurrentNote() when it's finished.
|
||||
|
||||
if (approximatelyEqual (tailOff, 0.0)) // we only need to begin a tail-off if it's not already doing so - the
|
||||
// stopNote method could be called more than once.
|
||||
tailOff = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we're being told to stop playing immediately, so reset everything..
|
||||
clearCurrentNote();
|
||||
phaseDelta = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
void notePressureChanged() override
|
||||
{
|
||||
level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||||
}
|
||||
|
||||
void notePitchbendChanged() override
|
||||
{
|
||||
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||||
}
|
||||
|
||||
void noteTimbreChanged() override
|
||||
{
|
||||
timbre.setTargetValue (currentlyPlayingNote.timbre.asUnsignedFloat());
|
||||
}
|
||||
|
||||
void noteKeyStateChanged() override {}
|
||||
|
||||
void setCurrentSampleRate (double newRate) override
|
||||
{
|
||||
if (! approximatelyEqual (currentSampleRate, newRate))
|
||||
{
|
||||
noteStopped (false);
|
||||
currentSampleRate = newRate;
|
||||
|
||||
level .reset (currentSampleRate, smoothingLengthInSeconds);
|
||||
timbre .reset (currentSampleRate, smoothingLengthInSeconds);
|
||||
frequency.reset (currentSampleRate, smoothingLengthInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
|
||||
int startSample,
|
||||
int numSamples) override
|
||||
{
|
||||
if (! approximatelyEqual (phaseDelta, 0.0))
|
||||
{
|
||||
if (tailOff > 0.0)
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = getNextSample() * (float) tailOff;
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
++startSample;
|
||||
|
||||
tailOff *= 0.99;
|
||||
|
||||
if (tailOff <= 0.005)
|
||||
{
|
||||
clearCurrentNote();
|
||||
|
||||
phaseDelta = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
while (--numSamples >= 0)
|
||||
{
|
||||
auto currentSample = getNextSample();
|
||||
|
||||
for (auto i = outputBuffer.getNumChannels(); --i >= 0;)
|
||||
outputBuffer.addSample (i, startSample, currentSample);
|
||||
|
||||
++startSample;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using MPESynthesiserVoice::renderNextBlock;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
float getNextSample() noexcept
|
||||
{
|
||||
auto levelDb = (level.getNextValue() - 1.0) * maxLevelDb;
|
||||
auto amplitude = pow (10.0f, 0.05f * levelDb) * maxLevel;
|
||||
|
||||
// timbre is used to blend between a sine and a square.
|
||||
auto f1 = std::sin (phase);
|
||||
auto f2 = copysign (1.0, f1);
|
||||
auto a2 = timbre.getNextValue();
|
||||
auto a1 = 1.0 - a2;
|
||||
|
||||
auto nextSample = float (amplitude * ((a1 * f1) + (a2 * f2)));
|
||||
|
||||
auto cyclesPerSample = frequency.getNextValue() / currentSampleRate;
|
||||
phaseDelta = MathConstants<double>::twoPi * cyclesPerSample;
|
||||
phase = std::fmod (phase + phaseDelta, MathConstants<double>::twoPi);
|
||||
|
||||
return nextSample;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
SmoothedValue<double> level, timbre, frequency;
|
||||
|
||||
double phase = 0.0;
|
||||
double phaseDelta = 0.0;
|
||||
double tailOff = 0.0;
|
||||
|
||||
const double maxLevel = 0.05;
|
||||
const double maxLevelDb = 31.0;
|
||||
const double smoothingLengthInSeconds = 0.01;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class MPEDemo final : public Component,
|
||||
private AudioIODeviceCallback,
|
||||
private MidiInputCallback,
|
||||
private MPEInstrument::Listener
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MPEDemo()
|
||||
{
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
audioDeviceManager.initialise (0, 2, nullptr, true, {}, nullptr);
|
||||
#endif
|
||||
|
||||
audioDeviceManager.addMidiInputDeviceCallback ({}, this);
|
||||
audioDeviceManager.addAudioCallback (this);
|
||||
|
||||
addAndMakeVisible (audioSetupComp);
|
||||
addAndMakeVisible (mpeSetupComp);
|
||||
addAndMakeVisible (zoneLayoutComp);
|
||||
addAndMakeVisible (keyboardComponent);
|
||||
|
||||
synth.setVoiceStealingEnabled (false);
|
||||
for (auto i = 0; i < 15; ++i)
|
||||
synth.addVoice (new MPEDemoSynthVoice());
|
||||
|
||||
mpeSetupComp.onSynthParametersChange = [this]
|
||||
{
|
||||
synth.setVoiceStealingEnabled (mpeSetupComp.isVoiceStealingEnabled());
|
||||
|
||||
auto numVoices = mpeSetupComp.getNumVoices();
|
||||
|
||||
if (numVoices < synth.getNumVoices())
|
||||
{
|
||||
synth.reduceNumVoices (numVoices);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (synth.getNumVoices() < numVoices)
|
||||
synth.addVoice (new MPEDemoSynthVoice());
|
||||
}
|
||||
};
|
||||
|
||||
instrument.addListener (this);
|
||||
|
||||
setSize (880, 720);
|
||||
}
|
||||
|
||||
~MPEDemo() override
|
||||
{
|
||||
audioDeviceManager.removeMidiInputDeviceCallback ({}, this);
|
||||
audioDeviceManager.removeAudioCallback (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void resized() override
|
||||
{
|
||||
auto zoneLayoutCompHeight = 60;
|
||||
auto audioSetupCompRelativeWidth = 0.55f;
|
||||
|
||||
auto r = getLocalBounds();
|
||||
|
||||
keyboardComponent.setBounds (r.removeFromBottom (150));
|
||||
r.reduce (10, 10);
|
||||
|
||||
zoneLayoutComp.setBounds (r.removeFromBottom (zoneLayoutCompHeight));
|
||||
audioSetupComp.setBounds (r.removeFromLeft (proportionOfWidth (audioSetupCompRelativeWidth)));
|
||||
mpeSetupComp .setBounds (r);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void audioDeviceIOCallbackWithContext (const float* const* inputChannelData, int numInputChannels,
|
||||
float* const* outputChannelData, int numOutputChannels,
|
||||
int numSamples, const AudioIODeviceCallbackContext& context) override
|
||||
{
|
||||
ignoreUnused (inputChannelData, numInputChannels, context);
|
||||
|
||||
AudioBuffer<float> buffer (outputChannelData, numOutputChannels, numSamples);
|
||||
buffer.clear();
|
||||
|
||||
MidiBuffer incomingMidi;
|
||||
midiCollector.removeNextBlockOfMessages (incomingMidi, numSamples);
|
||||
synth.renderNextBlock (buffer, incomingMidi, 0, numSamples);
|
||||
}
|
||||
|
||||
void audioDeviceAboutToStart (AudioIODevice* device) override
|
||||
{
|
||||
auto sampleRate = device->getCurrentSampleRate();
|
||||
midiCollector.reset (sampleRate);
|
||||
synth.setCurrentPlaybackSampleRate (sampleRate);
|
||||
}
|
||||
|
||||
void audioDeviceStopped() override {}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void handleIncomingMidiMessage (MidiInput* /*source*/,
|
||||
const MidiMessage& message) override
|
||||
{
|
||||
instrument.processNextMidiEvent (message);
|
||||
midiCollector.addMessageToQueue (message);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void zoneLayoutChanged() override
|
||||
{
|
||||
if (instrument.isLegacyModeEnabled())
|
||||
{
|
||||
colourPicker.setLegacyModeEnabled (true);
|
||||
|
||||
synth.enableLegacyMode (instrument.getLegacyModePitchbendRange(),
|
||||
instrument.getLegacyModeChannelRange());
|
||||
}
|
||||
else
|
||||
{
|
||||
colourPicker.setLegacyModeEnabled (false);
|
||||
|
||||
auto zoneLayout = instrument.getZoneLayout();
|
||||
|
||||
if (auto* midiOutput = audioDeviceManager.getDefaultMidiOutput())
|
||||
midiOutput->sendBlockOfMessagesNow (MPEMessages::setZoneLayout (zoneLayout));
|
||||
|
||||
synth.setZoneLayout (zoneLayout);
|
||||
colourPicker.setZoneLayout (zoneLayout);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
// 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
|
||||
|
||||
AudioDeviceSelectorComponent audioSetupComp { audioDeviceManager, 0, 0, 0, 256, true, true, true, false };
|
||||
MidiMessageCollector midiCollector;
|
||||
|
||||
MPEInstrument instrument { MPEZone (MPEZone::Type::lower, 15) };
|
||||
|
||||
ZoneColourPicker colourPicker;
|
||||
MPESetupComponent mpeSetupComp { instrument };
|
||||
ZoneLayoutComponent zoneLayoutComp { instrument, colourPicker};
|
||||
|
||||
MPESynthesiser synth { instrument };
|
||||
MPEKeyboardComponent keyboardComponent { instrument, MPEKeyboardComponent::horizontalKeyboard };
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEDemo)
|
||||
};
|
||||
+486
@@ -0,0 +1,486 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: MidiDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Handles incoming and outcoming midi messages.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: MidiDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
struct MidiDeviceListEntry final : ReferenceCountedObject
|
||||
{
|
||||
explicit MidiDeviceListEntry (MidiDeviceInfo info) : deviceInfo (info) {}
|
||||
|
||||
MidiDeviceInfo deviceInfo;
|
||||
std::unique_ptr<MidiInput> inDevice;
|
||||
std::unique_ptr<MidiOutput> outDevice;
|
||||
|
||||
using Ptr = ReferenceCountedObjectPtr<MidiDeviceListEntry>;
|
||||
|
||||
void stopAndReset()
|
||||
{
|
||||
if (inDevice != nullptr)
|
||||
inDevice->stop();
|
||||
|
||||
inDevice .reset();
|
||||
outDevice.reset();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class MidiDemo final : public Component,
|
||||
private MidiKeyboardState::Listener,
|
||||
private MidiInputCallback,
|
||||
private AsyncUpdater
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
MidiDemo()
|
||||
: midiKeyboard (keyboardState, MidiKeyboardComponent::horizontalKeyboard),
|
||||
midiInputSelector (new MidiDeviceListBox ("Midi Input Selector", *this, true)),
|
||||
midiOutputSelector (new MidiDeviceListBox ("Midi Output Selector", *this, false))
|
||||
{
|
||||
addLabelAndSetStyle (midiInputLabel);
|
||||
addLabelAndSetStyle (midiOutputLabel);
|
||||
addLabelAndSetStyle (incomingMidiLabel);
|
||||
addLabelAndSetStyle (outgoingMidiLabel);
|
||||
|
||||
midiKeyboard.setName ("MIDI Keyboard");
|
||||
addAndMakeVisible (midiKeyboard);
|
||||
|
||||
midiMonitor.setMultiLine (true);
|
||||
midiMonitor.setReturnKeyStartsNewLine (false);
|
||||
midiMonitor.setReadOnly (true);
|
||||
midiMonitor.setScrollbarsShown (true);
|
||||
midiMonitor.setCaretVisible (false);
|
||||
midiMonitor.setPopupMenuEnabled (false);
|
||||
midiMonitor.setText ({});
|
||||
addAndMakeVisible (midiMonitor);
|
||||
|
||||
if (! BluetoothMidiDevicePairingDialogue::isAvailable())
|
||||
pairButton.setEnabled (false);
|
||||
|
||||
addAndMakeVisible (pairButton);
|
||||
pairButton.onClick = []
|
||||
{
|
||||
RuntimePermissions::request (RuntimePermissions::bluetoothMidi,
|
||||
[] (bool wasGranted)
|
||||
{
|
||||
if (wasGranted)
|
||||
BluetoothMidiDevicePairingDialogue::open();
|
||||
});
|
||||
};
|
||||
keyboardState.addListener (this);
|
||||
|
||||
addAndMakeVisible (midiInputSelector .get());
|
||||
addAndMakeVisible (midiOutputSelector.get());
|
||||
|
||||
setSize (732, 520);
|
||||
|
||||
updateDeviceLists();
|
||||
}
|
||||
|
||||
~MidiDemo() override
|
||||
{
|
||||
midiInputs .clear();
|
||||
midiOutputs.clear();
|
||||
keyboardState.removeListener (this);
|
||||
|
||||
midiInputSelector .reset();
|
||||
midiOutputSelector.reset();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void handleNoteOn (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
|
||||
{
|
||||
MidiMessage m (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity));
|
||||
m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
|
||||
sendToOutputs (m);
|
||||
}
|
||||
|
||||
void handleNoteOff (MidiKeyboardState*, int midiChannel, int midiNoteNumber, float velocity) override
|
||||
{
|
||||
MidiMessage m (MidiMessage::noteOff (midiChannel, midiNoteNumber, velocity));
|
||||
m.setTimeStamp (Time::getMillisecondCounterHiRes() * 0.001);
|
||||
sendToOutputs (m);
|
||||
}
|
||||
|
||||
void paint (Graphics&) override {}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto margin = 10;
|
||||
|
||||
midiInputLabel.setBounds (margin, margin,
|
||||
(getWidth() / 2) - (2 * margin), 24);
|
||||
|
||||
midiOutputLabel.setBounds ((getWidth() / 2) + margin, margin,
|
||||
(getWidth() / 2) - (2 * margin), 24);
|
||||
|
||||
midiInputSelector->setBounds (margin, (2 * margin) + 24,
|
||||
(getWidth() / 2) - (2 * margin),
|
||||
(getHeight() / 2) - ((4 * margin) + 24 + 24));
|
||||
|
||||
midiOutputSelector->setBounds ((getWidth() / 2) + margin, (2 * margin) + 24,
|
||||
(getWidth() / 2) - (2 * margin),
|
||||
(getHeight() / 2) - ((4 * margin) + 24 + 24));
|
||||
|
||||
pairButton.setBounds (margin, (getHeight() / 2) - (margin + 24),
|
||||
getWidth() - (2 * margin), 24);
|
||||
|
||||
outgoingMidiLabel.setBounds (margin, getHeight() / 2, getWidth() - (2 * margin), 24);
|
||||
midiKeyboard.setBounds (margin, (getHeight() / 2) + (24 + margin), getWidth() - (2 * margin), 64);
|
||||
|
||||
incomingMidiLabel.setBounds (margin, (getHeight() / 2) + (24 + (2 * margin) + 64),
|
||||
getWidth() - (2 * margin), 24);
|
||||
|
||||
auto y = (getHeight() / 2) + ((2 * 24) + (3 * margin) + 64);
|
||||
midiMonitor.setBounds (margin, y,
|
||||
getWidth() - (2 * margin), getHeight() - y - margin);
|
||||
}
|
||||
|
||||
void openDevice (bool isInput, int index)
|
||||
{
|
||||
if (isInput)
|
||||
{
|
||||
jassert (midiInputs[index]->inDevice.get() == nullptr);
|
||||
midiInputs[index]->inDevice = MidiInput::openDevice (midiInputs[index]->deviceInfo.identifier, this);
|
||||
|
||||
if (midiInputs[index]->inDevice.get() == nullptr)
|
||||
{
|
||||
DBG ("MidiDemo::openDevice: open input device for index = " << index << " failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
midiInputs[index]->inDevice->start();
|
||||
}
|
||||
else
|
||||
{
|
||||
jassert (midiOutputs[index]->outDevice.get() == nullptr);
|
||||
midiOutputs[index]->outDevice = MidiOutput::openDevice (midiOutputs[index]->deviceInfo.identifier);
|
||||
|
||||
if (midiOutputs[index]->outDevice.get() == nullptr)
|
||||
{
|
||||
DBG ("MidiDemo::openDevice: open output device for index = " << index << " failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void closeDevice (bool isInput, int index)
|
||||
{
|
||||
auto& list = isInput ? midiInputs : midiOutputs;
|
||||
list[index]->stopAndReset();
|
||||
}
|
||||
|
||||
int getNumMidiInputs() const noexcept
|
||||
{
|
||||
return midiInputs.size();
|
||||
}
|
||||
|
||||
int getNumMidiOutputs() const noexcept
|
||||
{
|
||||
return midiOutputs.size();
|
||||
}
|
||||
|
||||
ReferenceCountedObjectPtr<MidiDeviceListEntry> getMidiDevice (int index, bool isInput) const noexcept
|
||||
{
|
||||
return isInput ? midiInputs[index] : midiOutputs[index];
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct MidiDeviceListBox final : private ListBoxModel,
|
||||
public ListBox
|
||||
{
|
||||
MidiDeviceListBox (const String& name,
|
||||
MidiDemo& contentComponent,
|
||||
bool isInputDeviceList)
|
||||
: ListBox (name),
|
||||
parent (contentComponent),
|
||||
isInput (isInputDeviceList)
|
||||
{
|
||||
setModel (this);
|
||||
setOutlineThickness (1);
|
||||
setMultipleSelectionEnabled (true);
|
||||
setClickingTogglesRowSelection (true);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
int getNumRows() override
|
||||
{
|
||||
return isInput ? parent.getNumMidiInputs()
|
||||
: parent.getNumMidiOutputs();
|
||||
}
|
||||
|
||||
void paintListBoxItem (int rowNumber, Graphics& g,
|
||||
int width, int height, bool rowIsSelected) override
|
||||
{
|
||||
auto textColour = getLookAndFeel().findColour (ListBox::textColourId);
|
||||
|
||||
if (rowIsSelected)
|
||||
g.fillAll (textColour.interpolatedWith (getLookAndFeel().findColour (ListBox::backgroundColourId), 0.5));
|
||||
|
||||
|
||||
g.setColour (textColour);
|
||||
g.setFont ((float) height * 0.7f);
|
||||
|
||||
if (isInput)
|
||||
{
|
||||
if (rowNumber < parent.getNumMidiInputs())
|
||||
g.drawText (parent.getMidiDevice (rowNumber, true)->deviceInfo.name,
|
||||
5, 0, width, height,
|
||||
Justification::centredLeft, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (rowNumber < parent.getNumMidiOutputs())
|
||||
g.drawText (parent.getMidiDevice (rowNumber, false)->deviceInfo.name,
|
||||
5, 0, width, height,
|
||||
Justification::centredLeft, true);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void selectedRowsChanged (int) override
|
||||
{
|
||||
auto newSelectedItems = getSelectedRows();
|
||||
if (newSelectedItems != lastSelectedItems)
|
||||
{
|
||||
for (auto i = 0; i < lastSelectedItems.size(); ++i)
|
||||
{
|
||||
if (! newSelectedItems.contains (lastSelectedItems[i]))
|
||||
parent.closeDevice (isInput, lastSelectedItems[i]);
|
||||
}
|
||||
|
||||
for (auto i = 0; i < newSelectedItems.size(); ++i)
|
||||
{
|
||||
if (! lastSelectedItems.contains (newSelectedItems[i]))
|
||||
parent.openDevice (isInput, newSelectedItems[i]);
|
||||
}
|
||||
|
||||
lastSelectedItems = newSelectedItems;
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void syncSelectedItemsWithDeviceList (const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices)
|
||||
{
|
||||
SparseSet<int> selectedRows;
|
||||
for (auto i = 0; i < midiDevices.size(); ++i)
|
||||
if (midiDevices[i]->inDevice != nullptr || midiDevices[i]->outDevice != nullptr)
|
||||
selectedRows.addRange (Range<int> (i, i + 1));
|
||||
|
||||
lastSelectedItems = selectedRows;
|
||||
updateContent();
|
||||
setSelectedRows (selectedRows, dontSendNotification);
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
MidiDemo& parent;
|
||||
bool isInput;
|
||||
SparseSet<int> lastSelectedItems;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
void handleIncomingMidiMessage (MidiInput* /*source*/, const MidiMessage& message) override
|
||||
{
|
||||
// This is called on the MIDI thread
|
||||
const ScopedLock sl (midiMonitorLock);
|
||||
incomingMessages.add (message);
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
// This is called on the message loop
|
||||
Array<MidiMessage> messages;
|
||||
|
||||
{
|
||||
const ScopedLock sl (midiMonitorLock);
|
||||
messages.swapWith (incomingMessages);
|
||||
}
|
||||
|
||||
String messageText;
|
||||
|
||||
for (auto& m : messages)
|
||||
messageText << m.getDescription() << "\n";
|
||||
|
||||
midiMonitor.insertTextAtCaret (messageText);
|
||||
}
|
||||
|
||||
void sendToOutputs (const MidiMessage& msg)
|
||||
{
|
||||
for (auto midiOutput : midiOutputs)
|
||||
if (midiOutput->outDevice != nullptr)
|
||||
midiOutput->outDevice->sendMessageNow (msg);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool hasDeviceListChanged (const Array<MidiDeviceInfo>& availableDevices, bool isInputDevice)
|
||||
{
|
||||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
|
||||
: midiOutputs;
|
||||
|
||||
if (availableDevices.size() != midiDevices.size())
|
||||
return true;
|
||||
|
||||
for (auto i = 0; i < availableDevices.size(); ++i)
|
||||
if (availableDevices[i] != midiDevices[i]->deviceInfo)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ReferenceCountedObjectPtr<MidiDeviceListEntry> findDevice (MidiDeviceInfo device, bool isInputDevice) const
|
||||
{
|
||||
const ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
|
||||
: midiOutputs;
|
||||
|
||||
for (auto& d : midiDevices)
|
||||
if (d->deviceInfo == device)
|
||||
return d;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void closeUnpluggedDevices (const Array<MidiDeviceInfo>& currentlyPluggedInDevices, bool isInputDevice)
|
||||
{
|
||||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices = isInputDevice ? midiInputs
|
||||
: midiOutputs;
|
||||
|
||||
for (auto i = midiDevices.size(); --i >= 0;)
|
||||
{
|
||||
auto& d = *midiDevices[i];
|
||||
|
||||
if (! currentlyPluggedInDevices.contains (d.deviceInfo))
|
||||
{
|
||||
if (isInputDevice ? d.inDevice .get() != nullptr
|
||||
: d.outDevice.get() != nullptr)
|
||||
closeDevice (isInputDevice, i);
|
||||
|
||||
midiDevices.remove (i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateDeviceList (bool isInputDeviceList)
|
||||
{
|
||||
auto availableDevices = isInputDeviceList ? MidiInput::getAvailableDevices()
|
||||
: MidiOutput::getAvailableDevices();
|
||||
|
||||
if (hasDeviceListChanged (availableDevices, isInputDeviceList))
|
||||
{
|
||||
ReferenceCountedArray<MidiDeviceListEntry>& midiDevices
|
||||
= isInputDeviceList ? midiInputs : midiOutputs;
|
||||
|
||||
closeUnpluggedDevices (availableDevices, isInputDeviceList);
|
||||
|
||||
ReferenceCountedArray<MidiDeviceListEntry> newDeviceList;
|
||||
|
||||
// add all currently plugged-in devices to the device list
|
||||
for (auto& newDevice : availableDevices)
|
||||
{
|
||||
MidiDeviceListEntry::Ptr entry = findDevice (newDevice, isInputDeviceList);
|
||||
|
||||
if (entry == nullptr)
|
||||
entry = new MidiDeviceListEntry (newDevice);
|
||||
|
||||
newDeviceList.add (entry);
|
||||
}
|
||||
|
||||
// actually update the device list
|
||||
midiDevices = newDeviceList;
|
||||
|
||||
// update the selection status of the combo-box
|
||||
if (auto* midiSelector = isInputDeviceList ? midiInputSelector.get() : midiOutputSelector.get())
|
||||
midiSelector->syncSelectedItemsWithDeviceList (midiDevices);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void addLabelAndSetStyle (Label& label)
|
||||
{
|
||||
label.setFont (Font (15.00f, Font::plain));
|
||||
label.setJustificationType (Justification::centredLeft);
|
||||
label.setEditable (false, false, false);
|
||||
label.setColour (TextEditor::textColourId, Colours::black);
|
||||
label.setColour (TextEditor::backgroundColourId, Colour (0x00000000));
|
||||
|
||||
addAndMakeVisible (label);
|
||||
}
|
||||
|
||||
void updateDeviceLists()
|
||||
{
|
||||
for (const auto isInput : { true, false })
|
||||
updateDeviceList (isInput);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Label midiInputLabel { "Midi Input Label", "MIDI Input:" };
|
||||
Label midiOutputLabel { "Midi Output Label", "MIDI Output:" };
|
||||
Label incomingMidiLabel { "Incoming Midi Label", "Received MIDI messages:" };
|
||||
Label outgoingMidiLabel { "Outgoing Midi Label", "Play the keyboard to send MIDI messages..." };
|
||||
MidiKeyboardState keyboardState;
|
||||
MidiKeyboardComponent midiKeyboard;
|
||||
TextEditor midiMonitor { "MIDI Monitor" };
|
||||
TextButton pairButton { "MIDI Bluetooth devices..." };
|
||||
|
||||
ReferenceCountedArray<MidiDeviceListEntry> midiInputs, midiOutputs;
|
||||
std::unique_ptr<MidiDeviceListBox> midiInputSelector, midiOutputSelector;
|
||||
|
||||
CriticalSection midiMonitorLock;
|
||||
Array<MidiMessage> incomingMessages;
|
||||
|
||||
MidiDeviceListConnection connection = MidiDeviceListConnection::make ([this]
|
||||
{
|
||||
updateDeviceLists();
|
||||
});
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiDemo)
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: PluckedStringsDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Simulation of a plucked string sound.
|
||||
|
||||
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, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: PluckedStringsDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A very basic generator of a simulated plucked string sound, implementing
|
||||
the Karplus-Strong algorithm.
|
||||
|
||||
Not performance-optimised!
|
||||
*/
|
||||
class StringSynthesiser
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Constructor.
|
||||
|
||||
@param sampleRate The audio sample rate to use.
|
||||
@param frequencyInHz The fundamental frequency of the simulated string in
|
||||
Hertz.
|
||||
*/
|
||||
StringSynthesiser (double sampleRate, double frequencyInHz)
|
||||
{
|
||||
doPluckForNextBuffer.set (false);
|
||||
prepareSynthesiserState (sampleRate, frequencyInHz);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Excite the simulated string by plucking it at a given position.
|
||||
|
||||
@param pluckPosition The position of the plucking, relative to the length
|
||||
of the string. Must be between 0 and 1.
|
||||
*/
|
||||
void stringPlucked (float pluckPosition)
|
||||
{
|
||||
jassert (pluckPosition >= 0.0 && pluckPosition <= 1.0);
|
||||
|
||||
// we choose a very simple approach to communicate with the audio thread:
|
||||
// simply tell the synth to perform the plucking excitation at the beginning
|
||||
// of the next buffer (= when generateAndAddData is called the next time).
|
||||
|
||||
if (doPluckForNextBuffer.compareAndSetBool (1, 0))
|
||||
{
|
||||
// plucking in the middle gives the largest amplitude;
|
||||
// plucking at the very ends will do nothing.
|
||||
amplitude = std::sin (MathConstants<float>::pi * pluckPosition);
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
/** Generate next chunk of mono audio output and add it into a buffer.
|
||||
|
||||
@param outBuffer Buffer to fill (one channel only). New sound will be
|
||||
added to existing content of the buffer (instead of
|
||||
replacing it).
|
||||
@param numSamples Number of samples to generate (make sure that outBuffer
|
||||
has enough space).
|
||||
*/
|
||||
void generateAndAddData (float* outBuffer, int numSamples)
|
||||
{
|
||||
if (doPluckForNextBuffer.compareAndSetBool (0, 1))
|
||||
exciteInternalBuffer();
|
||||
|
||||
// cycle through the delay line and apply a simple averaging filter
|
||||
for (auto i = 0; i < numSamples; ++i)
|
||||
{
|
||||
auto nextPos = (pos + 1) % delayLine.size();
|
||||
|
||||
delayLine[nextPos] = (float) (decay * 0.5 * (delayLine[nextPos] + delayLine[pos]));
|
||||
outBuffer[i] += delayLine[pos];
|
||||
|
||||
pos = nextPos;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void prepareSynthesiserState (double sampleRate, double frequencyInHz)
|
||||
{
|
||||
auto delayLineLength = (size_t) roundToInt (sampleRate / frequencyInHz);
|
||||
|
||||
// we need a minimum delay line length to get a reasonable synthesis.
|
||||
// if you hit this assert, increase sample rate or decrease frequency!
|
||||
jassert (delayLineLength > 50);
|
||||
|
||||
delayLine.resize (delayLineLength);
|
||||
std::fill (delayLine.begin(), delayLine.end(), 0.0f);
|
||||
|
||||
excitationSample.resize (delayLineLength);
|
||||
|
||||
// as the excitation sample we use random noise between -1 and 1
|
||||
// (as a simple approximation to a plucking excitation)
|
||||
|
||||
std::generate (excitationSample.begin(),
|
||||
excitationSample.end(),
|
||||
[] { return (Random::getSystemRandom().nextFloat() * 2.0f) - 1.0f; } );
|
||||
}
|
||||
|
||||
void exciteInternalBuffer()
|
||||
{
|
||||
// fill the buffer with the precomputed excitation sound (scaled with amplitude)
|
||||
|
||||
jassert (delayLine.size() >= excitationSample.size());
|
||||
|
||||
std::transform (excitationSample.begin(),
|
||||
excitationSample.end(),
|
||||
delayLine.begin(),
|
||||
[this] (double sample) { return static_cast<float> (amplitude * sample); } );
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
const double decay = 0.998;
|
||||
double amplitude = 0.0;
|
||||
|
||||
Atomic<int> doPluckForNextBuffer;
|
||||
|
||||
std::vector<float> excitationSample, delayLine;
|
||||
size_t pos = 0;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StringSynthesiser)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/*
|
||||
This component represents a horizontal vibrating musical string of fixed height
|
||||
and variable length. The string can be excited by calling stringPlucked().
|
||||
*/
|
||||
class StringComponent final : public Component,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
StringComponent (int lengthInPixels, Colour stringColour)
|
||||
: length (lengthInPixels), colour (stringColour)
|
||||
{
|
||||
// ignore mouse-clicks so that our parent can get them instead.
|
||||
setInterceptsMouseClicks (false, false);
|
||||
setSize (length, height);
|
||||
startTimerHz (60);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void stringPlucked (float pluckPositionRelative)
|
||||
{
|
||||
amplitude = maxAmplitude * std::sin (pluckPositionRelative * MathConstants<float>::pi);
|
||||
phase = MathConstants<float>::pi;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setColour (colour);
|
||||
g.strokePath (generateStringPath(), PathStrokeType (2.0f));
|
||||
}
|
||||
|
||||
Path generateStringPath() const
|
||||
{
|
||||
auto y = (float) height / 2.0f;
|
||||
|
||||
Path stringPath;
|
||||
stringPath.startNewSubPath (0, y);
|
||||
stringPath.quadraticTo ((float) length / 2.0f, y + (std::sin (phase) * amplitude), (float) length, y);
|
||||
return stringPath;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void timerCallback() override
|
||||
{
|
||||
updateAmplitude();
|
||||
updatePhase();
|
||||
repaint();
|
||||
}
|
||||
|
||||
void updateAmplitude()
|
||||
{
|
||||
// this determines the decay of the visible string vibration.
|
||||
amplitude *= 0.99f;
|
||||
}
|
||||
|
||||
void updatePhase()
|
||||
{
|
||||
// this determines the visible vibration frequency.
|
||||
// just an arbitrary number chosen to look OK:
|
||||
auto phaseStep = 400.0f / (float) length;
|
||||
|
||||
phase += phaseStep;
|
||||
|
||||
if (phase >= MathConstants<float>::twoPi)
|
||||
phase -= MathConstants<float>::twoPi;
|
||||
}
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
int length;
|
||||
Colour colour;
|
||||
|
||||
int height = 20;
|
||||
float amplitude = 0.0f;
|
||||
const float maxAmplitude = 12.0f;
|
||||
float phase = 0.0f;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StringComponent)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
class PluckedStringsDemo final : public AudioAppComponent
|
||||
{
|
||||
public:
|
||||
PluckedStringsDemo()
|
||||
#ifdef JUCE_DEMO_RUNNER
|
||||
: AudioAppComponent (getSharedAudioDeviceManager (0, 2))
|
||||
#endif
|
||||
{
|
||||
createStringComponents();
|
||||
setSize (800, 560);
|
||||
|
||||
// specify the number of input and output channels that we want to open
|
||||
auto audioDevice = deviceManager.getCurrentAudioDevice();
|
||||
auto numInputChannels = (audioDevice != nullptr ? audioDevice->getActiveInputChannels() .countNumberOfSetBits() : 0);
|
||||
auto numOutputChannels = jmax (audioDevice != nullptr ? audioDevice->getActiveOutputChannels().countNumberOfSetBits() : 2, 2);
|
||||
|
||||
setAudioChannels (numInputChannels, numOutputChannels);
|
||||
}
|
||||
|
||||
~PluckedStringsDemo() override
|
||||
{
|
||||
shutdownAudio();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
|
||||
{
|
||||
generateStringSynths (sampleRate);
|
||||
}
|
||||
|
||||
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
|
||||
{
|
||||
bufferToFill.clearActiveBufferRegion();
|
||||
|
||||
for (auto channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel)
|
||||
{
|
||||
auto* channelData = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
|
||||
|
||||
if (channel == 0)
|
||||
{
|
||||
for (auto synth : stringSynths)
|
||||
synth->generateAndAddData (channelData, bufferToFill.numSamples);
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy (channelData,
|
||||
bufferToFill.buffer->getReadPointer (0),
|
||||
((size_t) bufferToFill.numSamples) * sizeof (float));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void releaseResources() override
|
||||
{
|
||||
stringSynths.clear();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics&) override {}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
auto xPos = 20;
|
||||
auto yPos = 20;
|
||||
auto yDistance = 50;
|
||||
|
||||
for (auto stringLine : stringLines)
|
||||
{
|
||||
stringLine->setTopLeftPosition (xPos, yPos);
|
||||
yPos += yDistance;
|
||||
addAndMakeVisible (stringLine);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void mouseDown (const MouseEvent& e) override
|
||||
{
|
||||
mouseDrag (e);
|
||||
}
|
||||
|
||||
void mouseDrag (const MouseEvent& e) override
|
||||
{
|
||||
for (auto i = 0; i < stringLines.size(); ++i)
|
||||
{
|
||||
auto* stringLine = stringLines.getUnchecked (i);
|
||||
|
||||
if (stringLine->getBounds().contains (e.getPosition()))
|
||||
{
|
||||
auto position = (e.position.x - (float) stringLine->getX()) / (float) stringLine->getWidth();
|
||||
|
||||
stringLine->stringPlucked (position);
|
||||
stringSynths.getUnchecked (i)->stringPlucked (position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
struct StringParameters
|
||||
{
|
||||
StringParameters (int midiNote)
|
||||
: frequencyInHz (MidiMessage::getMidiNoteInHertz (midiNote)),
|
||||
lengthInPixels ((int) (760 / (frequencyInHz / MidiMessage::getMidiNoteInHertz (42))))
|
||||
{}
|
||||
|
||||
double frequencyInHz;
|
||||
int lengthInPixels;
|
||||
};
|
||||
|
||||
static Array<StringParameters> getDefaultStringParameters()
|
||||
{
|
||||
return Array<StringParameters> (42, 44, 46, 49, 51, 54, 56, 58, 61, 63, 66, 68, 70);
|
||||
}
|
||||
|
||||
void createStringComponents()
|
||||
{
|
||||
for (auto stringParams : getDefaultStringParameters())
|
||||
{
|
||||
stringLines.add (new StringComponent (stringParams.lengthInPixels,
|
||||
Colour::fromHSV (Random().nextFloat(), 0.6f, 0.9f, 1.0f)));
|
||||
}
|
||||
}
|
||||
|
||||
void generateStringSynths (double sampleRate)
|
||||
{
|
||||
stringSynths.clear();
|
||||
|
||||
for (auto stringParams : getDefaultStringParameters())
|
||||
{
|
||||
stringSynths.add (new StringSynthesiser (sampleRate, stringParams.frequencyInHz));
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
OwnedArray<StringComponent> stringLines;
|
||||
OwnedArray<StringSynthesiser> stringSynths;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluckedStringsDemo)
|
||||
};
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
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: SimpleFFTDemo
|
||||
version: 1.0.0
|
||||
vendor: JUCE
|
||||
website: http://juce.com
|
||||
description: Simple FFT application.
|
||||
|
||||
dependencies: juce_audio_basics, juce_audio_devices, juce_audio_formats,
|
||||
juce_audio_processors, juce_audio_utils, juce_core,
|
||||
juce_data_structures, juce_dsp, juce_events, juce_graphics,
|
||||
juce_gui_basics, juce_gui_extra
|
||||
exporters: xcode_mac, vs2022, linux_make, androidstudio, xcode_iphone
|
||||
|
||||
moduleFlags: JUCE_STRICT_REFCOUNTEDPOINTER=1
|
||||
|
||||
type: Component
|
||||
mainClass: SimpleFFTDemo
|
||||
|
||||
useLocalCopy: 1
|
||||
|
||||
END_JUCE_PIP_METADATA
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
//==============================================================================
|
||||
class SimpleFFTDemo final : public AudioAppComponent,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
SimpleFFTDemo() :
|
||||
#ifdef JUCE_DEMO_RUNNER
|
||||
AudioAppComponent (getSharedAudioDeviceManager (1, 0)),
|
||||
#endif
|
||||
forwardFFT (fftOrder),
|
||||
spectrogramImage (Image::RGB, 512, 512, true)
|
||||
{
|
||||
setOpaque (true);
|
||||
|
||||
#ifndef JUCE_DEMO_RUNNER
|
||||
RuntimePermissions::request (RuntimePermissions::recordAudio,
|
||||
[this] (bool granted)
|
||||
{
|
||||
int numInputChannels = granted ? 2 : 0;
|
||||
setAudioChannels (numInputChannels, 2);
|
||||
});
|
||||
#else
|
||||
setAudioChannels (2, 2);
|
||||
#endif
|
||||
|
||||
startTimerHz (60);
|
||||
setSize (700, 500);
|
||||
}
|
||||
|
||||
~SimpleFFTDemo() override
|
||||
{
|
||||
shutdownAudio();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void prepareToPlay (int /*samplesPerBlockExpected*/, double /*newSampleRate*/) override
|
||||
{
|
||||
// (nothing to do here)
|
||||
}
|
||||
|
||||
void releaseResources() override
|
||||
{
|
||||
// (nothing to do here)
|
||||
}
|
||||
|
||||
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
|
||||
{
|
||||
if (bufferToFill.buffer->getNumChannels() > 0)
|
||||
{
|
||||
const auto* channelData = bufferToFill.buffer->getReadPointer (0, bufferToFill.startSample);
|
||||
|
||||
for (auto i = 0; i < bufferToFill.numSamples; ++i)
|
||||
pushNextSampleIntoFifo (channelData[i]);
|
||||
|
||||
bufferToFill.clearActiveBufferRegion();
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::black);
|
||||
|
||||
g.setOpacity (1.0f);
|
||||
g.drawImage (spectrogramImage, getLocalBounds().toFloat());
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
if (nextFFTBlockReady)
|
||||
{
|
||||
drawNextLineOfSpectrogram();
|
||||
nextFFTBlockReady = false;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void pushNextSampleIntoFifo (float sample) noexcept
|
||||
{
|
||||
// if the fifo contains enough data, set a flag to say
|
||||
// that the next line should now be rendered..
|
||||
if (fifoIndex == fftSize)
|
||||
{
|
||||
if (! nextFFTBlockReady)
|
||||
{
|
||||
zeromem (fftData, sizeof (fftData));
|
||||
memcpy (fftData, fifo, sizeof (fifo));
|
||||
nextFFTBlockReady = true;
|
||||
}
|
||||
|
||||
fifoIndex = 0;
|
||||
}
|
||||
|
||||
fifo[fifoIndex++] = sample;
|
||||
}
|
||||
|
||||
void drawNextLineOfSpectrogram()
|
||||
{
|
||||
auto rightHandEdge = spectrogramImage.getWidth() - 1;
|
||||
auto imageHeight = spectrogramImage.getHeight();
|
||||
|
||||
// first, shuffle our image leftwards by 1 pixel..
|
||||
spectrogramImage.moveImageSection (0, 0, 1, 0, rightHandEdge, imageHeight);
|
||||
|
||||
// then render our FFT data..
|
||||
forwardFFT.performFrequencyOnlyForwardTransform (fftData);
|
||||
|
||||
// find the range of values produced, so we can scale our rendering to
|
||||
// show up the detail clearly
|
||||
auto maxLevel = FloatVectorOperations::findMinAndMax (fftData, fftSize / 2);
|
||||
|
||||
for (auto y = 1; y < imageHeight; ++y)
|
||||
{
|
||||
auto skewedProportionY = 1.0f - std::exp (std::log ((float) y / (float) imageHeight) * 0.2f);
|
||||
auto fftDataIndex = jlimit (0, fftSize / 2, (int) (skewedProportionY * (int) fftSize / 2));
|
||||
auto level = jmap (fftData[fftDataIndex], 0.0f, jmax (maxLevel.getEnd(), 1e-5f), 0.0f, 1.0f);
|
||||
|
||||
spectrogramImage.setPixelAt (rightHandEdge, y, Colour::fromHSV (level, 1.0f, level, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
enum
|
||||
{
|
||||
fftOrder = 10,
|
||||
fftSize = 1 << fftOrder
|
||||
};
|
||||
|
||||
private:
|
||||
dsp::FFT forwardFFT;
|
||||
Image spectrogramImage;
|
||||
|
||||
float fifo [fftSize];
|
||||
float fftData [2 * fftSize];
|
||||
int fifoIndex = 0;
|
||||
bool nextFFTBlockReady = false;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SimpleFFTDemo)
|
||||
};
|
||||
Reference in New Issue
Block a user