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