Mercurial > hg > opencollidoscope
changeset 3:7fb593d53361
added comments
author | Fiore Martin <f.martin@qmul.ac.uk> |
---|---|
date | Tue, 12 Jul 2016 18:29:38 +0200 |
parents | dd889fff8423 |
children | ab6db404403a |
files | CollidoscopeApp/include/DrawInfo.h CollidoscopeApp/include/EnvASR.h CollidoscopeApp/include/Log.h CollidoscopeApp/include/MIDI.h CollidoscopeApp/include/Messages.h CollidoscopeApp/include/PGranular.h CollidoscopeApp/include/PGranularNode.h CollidoscopeApp/include/ParticleController.h CollidoscopeApp/include/RingBufferPack.h CollidoscopeApp/include/Wave.h CollidoscopeApp/src/PGranularNode.cpp CollidoscopeApp/src/Wave.cpp |
diffstat | 12 files changed, 227 insertions(+), 44 deletions(-) [+] |
line wrap: on
line diff
--- a/CollidoscopeApp/include/DrawInfo.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/DrawInfo.h Tue Jul 12 18:29:38 2016 +0200 @@ -2,10 +2,20 @@ #include "cinder/Area.h" +/** + * The DrawInfo class holds size information for drawing the waves in the screen. + * Every time the screen is resized the draw info is updated with the new information about the window size. + * + * Every wave has its own drawInfo. + * + */ class DrawInfo { public: + /** + * Constructor. Takes the index of the wave as argument. + */ DrawInfo( size_t waveIndex ): mWaveIndex( waveIndex ), mWindowWidth(0), @@ -14,6 +24,10 @@ mShrinkFactor(1) {} + /** + * Reset this DrawInfo using the new bounding area for the wave. \a shrinkFactor + * makes the wave shrink on the y axis with respect to the area. A factor 1 makes the wave as big as the area, whereas a factor >1 makes it shrink. + */ void reset( const ci::Area &bounds, float shrinkFactor ) { mWindowWidth = bounds.getWidth(); @@ -22,6 +36,10 @@ mShrinkFactor = shrinkFactor; } + /** + * Maps a value in the audio space [-1.0, 1.0] to a position on the y axis of this DrawInf's bounding area. + * + */ float audioToHeigt(float audioSample) const { /* clip into range [-1.1] */ if (audioSample < -1.0f) { @@ -48,6 +66,9 @@ return mSelectionBarHeight; } + /** + * Returns the center position on the y axis of this DrawInfo's the bounding area. + */ int32_t getWaveCenterY() const { if ( mWaveIndex == 0 ) @@ -56,14 +77,21 @@ return mWindowHeight / (NUM_WAVES * 2); } + /** + * Flips y according to the index of the wave. It is needed because the second wave in collidoscope is upside down from the orientation oftthe screen. + */ int flipY(int y) const { if ( mWaveIndex == 0) - return mWindowHeight - y /*+ 24*/; + return mWindowHeight - y; else - return y /*- 24*/; + return y; } + /** + * Returns x. not used at he moment. + * + */ int flipX(int x) const { return x; @@ -86,6 +114,9 @@ return mWindowHeight; } + /** + * Draw infos cannot be copied and should be passed as const reference. + */ DrawInfo( const DrawInfo &original ) = delete; DrawInfo & operator=( const DrawInfo &original ) = delete;
--- a/CollidoscopeApp/include/EnvASR.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/EnvASR.h Tue Jul 12 18:29:38 2016 +0200 @@ -17,6 +17,7 @@ { public: + /** Possible states of the envelope. Idle means the envelope ouputs 0 */ enum class State { eAttack, eSustain, @@ -40,6 +41,7 @@ mReleaseRate = T( 1.0 ) / (releaseTime * sampleRate); } + /** Produces one sample worth of envelope */ T tick() {
--- a/CollidoscopeApp/include/Log.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/Log.h Tue Jul 12 18:29:38 2016 +0200 @@ -1,8 +1,17 @@ #pragma once - +/** + * Utility function to log errors using the cinder::log library. + * Errors are logged to collidoscope_error.log file. + * + */ void logError( const std::string &errorMsg ); -void logInfo( const std::string &infoMsg ); \ No newline at end of file +/** + * Utility function to log info using the cinder::log library. + * Errors are logged to the terminal. Used only for debugging. + * + */ +void logInfo( const std::string &infoMsg );
--- a/CollidoscopeApp/include/MIDI.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/MIDI.h Tue Jul 12 18:29:38 2016 +0200 @@ -29,6 +29,9 @@ std::string mMessage; }; +/** + * A MIDI message + */ class MIDIMessage { friend class MIDI; @@ -40,8 +43,14 @@ unsigned char getChannel() { return mChannel; } + /** + * First byte of MIDI data + */ unsigned char getData_1() { return mData1; } + /** + * Second byte of MIDI data + */ unsigned char getData_2() { return mData2; } private: @@ -54,7 +63,10 @@ }; - +/** + * Handles MIDI messages from the keyboards and Teensy. It uses RtMidi library. + * + */ class MIDI { @@ -65,22 +77,32 @@ void setup( const Config& ); + /** + * Check new incoming messages and stores them into the vector passed as argument by reference. + */ void checkMessages( std::vector< MIDIMessage >& ); private: + // callback passed to RtMidi library static void RtMidiInCallback( double deltatime, std::vector<unsigned char> *message, void *userData ); + // parse RtMidi messages and turns them into more readable collidoscope::MIDIMessages MIDIMessage parseRtMidiMessage( std::vector<unsigned char> *message ); - // messages to pass to checkMessages caller + // messages to pass to checkMessages caller std::vector< MIDIMessage > mMIDIMessages; + // use specific variables for pitch bend messages. Pitch bend messages are coming + // from the strip sensors that are very jerky and send a lot of values. So instead + // of saving all the messages in mMIDIMessages just save the last received in mPitchBendMessages + // and optimize away redundant messages. std::array< MIDIMessage, NUM_WAVES > mPitchBendMessages; + // Same principle of pitch bend messages std::array< MIDIMessage, NUM_WAVES > mFilterMessages; - - + // vecotr containing all the MIDI input devices detected. std::vector< std::unique_ptr <RtMidiIn> > mInputs; + // Used for mutual access to the MIDI messages by the MIDI thread and the graphic thread. std::mutex mMutex; };
--- a/CollidoscopeApp/include/Messages.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/Messages.h Tue Jul 12 18:29:38 2016 +0200 @@ -1,6 +1,9 @@ #pragma once - +/** + * Enumeration of all the possible commands exchanged between audio thread and graphic thread. + * + */ enum class Command { // message carrying info about one chunk of recorder audio. WAVE_CHUNK, @@ -17,19 +20,24 @@ LOOP_OFF }; -/* Messages sent from the audio thread to the graphic wave. - This includes the wave chunks when the audio is recorder in the buffer and - the cursor position when the grains are reset. -*/ +/** Message sent from the audio thread to the graphic wave when a new wave is recorded. + * + * The graphic thread set the chunks of the wave to reflect the level of the recorded audio. + * The algorithm takes the maximum and minimum value of a group of samples and this becomes the top and bottom of the samples. + * It contains the inde + * the cursor position when the grains are reset. + */ struct RecordWaveMsg { - Command cmd; + Command cmd; // WAVE_CHUNK or WAVE_START std::size_t index; float arg1; float arg2; }; - +/** + * Utility function to create a new RecordWaveMsg. + */ inline RecordWaveMsg makeRecordWaveMsg( Command cmd, std::size_t index, float arg1, float arg2 ) { RecordWaveMsg msg; @@ -41,13 +49,20 @@ return msg; } - +/** + * Message sent from the audio thread to the graphic thread when a new grain is triggered in the granular synthesizer. + * This creates a new cursor that travels from the beginning to the end of the selection to graphically represent the evolution of the grain in time. + * + */ struct CursorTriggerMsg { - Command cmd; + Command cmd; // TRIGGER_UPDATE or TRIGGER_END int synthID; }; +/** + * Utility function to create a new CursorTriggerMsg. + */ inline CursorTriggerMsg makeCursorTriggerMsg( Command cmd, std::uint8_t synthID ) { CursorTriggerMsg msg; @@ -58,13 +73,19 @@ return msg; } +/** + * Message sent from the graphic (main) thread to the audio thread to start a new voice of the granular synthesizer. + */ struct NoteMsg { - Command cmd; + Command cmd; // NOTE_ON/OFF ot LOOP_ON/OFF int midiNote; double rate; }; +/** + * Utility function to create a new NoteMsg. + */ inline NoteMsg makeNoteMsg( Command cmd, int midiNote, double rate ) { NoteMsg msg; @@ -74,4 +95,4 @@ msg.rate = rate; return msg; -} \ No newline at end of file +}
--- a/CollidoscopeApp/include/PGranular.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/PGranular.h Tue Jul 12 18:29:38 2016 +0200 @@ -10,6 +10,29 @@ using std::size_t; +/** + * The very core of the Collidoscope audio engine: the granular synthesizer. + * Based on SuperCollider's TGrains and Ross Becina's "Implementing Real-Time Granular Synthesis" + * + * It implements Collidoscope's selection-based approach to granular synthesis. + * A grain is basically a selection of a recorded sample of audio. + * Grains are played in a loop: they are retriggered each time they reach the end of the selection. + * However, if the duration coefficient is greater than one, a new grain is re-triggered before the previous one is done. + * The grains start to overlap with each other and create the typical eerie sound of grnular synthesis. + * Also every time a new grain is triggered, it is offset of a few samples from the initial position to make the timbre more interesting. + * + * + * PGranular uses a linear ASR envelope with 10 milliseconds attack and 50 milliseconds release. + * + * Note that PGranular is header based and only depends on std library and on "EnvASR.h" (also header based). + * This means you can embedd it in two your project just by copying these two files over. + * + * Template arguments: + * T: type of the audio samples (normally float or double) + * RandOffsetFunc: type of the callable passed as argument to the contructor + * TriggerCallbackFunc: type of the callable passed as argument to the contructor + * + */ template <typename T, typename RandOffsetFunc, typename TriggerCallbackFunc> class PGranular { @@ -24,6 +47,9 @@ return static_cast<T> ((1 - decimal) * xn + decimal * xn_1); } + /** + * A single grain of the granular synthesis + */ struct PGrain { double phase; // read pointer to mBuffer of this grain @@ -32,13 +58,24 @@ size_t age; // age of this grain in samples size_t duration; // duration of this grain in samples. minimum = 4 - double b1; // hann envelope from Ross Becina "Implementing real time Granular Synthesis" + double b1; // hann envelope from Ross Becina's "Implementing real time Granular Synthesis" double y1; double y2; }; + /** + * Constructor. + * + * \param buffer a pointer to an array of T that contains the original sample that will be granulized + * \param bufferLen length of buffer in samples + * \rand function returning of type size_t ()(void) that is called back each time a new grain is generated. The returned value is used + * to offset the starting sample of the grain. This adds more colour to the sound especially with small selections. + * \triggerCallback function of type void ()(char, int) that is called back each time a new grain is triggered. + * The function is passed the character 't' as first parameter when a new grain is triggered and the characted 't' when the synths becomes idle. + * \ID id of this PGrain is passed to the triggerCallback function as second parameter to identify this PGranular as the caller. + */ PGranular( const T* buffer, size_t bufferLen, size_t sampleRate, RandOffsetFunc & rand, TriggerCallbackFunc & triggerCallback, int ID ) : mBuffer( buffer ), mBufferLen( bufferLen ), @@ -71,29 +108,30 @@ ~PGranular(){} - /* sets multiplier of duration of grains in seconds */ + /** Sets multiplier of duration of grains in seconds */ void setGrainsDurationCoeff( double coeff ) { mGrainsDurationCoeff = coeff; - mGrainsDuration = std::lround( mTriggerRate * coeff ); // FIXME check if right rounding + mGrainsDuration = std::lround( mTriggerRate * coeff ); if ( mGrainsDuration < kMinGrainsDuration ) mGrainsDuration = kMinGrainsDuration; } - /* sets rate of grains. e.g rate = 2 means one octave higer */ + /** Sets rate of grains. e.g rate = 2 means one octave higer */ void setGrainsRate( double rate ) { mGrainsRate = rate; } - // sets trigger rate in samples + /** sets the selection start in samples */ void setSelectionStart( size_t start ) { mGrainsStart = start; } + /** Sets the selection size ( and therefore the trigger rate) in samples */ void setSelectionSize( size_t size ) { @@ -107,11 +145,14 @@ } + /** Sets the attenuation of the grains with respect to the level of the recorded sample + * attenuation is in amp value and defaule value is 0.25118864315096 (-12dB) */ void setAttenuation( T attenuation ) { mAttenuation = attenuation; } + /** Starts the synthesis engine */ void noteOn( double rate ) { if ( mEnvASR.getState() == EnvASR<T>::State::eIdle ){ @@ -125,6 +166,7 @@ } } + /** Stops the synthesis engine */ void noteOff() { if ( mEnvASR.getState() != EnvASR<T>::State::eIdle ){ @@ -132,11 +174,19 @@ } } + /** Whether the synthesis engine is active or not. After noteOff is called the synth stays active until the envelope decays to 0 */ bool isIdle() { return mEnvASR.getState() == EnvASR<T>::State::eIdle; } + /** + * Runs the granular engine and stores the output in \a audioOut + * + * \param pointer to an array of T. This will be filled with the output of PGranular. It needs to be at least \a numSamples lond + * \param tempBuffer a temporary buffer used to store the envelope value. It needs to be at leas \a numSamples long + * \param numSamples number of samples to be processed + */ void process( T* audioOut, T* tempBuffer, size_t numSamples ) { @@ -144,7 +194,7 @@ size_t envSamples = 0; bool becameIdle = false; - // do the envelope first and store it in the tempBuffer + // process the envelope first and store it in the tempBuffer for ( size_t i = 0; i < numSamples; i++ ){ tempBuffer[i] = mEnvASR.tick(); envSamples++; @@ -156,8 +206,10 @@ } } + // does the actual grains processing processGrains( audioOut, tempBuffer, envSamples ); + // becomes idle if the envelope goes to idle state if ( becameIdle ){ mTriggerCallback( 'e', mID ); reset(); @@ -174,7 +226,7 @@ synthesizeGrain( mGrains[grainIdx], audioOut, envelopeValues, numSamples ); if ( !mGrains[grainIdx].alive ){ - // this grain is dead so copyu the last of the active grains here + // this grain is dead so copy the last of the active grains here // so as to keep all active grains at the beginning of the array // don't increment grainIdx so the last active grain is processed next cycle // if this grain is the last active grain then mNumAliveGrains is decremented @@ -243,6 +295,7 @@ } } + // synthesize a single grain // audioOut = pointer to audio block to fill // numSamples = numpber of samples to process for this block void synthesizeGrain( PGrain &grain, T* audioOut, T* envelopeValues, size_t numSamples )
--- a/CollidoscopeApp/include/PGranularNode.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/PGranularNode.h Tue Jul 12 18:29:38 2016 +0200 @@ -30,12 +30,13 @@ explicit PGranularNode( ci::audio::Buffer *grainBuffer, CursorTriggerMsgRingBuffer &triggerRingBuffer ); ~PGranularNode(); - // set selection size in samples + /** Set selection size in samples */ void setSelectionSize( size_t size ) { mSelectionSize.set( size ); } + /** Set selection start in samples */ void setSelectionStart( size_t start ) { mSelectionStart.set( start ); @@ -46,7 +47,7 @@ mGrainDurationCoeff.set( coeff ); } - // used for trigger callback in PGRanular + /* PGranularNode passes itself as trigger callback in PGranular */ void operator()( char msgType, int ID ); ci::audio::dsp::RingBufferT<NoteMsg>& getNoteRingBuffer() { return mNoteMsgRingBufferPack.getBuffer(); } @@ -59,6 +60,8 @@ private: + // Wraps a std::atomic but get() returns a boost::optional that is set to a real value only when the atomic has changed. + // It is used to avoid calling PGranulat setter methods with * the same value at each audio callback. template< typename T> class LazyAtomic { @@ -90,17 +93,19 @@ T mPreviousVal; }; + // creates or re-start a PGranular and sets the pitch according to the MIDI note passed as argument void handleNoteMsg( const NoteMsg &msg ); - // pointer to PGranular object + // pointers to PGranular objects std::unique_ptr < collidoscope::PGranular<float, RandomGenerator, PGranularNode > > mPGranularLoop; std::array<std::unique_ptr < collidoscope::PGranular<float, RandomGenerator, PGranularNode > >, kMaxVoices> mPGranularNotes; + // maps midi notes to pgranulars. When a noteOff is received maks sure the right PGranular is turned off std::array<int, kMaxVoices> mMidiNotes; // pointer to the random generator struct passed over to PGranular std::unique_ptr< RandomGenerator > mRandomOffset; - /* buffer containing the recorder audio, to pass to PGranular in initialize() */ + // buffer containing the recorder audio, to pass to PGranular in initialize() ci::audio::Buffer *mGrainBuffer; ci::audio::BufferRef mTempBuffer;
--- a/CollidoscopeApp/include/ParticleController.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/ParticleController.h Tue Jul 12 18:29:38 2016 +0200 @@ -3,7 +3,9 @@ #include "cinder/gl/gl.h" #include <vector> - +/** + * The ParticleController creates/updates/draws and destroys particles + */ class ParticleController { struct Particle { @@ -23,19 +25,34 @@ std::vector<Particle> mParticles; std::vector< ci::vec2 > mParticlePositions; + // current number of active particles size_t mNumParticles; ci::gl::VboRef mParticleVbo; ci::gl::BatchRef mParticleBatch; public: + /** + * Every time addParticles is run, up to kMaxParticleAdd are added at once + */ static const int kMaxParticleAdd = 22; ParticleController(); + + /** + * Adds \a amount particles and places them in \a initialLocation. + * \cloudSize determines how far the particles can go + */ void addParticles(int amount, const ci::vec2 &initialLocation, const float cloudSize); + /** + * Updates position and age of the particles + */ void updateParticles(); + /** + * Draws all the particles + */ inline void draw() { mParticleBatch->draw();
--- a/CollidoscopeApp/include/RingBufferPack.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/RingBufferPack.h Tue Jul 12 18:29:38 2016 +0200 @@ -3,8 +3,7 @@ #include "cinder/audio/dsp/RingBuffer.h" -/* Packs together a RingBuffer and the erlated array used to exchange data (read/write) with the ring buffer -*/ +/** Packs together a cinder::audio::dsp::RingBuffer and the related array used passed as argument to exchange data (read/write) with the ring buffer */ template <typename T> class RingBufferPack { @@ -40,4 +39,4 @@ T* mArray; -}; \ No newline at end of file +};
--- a/CollidoscopeApp/include/Wave.h Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/include/Wave.h Tue Jul 12 18:29:38 2016 +0200 @@ -30,29 +30,42 @@ using ci::Color; using ci::ColorA; +/** + * A Cursor is the white thingy that loops through the selection when Collidoscope is played. + */ struct Cursor { static const int kNoPosition = -100; int pos; double lastUpdate; }; - +/** + * Collidoscope's graphical wave + * + */ class Wave { friend class ParticleController; public: + /** + * The selection of the wave that is controlled by the big horizontal knob + * + */ class Selection { public: Selection( Wave * w, Color color ); + /** Sets the start of selection. start is the index of the first chunk of the selection */ void setStart( size_t start ); + /** Sets the size of selection. size is the number of chunks the selection is made of */ void setSize( size_t size ); + /** Particle spread is used to calculate the size of the cloud of particles */ void inline setParticleSpread( float spread ){ mParticleSpread = spread; } @@ -70,6 +83,7 @@ float inline getParticleSpread() const { return mParticleSpread; } + /** When selection is null no selection is showed on the wave */ inline void setToNull(){ mParticleSpread = 1.0f; mNull = true; @@ -107,13 +121,14 @@ - /* there is one cursor for each Synth being played */ + /* Maps id of the synth to cursor. There is one cursor for each Synth being played */ std::map < SynthID, Cursor > mCursors; + /** Holds the positions of the cursor, namely on which chunk the cursor is currently */ std::vector<int> mCursorsPos; public: - // note used to identify the loop for cursor position + // value used to identify the loop for cursor position static const int kLoopNote = -1; static const cinder::Color CURSOR_CLR; /* must be in sync with supercollider durationFactor ControlSpec max */ @@ -122,12 +137,21 @@ static const int PARTICLESIZE_COEFF = 40; #endif + /** Resetting a wave makes it shrink until it disappears. Each time a new sample is recorder the wave is reset + * \param onlyChunks if false the selection is also set to null, if true only the chunks are reset + */ void reset(bool onlyChunks); + /** sets top and bottom values for the chunk. + * \a bottom and \a top are in audio coordinates [-1.0, 1.0] + */ void setChunk(size_t index, float bottom, float top); const Chunk & getChunk(size_t index); + /** places the cursor on the wave. Every cursor is associated to a synth voice of the audio engine. + * The synth id identifies uniquely the cursor in the internal map of the wave. + * If the cursor doesn't exist it is created */ inline void setCursorPos( SynthID id, int pos, const DrawInfo& di ){ Cursor & cursor = mCursors[id]; @@ -135,9 +159,8 @@ cursor.lastUpdate = ci::app::getElapsedSeconds(); #ifdef USE_PARTICLES - /* if the duration is greater than 1.0 carry on the cursor as a particle - the smaller the selection the more particles - the bigger the duration the more particles */ + // The idea is that, if the duration is greater than 1.0, the cursor continues in form of particles + // The smaller the selection the more particles; the bigger the duration the more particles if (mSelection.getParticleSpread() > 1.0f){ /* amountCoeff ranges from 1/8 to 1 */ const float amountCoeff = (mSelection.getParticleSpread() / MAX_DURATION); @@ -167,7 +190,7 @@ void removeCursor( SynthID id ) { mCursors.erase( id ); } - // parameter ranges from 0 to 1 + /** Sets the transparency of this wave. \a alpha ranges from 0 to 1 */ inline void setselectionAlpha(float alpha){ mFilterCoeff = alpha;} void draw( const DrawInfo& di ); @@ -180,7 +203,7 @@ Wave( size_t numChunks, Color selectionColor ); - // no copies + /** no copies */ Wave( const Wave © ) = delete; Wave & operator=(const Wave ©) = delete; @@ -197,6 +220,7 @@ float mFilterCoeff; + // cinder gl batch for batch drawing ci::gl::BatchRef mChunkBatch; };
--- a/CollidoscopeApp/src/PGranularNode.cpp Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/src/PGranularNode.cpp Tue Jul 12 18:29:38 2016 +0200 @@ -59,7 +59,6 @@ void PGranularNode::process (ci::audio::Buffer *buffer ) { // only update PGranular if the atomic value has changed from the previous time - const boost::optional<size_t> selectionSize = mSelectionSize.get(); if ( selectionSize ){ mPGranularLoop->setSelectionSize( *selectionSize ); @@ -112,6 +111,7 @@ } } +// Called back when new grnular is triggered of turned off. Sends notification message to graphic thread. void PGranularNode::operator()( char msgType, int ID ) { switch ( msgType ){
--- a/CollidoscopeApp/src/Wave.cpp Mon Jul 11 17:03:40 2016 +0200 +++ b/CollidoscopeApp/src/Wave.cpp Tue Jul 12 18:29:38 2016 +0200 @@ -96,7 +96,7 @@ const float wavePixelLen = ( mNumChunks * ( 2 + Chunk::kWidth ) ); - /* scale the x-axis for the wave to fit the window */ + /* scale the x-axis for the wave to fit the window precisely */ gl::scale( ((float)di.getWindowWidth() ) / wavePixelLen , 1.0f); /* draw the chunks */ if (mSelection.isNull()){