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.

390 lines
13KB

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