f@5: /*
f@5:
f@5: Copyright (C) 2016 Queen Mary University of London
f@5: Author: Fiore Martin
f@5:
f@5: This file is part of Collidoscope.
f@5:
f@5: Collidoscope is free software: you can redistribute it and/or modify
f@5: it under the terms of the GNU General Public License as published by
f@5: the Free Software Foundation, either version 3 of the License, or
f@5: (at your option) any later version.
f@5:
f@5: This program is distributed in the hope that it will be useful,
f@5: but WITHOUT ANY WARRANTY; without even the implied warranty of
f@5: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@5: GNU General Public License for more details.
f@5:
f@5: You should have received a copy of the GNU General Public License
f@5: along with this program. If not, see .
f@5: */
f@5:
f@6:
f@0: #include "cinder/app/App.h"
f@0: #include "cinder/app/RendererGl.h"
f@0: #include "cinder/gl/gl.h"
f@0: #include "cinder/Exception.h"
f@6: #include
f@0:
f@0:
f@0: #include "Config.h"
f@0: #include "Wave.h"
f@0: #include "DrawInfo.h"
f@0: #include "Log.h"
f@0: #include "AudioEngine.h"
f@0: #include "Oscilloscope.h"
f@0: #include "Messages.h"
f@0: #include "MIDI.h"
f@0:
f@0: using namespace ci;
f@0: using namespace ci::app;
f@0:
f@0: using namespace std;
f@0:
f@0:
f@0: class CollidoscopeApp : public App {
f@0: public:
f@0:
f@5: void setup() override;
f@0: void setupGraphics();
f@0:
f@6: /** Receives MIDI command messages from MIDI thread */
f@0: void receiveCommands();
f@6: /** Prints command line usage */
f@6: void usage();
f@0:
f@5: void keyDown( KeyEvent event ) override;
f@5: void update() override;
f@5: void draw() override;
f@0: void resize() override;
f@0:
f@5: Config mConfig;
f@0: collidoscope::MIDI mMIDI;
f@0: AudioEngine mAudioEngine;
f@5:
f@0: array< shared_ptr< Wave >, NUM_WAVES > mWaves;
f@0: array< shared_ptr< DrawInfo >, NUM_WAVES > mDrawInfos;
f@0: array< shared_ptr< Oscilloscope >, NUM_WAVES > mOscilloscopes;
f@16: // buffer to read the WAVE_* messages as a new wave gets recorded
f@0: array< RecordWaveMsg*, NUM_WAVES> mRecordWaveMessageBuffers;
f@16: //buffer to read the TRIGGER_* messages as the pgranulars play
f@0: array< vector< CursorTriggerMsg >, NUM_WAVES > mCursorTriggerMessagesBuffers;
f@0:
f@0: double mSecondsPerChunk;
f@0:
f@0: ~CollidoscopeApp();
f@0:
f@0: };
f@0:
f@0:
f@0: void CollidoscopeApp::setup()
f@0: {
f@0: hideCursor();
f@0: /* setup is logged: setup steps and errors */
f@0:
f@0: /*try {
f@0: mConfig.loadFromFile( "./collidoscope_config.xml" );
f@0: }
f@0: catch ( const Exception &e ){
f@0: logError( string("Exception loading config from file:") + e.what() );
f@0: }*/
f@0:
f@0: // setup buffers to read messages from audio thread
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0: mRecordWaveMessageBuffers[i] = new RecordWaveMsg[mConfig.getNumChunks()];
f@0: mCursorTriggerMessagesBuffers[i].reserve( mConfig.getCursorTriggerMessageBufSize() );
f@0: }
f@0:
f@0: mAudioEngine.setup( mConfig );
f@0:
f@0: setupGraphics();
f@0:
f@0: mSecondsPerChunk = mConfig.getWaveLen() / mConfig.getNumChunks();
f@0:
f@0: try {
f@0: mMIDI.setup( mConfig );
f@0: }
f@0: catch ( const collidoscope::MIDIException &e ){
f@0: logError( string( "Exception opening MIDI input device: " ) + e.getMessage() );
f@0: }
f@0:
f@0: }
f@0:
f@0: void CollidoscopeApp::setupGraphics()
f@0: {
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0:
f@0: mDrawInfos[i] = make_shared< DrawInfo >( i );
f@0: mWaves[i] = make_shared< Wave >(mConfig.getNumChunks(), mConfig.getWaveSelectionColor(i) );
f@0: mOscilloscopes[i] = make_shared< Oscilloscope >( mAudioEngine.getAudioOutputBuffer( i ).getNumFrames() / mConfig.getOscilloscopeNumPointsDivider() );
f@0:
f@0: }
f@0: }
f@0:
f@0: void CollidoscopeApp::keyDown( KeyEvent event )
f@0: {
f@0: char c = event.getChar();
f@0:
f@9: const size_t waveIdx = 0;
f@9:
f@0: switch (c){
f@0: case 'r' :
f@9: mAudioEngine.record( waveIdx );
f@0: break;
f@0:
f@0: case 'w': {
f@6:
f@9: mWaves[waveIdx]->getSelection().setSize(mWaves[waveIdx]->getSelection().getSize() + 1);
f@0:
f@9: size_t numSelectionChunks = mWaves[waveIdx]->getSelection().getSize();
f@0: // how many samples in one selection ?
f@0: size_t selectionSize = numSelectionChunks * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());
f@0:
f@9: mAudioEngine.setSelectionSize(waveIdx, selectionSize);
f@0: };
f@0: break;
f@0:
f@0: case 's': {
f@0:
f@9: mWaves[waveIdx]->getSelection().setSize( mWaves[waveIdx]->getSelection().getSize() - 1 );
f@0:
f@9: size_t selectionSize = mWaves[waveIdx]->getSelection().getSize() *(mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());
f@9: mAudioEngine.setSelectionSize( waveIdx, selectionSize );
f@0: };
f@0: break;
f@0:
f@0: case 'd': {
f@0:
f@9: size_t selectionStart = mWaves[waveIdx]->getSelection().getStart();
f@9: mWaves[waveIdx]->getSelection().setStart( selectionStart + 1 );
f@0:
f@9: selectionStart = mWaves[waveIdx]->getSelection().getStart();
f@9: mAudioEngine.setSelectionStart( waveIdx, selectionStart * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
f@0: };
f@0:
f@0: break;
f@0:
f@0: case 'a': {
f@9: size_t selectionStart = mWaves[waveIdx]->getSelection().getStart();
f@0:
f@0: if ( selectionStart == 0 )
f@0: return;
f@0:
f@9: mWaves[waveIdx]->getSelection().setStart( selectionStart - 1 );
f@0:
f@9: selectionStart = mWaves[waveIdx]->getSelection().getStart();
f@0:
f@9: mAudioEngine.setSelectionStart( waveIdx, selectionStart * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
f@0: };
f@0: break;
f@0:
f@0: case 'f':
f@0: setFullScreen( !isFullScreen() );
f@0: break;
f@0:
f@0: case ' ': {
f@0: static bool isOn = false;
f@0: isOn = !isOn;
f@0: if ( isOn ){
f@9: mAudioEngine.loopOn( waveIdx );
f@0: }
f@0: else{
f@9: mAudioEngine.loopOff( waveIdx );
f@0: }
f@0: };
f@0: break;
f@0:
f@0: case '9': {
f@9: int c = mWaves[waveIdx]->getSelection().getParticleSpread();
f@0: if ( c == 1 )
f@0: return;
f@0: else
f@0: c -= 1;
f@0:
f@9: mAudioEngine.setGrainDurationCoeff( waveIdx, c );
f@9: mWaves[waveIdx]->getSelection().setParticleSpread( float( c ) );
f@9:
f@0: }; break;
f@0:
f@0: case '0': {
f@9: int c = mWaves[waveIdx]->getSelection().getParticleSpread();
f@0: if ( c == 8 )
f@0: return;
f@0: else
f@0: c += 1;
f@0:
f@9: mAudioEngine.setGrainDurationCoeff( waveIdx, c );
f@9: mWaves[waveIdx]->getSelection().setParticleSpread( float( c ) );
f@0: }; break;
f@0: }
f@0:
f@0: }
f@0:
f@0: void CollidoscopeApp::update()
f@0: {
f@0: // check incoming commands
f@0: receiveCommands();
f@0:
f@0: // check new wave chunks from recorder buffer
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0: size_t availableRead = mAudioEngine.getRecordWaveAvailable( i );
f@0: mAudioEngine.readRecordWave( i, mRecordWaveMessageBuffers[i], availableRead );
f@0:
f@0: for ( size_t msgIndex = 0; msgIndex < availableRead; msgIndex++ ){
f@0: const RecordWaveMsg & msg = mRecordWaveMessageBuffers[i][msgIndex];
f@0:
f@0: if ( msg.cmd == Command::WAVE_CHUNK ){
f@0: mWaves[i]->setChunk( msg.index, msg.arg1, msg.arg2 );
f@0: }
f@0: else if ( msg.cmd == Command::WAVE_START ){
f@0: mWaves[i]->reset( true ); // reset only chunks but leave selection
f@0: }
f@0:
f@0: }
f@0: }
f@0:
f@0: // check if new cursors have been triggered
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0:
f@0: mAudioEngine.checkCursorTriggers( i, mCursorTriggerMessagesBuffers[i] );
f@0: for ( auto & trigger : mCursorTriggerMessagesBuffers[i] ){
f@0: const int nodeID = trigger.synthID;
f@0:
f@0: switch ( trigger.cmd ){
f@0:
f@0: case Command::TRIGGER_UPDATE: {
f@0: mWaves[i]->setCursorPos( nodeID, mWaves[i]->getSelection().getStart(), *mDrawInfos[i] );
f@0: };
f@0: break;
f@0:
f@0: case Command::TRIGGER_END: {
f@0: mWaves[i]->removeCursor( nodeID );
f@0: };
f@0: break;
f@0:
f@0: }
f@0:
f@0: }
f@0: mCursorTriggerMessagesBuffers[i].clear();
f@0: }
f@0:
f@0: // update cursors
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0: mWaves[i]->update( mSecondsPerChunk, *mDrawInfos[i] );
f@0: }
f@0:
f@0: // update oscilloscope
f@0:
f@0: for ( size_t i = 0; i < NUM_WAVES; i++ ){
f@0: const audio::Buffer &audioOutBuffer = mAudioEngine.getAudioOutputBuffer( i );
f@0: // one oscilloscope sample
f@0:
f@0: for ( size_t j = 0; j < mOscilloscopes[i]->getNumPoints(); j++ ){
f@0: mOscilloscopes[i]->setPoint( j, audioOutBuffer.getData()[j], *mDrawInfos[i] );
f@0: }
f@0: }
f@0:
f@0:
f@0:
f@0: }
f@0:
f@0: void CollidoscopeApp::draw()
f@0: {
f@5: gl::clear( Color( 0, 0, 0 ) );
f@0:
f@0: for ( int i = 0; i < NUM_WAVES; i++ ){
f@0: if ( i == 1 ){
f@0: /* for the upper wave flip the x over the center of the screen which is
f@0: the composition of rotate on the y-axis and translate by -screenwidth*/
f@0: gl::pushModelMatrix();
f@0: gl::rotate( float(M_PI), ci::vec3( 0, 1, 0 ) );
f@0: gl::translate( float( -getWindowWidth() ), 0.0f );
f@0: mOscilloscopes[i]->draw();
f@0: mWaves[i]->draw( *mDrawInfos[i] );
f@0: gl::popModelMatrix();
f@0: }
f@0: else{
f@0:
f@0: mOscilloscopes[i]->draw();
f@0: mWaves[i]->draw( *mDrawInfos[i] );
f@0: }
f@0: }
f@0: }
f@0:
f@0: void CollidoscopeApp::resize()
f@0: {
f@0: App::resize();
f@0:
f@0: for ( int i = 0; i < NUM_WAVES; i++ ){
f@0: // reset the drawing information with the new windows size and same shrink factor
f@0: mDrawInfos[i]->reset( getWindow()->getBounds(), 3.0f / 5.0f );
f@0:
f@0: /* reset the oscilloscope points to zero */
f@0: for ( int j = 0; j < mOscilloscopes[i]->getNumPoints(); j++ ){
f@0: mOscilloscopes[i]->setPoint(j, 0.0f, *mDrawInfos[i] );
f@0: }
f@0: }
f@0: }
f@0:
f@0:
f@0:
f@0: void CollidoscopeApp::receiveCommands()
f@0: {
f@0: // check new midi messages
f@0: static std::vector midiMessages;
f@0: mMIDI.checkMessages( midiMessages );
f@0:
f@0:
f@0: for ( auto &m : midiMessages ){
f@0:
f@0: const size_t waveIdx = mConfig.getWaveForMIDIChannel( m.getChannel() );
f@0: if ( waveIdx >= NUM_WAVES )
f@0: continue;
f@0:
f@0: if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eNoteOn ){
f@0: int midiNote = m.getData_1();
f@0: mAudioEngine.noteOn( waveIdx, midiNote );
f@0: }
f@0: else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eNoteOff ){
f@0: int midiNote = m.getData_1();
f@0: mAudioEngine.noteOff( waveIdx, midiNote );
f@0: }
f@0: else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::ePitchBend ){
f@0: const uint16_t MSB = m.getData_2() << 7;
f@0: uint16_t value = m.getData_1(); // LSB
f@0:
f@0: value |= MSB;
f@0:
f@0:
f@18: // value ranges from 0 to 149. check boundaries in case sensor gives bad values
f@18: if ( value > 149 ){ // FIXME can use wave.size()
f@0: continue;
f@0: }
f@0:
f@0: size_t startChunk = value;
f@0:
f@0: const size_t selectionSizeBeforeStartUpdate = mWaves[waveIdx]->getSelection().getSize();
f@0: mWaves[waveIdx]->getSelection().setStart( startChunk );
f@0:
f@0: mAudioEngine.setSelectionStart( waveIdx, startChunk * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
f@0:
f@0: const size_t newSelectionSize = mWaves[waveIdx]->getSelection().getSize();
f@0: if ( selectionSizeBeforeStartUpdate != newSelectionSize ){
f@0: mAudioEngine.setSelectionSize( waveIdx, newSelectionSize * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks()) );
f@0: }
f@0:
f@0:
f@0: }
f@0: else if ( m.getVoice() == collidoscope::MIDIMessage::Voice::eControlChange ){
f@0:
f@0: switch ( m.getData_1() ){ //controller number
f@0: case 1: { // selection size
f@0: const size_t midiVal = m.getData_2();
f@0: size_t numSelectionChunks = ci::lmap( midiVal, 0, 127, 1, mConfig.getMaxSelectionNumChunks() );
f@0:
f@0: mWaves[waveIdx]->getSelection().setSize( numSelectionChunks );
f@0:
f@0: // how many samples in one selection ?
f@5: size_t selectionSize = mWaves[waveIdx]->getSelection().getSize() * (mConfig.getWaveLen() * mAudioEngine.getSampleRate() / mConfig.getNumChunks());
f@0: mAudioEngine.setSelectionSize( waveIdx, selectionSize );
f@0:
f@0: };
f@0: break;
f@0:
f@0: case 4: { // loop on off
f@0: unsigned char midiVal = m.getData_2();
f@0:
f@0: if ( midiVal > 0 )
f@0: mAudioEngine.loopOn( waveIdx );
f@0: else
f@0: mAudioEngine.loopOff( waveIdx );
f@0: };
f@0: break;
f@0:
f@0: case 5: // trigger record
f@0: mAudioEngine.record( waveIdx );
f@0: break;
f@0:
f@0: case 2: { // duration
f@0: const double midiVal = m.getData_2(); // 0-127
f@0: const double coeff = ci::lmap( midiVal, 0.0, 127, 1.0, mConfig.getMaxGrainDurationCoeff() );
f@0: mAudioEngine.setGrainDurationCoeff( waveIdx, coeff );
f@0: mWaves[waveIdx]->getSelection().setParticleSpread( float( coeff ) );
f@0: };
f@0: break;
f@0:
f@0: case 7: { // filter
f@0: const double midiVal = m.getData_2(); // 0-127
f@0: const double minCutoff = mConfig.getMinFilterCutoffFreq();
f@0: const double maxCutoff = mConfig.getMaxFilterCutoffFreq();
f@0: const double cutoff = pow( maxCutoff / 200., midiVal / 127.0 ) * minCutoff;
f@0: mAudioEngine.setFilterCutoff( waveIdx, cutoff );
f@0: const float alpha = ci::lmap( midiVal, 0.0f, 127.0f, 0.f, 1.f );
f@0: mWaves[waveIdx]->setselectionAlpha( alpha );
f@0: };
f@0: break;
f@0:
f@0:
f@0:
f@0: }
f@0: }
f@0: }
f@0:
f@0: midiMessages.clear();
f@0: }
f@0:
f@0:
f@0:
f@0: CollidoscopeApp::~CollidoscopeApp()
f@0: {
f@0: for ( int chan = 0; chan < NUM_WAVES; chan++ ){
f@0: /* delete the array for wave messages from audio thread */
f@0: delete[] mRecordWaveMessageBuffers[chan];
f@0: }
f@0: }
f@0:
f@6:
f@0: CINDER_APP( CollidoscopeApp, RendererGl, [] ( App::Settings *settings) {
f@6:
f@6: const std::vector< string > args = settings->getCommandLineArgs();
f@6:
f@6: int width = 0;
f@6: int height = 0;
f@6:
f@6: try {
f@6: if( args.size() != 3 )
f@6: throw std::invalid_argument("");
f@6:
f@6: width = std::stoi( args[1] );
f@6: height = std::stoi( args[2] );
f@6:
f@6: }
f@6: catch( std::invalid_argument & e ){
f@6: console() << "Error: invalid arguments" << std::endl;
f@18: console() << "Usage: ./CollidoscopeApp window_width window_height" << std::endl;
f@18: console() << "For example: ./CollidoscopeApp 1024 768 " << std::endl;
f@6:
f@6: settings->setShouldQuit( true );
f@6: return;
f@6: }
f@6:
f@6: settings->setWindowSize( width, height );
f@6: settings->setMultiTouchEnabled( false );
f@6: settings->disableFrameRate();
f@0:
f@0: } )