The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
13KB

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