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