/*******************************************************************************/ /* Copyright (C) 2013 Jonathan Moore Liles */ /* */ /* This program 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 2 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; see the file COPYING. If not,write to the Free Software */ /* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ /*******************************************************************************/ #include #include #include #include #include "debug.h" #include #include #include using namespace MIDI; #include #include #include #include #include #include #include #include /* usleep */ /* simple program to translate from MIDI<->OSC Signals using a fixed mapping */ #include #undef APP_NAME const char *APP_NAME = "non-midi-mapper"; #undef APP_TITLE const char *APP_TITLE = "Non-MIDI-Mapper"; #undef VERSION const char *VERSION = "1.1"; const int FILE_VERSION = 1; nsm_client_t *nsm; char *instance_name; static nframes_t buffers = 0; OSC::Endpoint *osc = 0; /* const double NSM_CHECK_INTERVAL = 0.25f; */ void handle_hello ( lo_message msg ) { int argc = lo_message_get_argc( msg ); lo_arg **argv = lo_message_get_argv( msg ); if ( argc >= 4 ) { const char *url = &argv[0]->s; const char *name = &argv[1]->s; const char *version = &argv[2]->s; const char *id = &argv[3]->s; MESSAGE( "Discovered NON peer %s (%s) @ %s with ID \"%s\"", name, version, url, id ); /* register peer */ osc->handle_hello( id, url ); } } void check_nsm ( void ) { nsm_check_nowait( nsm ); // Fl::repeat_timeout( NSM_CHECK_INTERVAL, &check_nsm, v ); } static int osc_non_hello ( const char *, const char *, lo_arg **, int , lo_message msg, void * ) { handle_hello( msg ); return 0; } static void say_hello ( void ) { if ( nsm_is_active( nsm ) ) { lo_message m = lo_message_new(); lo_message_add( m, "sssss", "/non/hello", osc->url(), APP_TITLE, VERSION, instance_name ); nsm_send_broadcast( nsm, m ); } } class Engine : public JACK::Client { public: jack_ringbuffer_t *input_ring_buf; jack_ringbuffer_t *output_ring_buf; JACK::Port *midi_input_port; JACK::Port *midi_output_port; Engine ( ) { input_ring_buf = jack_ringbuffer_create( 32 * 32 * sizeof( jack_midi_event_t )); jack_ringbuffer_reset( input_ring_buf ); output_ring_buf = jack_ringbuffer_create( 32 * 32 * sizeof( jack_midi_event_t )); jack_ringbuffer_reset( output_ring_buf ); midi_input_port = 0; midi_output_port = 0; } virtual ~Engine ( ) { deactivate(); } int process ( nframes_t nframes ) { ++buffers; /* process input */ { if ( !midi_input_port ) return 0; /* jack_position_t pos; */ /* jack_transport_query( this->jack_client(), &pos ); */ void *buf = midi_input_port->buffer( nframes ); jack_midi_event_t ev; jack_nframes_t count = jack_midi_get_event_count( buf ); /* if ( count > 0 ) */ /* { */ /* DMESSAGE( "Event count: %lu", count); */ /* } */ /* place MIDI events into ringbuffer for non-RT thread */ for ( uint i = 0; i < count; ++i ) { // MESSAGE( "Got midi input!" ); jack_midi_event_get( &ev, buf, i ); midievent e; /* e.timestamp( pos.frame + ev.time ); */ e.timestamp( ev.time ); e.status( ev.buffer[0] ); e.lsb( ev.buffer[1] ); if ( ev.size == 3 ) e.msb( ev.buffer[2] ); else e.msb( 0 ); /* /\* time is frame within cycle, convert to absolute tick *\/ */ /* e.timestamp( ph + (ev.time / transport.frames_per_tick) ); */ /* e.status( ev.buffer[0] ); */ /* e.lsb( ev.buffer[1] ); */ /* if ( ev.size == 3 ) */ /* e.msb( ev.buffer[2] ); */ if ( jack_ringbuffer_write( input_ring_buf, (char*)&e, sizeof( midievent ) ) != sizeof( midievent ) ) WARNING( "input buffer overrun" ); } } /* process output */ { void *buf = midi_output_port->buffer(nframes); jack_midi_clear_buffer( buf ); jack_midi_event_t ev; nframes_t frame = 0; while ( true ) { /* jack_ringbuffer_data_t vec[2]; */ /* jack_ringbuffer_get_read_vector( output_ring_buf, vec ); */ if ( jack_ringbuffer_peek( output_ring_buf, (char*)&ev, sizeof( jack_midi_event_t )) <= 0 ) break; unsigned char *buffer = jack_midi_event_reserve( buf, frame, ev.size ); if ( !buffer ) { WARNING("Output buffer overrun, will send later" ); break; } memcpy( buffer, &ev, ev.size ); jack_ringbuffer_read_advance( output_ring_buf, sizeof( jack_midi_event_t ) ); } } return 0; } void freewheel ( bool starting ) { } int xrun ( void ) { return 0; } int buffer_size ( nframes_t nframes ) { return 0; } void shutdown ( void ) { } void thread_init ( void ) { } }; Engine *engine; const unsigned int MAX_14BIT = 16383; const unsigned int MAX_7BIT = 127; static char get_lsb( int i ) { return i & 0x7F; } static char get_msb( int i ) { return ( i >> 7 ) & 0x7F; } static unsigned int get_14bit ( char msb, char lsb ) { return msb * (MAX_7BIT + 1) + lsb; } class signal_mapping { public: bool is_nrpn; bool is_nrpn14; bool is_toggle; midievent event; std::string signal_name; OSC::Signal *signal; nframes_t last_midi_tick; nframes_t last_feedback_tick; int learning_value_msb; int learning_value_lsb; bool is_learning ( ) { return NULL == signal; } signal_mapping ( ) { is_nrpn = false; is_nrpn14 = false; is_toggle =- false; signal = NULL; last_midi_tick = 0; last_feedback_tick = 0; learning_value_lsb = 0; learning_value_msb = 0; } ~signal_mapping ( ) { if ( signal ) delete signal; signal = NULL; } /* char *serialize ( void ) const */ /* { */ /* char *s; */ /* const char *opcode = 0; */ /* int v1 = 0; */ /* if ( is_nrpn ) */ /* { */ /* opcode = is_nrpn14 ? "NRPN14" : "NRPN"; */ /* v1 = get_14bit( event.msb(), event.lsb() ); */ /* } */ /* else */ /* switch ( event.opcode() ) */ /* { */ /* case MIDI::midievent::CONTROL_CHANGE: */ /* opcode = "CC"; */ /* v1 = event.lsb(); */ /* break; */ /* case MIDI::midievent::NOTE_ON: */ /* opcode = "NOTE_ON"; */ /* v1 = event.note(); */ /* break; */ /* default: */ /* // unsupported */ /* break; */ /* } */ /* asprintf( &s, "%s %d %d%s", opcode, event.channel(), v1, is_toggle ? " T" : "" ); */ /* return s; */ /* } */ void deserialize ( const char *s ) { int channel; char *opcode; int control; if ( 3 == sscanf( s, "%ms %d %d", &opcode, &channel, &control ) ) { event.channel( channel ); event.opcode( MIDI::midievent::CONTROL_CHANGE ); is_nrpn = false; if ( !strcmp( opcode, "NRPN" ) ) { is_nrpn = true; event.lsb( get_lsb( control )); event.msb( get_msb( control )); } else if ( !strcmp( opcode, "CC" ) ) { event.lsb( control ); } free(opcode); } else { DMESSAGE( "Failed to parse midi event descriptor: %s", s ); } } }; int signal_handler ( float value, void *user_data ) { signal_mapping *m = (signal_mapping*)user_data; /* DMESSAGE( "Received value: %f", value ); */ m->last_feedback_tick = buffers; /* magic number to give a release time to prevent thrashing. */ /* if ( ! ( m->last_feedback_tick > m->last_midi_tick + 4 )) */ /* return 0; */ if ( m->is_nrpn ) { jack_midi_event_t jev[4]; { midievent e; e.opcode( MIDI::midievent::CONTROL_CHANGE ); e.channel( m->event.channel() ); e.lsb( 99 ); e.msb( m->event.msb() ); jev[0].size = e.size(); e.raw( (byte_t*)&jev[0], e.size() ); // e.pretty_print(); } { midievent e; e.opcode( MIDI::midievent::CONTROL_CHANGE ); e.channel( m->event.channel() ); e.lsb( 98 ); e.msb( m->event.lsb() ); jev[1].size = e.size(); e.raw( (byte_t*)&jev[1], e.size() ); // e.pretty_print(); } { midievent e; e.opcode( MIDI::midievent::CONTROL_CHANGE ); e.channel( m->event.channel() ); e.lsb( 6 ); e.msb( (int)(value * (float)MAX_14BIT) >> 7 ); jev[2].size = e.size(); e.raw( (byte_t*)&jev[2], e.size() ); // e.pretty_print(); } { midievent e; e.opcode( MIDI::midievent::CONTROL_CHANGE ); e.channel( m->event.channel() ); e.lsb( 38 ); e.msb( (int)(value * (float)MAX_14BIT) & 0x7F ); jev[3].size = e.size(); e.raw( (byte_t*)&jev[3], e.size() ); // e.pretty_print(); } for ( int i = 0; i < 4; i++ ) { if ( jack_ringbuffer_write( engine->output_ring_buf, (char*)&jev[i], sizeof( jack_midi_event_t ) ) != sizeof( jack_midi_event_t ) ) WARNING( "output buffer overrun" ); } } else { jack_midi_event_t ev; m->event.msb( value * (float)MAX_7BIT ); ev.size = m->event.size(); m->event.raw( (byte_t*)&ev, m->event.size() ); // m->event.pretty_print(); if ( jack_ringbuffer_write( engine->output_ring_buf, (char*)&ev, sizeof( jack_midi_event_t ) ) != sizeof( jack_midi_event_t ) ) WARNING( "output buffer overrun" ); } return 0; } std::map sig_map; std::map sig_map_ordered; bool save_settings ( void ) { FILE *fp = fopen( "signals", "w" ); if ( !fp ) return false; fprintf( fp, "# Non-MIDI-Mapper version %i\n", FILE_VERSION ); for ( std::map::const_iterator i = sig_map_ordered.begin(); i != sig_map_ordered.end(); i++ ) { signal_mapping &m = sig_map[i->second.c_str()]; /* char *midi_event = m.serialize(); */ /* FIXME: instead of T and NRPN14, use 1 7 and 14 (value significant bits). Also, use syntax like so: [NRPN 0 0] 1bit :: /foo/bar */ fprintf( fp, "%s\t%s\t%s\n", i->second.c_str(), // midi_event, m.is_toggle ? "1-BIT" : m.is_nrpn && m.is_nrpn14 ? "14-BIT" : "7-BIT", m.signal_name.c_str() ); /* free(midi_event); */ } fclose(fp); return true; } static int max_signal = 0; bool load_settings ( void ) { FILE *fp = fopen( "signals", "r" ); if ( !fp ) return false; sig_map.clear(); sig_map_ordered.clear(); char *signal_name; char *midi_event; char *flags = NULL; max_signal = 0; int version = 0; if ( 1 == fscanf( fp, "# Non-MIDI-Mapper version %i\n", &version ) ) { } else { version = 0; rewind(fp); } DMESSAGE( "Detected file version %i", version); while ( ( 1 == version && 3 == fscanf( fp, "%m[^\t]\t%m[^\t]\t%m[^\n]\n", &midi_event, &flags, &signal_name ) ) || ( 0 == version && 2 == fscanf( fp, "[%m[^]]] %m[^\n]\n", &midi_event, &signal_name ) ) ) { DMESSAGE( "Read mapping: %s, %s (%s)", midi_event, signal_name, flags ); if ( sig_map.find( midi_event ) == sig_map.end() ) { ++max_signal; signal_mapping m; m.deserialize( midi_event ); if ( flags ) { m.is_toggle = !strcmp( "1-BIT", flags ); m.is_nrpn14 = !strcmp( "14-BIT", flags ); } sig_map[midi_event] = m; sig_map[midi_event].signal_name = signal_name; sig_map[midi_event].signal = osc->add_signal( signal_name, OSC::Signal::Output, 0, 1, 0, signal_handler, &sig_map[midi_event] ); sig_map_ordered[max_signal] = midi_event; } free(signal_name); free(midi_event); /* if ( sig_map.find( s ) == sig_map.end() ) */ /* { */ /* int channel, control; */ /* if ( 2 == sscanf( s, "/midi/%d/CC/%d", &channel, &control ) ) */ /* { */ /* signal_mapping m; */ /* m.event.channel( channel ); */ /* m.event.opcode( MIDI::midievent::CONTROL_CHANGE ); */ /* m.event.lsb( control ); */ /* MESSAGE( "creating signal %s", s ); */ /* sig_map[s] = m; */ /* sig_map[s].signal = osc->add_signal( s, OSC::Signal::Output, 0, 1, 0, signal_handler, &sig_map[s] ); */ /* } */ /* if ( 2 == sscanf( s, "/midi/%d/NRPN/%d", &channel, &control ) ) */ /* { */ /* signal_mapping m; */ /* m.event.channel( channel ); */ /* m.event.opcode( MIDI::midievent::CONTROL_CHANGE ); */ /* m.event.lsb( get_lsb( control ) ); */ /* m.event.msb( get_msb( control ) ); */ /* m.is_nrpn = true; */ /* MESSAGE( "creating signal %s", s ); */ /* sig_map[s] = m; */ /* sig_map[s].signal = osc->add_signal( s, OSC::Signal::Output, 0, 1, 0, signal_handler, &sig_map[s] ); */ /* } */ /* else */ /* WARNING( "Could not decode signal spec \"%s\"", s ); */ /* } */ /* free(s); */ } return true; } static bool create_engine ( void ) { if ( engine ) { delete engine->midi_input_port; delete engine->midi_output_port; delete engine; } DMESSAGE( "Creating JACK engine" ); engine = new Engine(); if ( ! engine->init( instance_name ) ) { WARNING( "Failed to create JACK client" ); return false; } engine->midi_input_port = new JACK::Port( engine, NULL, "midi-in", JACK::Port::Input, JACK::Port::MIDI ); engine->midi_output_port = new JACK::Port( engine, NULL, "midi-out", JACK::Port::Output, JACK::Port::MIDI ); if ( !engine->midi_input_port->activate() ) { WARNING( "Failed to activate JACK port" ); return false; } if ( !engine->midi_output_port->activate() ) { WARNING( "Failed to activate JACK port" ); return false; } return true; } static int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg, void *userdata ) { if ( instance_name ) free( instance_name ); instance_name = strdup( client_id ); osc->name( client_id ); mkdir( name, 0777 ); chdir( name ); if ( ! create_engine() ) { return ERR_GENERAL; } load_settings(); say_hello(); return ERR_OK; } static int command_save ( char **out_msg, void *userdata ) { if ( save_settings() ) { nsm_send_is_clean(nsm); return ERR_OK; } else return ERR_GENERAL; } static int command_broadcast ( const char *path, lo_message msg, void *userdata ) { lo_message_get_argc( msg ); // lo_arg **argv = lo_message_get_argv( msg ); if ( !strcmp( path, "/non/hello" ) ) { handle_hello( msg ); return 0; } else return -1; } enum nrpn_awaiting { CONTROL_MSB, CONTROL_LSB, VALUE_MSB, VALUE_LSB, COMPLETE }; struct nrpn_state { byte_t control_msb; byte_t control_lsb; byte_t value_msb; byte_t value_lsb; bool value_lsb_exists; bool complete; nrpn_awaiting awaiting; }; static struct nrpn_state * decode_nrpn ( nrpn_state *state, midievent e, bool *emit_one ) { /* use a bit of state machine to allow people to misuse value LSB and value MSB CCs as regular CCs */ nrpn_state *n = &state[e.channel()]; *emit_one = false; switch ( e.lsb() ) { case 6: if ( VALUE_MSB == n->awaiting ) { n->value_msb = e.msb(); n->value_lsb = 0 == e.msb() ? 0 : MAX_7BIT; n->complete = true; n->awaiting = VALUE_LSB; *emit_one = true; return n; } break; case 38: if ( VALUE_LSB == n->awaiting ) { n->value_lsb_exists = true; n->value_lsb = e.msb(); *emit_one = true; n->complete = true; n->awaiting = COMPLETE; return n; } break; case 99: n->complete = false; n->value_lsb_exists = false; n->control_msb = e.msb(); n->control_lsb = 0; n->awaiting = CONTROL_LSB; n->value_msb = 0; n->value_lsb = 0; return n; case 98: n->awaiting = VALUE_MSB; n->complete = false; n->control_lsb = e.msb(); return n; } return NULL; } static volatile int got_sigterm = 0; void sigterm_handler ( int ) { got_sigterm = 1; } void emit_signal_for_event ( const char *midi_event, midievent &e, struct nrpn_state *st ) { bool is_nrpn = st != NULL; if ( sig_map.find( midi_event ) == sig_map.end() ) { /* first time seeing this control. */ signal_mapping m; m.event.lsb( e.lsb() ); m.event.msb( e.msb() ); m.event.opcode( e.opcode() ); m.event.channel( e.channel() ); m.is_nrpn = is_nrpn; if ( is_nrpn ) { m.event.lsb( st->control_lsb ); m.event.msb( st->control_msb ); if ( st->value_lsb_exists ) m.learning_value_lsb = st->value_lsb; m.learning_value_msb = st->value_msb; } else m.learning_value_msb = e.msb(); /* wait until we see it again to remember it */ DMESSAGE("First time seeing control %s, will map on next event instance.", midi_event ); sig_map[midi_event] = m; return; } /* if we got this far, it means we are on the second event for a the event type being learned */ signal_mapping *m = &sig_map[midi_event]; if ( m->is_learning() ) { /* FIXME: need to gather NRPN LSB value that (maybe) arrives between first and second event for this NRPN controller. 14-bit flag is set if there was an LSB, otherwise, 7 or 1 bit mode is applied. In normal operation, 14-bit NRPN controls should await the LSB before sending signal, 7 and 1 bit can send on MSB. */ DMESSAGE( "Going to learn event %s now", midi_event ); char *s; asprintf( &s, "/control/%i", ++max_signal ); DMESSAGE( "Creating signal %s for event %s.", s, midi_event ); bool is_toggle = false; bool is_14bit_nrpn = false; if ( !is_nrpn ) { DMESSAGE( "Learning value msb: %u, msb: %u", m->learning_value_msb, e.msb() ); is_toggle = ( m->learning_value_msb == 0 && e.msb() == MAX_7BIT ) || ( m->learning_value_msb == MAX_7BIT && e.msb() == 0 ); } else { is_14bit_nrpn = m->learning_value_lsb != -1; unsigned int val1 = get_14bit( m->learning_value_msb, m->learning_value_lsb ); unsigned int val2 = get_14bit( st->value_msb, st->value_lsb ); is_toggle = !is_14bit_nrpn ? ( m->learning_value_msb == 0 && st->value_msb == MAX_7BIT ) || ( m->learning_value_msb == MAX_7BIT && st->value_msb == 0 ) : ( val1 == 0 && val2 == MAX_14BIT ) || ( val1 == MAX_14BIT && val2 == 0); } DMESSAGE( "is toggle %i", is_toggle ); m->is_toggle = is_toggle; m->is_nrpn14 = is_14bit_nrpn; m->learning_value_msb = m->learning_value_lsb = 0; m->signal_name = s; m->signal = osc->add_signal( s, OSC::Signal::Output, 0, 1, 0, signal_handler, m ); sig_map_ordered[max_signal] = midi_event; nsm_send_is_dirty( nsm ); free(s); } float val = 0; if ( is_nrpn ) { unsigned int fbv = get_14bit( st->value_msb, st->value_lsb ); if ( m->is_nrpn14 ) val = fbv / (float)MAX_14BIT; else /* also covers toggles */ val = st->value_msb / (float)MAX_7BIT; } else if ( e.opcode() == MIDI::midievent::CONTROL_CHANGE ) val = e.msb() / (float)MAX_7BIT; else if ( e.opcode() == MIDI::midievent::PITCH_WHEEL ) val = e.pitch() / (float)MAX_14BIT; /* DMESSAGE( "Val: %f, sigval %f", val, m->signal->value() ); */ /* wait for values to sync for continuous controls (faders and knobs) before emitting signal. For toggles, just send it immediately. */ if ( /* magic number to give a release time to prevent thrashing. */ m->last_feedback_tick > m->last_midi_tick + 100 && !m->is_toggle ) { float percent_off = fabs( val - m->signal->value() ) * 100.0f; if ( percent_off > 5 ) { DMESSAGE( "Wating for controls to sync. %s: %f percent off target (must be < 5%) [ M:%f S:%f ] ", m->signal_name.c_str(), percent_off, val, m->signal->value() ); return; } } m->last_midi_tick = buffers; /* DMESSAGE( "Sent value: %f", val ); */ m->signal->value( val ); } void handle_control_change ( nrpn_state *nrpn_state, midievent &e ) { bool emit_one = false; char midi_event[51]; struct nrpn_state *st = decode_nrpn( nrpn_state, e, &emit_one ); if ( st != NULL && ( VALUE_LSB == st->awaiting || COMPLETE == st->awaiting ) ) { snprintf( midi_event, 50, "NRPN %d %d", e.channel(), get_14bit( st->control_msb, st->control_lsb )); if ( VALUE_LSB == st->awaiting ) { if ( sig_map.find(midi_event) != sig_map.end() ) { signal_mapping &m = sig_map[midi_event]; if ( m.is_nrpn14 ) { /* we know there's an LSB coming, so hold off on emitting until we get it */ return; } } } emit_signal_for_event(midi_event, e, st); } if ( st == NULL ) { if ( e.opcode() == MIDI::midievent::CONTROL_CHANGE ) { snprintf( midi_event, 50, "CC %d %d", e.channel(), e.lsb() ); emit_signal_for_event( midi_event, e, NULL); } } } /* else if ( e.opcode() == MIDI::midievent::PITCH_WHEEL ) */ /* asprintf( &s, "/midi/%i/PB", e.channel() ); */ int main ( int argc, char **argv ) { nrpn_state nrpn_state[16]; memset( &nrpn_state, 0, sizeof(struct nrpn_state) * 16 ); signal( SIGTERM, sigterm_handler ); signal( SIGHUP, sigterm_handler ); signal( SIGINT, sigterm_handler ); nsm = nsm_new(); // set_nsm_callbacks( nsm ); nsm_set_open_callback( nsm, command_open, 0 ); nsm_set_broadcast_callback( nsm, command_broadcast, 0 ); nsm_set_save_callback( nsm, command_save, 0 ); char *nsm_url = getenv( "NSM_URL" ); if ( nsm_url ) { if ( ! nsm_init( nsm, nsm_url ) ) { nsm_send_announce( nsm, APP_TITLE, ":dirty:", basename( argv[0] ) ); /* poll so we can keep OSC handlers running in the GUI thread and avoid extra sync */ // Fl::add_timeout( NSM_CHECK_INTERVAL, check_nsm, NULL ); } } osc = new OSC::Endpoint(); osc->init( LO_UDP, NULL ); osc->add_method( "/non/hello", "ssss", osc_non_hello, osc, "" ); MESSAGE( "OSC URL = %s", osc->url() ); /* now we just read from the MIDI ringbuffer and output OSC */ DMESSAGE( "waiting for events" ); /* jack_midi_event_t ev; */ while ( ! got_sigterm ) { osc->wait(20); check_nsm(); if ( ! engine ) continue; midievent e; while ( jack_ringbuffer_read( engine->input_ring_buf, (char *)&e, sizeof( midievent ) ) ) { /* midievent e; */ /* e.timestamp( ev.time ); */ /* e.status( ev.buffer[0] ); */ /* e.lsb( ev.buffer[1] ); */ /* if ( ev.size == 3 ) */ /* e.msb( ev.buffer[2] ); */ /* else */ /* e.msb( 0 ); */ /* DMESSAGE( "[%lu] %u %u %u", e.timestamp(), e.status(), e.lsb(), e.msb() ); */ switch ( e.opcode() ) { case MIDI::midievent::CONTROL_CHANGE: case MIDI::midievent::PITCH_WHEEL: { handle_control_change(nrpn_state,e); break; } default: break; } // e.pretty_print(); } // usleep( 500 ); } delete engine; return 0; }