1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE examples.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    The code included in this file is provided under the terms of the ISC license
8    http://www.isc.org/downloads/software-support-policy/isc-license. Permission
9    To use, copy, modify, and/or distribute this software for any purpose with or
10    without fee is hereby granted provided that the above copyright notice and
11    this permission notice appear in all copies.
12 
13    THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
14    WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
15    PURPOSE, ARE DISCLAIMED.
16 
17   ==============================================================================
18 */
19 
20 /*******************************************************************************
21  The block below describes the properties of this PIP. A PIP is a short snippet
22  of code that can be read by the Projucer and used to generate a JUCE project.
23 
24  BEGIN_JUCE_PIP_METADATA
25 
26  name:             PluckedStringsDemo
27  version:          1.0.0
28  vendor:           JUCE
29  website:          http://juce.com
30  description:      Simulation of a plucked string sound.
31 
32  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
33                    juce_audio_processors, juce_audio_utils, juce_core,
34                    juce_data_structures, juce_events, juce_graphics,
35                    juce_gui_basics, juce_gui_extra
36  exporters:        xcode_mac, vs2019, linux_make, androidstudio, xcode_iphone
37 
38  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
39 
40  type:             Component
41  mainClass:        PluckedStringsDemo
42 
43  useLocalCopy:     1
44 
45  END_JUCE_PIP_METADATA
46 
47 *******************************************************************************/
48 
49 #pragma once
50 
51 
52 //==============================================================================
53 /**
54     A very basic generator of a simulated plucked string sound, implementing
55     the Karplus-Strong algorithm.
56 
57     Not performance-optimised!
58 */
59 class StringSynthesiser
60 {
61 public:
62     //==============================================================================
63     /** Constructor.
64 
65         @param sampleRate      The audio sample rate to use.
66         @param frequencyInHz   The fundamental frequency of the simulated string in
67                                Hertz.
68     */
StringSynthesiser(double sampleRate,double frequencyInHz)69     StringSynthesiser (double sampleRate, double frequencyInHz)
70     {
71         doPluckForNextBuffer.set (false);
72         prepareSynthesiserState (sampleRate, frequencyInHz);
73     }
74 
75     //==============================================================================
76     /** Excite the simulated string by plucking it at a given position.
77 
78         @param pluckPosition The position of the plucking, relative to the length
79                              of the string. Must be between 0 and 1.
80     */
stringPlucked(float pluckPosition)81     void stringPlucked (float pluckPosition)
82     {
83         jassert (pluckPosition >= 0.0 && pluckPosition <= 1.0);
84 
85         // we choose a very simple approach to communicate with the audio thread:
86         // simply tell the synth to perform the plucking excitation at the beginning
87         // of the next buffer (= when generateAndAddData is called the next time).
88 
89         if (doPluckForNextBuffer.compareAndSetBool (1, 0))
90         {
91             // plucking in the middle gives the largest amplitude;
92             // plucking at the very ends will do nothing.
93             amplitude = std::sin (MathConstants<float>::pi * pluckPosition);
94         }
95     }
96 
97     //==============================================================================
98     /** Generate next chunk of mono audio output and add it into a buffer.
99 
100         @param outBuffer  Buffer to fill (one channel only). New sound will be
101                           added to existing content of the buffer (instead of
102                           replacing it).
103         @param numSamples Number of samples to generate (make sure that outBuffer
104                           has enough space).
105     */
generateAndAddData(float * outBuffer,int numSamples)106     void generateAndAddData (float* outBuffer, int numSamples)
107     {
108         if (doPluckForNextBuffer.compareAndSetBool (0, 1))
109             exciteInternalBuffer();
110 
111         // cycle through the delay line and apply a simple averaging filter
112         for (auto i = 0; i < numSamples; ++i)
113         {
114             auto nextPos = (pos + 1) % delayLine.size();
115 
116             delayLine[nextPos] = (float) (decay * 0.5 * (delayLine[nextPos] + delayLine[pos]));
117             outBuffer[i] += delayLine[pos];
118 
119             pos = nextPos;
120         }
121     }
122 
123 private:
124     //==============================================================================
prepareSynthesiserState(double sampleRate,double frequencyInHz)125     void prepareSynthesiserState (double sampleRate, double frequencyInHz)
126     {
127         auto delayLineLength = (size_t) roundToInt (sampleRate / frequencyInHz);
128 
129         // we need a minimum delay line length to get a reasonable synthesis.
130         // if you hit this assert, increase sample rate or decrease frequency!
131         jassert (delayLineLength > 50);
132 
133         delayLine.resize (delayLineLength);
134         std::fill (delayLine.begin(), delayLine.end(), 0.0f);
135 
136         excitationSample.resize (delayLineLength);
137 
138         // as the excitation sample we use random noise between -1 and 1
139         // (as a simple approximation to a plucking excitation)
140 
141         std::generate (excitationSample.begin(),
142                        excitationSample.end(),
143                        [] { return (Random::getSystemRandom().nextFloat() * 2.0f) - 1.0f; } );
144     }
145 
exciteInternalBuffer()146     void exciteInternalBuffer()
147     {
148         // fill the buffer with the precomputed excitation sound (scaled with amplitude)
149 
150         jassert (delayLine.size() >= excitationSample.size());
151 
152         std::transform (excitationSample.begin(),
153                         excitationSample.end(),
154                         delayLine.begin(),
155                         [this] (double sample) { return static_cast<float> (amplitude * sample); } );
156     }
157 
158     //==============================================================================
159     const double decay = 0.998;
160     double amplitude = 0.0;
161 
162     Atomic<int> doPluckForNextBuffer;
163 
164     std::vector<float> excitationSample, delayLine;
165     size_t pos = 0;
166 
167     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StringSynthesiser)
168 };
169 
170 //==============================================================================
171 /*
172     This component represents a horizontal vibrating musical string of fixed height
173     and variable length. The string can be excited by calling stringPlucked().
174 */
175 class StringComponent   : public Component,
176                           private Timer
177 {
178 public:
StringComponent(int lengthInPixels,Colour stringColour)179     StringComponent (int lengthInPixels, Colour stringColour)
180         : length (lengthInPixels), colour (stringColour)
181     {
182         // ignore mouse-clicks so that our parent can get them instead.
183         setInterceptsMouseClicks (false, false);
184         setSize (length, height);
185         startTimerHz (60);
186     }
187 
188     //==============================================================================
stringPlucked(float pluckPositionRelative)189     void stringPlucked (float pluckPositionRelative)
190     {
191         amplitude = maxAmplitude * std::sin (pluckPositionRelative * MathConstants<float>::pi);
192         phase = MathConstants<float>::pi;
193     }
194 
195     //==============================================================================
paint(Graphics & g)196     void paint (Graphics& g) override
197     {
198         g.setColour (colour);
199         g.strokePath (generateStringPath(), PathStrokeType (2.0f));
200     }
201 
generateStringPath()202     Path generateStringPath() const
203     {
204         auto y = (float) height / 2.0f;
205 
206         Path stringPath;
207         stringPath.startNewSubPath (0, y);
208         stringPath.quadraticTo ((float) length / 2.0f, y + (std::sin (phase) * amplitude), (float) length, y);
209         return stringPath;
210     }
211 
212     //==============================================================================
timerCallback()213     void timerCallback() override
214     {
215         updateAmplitude();
216         updatePhase();
217         repaint();
218     }
219 
updateAmplitude()220     void updateAmplitude()
221     {
222         // this determines the decay of the visible string vibration.
223         amplitude *= 0.99f;
224     }
225 
updatePhase()226     void updatePhase()
227     {
228         // this determines the visible vibration frequency.
229         // just an arbitrary number chosen to look OK:
230         auto phaseStep = 400.0f / (float) length;
231 
232         phase += phaseStep;
233 
234         if (phase >= MathConstants<float>::twoPi)
235             phase -= MathConstants<float>::twoPi;
236     }
237 
238 private:
239     //==============================================================================
240     int length;
241     Colour colour;
242 
243     int height = 20;
244     float amplitude = 0.0f;
245     const float maxAmplitude = 12.0f;
246     float phase = 0.0f;
247 
248     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StringComponent)
249 };
250 
251 //==============================================================================
252 class PluckedStringsDemo   : public AudioAppComponent
253 {
254 public:
PluckedStringsDemo()255     PluckedStringsDemo()
256        #ifdef JUCE_DEMO_RUNNER
257         : AudioAppComponent (getSharedAudioDeviceManager (0, 2))
258        #endif
259     {
260         createStringComponents();
261         setSize (800, 560);
262 
263         // specify the number of input and output channels that we want to open
264         auto audioDevice = deviceManager.getCurrentAudioDevice();
265         auto numInputChannels  = (audioDevice != nullptr ? audioDevice->getActiveInputChannels() .countNumberOfSetBits() : 0);
266         auto numOutputChannels = jmax (audioDevice != nullptr ? audioDevice->getActiveOutputChannels().countNumberOfSetBits() : 2, 2);
267 
268         setAudioChannels (numInputChannels, numOutputChannels);
269     }
270 
~PluckedStringsDemo()271     ~PluckedStringsDemo() override
272     {
273         shutdownAudio();
274     }
275 
276     //==============================================================================
prepareToPlay(int,double sampleRate)277     void prepareToPlay (int /*samplesPerBlockExpected*/, double sampleRate) override
278     {
279         generateStringSynths (sampleRate);
280     }
281 
getNextAudioBlock(const AudioSourceChannelInfo & bufferToFill)282     void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
283     {
284         bufferToFill.clearActiveBufferRegion();
285 
286         for (auto channel = 0; channel < bufferToFill.buffer->getNumChannels(); ++channel)
287         {
288             auto* channelData = bufferToFill.buffer->getWritePointer (channel, bufferToFill.startSample);
289 
290             if (channel == 0)
291             {
292                 for (auto synth : stringSynths)
293                     synth->generateAndAddData (channelData, bufferToFill.numSamples);
294             }
295             else
296             {
297                 memcpy (channelData,
298                         bufferToFill.buffer->getReadPointer (0),
299                         ((size_t) bufferToFill.numSamples) * sizeof (float));
300             }
301         }
302     }
303 
releaseResources()304     void releaseResources() override
305     {
306         stringSynths.clear();
307     }
308 
309     //==============================================================================
paint(Graphics &)310     void paint (Graphics&) override {}
311 
resized()312     void resized() override
313     {
314         auto xPos = 20;
315         auto yPos = 20;
316         auto yDistance = 50;
317 
318         for (auto stringLine : stringLines)
319         {
320             stringLine->setTopLeftPosition (xPos, yPos);
321             yPos += yDistance;
322             addAndMakeVisible (stringLine);
323         }
324     }
325 
326 private:
mouseDown(const MouseEvent & e)327     void mouseDown (const MouseEvent& e) override
328     {
329         mouseDrag (e);
330     }
331 
mouseDrag(const MouseEvent & e)332     void mouseDrag (const MouseEvent& e) override
333     {
334         for (auto i = 0; i < stringLines.size(); ++i)
335         {
336             auto* stringLine = stringLines.getUnchecked (i);
337 
338             if (stringLine->getBounds().contains (e.getPosition()))
339             {
340                 auto position = (e.position.x - (float) stringLine->getX()) / (float) stringLine->getWidth();
341 
342                 stringLine->stringPlucked (position);
343                 stringSynths.getUnchecked (i)->stringPlucked (position);
344             }
345         }
346     }
347 
348     //==============================================================================
349     struct StringParameters
350     {
StringParametersStringParameters351         StringParameters (int midiNote)
352             : frequencyInHz (MidiMessage::getMidiNoteInHertz (midiNote)),
353               lengthInPixels ((int) (760 / (frequencyInHz / MidiMessage::getMidiNoteInHertz (42))))
354         {}
355 
356         double frequencyInHz;
357         int lengthInPixels;
358     };
359 
getDefaultStringParameters()360     static Array<StringParameters> getDefaultStringParameters()
361     {
362         return Array<StringParameters> (42, 44, 46, 49, 51, 54, 56, 58, 61, 63, 66, 68, 70);
363     }
364 
createStringComponents()365     void createStringComponents()
366     {
367         for (auto stringParams : getDefaultStringParameters())
368         {
369             stringLines.add (new StringComponent (stringParams.lengthInPixels,
370                                                   Colour::fromHSV (Random().nextFloat(), 0.6f, 0.9f, 1.0f)));
371         }
372     }
373 
generateStringSynths(double sampleRate)374     void generateStringSynths (double sampleRate)
375     {
376         stringSynths.clear();
377 
378         for (auto stringParams : getDefaultStringParameters())
379         {
380             stringSynths.add (new StringSynthesiser (sampleRate, stringParams.frequencyInHz));
381         }
382     }
383 
384     //==============================================================================
385     OwnedArray<StringComponent> stringLines;
386     OwnedArray<StringSynthesiser> stringSynths;
387 
388     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluckedStringsDemo)
389 };
390