view CollidoscopeApp/src/CollidoscopeApp.cpp @ 16:4dad0b810f18

Comment tidy up + attributions
author Fiore Martin <f.martin@qmul.ac.uk>
date Tue, 16 Aug 2016 14:27:53 +0100
parents 20bb004a36de
children f1ff1a81be20
line wrap: on
line source
/*

 Copyright (C) 2016  Queen Mary University of London 
 Author: Fiore Martin

 This file is part of Collidoscope.
 
 Collidoscope is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/


#include "cinder/app/App.h"
#include "cinder/app/RendererGl.h"
#include "cinder/gl/gl.h"
#include "cinder/Exception.h"
#include <stdexcept>


#include "Config.h"
#include "Wave.h"
#include "DrawInfo.h"
#include "Log.h"
#include "AudioEngine.h"
#include "Oscilloscope.h"
#include "Messages.h"
#include "MIDI.h"

using namespace ci;
using namespace ci::app;

using namespace std;


class CollidoscopeApp : public App {
  public:

    void setup() override;
    void setupGraphics();

    /** Receives MIDI command messages from MIDI thread */
    void receiveCommands();
    /** Prints command line usage */
    void usage();

    void keyDown( KeyEvent event ) override;
    void update() override;
    void draw() override;
    void resize() override;

    Config mConfig;
    collidoscope::MIDI mMIDI;
    AudioEngine mAudioEngine;
    
    array< shared_ptr< Wave >, NUM_WAVES > mWaves;
    array< shared_ptr< DrawInfo >, NUM_WAVES > mDrawInfos;
    array< shared_ptr< Oscilloscope >, NUM_WAVES > mOscilloscopes;
    // buffer to read the WAVE_* messages as a new wave gets recorded 
    array< RecordWaveMsg*, NUM_WAVES> mRecordWaveMessageBuffers;
    //buffer to read the TRIGGER_* messages as the pgranulars play
    array< vector< CursorTriggerMsg >, NUM_WAVES > mCursorTriggerMessagesBuffers;

    double mSecondsPerChunk;

    ~CollidoscopeApp();

};


void CollidoscopeApp::setup()
{
    hideCursor();
    /* setup is logged: setup steps and errors */
    
    /*try {
        mConfig.loadFromFile( "./collidoscope_config.xml" );
    }
    catch ( const Exception &e ){
        logError( string("Exception loading config from file:") + e.what() );
    }*/

    // setup buffers to read messages from audio thread 
    for ( size_t i = 0; i < NUM_WAVES; i++ ){
        mRecordWaveMessageBuffers[i] = new RecordWaveMsg[mConfig.getNumChunks()];
        mCursorTriggerMessagesBuffers[i].reserve( mConfig.getCursorTriggerMessageBufSize() );
    }

    mAudioEngine.setup( mConfig );

    setupGraphics();

    mSecondsPerChunk = mConfig.getWaveLen() / mConfig.getNumChunks();

    try {
        mMIDI.setup( mConfig );
    }
    catch ( const collidoscope::MIDIException &e ){
        logError( string( "Exception opening MIDI input device: " ) + e.getMessage() );
    }

}

void CollidoscopeApp::setupGraphics()
{
    for ( size_t i = 0; i < NUM_WAVES; i++ ){

        mDrawInfos[i] = make_shared< DrawInfo >( i );
        mWaves[i] = make_shared< Wave >(mConfig.getNumChunks(), mConfig.getWaveSelectionColor(i) );
        mOscilloscopes[i] = make_shared< Oscilloscope >( mAudioEngine.getAudioOutputBuffer( i ).getNumFrames() / mConfig.getOscilloscopeNumPointsDivider() );

    }
}

void CollidoscopeApp::keyDown( KeyEvent event )
{
    char c = event.getChar();

    const size_t waveIdx = 0;

    switch (c){
    case 'r' : 
        mAudioEngine.record( waveIdx );
        break;

    case 'w': {

        mWaves[waveIdx]->getSelection().setSize(mWaves[waveIdx]->getSelection().getSize() + 1);

        size_t numSelectionChunks = mWaves[waveIdx]->getSelection().getSize();
        // how many samples in one selection ?
        size_t selectionSize = numSelectionChunks * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());

        mAudioEngine.setSelectionSize(waveIdx, selectionSize);
    };
        break;

    case 's': {

        mWaves[waveIdx]->getSelection().setSize( mWaves[waveIdx]->getSelection().getSize() - 1 );

        size_t selectionSize = mWaves[waveIdx]->getSelection().getSize() *(mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());
        mAudioEngine.setSelectionSize( waveIdx, selectionSize );
    };
        break;

    case 'd': {

        size_t selectionStart = mWaves[waveIdx]->getSelection().getStart();
        mWaves[waveIdx]->getSelection().setStart( selectionStart + 1 );

        selectionStart = mWaves[waveIdx]->getSelection().getStart();
        mAudioEngine.setSelectionStart( waveIdx, selectionStart * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
    };

        break;

    case 'a': {
        size_t selectionStart = mWaves[waveIdx]->getSelection().getStart();
        
        if ( selectionStart == 0 )
            return;

        mWaves[waveIdx]->getSelection().setStart( selectionStart - 1 );

        selectionStart = mWaves[waveIdx]->getSelection().getStart();

        mAudioEngine.setSelectionStart( waveIdx, selectionStart * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
    };
        break;

    case 'f':
        setFullScreen( !isFullScreen() );
        break;

    case ' ': { 
        static bool isOn = false;
        isOn = !isOn;
        if ( isOn ){
            mAudioEngine.loopOn( waveIdx );
        }
        else{
            mAudioEngine.loopOff( waveIdx );
        }
    };
        break;

    case '9': {
        int c = mWaves[waveIdx]->getSelection().getParticleSpread();
        if ( c == 1 )
            return;
        else
            c -= 1;

        mAudioEngine.setGrainDurationCoeff( waveIdx, c );
        mWaves[waveIdx]->getSelection().setParticleSpread( float( c ) );

    }; break;

    case '0': {
        int c = mWaves[waveIdx]->getSelection().getParticleSpread();
        if ( c == 8 )
            return;
        else
            c += 1;

        mAudioEngine.setGrainDurationCoeff( waveIdx, c );
        mWaves[waveIdx]->getSelection().setParticleSpread( float( c ) );
    }; break;
    }

}

void CollidoscopeApp::update()
{
    // check incoming commands 
    receiveCommands();

    // check new wave chunks from recorder buffer 
    for ( size_t i = 0; i < NUM_WAVES; i++ ){
        size_t availableRead = mAudioEngine.getRecordWaveAvailable( i );
        mAudioEngine.readRecordWave( i, mRecordWaveMessageBuffers[i], availableRead );

        for ( size_t msgIndex = 0; msgIndex < availableRead; msgIndex++ ){
            const RecordWaveMsg & msg = mRecordWaveMessageBuffers[i][msgIndex];

            if ( msg.cmd == Command::WAVE_CHUNK ){
                mWaves[i]->setChunk( msg.index, msg.arg1, msg.arg2 );
            }
            else if ( msg.cmd == Command::WAVE_START ){
                mWaves[i]->reset( true ); // reset only chunks but leave selection 
            }
            
        }
    }

    // check if new cursors have been triggered 
    for ( size_t i = 0; i < NUM_WAVES; i++ ){
        
        mAudioEngine.checkCursorTriggers( i, mCursorTriggerMessagesBuffers[i] );
        for ( auto & trigger : mCursorTriggerMessagesBuffers[i] ){
            const int nodeID = trigger.synthID;

            switch ( trigger.cmd ){

            case Command::TRIGGER_UPDATE: {
                mWaves[i]->setCursorPos( nodeID, mWaves[i]->getSelection().getStart(), *mDrawInfos[i] );
            };
                break;

            case Command::TRIGGER_END: {
                mWaves[i]->removeCursor( nodeID );
            };
                break;

            }
            
        }
        mCursorTriggerMessagesBuffers[i].clear();
    }

    // update cursors 
    for ( size_t i = 0; i < NUM_WAVES; i++ ){
        mWaves[i]->update( mSecondsPerChunk, *mDrawInfos[i] );
    }
    
    // update oscilloscope 

    for ( size_t i = 0; i < NUM_WAVES; i++ ){
        const audio::Buffer &audioOutBuffer = mAudioEngine.getAudioOutputBuffer( i );
        // one oscilloscope sample 

        for ( size_t j = 0; j < mOscilloscopes[i]->getNumPoints(); j++ ){
            mOscilloscopes[i]->setPoint( j, audioOutBuffer.getData()[j], *mDrawInfos[i] );
        }
    }

    
    
}

void CollidoscopeApp::draw()
{
    gl::clear( Color( 0, 0, 0 ) );

    for ( int i = 0; i < NUM_WAVES; i++ ){
        if ( i == 1 ){
            /* for the upper wave flip the x over the center of the screen which is
            the composition of rotate on the y-axis and translate by -screenwidth*/
            gl::pushModelMatrix();
            gl::rotate( float(M_PI), ci::vec3( 0, 1, 0 ) );
            gl::translate( float( -getWindowWidth() ), 0.0f );
            mOscilloscopes[i]->draw();
            mWaves[i]->draw( *mDrawInfos[i] );
            gl::popModelMatrix();
        }
        else{

            mOscilloscopes[i]->draw();
            mWaves[i]->draw( *mDrawInfos[i] );
        }
    }
}

void CollidoscopeApp::resize()
{
    App::resize();
    
    for ( int i = 0; i < NUM_WAVES; i++ ){
        // reset the drawing information with the new windows size and same shrink factor  
        mDrawInfos[i]->reset( getWindow()->getBounds(), 3.0f / 5.0f );

        /* reset the oscilloscope points to zero */
        for ( int j = 0; j < mOscilloscopes[i]->getNumPoints(); j++ ){
            mOscilloscopes[i]->setPoint(j, 0.0f, *mDrawInfos[i] );
        }
    }
}



void CollidoscopeApp::receiveCommands()
{
    // check new midi messages 
    static std::vector<collidoscope::MIDIMessage> midiMessages;
    mMIDI.checkMessages( midiMessages );


    for ( auto &m : midiMessages ){
        
        const size_t waveIdx = mConfig.getWaveForMIDIChannel( m.getChannel() );
        if ( waveIdx >= NUM_WAVES )
            continue;

        if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eNoteOn ){
            int midiNote = m.getData_1();
            mAudioEngine.noteOn( waveIdx, midiNote );
        }
        else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eNoteOff ){
            int midiNote = m.getData_1();
            mAudioEngine.noteOff( waveIdx, midiNote );
        } 
        else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::ePitchBend ){
            const uint16_t MSB = m.getData_2() << 7;
            uint16_t value = m.getData_1(); // LSB 

            value |= MSB;
            

            // value ranges from 0 to 1050. check boundaries in case sensor gives bad values 
            if ( value > 149 ){ // FIXME pareametrizer 
                continue;
            }

            size_t startChunk = value;

            const size_t selectionSizeBeforeStartUpdate = mWaves[waveIdx]->getSelection().getSize();
            mWaves[waveIdx]->getSelection().setStart( startChunk );

            mAudioEngine.setSelectionStart( waveIdx, startChunk * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
            
            const size_t newSelectionSize = mWaves[waveIdx]->getSelection().getSize();
            if ( selectionSizeBeforeStartUpdate != newSelectionSize ){
                mAudioEngine.setSelectionSize( waveIdx, newSelectionSize * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
            }


        }
        else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eControlChange ){

            switch ( m.getData_1() ){ //controller number 
            case 1: { // selection size 
                const size_t midiVal = m.getData_2();
                size_t numSelectionChunks = ci::lmap<size_t>( midiVal, 0, 127, 1, mConfig.getMaxSelectionNumChunks() );

                mWaves[waveIdx]->getSelection().setSize( numSelectionChunks );

                // how many samples in one selection ?
                size_t selectionSize = mWaves[waveIdx]->getSelection().getSize() * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());
                mAudioEngine.setSelectionSize( waveIdx, selectionSize );

            };
                break;

            case 4: { // loop on off
                unsigned char midiVal = m.getData_2();

                if ( midiVal > 0 )
                    mAudioEngine.loopOn( waveIdx );
                else
                    mAudioEngine.loopOff( waveIdx );
            };
                break;

            case 5: // trigger record
                mAudioEngine.record( waveIdx );
                break;

            case 2: { // duration 
                const double midiVal = m.getData_2(); // 0-127
                const double coeff = ci::lmap<double>( midiVal, 0.0, 127, 1.0, mConfig.getMaxGrainDurationCoeff() );
                mAudioEngine.setGrainDurationCoeff( waveIdx, coeff );
                mWaves[waveIdx]->getSelection().setParticleSpread( float( coeff ) );
            };
                break;

            case 7: { // filter 
                const double midiVal = m.getData_2(); // 0-127
                const double minCutoff = mConfig.getMinFilterCutoffFreq();
                const double maxCutoff = mConfig.getMaxFilterCutoffFreq();
                const double cutoff = pow( maxCutoff / 200., midiVal / 127.0 ) * minCutoff;
                mAudioEngine.setFilterCutoff( waveIdx, cutoff );
                const float alpha = ci::lmap<double>( midiVal, 0.0f, 127.0f, 0.f, 1.f );
                mWaves[waveIdx]->setselectionAlpha( alpha );
            };
                break;


                
            }
        }
    }

    midiMessages.clear();
}



CollidoscopeApp::~CollidoscopeApp()
{
    for ( int chan = 0; chan < NUM_WAVES; chan++ ){
        /* delete the array for wave messages from audio thread */
        delete[] mRecordWaveMessageBuffers[chan];
    }
}


CINDER_APP( CollidoscopeApp, RendererGl, [] ( App::Settings *settings) {

    const std::vector< string > args = settings->getCommandLineArgs();

    int width = 0;
    int height = 0;

    try {
        if( args.size() != 3 )
            throw std::invalid_argument("");

        width  = std::stoi( args[1] );
        height = std::stoi( args[2] );

    }
    catch( std::invalid_argument & e ){
        console() << "Error: invalid arguments" << std::endl;
        console() << "Usage: ./Collidoscope window_width window_height" << std::endl;  
        console() << "For example: ./Collidoscope 1024 768 " << std::endl;  

        settings->setShouldQuit( true );
        return;
    }

    settings->setWindowSize( width, height );
    settings->setMultiTouchEnabled( false );
    settings->disableFrameRate();

} )