paulxstretch/deps/juce/examples/Audio/PluckedStringsDemo.h

390 lines
13 KiB
C
Raw Permalink Normal View History

/*
==============================================================================
This file is part of the JUCE examples.
Copyright (c) 2020 - 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, vs2019, 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 : 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 : 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)
};