diff --git a/Makefile b/Makefile index c1b22c1..888c063 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################### -SUBDIRS=nonlib FL timeline mixer +SUBDIRS=nonlib FL timeline mixer session all: @ for dir in $(SUBDIRS); do $(MAKE) -s -C $$dir; done diff --git a/mixer/src/Chain.C b/mixer/src/Chain.C index 49db7da..a57a05f 100644 --- a/mixer/src/Chain.C +++ b/mixer/src/Chain.C @@ -422,7 +422,7 @@ Chain::can_configure_outputs ( Module *m, int n ) const unsigned int Chain::maximum_name_length ( void ) { - return JACK::Client::maximum_name_length() - ( strlen( APP_NAME ) + 1 + ( instance_name ? strlen( instance_name ) + 1 : 0 ) ); + return JACK::Client::maximum_name_length() - ( strlen( instance_name ) + 1 ); } /* rename chain... we have to let our modules know our name has @@ -432,7 +432,7 @@ void Chain::name ( const char *name ) { char ename[512]; - snprintf( ename, sizeof(ename), "%s%s%s/%s", APP_NAME, instance_name ? "." : "", instance_name ? instance_name : "", name ); + snprintf( ename, sizeof(ename), "%s/%s", instance_name, name ); if ( ! _engine ) { diff --git a/mixer/src/Controller_Module.C b/mixer/src/Controller_Module.C index 09d08c7..f245e6d 100644 --- a/mixer/src/Controller_Module.C +++ b/mixer/src/Controller_Module.C @@ -71,14 +71,14 @@ Controller_Module::Controller_Module ( bool is_default ) : Module( is_default, 5 end(); - Fl::add_timeout( CONTROL_UPDATE_FREQ, update_cb, this ); +// Fl::add_timeout( CONTROL_UPDATE_FREQ, update_cb, this ); log_create(); } Controller_Module::~Controller_Module ( ) { - Fl::remove_timeout( update_cb, this ); +// Fl::remove_timeout( update_cb, this ); log_destroy(); diff --git a/mixer/src/Meter_Module.C b/mixer/src/Meter_Module.C index bace39d..717415c 100644 --- a/mixer/src/Meter_Module.C +++ b/mixer/src/Meter_Module.C @@ -32,7 +32,7 @@ -const float METER_UPDATE_FREQ = 0.1f; +const float METER_UPDATE_FREQ = 0.2f; diff --git a/mixer/src/Mixer.C b/mixer/src/Mixer.C index ed17c05..7833177 100644 --- a/mixer/src/Mixer.C +++ b/mixer/src/Mixer.C @@ -28,31 +28,39 @@ #include #include #include -#include #include "New_Project_Dialog.H" #include "Engine/Engine.H" #include "FL/Fl_Flowpack.H" #include "Project.H" #include "FL/Fl_Menu_Settings.H" #include "About_Dialog.H" +#include #include "file.h" #include #include "debug.h" +#include +#include #include "FL/color_scheme.H" #include "OSC/Endpoint.H" #include +#include "FL/Fl_Blinker.H" const double STATUS_UPDATE_FREQ = 0.2f; -const double OSC_INTERVAL = 0.1f; +const double OSC_INTERVAL = 0.2f; extern char *user_config_dir; +extern char *instance_name; #include "debug.h" +#include "NSM.H" + +extern NSM_Client *nsm; + /* static void update_cb( void *v ) { */ /* Fl::repeat_timeout( STATUS_UPDATE_FREQ, update_cb, v ); */ @@ -65,69 +73,19 @@ extern char *user_config_dir; /* OSC Message Handlers */ /************************/ +#undef OSC_REPLY_OK +#undef OSC_REPLY_ERR +#undef OSC_REPLY -OSC_HANDLER( quit ) -{ - OSC_DMSG(); - - ((Mixer*)user_data)->command_quit(); - - OSC_REPLY_OK(); - - return 0; -} - -OSC_HANDLER( save ) -{ - OSC_DMSG(); - - if ( ((Mixer*)user_data)->command_save() ) - OSC_REPLY_OK(); - else - OSC_REPLY_ERR(); - - return 0; -} - -OSC_HANDLER( load ) -{ - OSC_DMSG(); - - const char *project_path = &argv[0]->s; - const char *project_display_name = &argv[1]->s; - - if ( ((Mixer*)user_data)->command_load( project_path, project_display_name ) ) - OSC_REPLY_OK(); - else - OSC_REPLY_ERR(); - - return 0; -} - -OSC_HANDLER( new ) -{ - OSC_DMSG(); - - const char *project_path = &argv[0]->s; - const char *project_display_name = &argv[1]->s; - - if ( ((Mixer*)user_data)->command_new( project_path, project_display_name ) ) - OSC_REPLY_OK(); - else - OSC_REPLY_ERR(); - - return 0; -} - -// OSC_HANDLER( root ) -// { -// } +#define OSC_REPLY_OK() ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, 0, "OK" ) +#define OSC_REPLY( value ) ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, value ) +#define OSC_REPLY_ERR(errcode, value) ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path,errcode, value ) OSC_HANDLER( add_strip ) { OSC_DMSG(); - ((Mixer*)user_data)->command_add_strip(); + ((Mixer*)(OSC_ENDPOINT())->owner)->command_add_strip(); OSC_REPLY_OK(); @@ -138,20 +96,45 @@ OSC_HANDLER( finger ) { OSC_DMSG(); - lo_address src = lo_message_get_source( msg ); + OSC::Endpoint *ep = ((OSC::Endpoint*)user_data); + + lo_address reply = lo_address_new_from_url( &argv[0]->s ); + + ep->send( reply, + "/reply", + path, + ep->url(), + APP_NAME, + VERSION, + instance_name ); - const char *s = "APP_TITLE\n"; - -/* if ( 1 >= lo_send_from( src, ((Mixer*)user_data)->osc_endpoint, LO_TT_IMMEDIATE, "/finger-reply", "s", s ) ) */ -/* { */ -/* DMESSAGE( "Failed to send reply" ); */ -/* } */ + lo_address_free( reply ); return 0; } + +static +Fl_Menu_Item * +find_item( Fl_Menu_ *menu, const char *path ) + { + return const_cast(menu->find_item( path )); + } + +void +Mixer::sm_active ( bool b ) +{ + sm_blinker->value( b ); + sm_blinker->tooltip( nsm->session_manager_name() ); + + if ( b ) + { + find_item( menubar, "&Project/&Open" )->deactivate(); + find_item( menubar, "&Project/&New" )->deactivate(); + } +} void Mixer::cb_menu(Fl_Widget* o) { Fl_Menu_Bar *menu = (Fl_Menu_Bar*)o; @@ -347,6 +330,19 @@ Mixer::Mixer ( int X, int Y, int W, int H, const char *L ) : o->align( FL_ALIGN_INSIDE | FL_ALIGN_CENTER ); o->labeltype( FL_SHADOW_LABEL ); } + { sm_blinker = new Fl_Blinker( ( X + W) - 52, Y + 4, 50, 15, "SM"); + sm_blinker->box(FL_ROUNDED_BOX); + sm_blinker->down_box(FL_ROUNDED_BOX); + sm_blinker->color((Fl_Color)75); + sm_blinker->selection_color((Fl_Color)86); + sm_blinker->labeltype(FL_NORMAL_LABEL); + sm_blinker->labelfont(2); + sm_blinker->labelsize(14); + sm_blinker->labelcolor(FL_DARK3); + sm_blinker->align(Fl_Align(FL_ALIGN_CENTER)); + sm_blinker->when(FL_WHEN_RELEASE); + sm_blinker->deactivate(); + } // Fl_Blinker* sm_blinker { Fl_Scroll *o = scroll = new Fl_Scroll( X, Y + 24, W, H - 24 ); o->box( FL_NO_BOX ); // o->type( Fl_Scroll::HORIZONTAL_ALWAYS ); @@ -375,22 +371,20 @@ Mixer::Mixer ( int X, int Y, int W, int H, const char *L ) : load_options(); } -void +int Mixer::init_osc ( const char *osc_port ) { - osc_endpoint = new OSC::Endpoint(osc_port); + osc_endpoint = new OSC::Endpoint(); - osc_endpoint->url(); + if ( int r = osc_endpoint->init( osc_port ) ) + return r; - // if ( 1 >= lo_send_from( src, ((Mixer*)user_data)->osc_endpoint, LO_TT_IMMEDIATE, "/finger-reply", "s", s ) ) + osc_endpoint->owner = this; + + printf( "OSC=%s\n", osc_endpoint->url() ); - osc_endpoint->add_method( "/nsm/quit", "", OSC_NAME( quit ), this, "" ); - osc_endpoint->add_method( "/nsm/load", "ss", OSC_NAME( load ), this, "path,display_name" ); - osc_endpoint->add_method( "/nsm/save", "", OSC_NAME( save ), this, "" ); - osc_endpoint->add_method( "/nsm/new", "ss", OSC_NAME( new ), this, "path,display_name" ); -// osc_endpoint->add_method( "/nsm/", "", OSC_NAME( root ), this ); - osc_endpoint->add_method( "/finger", "", OSC_NAME( finger ), this, "" ); - osc_endpoint->add_method( "/mixer/add_strip", "", OSC_NAME( add_strip ), this, "" ); + osc_endpoint->add_method( "/non/finger", "s", OSC_NAME( finger ), osc_endpoint, "" ); + osc_endpoint->add_method( "/non/mixer/add_strip", "", OSC_NAME( add_strip ), osc_endpoint, "" ); // osc_endpoint->start(); @@ -680,6 +674,8 @@ Mixer::command_save ( void ) bool Mixer::command_load ( const char *path, const char *display_name ) { + mixer->hide(); + if ( int err = Project::open( path ) ) { // fl_alert( "Error opening project specified on commandline: %s", Project::errstr( err ) ); @@ -691,6 +687,8 @@ Mixer::command_load ( const char *path, const char *display_name ) update_menu(); + mixer->show(); + return true; } diff --git a/mixer/src/Mixer.H b/mixer/src/Mixer.H index 6e2710f..c967799 100644 --- a/mixer/src/Mixer.H +++ b/mixer/src/Mixer.H @@ -26,6 +26,7 @@ #include #include "Mixer_Strip.H" +class Fl_Blinker; class Fl_Flowpack; class Fl_Menu_Bar; @@ -37,9 +38,10 @@ class Mixer : public Fl_Group public: OSC::Endpoint *osc_endpoint; + Fl_Blinker *sm_blinker; private: - + int _rows; Fl_Color system_colors[3]; @@ -93,9 +95,11 @@ public: static void check_osc ( void * v ); - void announce ( const char *nash_url ); + void announce ( const char *nash_url, const char *process_name ); - void init_osc ( const char* osc_port ); + int init_osc ( const char* osc_port ); + + void sm_active ( bool b ); public: diff --git a/mixer/src/Module.C b/mixer/src/Module.C index 1f30ad0..b8d9cb0 100644 --- a/mixer/src/Module.C +++ b/mixer/src/Module.C @@ -213,11 +213,9 @@ Module::Port::generate_osc_path () int n = module()->chain()->get_module_instance_number( module() ); if ( n > 0 ) - asprintf( &path, "/mixer/strip/%s/control/%s.%i/%s", module()->chain()->name(), p->module()->label(), n, p->name() ); + asprintf( &path, "/non/mixer/strip/%s/control/%s.%i/%s", module()->chain()->name(), p->module()->label(), n, p->name() ); else - asprintf( &path, "/mixer/strip/%s/control/%s/%s", module()->chain()->name(), p->module()->label(), p->name() ); - -// asprintf( &path, "/mixer/strip/control/%s/%s", p->module()->label(), p->name() ); + asprintf( &path, "/non/mixer/strip/%s/control/%s/%s", module()->chain()->name(), p->module()->label(), p->name() ); // Hack to keep spaces out of OSC URL... Probably need to handle other special characters similarly. for ( int i = strlen( path ); i--; ) @@ -254,6 +252,30 @@ Module::Port::change_osc_path ( char *path ) mixer->osc_endpoint->add_method( _osc_path, "f", &Module::Port::osc_control_change_cv, this, "value" ); mixer->osc_endpoint->add_method( _osc_path_unscaled, "f", &Module::Port::osc_control_change_exact, this, "value" ); + + if ( hints.ranged ) + { + mixer->osc_endpoint->set_parameter_limits( _osc_path_unscaled, "f", 0, + hints.minimum, + hints.maximum, + hints.default_value ); + + } + + float scaled_default = 0.5f; + + if ( hints.ranged ) + { + float scale = hints.maximum - hints.minimum; +// float offset = hints.minimum; + + scaled_default = ( hints.default_value / scale ); + } + + mixer->osc_endpoint->set_parameter_limits( _osc_path, "f", 0, + 0.0f, + 1.0f, + scaled_default ); } } diff --git a/mixer/src/NSM.C b/mixer/src/NSM.C new file mode 100644 index 0000000..081485d --- /dev/null +++ b/mixer/src/NSM.C @@ -0,0 +1,99 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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 "const.h" +#include "debug.h" +#include "Mixer.H" +#include "NSM.H" +#include "Project.H" + +extern char *instance_name; +extern Mixer *mixer; + +extern NSM_Client *nsm; + +NSM_Client::NSM_Client ( ) +{ +} + +int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ); +int command_save ( char **out_msg ); + + +int +NSM_Client::command_save ( char **out_msg ) +{ + Fl::lock(); + + int r = ERR_OK; + + if ( ! mixer->command_save() ) + { + *out_msg = strdup( "Failed to save for unknown reason"); + return r = ERR_GENERAL; + } + + Fl::unlock(); + + return r; +} + +int +NSM_Client::command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ) +{ + Fl::lock(); + + if ( instance_name ) + free( instance_name ); + + instance_name = strdup( client_id ); + + int r = ERR_OK; + + if ( Project::validate( name ) ) + { + if ( ! mixer->command_load( name, display_name ) ) + { + *out_msg = strdup( "Failed to load for unknown reason" ); + r = ERR_GENERAL; + } + } + else + { + if ( ! mixer->command_new( name, display_name ) ) + { + *out_msg = strdup( "Failed to load for unknown reason" ); + r = ERR_GENERAL; + } + } + + Fl::unlock(); + + return r; +} + +void +NSM_Client::command_active ( bool active ) +{ + Fl::lock(); + + mixer->sm_active( active ); + + Fl::unlock(); +} diff --git a/mixer/src/NSM.H b/mixer/src/NSM.H new file mode 100644 index 0000000..bb62ae8 --- /dev/null +++ b/mixer/src/NSM.H @@ -0,0 +1,38 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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. */ +/*******************************************************************************/ + +#pragma once + +#include "NSM/Client.H" + +class NSM_Client : public NSM::Client +{ + +public: + + NSM_Client ( ); + ~NSM_Client ( ) { }; + +protected: + + int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ); + int command_save ( char **out_msg ); + + void command_active ( bool active ); +}; diff --git a/mixer/src/Project.C b/mixer/src/Project.C index f82d16d..195c90d 100644 --- a/mixer/src/Project.C +++ b/mixer/src/Project.C @@ -304,7 +304,7 @@ Project::create ( const char *name, const char *template_name ) if ( mkdir( name, 0777 ) ) { - WARNING( "Cannot create project directory" ); + WARNING( "Cannot create project directory: %s", name ); return false; } diff --git a/mixer/src/main.C b/mixer/src/main.C index d3b01c2..1de232e 100644 --- a/mixer/src/main.C +++ b/mixer/src/main.C @@ -52,14 +52,21 @@ #include "Chain.H" #include "Mixer_Strip.H" +#include "NSM.H" + +#include /* TODO: put these in a header */ #define USER_CONFIG_DIR ".non-mixer/" + +const double OSC_INTERVAL = 0.1f; + char *user_config_dir; Mixer *mixer; +NSM_Client *nsm; -const char *instance_name; +char *instance_name; #include @@ -83,6 +90,32 @@ static void cb_main ( Fl_Double_Window *o, void *) mixer->command_quit(); } +void +check_osc ( void * v ) +{ + nsm->check(); + Fl::repeat_timeout( OSC_INTERVAL, check_osc, v ); +} + +static int got_sigterm = 0; + +void +sigterm_handler ( int ) +{ + got_sigterm = 1; + Fl::awake(); +} + +void +check_sigterm ( void * ) +{ + if ( got_sigterm ) + { + MESSAGE( "Got SIGTERM, quitting..." ); + mixer->command_quit(); + } +} + int main ( int argc, char **argv ) { @@ -93,6 +126,8 @@ main ( int argc, char **argv ) ensure_dirs(); + signal( SIGTERM, sigterm_handler ); + Fl_Tooltip::color( FL_BLACK ); Fl_Tooltip::textcolor( FL_YELLOW ); Fl_Tooltip::size( 14 ); @@ -119,6 +154,8 @@ main ( int argc, char **argv ) Fl::get_system_colors(); Fl::scheme( "plastic" ); + Fl::lock(); + Plugin_Module::spawn_discover_thread(); Fl_Double_Window *main_window; @@ -142,6 +179,10 @@ main ( int argc, char **argv ) const char *osc_port = NULL; + nsm = new NSM_Client; + + instance_name = strdup( APP_NAME ); + { int r = argc - 1; int i = 1; @@ -152,7 +193,7 @@ main ( int argc, char **argv ) if ( r > 1 ) { MESSAGE( "Using instance name \"%s\"", argv[i+1] ); - instance_name = argv[i+1]; + instance_name = strdup( argv[i+1] ); --r; ++i; } @@ -185,17 +226,33 @@ main ( int argc, char **argv ) mixer->init_osc( osc_port ); - if ( r >= 1 ) + char *nsm_url = getenv( "NSM_URL" ); + + if ( nsm_url ) { - MESSAGE( "Loading \"%s\"", argv[i] ); - - if ( ! mixer->command_load( argv[i] ) ) + if ( ! nsm->init() ) { - fl_alert( "Error opening project specified on commandline" ); + nsm->announce( nsm_url, APP_NAME, ":switch:dirty:", argv[0] ); + } + } + else + { + if ( r >= 1 ) + { + MESSAGE( "Loading \"%s\"", argv[i] ); + + if ( ! mixer->command_load( argv[i] ) ) + { + fl_alert( "Error opening project specified on commandline" ); + } } } - } + + // poll so we can keep OSC handlers running in the GUI thread and avoid extra sync + Fl::add_timeout( OSC_INTERVAL, check_osc, NULL ); + + Fl::add_check( check_sigterm ); Fl::run(); diff --git a/nonlib/JACK/Client.C b/nonlib/JACK/Client.C index 7abfc5d..7362998 100644 --- a/nonlib/JACK/Client.C +++ b/nonlib/JACK/Client.C @@ -44,7 +44,7 @@ namespace JACK jack_client_close( _client ); } - /** Tell JACK to calling process callback. This MUST be called in + /** Tell JACK to stop calling process callback. This MUST be called in * an inheriting class' destructor */ void Client::deactivate ( ) @@ -201,6 +201,15 @@ namespace JACK } } + void + Client::close ( void ) + { + jack_deactivate( _client ); + jack_client_close( _client ); + + _client = NULL; + } + const char * Client::name ( const char *s ) { @@ -222,4 +231,28 @@ namespace JACK return s; } + + void + Client::transport_stop ( ) + { + jack_transport_stop( _client ); + } + + void + Client::transport_start ( ) + { + jack_transport_start( _client ); + } + + void + Client::transport_locate ( nframes_t frame ) + { + jack_transport_locate( _client, frame ); + } + + jack_transport_state_t + Client::transport_query ( jack_position_t *pos ) + { + return jack_transport_query( _client, pos ); + } } diff --git a/nonlib/JACK/Client.H b/nonlib/JACK/Client.H index 06edb64..697156e 100644 --- a/nonlib/JACK/Client.H +++ b/nonlib/JACK/Client.H @@ -89,6 +89,7 @@ namespace JACK const char * init ( const char *client_name, unsigned int opts = 0 ); const char * name ( const char * ); + void close ( void ); nframes_t nframes ( void ) const { return jack_get_buffer_size( _client ); } float frame_rate ( void ) const { return jack_get_sample_rate( _client ); } static nframes_t sample_rate ( void ) { return _sample_rate; } @@ -98,6 +99,13 @@ namespace JACK bool zombified ( void ) const { return _zombified; } float cpu_load ( void ) const { return jack_cpu_load( _client ); } + + void transport_stop ( void ); + void transport_start ( void ); + void transport_locate ( nframes_t frame ); + jack_transport_state_t transport_query ( jack_position_t *pos ); + + static int maximum_name_length ( void ) { return jack_client_name_size(); } }; } diff --git a/nonlib/NSM/Client.C b/nonlib/NSM/Client.C new file mode 100644 index 0000000..526d520 --- /dev/null +++ b/nonlib/NSM/Client.C @@ -0,0 +1,264 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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 "../debug.h" +#include "Client.H" +#include +#include +#include +#include + +namespace NSM +{ + +/************************/ +/* OSC Message Handlers */ +/************************/ + +#undef OSC_REPLY +#undef OSC_REPLY_ERR + +#define OSC_REPLY( value ) lo_send_from( ((NSM::Client*)user_data)->nsm_addr, ((NSM::Client*)user_data)->_server, LO_TT_IMMEDIATE, "/reply", "ss", path, value ) + +#define OSC_REPLY_ERR( errcode, value ) lo_send_from( ((NSM::Client*)user_data)->nsm_addr, ((NSM::Client*)user_data)->_server, LO_TT_IMMEDIATE, "/error", "sis", path, errcode, value ) + + Client::Client ( ) + { + nsm_addr = 0; + nsm_client_id = 0; + _session_manager_name = 0; + nsm_is_active = false; + _server = 0; + _st = 0; + } + + Client::~Client ( ) + { + if ( _st ) + stop(); + + if ( _st ) + lo_server_thread_free( _st ); + else + lo_server_free ( _server ); + } + + void + Client::announce ( const char *nash_url, const char *application_name, const char *capabilities, const char *process_name ) + { + MESSAGE( "Announcing to NSM" ); + + lo_address to = lo_address_new_from_url( nash_url ); + + if ( ! to ) + { + MESSAGE( "Bad address" ); + return; + } + + int pid = (int)getpid(); + + lo_send_from( to, _server, LO_TT_IMMEDIATE, "/nsm/server/announce", "sssiii", + application_name, + capabilities, + process_name, + 0, /* api_major_version */ + 5, /* api_minor_version */ + pid ); + + lo_address_free( to ); + } + + void + Client::progress ( float p ) + { + if ( nsm_is_active ) + { + lo_send_from( nsm_addr, _server, LO_TT_IMMEDIATE, "/nsm/client/progress", "f", p ); + } + } + + void + Client::is_dirty ( void ) + { + if ( nsm_is_active ) + { + lo_send_from( nsm_addr, _server, LO_TT_IMMEDIATE, "/nsm/client/is_dirty", "" ); + } + } + + void + Client::is_clean ( void ) + { + if ( nsm_is_active ) + { + lo_send_from( nsm_addr, _server, LO_TT_IMMEDIATE, "/nsm/client/is_clean", "" ); + } + } + + void + Client::message ( int priority, const char *msg ) + { + if ( nsm_is_active ) + { + lo_send_from( nsm_addr, _server, LO_TT_IMMEDIATE, "/nsm/client/message", "is", priority, msg ); + } + } + + void + Client::check ( ) + { + lo_server_recv_noblock( _server, 0 ); + } + + void + Client::start ( ) + { + lo_server_thread_start( _st ); + } + + void + Client::stop ( ) + { + lo_server_thread_stop( _st ); + } + + + int + Client::init ( ) + { + _server = lo_server_new( NULL, NULL ); + + if ( ! _server ) + return -1; + + lo_server_add_method( _server, "/error", "sis", &Client::osc_error, this ); + lo_server_add_method( _server, "/reply", "ssss", &Client::osc_announce_reply, this ); + lo_server_add_method( _server, "/nsm/client/open", "sss", &Client::osc_open, this ); + lo_server_add_method( _server, "/nsm/client/save", "", &Client::osc_save, this ); + + return 0; + } + + int + Client::init_thread ( ) + { + _st = lo_server_thread_new( NULL, NULL ); + _server = lo_server_thread_get_server( _st ); + + if ( ! _server || ! _st ) + return -1; + + lo_server_thread_add_method( _st, "/error", "sis", &Client::osc_error, this ); + lo_server_thread_add_method( _st, "/reply", "ssss", &Client::osc_announce_reply, this ); + lo_server_thread_add_method( _st, "/nsm/client/open", "sss", &Client::osc_open, this ); + lo_server_thread_add_method( _st, "/nsm/client/save", "", &Client::osc_save, this ); + + return 0; + } + +/************************/ +/* OSC Message Handlers */ +/************************/ + + int + Client::osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + char *out_msg = NULL; + + int r = ((NSM::Client*)user_data)->command_save(&out_msg); + + if ( r ) + OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); + else + OSC_REPLY( "OK" ); + + if ( out_msg ) + free( out_msg ); + + return 0; + } + + int + Client::osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + char *out_msg = NULL; + + NSM::Client *nsm = (NSM::Client*)user_data; + + nsm->nsm_client_id = strdup( &argv[2]->s ); + + int r = ((NSM::Client*)user_data)->command_open( &argv[0]->s, &argv[1]->s, &argv[2]->s, &out_msg); + + if ( r ) + OSC_REPLY_ERR( r, ( out_msg ? out_msg : "") ); + else + OSC_REPLY( "OK" ); + + if ( out_msg ) + free( out_msg ); + + return 0; + } + + int + Client::osc_session_is_loaded ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + NSM::Client *nsm = (NSM::Client*)user_data; + + nsm->command_session_is_loaded(); + + return 0; + } + + int + Client::osc_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) + return -1; + + NSM::Client *nsm = (NSM::Client*)user_data; + + + WARNING( "Failed to register with NSM: %s", &argv[2]->s ); + nsm->nsm_is_active = false; + + nsm->command_active( nsm->nsm_is_active ); + + return 0; + } + + int + Client::osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + if ( strcmp( &argv[0]->s, "/nsm/server/announce" ) ) + return -1; + + NSM::Client *nsm = (NSM::Client*)user_data; + + MESSAGE( "Successfully registered. NSM says: %s", &argv[1]->s ); + nsm->nsm_is_active = true; + nsm->_session_manager_name = strdup( &argv[2]->s ); + nsm->nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); + + nsm->command_active( nsm->nsm_is_active ); + + return 0; + } +}; diff --git a/nonlib/NSM/Client.H b/nonlib/NSM/Client.H new file mode 100644 index 0000000..6aa45f9 --- /dev/null +++ b/nonlib/NSM/Client.H @@ -0,0 +1,101 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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. */ +/*******************************************************************************/ + +#pragma once + +#include + +namespace NSM +{ + + class Client + { + + private: + + lo_server _server; + lo_server_thread _st; + lo_address nsm_addr; + + bool nsm_is_active; + char *nsm_client_id; + char *_session_manager_name; + + public: + + enum + { + ERR_OK = 0, + ERR_GENERAL = -1, + ERR_INCOMPATIBLE_API = -2, + ERR_BLACKLISTED = -3, + ERR_LAUNCH_FAILED = -4, + ERR_NO_SUCH_FILE = -5, + ERR_NO_SESSION_OPEN = -6, + ERR_UNSAVED_CHANGES = -7, + ERR_NOT_NOW = -8 + }; + + Client ( ); + virtual ~Client ( ); + + bool is_active ( void ) { return nsm_is_active; } + + const char *session_manager_name ( void ) { return _session_manager_name; } + + /* Client->Server methods */ + void is_dirty ( void ); + void is_clean ( void ); + void progress ( float f ); + void message( int priority, const char *msg ); + void announce ( const char *nsm_url, const char *appliction_name, const char *capabilities, const char *process_name ); + + /* init without threading */ + int init ( void ); + /* init with threading */ + int init_thread ( void ); + + /* call this periodically to check for new messages */ + void check ( void ); + + /* or call these to start and stop a thread (must do your own locking in handler!) */ + void start ( void ); + void stop ( void ); + + protected: + + /* Server->Client methods */ + virtual int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ) = 0; + virtual int command_save ( char **out_msg ) = 0; + + virtual void command_active ( bool active ) { } + + virtual void command_session_is_loaded ( void ) { } + + private: + + /* osc handlers */ + static int osc_open ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + static int osc_save ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + static int osc_announce_reply ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + static int osc_error ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + static int osc_session_is_loaded ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + + }; +}; diff --git a/nonlib/OSC/Endpoint.C b/nonlib/OSC/Endpoint.C index 6b2c17b..ec89cb5 100644 --- a/nonlib/OSC/Endpoint.C +++ b/nonlib/OSC/Endpoint.C @@ -22,51 +22,120 @@ #include #include #include +#include #include "Endpoint.H" +#include "Thread.H" + namespace OSC { + void Endpoint::error_handler(int num, const char *msg, const char *path) { WARNING( "LibLO server error %d in path %s: %s\n", num, path, msg); } - Endpoint::Endpoint ( const char *port ) + Endpoint::Endpoint ( ) + { + } + + int + Endpoint::init ( const char *port ) { DMESSAGE( "Creating OSC server" ); -// _st = lo_server_thread_new( s, error_handler ); - // _server = lo_server_thread_get_server( _st ); - _server = lo_server_new( port, error_handler ); if ( ! _server ) - FATAL( "Error creating OSC server" ); - - char *url = lo_server_get_url(_server); - printf("OSC: %s\n",url); - free(url); + { + WARNING( "Error creating OSC server" ); + return -1; + } + // char *url = lo_server_get_url(_server); + // printf("OSC: %s\n",url); + // free(url); + // add generic handler for path reporting. + add_method( "/osc/query/parameters", "s", osc_query_parameters, this, "" ); add_method( NULL, "", &Endpoint::osc_generic, this, "" ); -// _path_names = new std::list(); + return 0; } + Endpoint::~Endpoint ( ) { // lo_server_thread_free( _st ); lo_server_free( _server ); } + int + Endpoint::osc_query_parameters ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + OSC_DMSG(); + + Endpoint *ep = (Endpoint*)user_data; + + const char *qpath = &argv[0]->s; + + Method_Data *md = NULL; + + for ( std::list::iterator i = ep->_methods.begin(); i != ep->_methods.end(); ++i ) + { + if ( ! (*i)->path ) + continue; + + if ( ! strcmp( qpath, (*i)->path ) && (*i)->typespec ) + { + md = *i; + + /* FIXME: what about the fact that there could be multiple messages with the same path but + different typespecs ? */ + break; + } + } + + if ( ! md ) + { + ep->send( lo_message_get_source( msg ), "/error", path, + "Could not find specified path" ); + + return 0; + } + + char *r = (char*) malloc( 256 ); + r[0] = 0; + + for ( int i = 0; i < strlen( md->typespec ); ++i ) + { + char desc[50]; + + snprintf( desc, sizeof(desc), "f:%f:%f:%f\n", + md->parameter_limits[i].min, + md->parameter_limits[i].max, + md->parameter_limits[i].default_value ); + + r = (char*)realloc( r, strlen( r ) + strlen( desc ) + 2 ); + + strcat( r, desc ); + strcat( r, "\n" ); + } + + ep->send( lo_message_get_source( msg ), "/reply", path, + qpath, + r ); + + return 0; + } int Endpoint::osc_generic ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) { - OSC_DMSG(); +// OSC_DMSG(); if ( path[ strlen(path) - 1 ] != '/' ) return -1; @@ -87,16 +156,18 @@ namespace OSC char *r = (char*)malloc( 1024 ); r[0] = 0; - for ( std::list::iterator i = _path_names.begin(); i != _path_names.end(); ++i ) + for ( std::list::const_iterator i = _methods.begin(); i != _methods.end(); ++i ) { - if ( ! *i ) + if ( ! (*i)->path ) continue; - if (! strncmp( *i, prefix, strlen(prefix) ) ) + if (! strncmp( (*i)->path, prefix, strlen(prefix) ) ) { - r = (char*)realloc( r, strlen( r ) + strlen( *i ) + 2 ); + r = (char*)realloc( r, strlen( r ) + strlen( (*i)->path ) + 2 ); - strcat( r, *i ); + /* asprintf( &stored_path, "%s (%s); %s", path, typespec, argument_description ); */ + + strcat( r, (*i)->path ); strcat( r, "\n" ); } } @@ -104,67 +175,139 @@ namespace OSC return r; } - void + void + Endpoint::set_parameter_limits ( const char *path, const char *typespec, + int index, + float min, float max, float default_value ) + { + assert( typespec ); + + assert( index < strlen( typespec ) ); + + for ( std::list::iterator i = _methods.begin(); i != _methods.end(); ++i ) + { + if ( ! (*i)->path ) + continue; + + if ( ! strcmp( path, (*i)->path ) && + ! strcmp( typespec, (*i)->typespec ) ) + { + (*i)->parameter_limits[index].min = min; + (*i)->parameter_limits[index].max = max; + (*i)->parameter_limits[index].default_value = default_value; + + break; + } + } + } + + method_handle Endpoint::add_method ( const char *path, const char *typespec, lo_method_handler handler, void *user_data, const char *argument_description ) { - DMESSAGE( "Added OSC method %s (%s)", path, typespec ); +// DMESSAGE( "Added OSC method %s (%s)", path, typespec ); lo_server_add_method( _server, path, typespec, handler, user_data ); - char *stored_path = NULL; + Method_Data *md = new Method_Data; - asprintf( &stored_path, "%s (%s); %s", path, typespec, argument_description ); + if ( path ) + md->path = strdup( path ); + if ( typespec ) + md->typespec = strdup( typespec ); + if ( argument_description ) + md->documentation = strdup( argument_description ); - _path_names.push_back( stored_path ); + if ( typespec ) + md->parameter_limits = new Parameter_Limits[strlen(typespec)]; + + _methods.push_back( md ); + + return md; + + /* asprintf( &stored_path, "%s (%s); %s", path, typespec, argument_description ); */ + + /* _path_names.push_back( stored_path ); */ } void Endpoint::del_method ( const char *path, const char *typespec ) { - DMESSAGE( "Deleted OSC method %s (%s)", path, typespec ); +// DMESSAGE( "Deleted OSC method %s (%s)", path, typespec ); lo_server_del_method( _server, path, typespec ); - for ( std::list::iterator i = _path_names.begin(); i != _path_names.end(); ++i ) + for ( std::list::iterator i = _methods.begin(); i != _methods.end(); ++i ) { - if ( ! *i ) + if ( ! (*i)->path ) continue; - if ( ! strncmp( path, *i, index( *i, ' ' ) - *i ) ) + if ( ! strcmp( path, (*i)->path ) && + ! strcmp( typespec, (*i)->typespec ) ) { - free( *i ); - i = _path_names.erase( i ); + delete *i; + i = _methods.erase( i ); + + break; } } } -/* void * */ -/* Endpoint::osc_thread ( void * arg ) */ -/* { */ -/* ((Endpoint*)arg)->osc_thread(); */ + void + Endpoint::del_method ( const method_handle mh ) + { +// DMESSAGE( "Deleted OSC method %s (%s)", path, typespec ); -/* return NULL; */ -/* } */ + Method_Data *meth = const_cast( (const Method_Data*)mh ); -/* void */ -/* Endpoint::osc_thread ( void ) */ -/* { */ -/* _thread.name( "OSC" ); */ + lo_server_del_method( _server, meth->path, meth->typespec ); -/* DMESSAGE( "OSC Thread running" ); */ + delete meth; -/* for ( ;; ) */ -/* { */ -/* lo_server_recv( _sever ); */ -/* } */ -/* } */ + _methods.remove( meth ); + + + /* for ( std::list::iterator i = _methods.begin(); i != _methods.end(); ++i ) */ + /* { */ + /* if ( ! (*i)->path ) */ + /* continue; */ + + /* if ( ! strcmp( path, (*i)->path ) && */ + /* ! strcmp( typespec, (*i)->typespec ) ) */ + /* { */ + /* delete *i; */ + /* i = _methods.erase( i ); */ + + /* break; */ + /* } */ + /* } */ + } + + + + void * + Endpoint::osc_thread ( void * arg ) + { + ((Endpoint*)arg)->osc_thread(); + + return NULL; + } + + void + Endpoint::osc_thread ( void ) + { + _thread.name( "OSC" ); + + DMESSAGE( "OSC Thread running" ); + + run(); + } void Endpoint::start ( void ) { -/* if ( !_thread.clone( &Endpoint::osc_thread, this ) ) */ -/* FATAL( "Could not create OSC thread" ); */ + if ( !_thread.clone( &Endpoint::osc_thread, this ) ) + FATAL( "Could not create OSC thread" ); /* lo_server_thread_start( _st ); */ @@ -173,6 +316,7 @@ namespace OSC void Endpoint::stop ( void ) { + _thread.join(); // lo_server_thread_stop( _st ); } @@ -227,9 +371,11 @@ namespace OSC switch ( ov->type() ) { case 'f': + DMESSAGE( "Adding float %f", ((OSC_Float*)ov)->value() ); lo_message_add_float( m, ((OSC_Float*)ov)->value() ); break; case 'i': + DMESSAGE( "Adding int %i", ((OSC_Int*)ov)->value() ); lo_message_add_int32( m, ((OSC_Int*)ov)->value() ); break; case 's': @@ -287,10 +433,83 @@ namespace OSC return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "s", v ); } + int + Endpoint::send ( lo_address to, const char *path, const char * v1, float v2 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sf", v1, v2 ); + } + int Endpoint::send ( lo_address to, const char *path, const char * v1, const char *v2 ) { return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ss", v1, v2 ); } + int + Endpoint::send ( lo_address to, const char *path, const char * v1, const char *v2, const char *v3 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sss", v1, v2, v3 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, int v3, int v4 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "siii", v1, v2, v3, v4 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, int v4, int v5 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ssiii", v1, v2, v3, v4, v5 ); + } + + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, int v5, int v6 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssiii", v1, v2, v3, v4, v5, v6 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, int v2 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "si", v1, v2 ); + } + + int + Endpoint::send ( lo_address to, const char *path, int v1, const char *v2 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "is", v1, v2 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, const char *v3 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sis", v1, v2, v3 ); + } + + int + Endpoint::send ( lo_address to, const char *path, int v1, const char *v2, const char *v3, const char *v4 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "isss", v1, v2, v3, v4 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, int v2, const char *v3, const char *v4, const char *v5 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sisss", v1, v2, v3, v4, v5 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4, const char *v5 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "sssss", v1, v2, v3, v4, v5 ); + } + + int + Endpoint::send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4 ) + { + return lo_send_from( to, _server, LO_TT_IMMEDIATE, path, "ssss", v1, v2, v3, v4 ); + } + } diff --git a/nonlib/OSC/Endpoint.H b/nonlib/OSC/Endpoint.H index d86048e..d2a1d76 100644 --- a/nonlib/OSC/Endpoint.H +++ b/nonlib/OSC/Endpoint.H @@ -20,11 +20,13 @@ #pragma once #include -//#include "util/Thread.H" +#include "Thread.H" #include +#include namespace OSC { + typedef void * method_handle; class OSC_Value { @@ -111,27 +113,77 @@ namespace OSC static int osc_generic ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); + static int osc_query_parameters ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ); -// Thread _thread; + Thread _thread; -// lo_server_thread _st; +// lo_server_thread _st; lo_server _server; - std::list _path_names; + struct Parameter_Limits + { + float min; + float max; + float default_value; + }; + + struct Method_Data + { + char *path; + char *typespec; + char *documentation; + struct Parameter_Limits *parameter_limits; + std::list subscribers; + + Method_Data ( ) + { + path = typespec = documentation = 0; + parameter_limits = 0; + } + + ~Method_Data ( ) + { + if ( path ) + free( path ); + if ( typespec ) + free( typespec ); + if ( parameter_limits ) + free( parameter_limits ); + + for ( std::list::iterator i = subscribers.begin(); + i != subscribers.end(); + ++i ) + { + lo_address_free( *i ); + } + + subscribers.clear(); + } + }; + + std::list _methods; + + static void *osc_thread ( void *arg ); + void osc_thread ( void ); + public: - Endpoint ( const char *port = 0 ); + int init ( const char *port = 0 ); + Endpoint ( ); ~Endpoint ( ); char *get_paths ( const char *prefix ); - void add_method ( const char *path, const char *typespec, lo_method_handler handler, void *user_data, const char *argument_description ); + method_handle add_method ( const char *path, const char *typespec, lo_method_handler handler, void *user_data, const char *argument_description ); void del_method ( const char *path, const char *typespec ); + void del_method ( const method_handle ); void start ( void ); void stop ( void ); int port ( void ) const; char * url ( void ) const; + void set_parameter_limits ( const char *path, const char *typespec, int index, float min, float max, float default_value ); + void check ( void ) const; void wait ( int timeout ) const; @@ -145,20 +197,36 @@ namespace OSC int send ( lo_address to, const char *path, double v ); int send ( lo_address to, const char *path, int v ); int send ( lo_address to, const char *path, long v ); + int send ( lo_address to, const char *path, const char * v1, float v2 ); int send ( lo_address to, const char *path, const char *v ); int send ( lo_address to, const char *path, const char *v1, const char *v2 ); + int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3 ); + int send ( lo_address to, const char *path, const char *v1, int v2, int v3, int v4 ); + int send ( lo_address to, const char *path, const char *v1, const char *v2, int v3, int v4, int v5 ); + + int send ( lo_address to, const char *path, const char *v1, int v2 ); + int send ( lo_address to, const char *path, int v1, const char *v2 ); + int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, int v4, int v5, int v6 ); + int send ( lo_address to, const char *path, const char *v1, int v2, const char *v3 ); + int send ( lo_address to, const char *path, int v1, const char *v2, const char *v3, const char *v4 ); + int send ( lo_address to, const char *path, const char *v1, int v2, const char *v3, const char *v4, const char *v5 ); + int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4, const char *v5 ); + int send ( lo_address to, const char *path, const char *v1, const char *v2, const char *v3, const char *v4 ); + + // can be used to point back to owning object. + void *owner; }; }; - /* helper macros for defining OSC handlers */ #define OSC_NAME( name ) osc_ ## name #define OSC_DMSG() DMESSAGE( "Got OSC message: %s", path ); #define OSC_HANDLER( name ) static int OSC_NAME( name ) ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) -#define OSC_REPLY_OK() ((Mixer*)user_data)->osc_endpoint->send( lo_message_get_source( msg ), "/reply", path, "ok" ) -#define OSC_REPLY( msg_str ) ((Mixer*)user_data)->osc_endpoint->send( lo_message_get_source( msg ), "/reply", path, msg_str ) -#define OSC_REPLY_ERR() ((Mixer*)user_data)->osc_endpoint->send( lo_message_get_source( msg ), "/reply", path, "err" ) +#define OSC_REPLY_OK() ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, "ok" ) +#define OSC_REPLY( value ) ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, value ) +#define OSC_REPLY_ERR() ((OSC::Endpoint*)user_data)->send( lo_message_get_source( msg ), path, "err" ) +#define OSC_ENDPOINT() ((OSC::Endpoint*)user_data) diff --git a/nonlib/makefile.inc b/nonlib/makefile.inc index 22489e6..3041d60 100644 --- a/nonlib/makefile.inc +++ b/nonlib/makefile.inc @@ -1,6 +1,6 @@ # -*- mode: makefile; -*- -nonlib_SRCS := $(wildcard nonlib/*.C nonlib/JACK/*.C nonlib/LASH/*.C nonlib/OSC/*.C) +nonlib_SRCS := $(wildcard nonlib/*.C nonlib/JACK/*.C nonlib/LASH/*.C nonlib/OSC/*.C nonlib/NSM/*.C) nonlib_SRCS:=$(sort $(nonlib_SRCS)) nonlib_OBJS:=$(nonlib_SRCS:.C=.o) diff --git a/scripts/config-funcs b/scripts/config-funcs index 9f4bf5b..91fca40 100644 --- a/scripts/config-funcs +++ b/scripts/config-funcs @@ -282,18 +282,18 @@ require_package () _test_version () { - if [ $# == 6 ] + if [ $# = 6 ] then [ $1 -gt $4 ] && return 0 [ $1 -eq $4 ] && [ $2 -gt $5 ] && return 0 [ $1 -eq $4 ] && [ $2 -eq $5 ] && [ $3 -gt $6 ] && return 0 [ $1 -eq $4 ] && [ $2 -eq $5 ] && [ $3 -eq $6 ] && return 0 return 1 - elif [ $# == 4 ] + elif [ $# = 4 ] then [ $1 -gt $3 ] && return 0 [ $1 -eq $3 ] && [ $2 -eq $4 ] && return 0 - return 1; + return 1 fi } diff --git a/session/FL b/session/FL new file mode 120000 index 0000000..d025fc7 --- /dev/null +++ b/session/FL @@ -0,0 +1 @@ +../FL \ No newline at end of file diff --git a/session/Makefile b/session/Makefile new file mode 100644 index 0000000..63a16d7 --- /dev/null +++ b/session/Makefile @@ -0,0 +1,137 @@ + +############################################################################### +# Copyright (C) 2008 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. # +############################################################################### + +## Makefile for the Non-DAW. + +## +## Do not edit this file; run `make config` instead. +## + +VERSION := 1.0.0 +PACKAGE := SESSION + +all: .config + +.config: configure + @ echo '<<< Configuring '$(PACKAGE) + @ ./configure + +config: + @ echo '<<< Configuring '$(PACKAGE) + @ ./configure + +-include .config + +export SYSTEM_PATH:=$(prefix)/share/ +export DOCUMENT_PATH:=$(prefix)/share/doc/ +export PIXMAP_PATH:=$(prefix)/share/pixmaps/ + +# a bit of a hack to make sure this runs before any rules +ifneq ($(CALCULATING),yes) +TOTAL := $(shell $(MAKE) CALCULATING=yes -n 2>/dev/null | sed -n 's/^.*Compiling: \([^"]\+\)"/\1/p' > .files ) +endif + +ifeq ($(USE_DEBUG),yes) + CFLAGS := -pipe -ggdb -fno-inline -Wall -Wextra -O0 + CXXFLAGS := -Wnon-virtual-dtor -Wno-missing-field-initializers -fno-rtti -fno-exceptions +else + CFLAGS := -pipe -O2 -DNDEBUG + CXXFLAGS := -fno-rtti -fno-exceptions +endif + + +CFLAGS+=-DVERSION=\"$(VERSION)\" \ + -DINSTALL_PREFIX=\"$(prefix)\" \ + -DSYSTEM_PATH=\"$(SYSTEM_PATH)\" \ + -DDOCUMENT_PATH=\"$(DOCUMENT_PATH)\" \ + -DPIXMAP_PATH=\"$(PIXMAP_PATH)\" + +CXXFLAGS += $(SNDFILE_CFLAGS) $(FLTK_CFLAGS) $(JACK_CFLAGS) +CXXFLAGS := $(CFLAGS) $(CXXFLAGS) + +INCLUDES := -I. -Iutil -IFL -Inonlib + +include scripts/colors + +ifneq ($(CALCULATING),yes) + COMPILING="$(BOLD)$(BLACK)${PACKAGE} [$(SGR0)$(CYAN)`scripts/percent-complete .files "$<"`$(SGR0)$(BOLD)$(BLACK)]$(SGR0) $(BOLD)$(YELLOW)$<$(SGR0)" +else + COMPILING="Compiling: $<" +endif + +.C.o: + @ echo $(COMPILING) + @ $(CXX) $(CXXFLAGS) $(INCLUDES) -c $< -o $@ + +%.C : %.fl + @ cd `dirname $<` && fluid -c ../$< + +DONE := $(BOLD)$(GREEN)done$(SGR0) + +include FL/makefile.inc +#include nonlib/makefile.inc +include makefile.inc + +SRCS:=$(Session_SRCS) +OBJS:=$(Session_OBJS) + +# FIXME: isn't there a better way? +$(OBJS): .config Makefile + +TAGS: $(SRCS) + etags $(SRCS) + +.deps: .config $(SRCS) +ifneq ($(CALCULATING),yes) + @ echo -n Calculating dependencies... + @ makedepend -f- -- $(CXXFLAGS) $(INCLUDES) -- $(SRCS) 2>/dev/null > .deps && echo $(DONE) + @ # gcc -M $(CXXFLAGS) $(INCLUDES) $(SRCS) > .deps && echo $(DONE) +endif + + +install: all + @ echo -n "Installing..." + @ install src/nsmd $(prefix)/bin/nsmd + @ install src/session-manager $(prefix)/bin/non-session-manager + @ # mkdir -p $(SYSTEM_PATH)/non-mixer + @ # mkdir -p $(PIXMAP_PATH)/non-mixer + @ # cp pixmaps/*.png $(PIXMAP_PATH)/non-mixer + @ # $(MAKE) -s -C doc install + @ echo "$(DONE)" +ifneq ($(USE_DEBUG),yes) + @ echo -n "Stripping..." + @ strip $(prefix)/bin/nsmd + @ strip $(prefix)/bin/non-session-manager + @ echo "$(DONE)" +endif + +clean_deps: + @ rm -f .deps + +.PHONEY: clean config depend clean_deps + +clean: FL_clean Session_clean + +dist: + git archive --prefix=non-session-$(VERSION)/ v$(VERSION) | bzip2 > non-session-$(VERSION).tar.bz2 + +scan-gpl: + @ scripts/scan-gpl $(SRCS) || echo $(BOLD)$(RED)Some source files do not contain proper license information! + +-include .deps diff --git a/session/configure b/session/configure new file mode 100755 index 0000000..0ffe863 --- /dev/null +++ b/session/configure @@ -0,0 +1,27 @@ +#!/bin/sh +# +# Copyright (C) 2008 Jonathan Moore Liles +# This file is licensed under version 2 of the GPL. + +. scripts/config-funcs + +begin + +begin_options + +ask "Installation prefix" prefix /usr/local +ask "Build for debugging" USE_DEBUG no + +begin_tests + +require_FLTK 1.1.7 images +require_command FLUID fluid +require_command ar ar +require_command makedepend makedepend +# require_package JACK 0.103.0 jack +require_package lrdf 0.4.0 lrdf +require_package liblo 0.23 liblo + +test_version `version_of liblo` 0.26 || warn "Version $(version_of liblo) of liblo is slow to create servers. Consider upgrading to 0.26 or later" + +end diff --git a/session/doc/NSM.mu b/session/doc/NSM.mu new file mode 100644 index 0000000..c833a5a --- /dev/null +++ b/session/doc/NSM.mu @@ -0,0 +1,492 @@ + +! title Non Session Management API +! author Jonathan Moore Liles #(email,male@tuxfamily.org) +! date August 1, 2010 + +-- Table Of Contents + +: Non Session Management API version 0.7 + + The Non Session Management API is an API for session management used + by the various parts of the Non music production suite. It comprises + a simple OSC based protocol which can easily be implemented by other + applications. NSM provides robust session management, including + interactive features. + + The Non project contains an implementation of the NSM server API + called `nsmd` which can be controlled by the `non-session-manager` + GUI, but the same server API can easily be implemented by other + session managers (such as LADISH). + + The only dependency for clients `liblo` (the OSC library), which + several Linux audio applications already link to or plan to link to + in the future. + + The aim of this project is to thoroughly define the behavior + required of clients. This is an area where other attempts at session + management (LASH and JACK-Session) have failed. Often the difficulty + with these previous system been, not in implementing support for + them, but in attempting to interpret the confusing and ambiguous API + documentation. For this reason, all LASH support has been removed + from Non. + + You *WILL* see a lot of unambiguous language in this document. These + rules are meant to be followed and are non-negotiable. If an + application does not conform to this specification it should be + considered broken. Consistency across applications under session + management is very important for a good user experience. + +:: Client Behavior Under Session Management + + Most graphical applications make available to the user a common set + of file operations, typically presented under a File or Project + menu. + + These are: New, Open, Save, Save As, Close and Quit or Exit. + + The following sub-sections describe how these options should behave when + the application is part of an NSM session. These rules only apply + when session management is active (that is, after the `announce` + handshake described in the #(ref,NSM OSC Protocol) section). + + In order to provide a consistent and predictable user experience, it + is important for applications to adhere to these guidelines. + +::: New + + This option may empty\/reset the current file or project (possibly + after user confirmation). *UNDER NO CIRCUMSTANCES* should it allow + the user to create a new project\/file in another location. + +::: Open + + This option should be disabled. + + The application may, however, elect to implement an option called + 'Import into Session', creates a copy of a file\/project which is + then saved in the session path provided by NSM. + +::: Save + + This option should behave as normal, saving the current + file\/project as established by the NSM `open` message. + + *UNDER NO CIRCUMSTANCES* should this option present the user with a + choice of where to save the file! + +::: Save As + + This option should be disabled. + + The application may, however, elect to implement an option called + 'Export from Session', which creates a copy of the current + file\/project which is then saved in a user-specified location + outside of the session path provided by NSM. + +::: Close (as distinguished from Quit or Exit) + + This option should be disabled, unless its meaning is to disconnect + the application from session management. + +::: Quit or Exit + + This option may behave as normal (even possibly asking the user to + confirm exiting). + +:: NSM OSC Protocol + + All message parameters are *REQUIRED*. All messages *MUST* be sent + from the same socket as the `announce` message, using the + `lo\_send\_from` method of liblo (the server uses the return + addresses to distinguish between clients). + +::: Establishing a Connection + +:::: Announce + + When started clients *MUST* check the environment for the value of + `NSM\_URL`. If present, the client *MUST* send the following message + to the provided address as soon as it is ready to respond to the + `\/nsm\/client\/open` event: + +> /nsm/server/announce s:application_name s:capabilities i:api_version_major i:api_version_minor i:pid + + If `NSM\_URL` is undefined, invalid, or unreachable, then the client + should proceed assuming that session management is unavailable. + + `api\_version\_major` and `api\_version\_minor` must be the two parts of + the version number of the NSM API as defined by this document. + + Note that if the application intends to register JACK clients, + `application\_name` *MUST* be the same as the name that would + normally by passed to `jack\_client\_open`. For example, Non-Mixer + sends "Non-Mixer" as its `application\_name`. Applications *MUST + NOT* register their JACK clients until receiving an `open` message; + the `open` message will provide a unique client name prefix suitable + for passing to JACK. This is probably the most complex requirement + of the NSM API, but it isn't difficult to implement. + + `capabilities` *MUST* be a string containing a colon separated list + of the special capabilities the client + possesses. e.g. ":dirty:switch:progress:" + +// Available Client Capabilities +[[ Name, Description +[[ switch, client is capable of responding to multiple `open` messages without restarting +[[ dirty, client knows when it has unsaved changes +[[ progress, client can send progress updates during time-consuming operations +[[ status, client can send textual status updates + +:::: Response + + The server will respond to the client's `announce` with the following message: + +> /reply "/nsm/server/announce" s:message s:name_of_session_manager s:capabilities + + `message` is a welcome message. + + The value of `name\_of\_session\_manager` will depend on the + implementation of the NSM server. It might say "Non Session Manager", + or it might say "LADISH". + + `capabilities` will be a string containing a colon separated list of + special server capabilities. + + Presently, the server `capabilities` are: + +// Available Server Capabilities +[[ Name, Description +[[ server_control, client-to-server control + + A client should not consider itself to be under session management + until it receives this response (the Non programs activate their + "SM" blinkers at this time.) + + If there is an error, a reply of the following form will be sent to + the client: + +> /error "/nsm/server/announce" i:error_code s:error_message + + The following table defines possible values of `error\_code`: + +// Response codes +[[ Code, Meaning +[[ ERR_GENERAL, General Error +[[ ERR_INCOMPATIBLE_API, Incompatible API version +[[ ERR_BLACKLISTED, Client has been blacklisted. + +::: Server to Client Control Messages + + Compliant clients *MUST* accept the client control messages + described in this section. All client control messages *REQUIRE* a + response. Responses *MUST* be delivered back to the sender (NSM) + from the same socket used by the client in its `announce` message + (by using `lo\_send\_from`) *AFTER* the action has been completed or + if an error is encountered. The required response is described in + the subsection for each message. + + If there is an error and the action cannot be completed, then + `error\_code` *MUST* be set to a valid error code (see #(fig,Error Code Definitions)) + and `message` to a string describing the problem (suitable + for display to the user). + + The reply can take one of the following two forms, where `path` *MUST* be + the path of the message being replied to (e.g. "/nsm\/client\/save"): + +> /reply s:path s:message + +> /error s:path i:error_code s:message + +:::: Quit + + There is no message for this. Clients will receive the Unix SIGTERM + signal and *MUST* close cleanly *IMMEDIATELY*, without displaying + any kind of dialog to the user and regardless of whether or not + unsaved changes would be lost (when a session is closed the + application will receive this signal soon after having responded to + a `save` message). + +:::: Open + +> /nsm/client/open s:path_to_instance_specific_project s:client_id + + The client *MUST* open an existing project, or create new one if one + doesn't already exist, at `path\_to\_instance_specific\_project` + + If the path provided doesn't exist, then the client *MUST* + immediately create and open a new file\/project at the specified + path (whether that means creating a single file or a project + directory). + + No file or directory will be created at the specified path by the + server. It is up to the client to create what it needs. + + The client may append to the path, creating a subdirectory, + e.g. '/song.foo' or simply append the client's native file extension + (e.g. '.non' or '.XML'). The same transformation *MUST* be applied + to the name when opening an existing project, as NSM will only + provide the instance specific part of the path. + + For clients which *HAVE NOT* specified the 'switch' capability, the + `open` message will only be delivered once, immediately after the + 'announce' response. + + For client which *HAVE* specified the `:switch:` capability, the + client *MUST* immediately switch to the specified project or create + a new one if it doesn't exist. + + Clients which are incapable of switching projects or are prone to + crashing upon switching *MUST NOT* include `:switch:` in their + capability string. + + If the user the is allowed to run two or more instances of the + application simultaneously (that is to say, there is no technical + limitation preventing them from doing so, even if it doesn't make + sense to the author), then such an application *MUST* prepend the + provided `client\_id` string to any names it registers with common + subsystems (e.g. JACK client names). This ensures that the multiple + instances of the same application can be restored in any order + without scrambling the JACK connections or causing other + conflicts. The provided `client\_id` will be a concatenation of the + value of `application\_name` sent by the client in its `announce` + message and a unique identifier. Therefore, applications which + create single JACK clients can use the value of `client\_id` directly + as their JACK client name. Applications which register multiple JACK + clients (e.g. Non-Mixer) *MUST* prepend `client_id` value to the + client names they register with JACK and the application determined + part *MUST* be unique for that (JACK) client. + + For example, a suitable JACK client name would be: + +> $CLIENT_ID/track-1 + + A response is *REQUIRED* *AFTER* the load\/new operation has been + completed. Ongoing progress may be indicated by sending messages to + `\/nsm\/client\/progress`. + +::::: Response + + The client *MUST* respond to the 'open' message with: + +> /reply "/nsm/client/open" s:message + + Or + +> /error "/nsm/client/open" i:error_code s:message + +// Response Codes +[[ Code, Meaning +[[ ERR, General Error +[[ ERR_BAD_PROJECT, An existing project file was found to be corrupt +[[ ERR_CREATE_FAILED, A new project could not be created +[[ ERR_UNSAVED_CHANGES, Unsaved changes would be lost +[[ ERR_NOT_NOW, Operation cannot be completed at this time + +:::: Save + +> /nsm/client/save + + The client *MUST* immediately save the current application specific + project data to the project path previously established in the + 'open' message. *UNDER NO CIRCUMSTANCES* should a dialog be + displayed to the user (giving a choice of where to save, etc.) + +::::: Response + + The client *MUST* respond to the 'save' message with: + +> /reply "/nsm/client/save" s:message + + Or + +> /error "/nsm/client/save" i:error_code s:message + +// Response Codes +[[ Code, Meaning +[[ ERR, General Error +[[ ERR_SAVE_FAILED, Project could not be saved +[[ ERR_NOT_NOW, Operation cannot be completed at this time + +::: Server to Client Informational Messages + +:::: Session is Loaded + + Accepting this message is optional. The intent is to signal to + clients which may have some interdependency (say, peer to peer OSC + connections) that the session is fully loaded and all their peers + are available. + +> /nsm/client/session_is_loaded + + This message does not require a response. + +::: Client to Server Informational Messages + + These are optional messages which a client can send to the NSM + server to inform it about the client's status. The client should not + expect any reply to these messages. If a client intends to send a + message described in this section, then it *MUST* add the + appropriate value to its `capabilities` string when composing the + `announce` message. + +:::: Progress + +> /nsm/client/progress f:progress + + For potentially time-consuming operations, such as `save` and + `open`, progress updates may be indicated throughout the duration by + sending a floating point value between 0.0 and 1.0, 1.0 indicating + completion, to the NSM server. + + The server will not send a response to these messages, but will + relay the information to the user. + + Note that, even when using the `progress` feature, the final + response to the `save` or `open` message is still *REQUIRED*. + + Clients which intend to send `progress` messages should include + `:progress:` in their `announce` capability string. + +:::: Dirtiness + +> /nsm/client/is_dirty + +> /nsm/client/is_clean + + Some clients may be able to inform the server when they have unsaved + changes pending. Such clients may optionally send `is\_dirty` and `is\_clean` + messages. + + Clients which have this capability should include `:dirty:` in their + `announce` capability string. + +:::: Status Messages + +> /nsm/client/message i:priority s:message + + Clients may send miscellaneous status updates to the server for + possible display to the user. This may simply be chatter that is normally + written to the console. `priority` should be a number from 0 to 3, 3 + being the most important. + + Clients which have this capability should include `:message:` in their + `announce` capability string. + +::: Error Code Definitions + +// Error Code Definitions +[[ Symbolic Name, Integer Value +[[ ERR_GENERAL, -1 +[[ ERR_INCOMPATIBLE_API, -2 +[[ ERR_BLACKLISTED, -3 +[[ ERR_LAUNCH_FAILED, -4 +[[ ERR_NO_SUCH_FILE, -5 +[[ ERR_NO_SESSION_OPEN, -6 +[[ ERR_UNSAVED_CHANGES, -7 +[[ ERR_NOT_NOW, -8 +[[ ERR_BAD_PROJECT, -9 +[[ ERR_CREATE_FAILED, -10 + +::: Client to Server Control + + If the server publishes the `server\_control` capability, then + clients can also initiate action by the server. For example, a + client might implement a 'Save All' option which sends a + `\/nsm\/server\/save` message to the server, rather than requiring + the user to switch to the session management interface to effect the + save. + +::: Server Control API + + The session manager not only manages clients via OSC, but it is itself + controlled via OSC messages. The server responds to the following + messages. + + All of the following messages will be responded to back to the sender's address + with one of the two following messages: + +> /reply s:path s:message + +> /error s:path i:error_code s:message + + The first parameter of the reply is the path to the message being + replied to. The `\/error` reply includes an integer error code + (non-zero indicates error). `message` will be a description of the + error. + + The possible errors are: + +// Responses +[[ Code, Meaning +[[ ERR_GENERAL, General Error +[[ ERR_LAUNCH_FAILED, Launch failed +[[ ERR_NO_SUCH_FILE, No such file +[[ ERR_NO_SESSION, No session is open +[[ ERR_UNSAVED_CHANGES, Unsaved changes would be lost + += /nsm/server/add s:path_to_executable + Adds a client to the current session. + += /nsm/server/save + Saves the current session. + += /nsm/server/load s:project_name + Saves the current session and loads a new session. + += /nsm/server/new s:project_name + Saves the current session and creates a new session. + += /nsm/server/close + Saves and closes the current session. + += /nsm/server/abort + Closes the current session *WITHOUT SAVING* + += /nsm/server/quit + Saves and closes the current session and terminates the server. + += /nsm/server/duplicate s:new_project + Saves and closes the current session, creates a complete copy of + it as `new_project` and opens it. The existing project should ideally be + a lightweight template, as copying any audio data could be very time + consuming. + += /nsm/server/list + Lists available projects. One `\/reply` message will be sent for each existing project. + + + +# = /nsm/server/client/list +# Lists clients in the current session, their client IDs and statuses +# = /nsm/server/ve + +:::: Client to Client Communication + + If the server includes `:broadcast:` in its capability string, then + clients may send broadcast messages to each other through the NSM + server. + + Clients may send messages to the server at the path + `\/broadcast`. + + The format of this message is as follows: + +> /nsm/server/broadcast s:path [other parameters...] + + The message will then be relayed to all clients in the session at + the path given in the `path` parameter and with the other parameters + shifted forward by one. + + For example the message: + +> /nsm/server/broadcast /tempomap/update "0,120,4/4:12351234,240,4/4" + + Would broadcast the following message to all clients in the session + (except for the sender), some of which might respond to the message + by updating their own tempo maps. + +> /tempomap/update "0,120,4/4:12351234,240,4/4" + + Clients may use this feature to establish peer to peer OSC + communication with symbolic names without having to remember the OSC + URLs of peers. diff --git a/session/doc/mup.css b/session/doc/mup.css new file mode 100644 index 0000000..3d5a404 --- /dev/null +++ b/session/doc/mup.css @@ -0,0 +1,449 @@ + +/* Example CSS Style for MUP */ + +a:link { + color: yellow; +} +a:visited { + color: olive; +} +a:active { + color: white; +} +a:link:hover { + text-decoration: underline; +} + +/* #(url) */ +a.ext:link { + color: red; + text-decoration: none; + border-bottom: dashed silver 1; +} +a.ext:visited { + color: darkred; + border-bottom: dashed silver 1; + text-decoration: none; +} + +/* #(ref) */ +a.int:link { + border-bottom: dashed silver 0.15em; +} +a.int:link:hover { + text-decoration: none; + color: white; +} + +/* + a[href^="#"]:link { + border-bottom: dashed silver 0.15em; + } + a[href^="#"]:link:hover { + text-decoration: none; + color: white; + } + */ + +p:contains("Warning:") { + background: #d00; + color: white; + border: dotted gray 0.5em; + display: block; +} + +/* First letter of first paragraph of every chapter */ +/* + h1 + p:first-letter { + text-transform: uppercase; + float: left; + line-height: 0.8em; + font-size: 350%; + font-family: Serif; + letter-spacing: 0; + margin-right: 0.1em; + margin-top: 0.1em; + border: solid gray 1px; + padding: 1px; + color: #d00; + text-shadow: #666 3px 3px 3px; + } + */ +/* First paragraph of every chapter */ +/* + h1 + p { + text-indent: 0; + } + */ + +/* cover */ +#cover * { + background: transparent; +} +#cover { + position: relative; + background: #da0; + color: black; + text-align: center; + margin: 0; + padding: 0.5em; +} +#cover h1, #cover h3 { + text-shadow: #444 0.2em 0.2em 0.2em; + color: white; + border: none; + letter-spacing: 0.2em; + line-height: 0.8em; + margin-left: 2em; + margin-right: 2em; +} +#cover h1:before, #cover h1:after { + content: "::"; + font-size: 300%; + color: black; +} +#cover h1:before { + position: absolute; + top: 0.2em; + left: 0.1em; +} +#cover h1:after { + position: absolute; + top: 0.2em; + right: 0.1em; +} +#cover hr { + display: none; +} + +hr:first-child { + display: none; +} + +hr { + height: 0.2em; + background: #555; + color: #555; + margin-left: 0.5em; +} + +#cover a:visited { + color: black; +} + +/* endnote */ +#endnote { + color: black; +} + +/* TOC */ +#toc { + position: relative; +} +#toc hr { +} +#toc h1 { +} +#toc ul { + font-size: 125%; + font-weight: bold; + margin-bottom: 1em; +} +#toc ul ul { + font-size: 90%; + font-weight: normal; + margin-bottom: 0; +} +#toc li { + list-style: none; + +} +#toc a:link { + border-bottom: 0; +} + +body { + margin: 0; + background: #222; + color: white; + font-family: Arial, sans-serif; +} +/* */ +#body { + position: relative; + margin: 0.5em; + padding: 0.5em; +} +/* ;, : */ +h1 { + color: #ff0; + border-bottom: solid #444 0.1em; +} +/* ::, :::, ::::, :::::, :::::: */ +h2, h3, h4, h5, h6 { + color: #dd0; +} + +/* tables, figures */ +.fig caption { + color: gray; + text-align: center; + /* Required for Mozilla */ + margin: auto; +} +.fig table { + border: none; + margin: auto; +/* border-collapse: collapse; */ +} +/* / */ +.fig.table th { + border: none; + background: gray; + color: black; +} +/* [ */ +.fig.table td { + border: none; + background: silver; + color: black; + padding-left: 1em; + padding-right: 1em; + padding-top: 0.2em; + padding-bottom: 0.2em; +} +/* < */ + +.fig.image table { + border: dashed silver 0.2em; + background: transparent; + /* Every browser should support border radii */ + -moz-border-radius: 0.5em; + border-radius: 0.5em; + +} +.fig.image tr, .fig.image td { + border: none; + background: transparent; + padding: 0; +} + +/* */ +p { + margin-right: 2%; + text-align: justify; + text-indent: 1em; +} +/* > */ +.example * +{ + background: transparent; +} +.example table +{ + margin: 0; + padding: 0; + table-layout: fixed; + width: 100%; + caption-side: top; + overflow: auto; +} +.example caption +{ + caption-side: top; + +} +.example { +} +.example p { + display: inline; + margin: 0; + padding: 0; + text-align: center; +} +.example pre { + margin-top: 0; + font-family: Monospace; + padding: 1em; + border: dashed 0.3em gray; + background: #111; + color: white; + display: block; + overflow: auto; + /* Every browser should support border radii */ + -moz-border-radius: 0.5em; + border-radius: 0.5em; +} +/* " */ +/* + .quote:before { + float: left; + font-size: 500%; + content: "\201C"; + } + */ +/* + blockquote:after { + content: "\201D"; + }*/ +.quote blockquote { + padding: 0.5em; + margin-left: 0.5em; + font-family: Serif; + border-left: solid 0.4em gray; + /* background: #333; */ + color: white; +} +/* ^ */ +small { +/* + color: silver; + font-size: 50%; + */ +} +.footnote p { + color: silver; + margin: 0; +} + +/* Popup footnotes */ +.footnote p { + display: none; +} +.footnote p:target { + display: block; + overflow: auto; + position: fixed; + left: auto; + bottom: 0; + right: 0; + max-width: 50%; + border: solid 0.3em white; + -moz-border-radius: 0.5em; + background: black; + padding: 0.2em; +} + +/* { */ +.admonition * { + background: transparent; + color: white; +} +.admonition dl +{ + display: table; + margin: 0; + padding: 0; + background: #333; + border: dotted black 0.3em; + width: 90% + margin-top: 0.5em; + margin-bottom: 0.5em; +} +.admonition dt +{ + display: table-cell; + vertical-align: center; + border-right: solid silver 0.4em; + font-weight: bold; + font-size: 115%; + font-family: Serif; + background: gray; + width: 0; + text-shadow: black 0.15em 0.15em 0.15em; +} +.admonition dd +{ + padding-left: 0.4em; + display: table-cell; + width: 100%; + text-align: justify; +} + +.admonition table +{ + margin: 0; + padding: 0; + background: #333; + border: dotted black 0.3em; + width: 90% + margin-top: 0.5em; + margin-bottom: 0.5em; +} +.admonition td { + width: 100%; + text-align: justify; +} +.admonition td:first-child:contains("Warning:") { + background: #900; +} +.admonition td:first-child:contains("Caution:") { + background: #960; +} +.admonition td:first-child:contains("Note:") { + background: #690; +} +.admonition td:first-child { + border-right: solid silver 0.4em; + font-weight: bold; + font-size: 115%; + font-family: Serif; + background: gray; + width: 0; + text-shadow: black 0.15em 0.15em 0.15em; +} + + +/* #(b) */ +/* b { color: olive; } */ +/* #(c) */ +tt { + color: #7f0; +} +/* ! keywords ... */ +p em { + color: gray; + font-style: normal; + font-weight: bold; +} + +/* *, + */ +/* Bullet, numbe */ +li { + color: #f0f; +} +/* Text */ +li span, li p { + color: white; +} +li p { + color: red; + display: block; +} +ul { + list-style-type: square; +} +dl { + margin-left: 2%; + margin-top: 1em; +} +/* = */ +dt { + background: #181818; + padding: 0.2em; +/* font-variant: small-caps; */ + font-weight: bold; + color: #f0f; +} +dd { + color: white; + text-align: justify; + margin-right: 5%; +} +dt a:link, dt a:visited { + color: #f0f; +} +dt a:link:hover { + color: silver; + text-decoration: underline; +} diff --git a/session/makefile.inc b/session/makefile.inc new file mode 100644 index 0000000..30ea6f6 --- /dev/null +++ b/session/makefile.inc @@ -0,0 +1,28 @@ +# -*- mode: makefile; -*- + +all: Session + +Session_SRCS := $(wildcard src/*.C src/*.fl) +# Session_SRCS += util/debug.C util/Thread.C util/file.C + +Session_SRCS:=$(Session_SRCS:.fl=.C) +Session_SRCS:=$(sort $(Session_SRCS)) +Session_OBJS:=$(Session_SRCS:.C=.o) + +Session_LIBS := $(LIBLO_LIBS) + +src/nsmd: src/nsmd.o nonlib/libnonlib.a + @ echo -n Linking session handler. + @ $(CXX) $(CXXFLAGS) $(Session_LIBS) src/nsmd.o -o $@ -Lnonlib -lnonlib -ldl && echo $(DONE) + +src/session-manager: src/session-manager.o nonlib/libnonlib.a + @ echo -n Linking session handler. + @ $(CXX) $(CXXFLAGS) $(FLTK_LIBS) $(Session_LIBS) src/session-manager.o -o $@ -Lnonlib -lnonlib -ldl && echo $(DONE) + +src/send_osc: src/send_osc.o nonlib/libnonlib.a + @ $(CXX) $(CXXFLAGS) $(Session_LIBS) src/send_osc.o -o $@ -Lnonlib -lnonlib -ddl && echo $(DONE) + +Session: src/send_osc src/nsmd src/session-manager + +Session_clean: + rm -f $(Session_OBJS) src/nsmd diff --git a/session/nonlib b/session/nonlib new file mode 120000 index 0000000..1737b4f --- /dev/null +++ b/session/nonlib @@ -0,0 +1 @@ +../nonlib/ \ No newline at end of file diff --git a/session/scripts b/session/scripts new file mode 120000 index 0000000..a339954 --- /dev/null +++ b/session/scripts @@ -0,0 +1 @@ +../scripts \ No newline at end of file diff --git a/session/src/FL b/session/src/FL new file mode 120000 index 0000000..46ae6eb --- /dev/null +++ b/session/src/FL @@ -0,0 +1 @@ +../../FL \ No newline at end of file diff --git a/session/src/nsmd.C b/session/src/nsmd.C new file mode 100644 index 0000000..b3ca641 --- /dev/null +++ b/session/src/nsmd.C @@ -0,0 +1,1756 @@ + +/*******************************************************************************/ +/* Copyright (C) 2010 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. */ +/*******************************************************************************/ + +#define __MODULE__ "nsmd" + +#include "debug.h" + +// #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define _GNU_SOURCE + + +#pragma GCC diagnostic ignored "-Wunused-parameter" + +static OSC::Endpoint *osc_server; +static lo_address gui_addr; +static bool gui_is_active = false; +static int signal_fd; + +static char *session_root; + +#define NSM_API_VERSION_MAJOR 0 +#define NSM_API_VERSION_MINOR 3 + +#define ERR_OK 0 +#define ERR_GENERAL_ERROR -1 +#define ERR_INCOMPATIBLE_API -2 +#define ERR_BLACKLISTED -3 +#define ERR_LAUNCH_FAILED -4 +#define ERR_NO_SUCH_FILE -5 +#define ERR_NO_SESSION_OPEN -6 +#define ERR_UNSAVED_CHANGES -7 +#define ERR_NOT_NOW -8 +#define ERR_BAD_PROJECT -9 +#define ERR_CREATE_FAILED -10 + +#define APP_TITLE "Non Session Manager" + +enum { + COMMAND_NONE = 0, + COMMAND_QUIT, + COMMAND_KILL, + COMMAND_SAVE, + COMMAND_OPEN +}; + +struct Client +{ +private: + + int _reply_errcode; + char *_reply_message; + +public: + + lo_address addr; /* */ + char *name; /* client application name */ + char *executable_path; /* path to client executable */ + int pending_command; /* */ + int pid; /* PID of client process */ + float progress; /* */ + bool active; /* client has registered via announce */ + bool dead_because_we_said; +// bool stopped; /* the client quit, but not because we told it to--user still has to decide to remove it from the session */ + char *client_id; /* short part of client ID */ + char *capabilities; /* client capabilities... will be null for dumb clients */ + bool dirty; /* flag for client self-reported dirtiness */ + bool pre_existing; + const char *status; + + bool + has_error ( void ) + { + return _reply_errcode != 0; + } + + int + error_code ( void ) + { + return _reply_errcode; + } + + const char * message ( void ) + { + return _reply_message; + } + + void + set_reply ( int errcode, const char *message ) + { + if ( _reply_message ) + free( _reply_message ); + + _reply_message = strdup( message ); + _reply_errcode = errcode; + } + + bool reply_pending ( void ) + { + return pending_command != COMMAND_NONE; + } + + bool is_dumb_client ( void ) + { + return capabilities == NULL; + } + + Client ( ) + { + addr = 0; + _reply_errcode = 0; + _reply_message = 0; + dead_because_we_said = false; + pid = 0; + progress = -0; + pending_command = 0; + active = false; + client_id = 0; + capabilities = 0; + name = 0; + executable_path = 0; + pre_existing = false; + } + + ~Client ( ) + { + if ( name ) + free(name); + if (executable_path) + free(executable_path); + if (client_id) + free(client_id); + if (capabilities) + free(capabilities); + + name = executable_path = client_id = capabilities = NULL; + } +}; + + +static std::list< Client* > client; + +/* helper macros for defining OSC handlers */ +#define OSC_NAME( name ) osc_ ## name +// #define OSCDMSG() DMESSAGE( "Got OSC message: %s", path ); +#define OSC_HANDLER( name ) static int OSC_NAME( name ) ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + +static char *session_path = NULL; +static char *session_name = NULL; + +bool +clients_have_errors ( ) +{ + for ( std::list::const_iterator i = client.begin(); + i != client.end(); + ++i ) + if ( (*i)->active && (*i)->has_error() ) + return true; + + return false; +} + + +Client * +get_client_by_pid ( int pid ) +{ + std::list *cl = &client; + + for ( std::list::const_iterator i = cl->begin(); + i != cl->end(); + ++i ) + if ( (*i)->pid == pid ) + return *i; + + return NULL; +} + +void clear_clients ( void ) +{ + std::list *cl = &client; + + for ( std::list::iterator i = cl->begin(); + i != cl->end(); + ++i ) + { + delete *i; + i = cl->erase( i ); + } +} + +void handle_sigchld ( ) +{ + for ( ;; ) + { + int status; + pid_t pid = waitpid(-1, &status, WNOHANG); + if (pid <= 0) { + break; + } + + Client *c = get_client_by_pid( (int)pid ); + + if ( c ) + { + MESSAGE( "Client %s died.", c->name ); + + if ( c->pending_command == COMMAND_KILL || + c->pending_command == COMMAND_QUIT ) + { + c->dead_because_we_said = true; + } + + c->pending_command = COMMAND_NONE; + + if ( gui_is_active ) + { + if ( ! c->dead_because_we_said ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "stopped" ); + else + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "removed" ); + } + + c->active = false; + c->pid = 0; + + if ( c->dead_because_we_said ) + { + client.remove( c ); + delete c; + } + } + } +} + + +int +path_is_valid ( const char *path ) +{ + char *s; + + asprintf( &s, "/%s/", path ); + + int r = strstr( s, "/../" ) == NULL; + + free( s ); + + return r; +} + + +int +mkpath ( const char *path, bool create_final_directory ) +{ + char *p = strdup( path ); + + char *i = p + 1; + + while ( i = index( i, '/' ) ) + { + *i = 0; + + struct stat st; + + if ( stat( p, &st ) ) + { + if ( mkdir( p, 0711 ) ) + { + free( p ); + return -1; + } + } + + *i = '/'; + i++; + } + + if ( create_final_directory ) + { + if ( mkdir( p, 0711 ) ) + { + free( p ); + return -1; + } + } + + free( p ); + + return 0; +} + +void +set_name ( const char *name ) +{ + if ( session_name ) + free( session_name ); + + char *s = strdup( name ); + + session_name = strdup( basename( s ) ); + + free( s ); +} + +bool +address_matches ( lo_address addr1, lo_address addr2 ) +{ + char *url1 = lo_address_get_url( addr1 ); + char *url2 = lo_address_get_url( addr2 ); + + bool r = !strcmp( url1, url2 ); + + free( url1 ); + free( url2 ); + + return r; +} + +Client * +get_client_by_id ( std::list *cl, const char *id ) +{ + for ( std::list::const_iterator i = cl->begin(); + i != cl->end(); + ++i ) + if ( !strcmp( (*i)->client_id, id ) ) + return *i; + + return NULL; +} + + +Client * +get_client_by_name_and_id ( std::list *cl, const char *name, const char *id ) +{ + for ( std::list::const_iterator i = cl->begin(); + i != cl->end(); + ++i ) + if ( !strcmp( (*i)->client_id, id ) && + ! strcmp( (*i)->name, name ) ) + return *i; + + return NULL; +} + +Client * +get_client_by_address ( lo_address addr ) +{ + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + if ( (*i)->addr && address_matches( (*i)->addr, addr ) ) + return *i; + + return NULL; +} + +bool +replies_still_pending ( ) +{ + for ( std::list::const_iterator i = client.begin(); + i != client.end(); + ++i ) + if ( (*i)->active && (*i)->reply_pending() ) + return true; + + return false; +} + +char * +generate_client_id ( Client *c ) +{ + char id_str[6]; + + id_str[0] = 'n'; + id_str[5] = 0; + + for ( int i = 1; i < 5; i++) + id_str[i] = 'A' + (rand() % 25); + + return strdup(id_str); +} + +void +wait_for_replies ( ) +{ + if ( ! replies_still_pending() ) + return; + + fprintf( stdout, "Waiting..." ); + fflush(stdout); + + int n = 7; + + while ( n-- ) + { + printf( "." ); + fflush(stdout); + + osc_server->wait( 1000 ); + + if ( ! replies_still_pending() ) + break; + } + + /* FIXME: do something about unresponsive clients */ +} + + +char * +get_client_project_path ( const char *session_path, Client *c ) +{ + char *client_project_path; + + asprintf( &client_project_path, "%s/%s.%s", session_path, c->name, c->client_id ); + + return client_project_path; +} + +bool +launch ( const char *executable, const char *client_id ) +{ + Client *c; + + if ( !client_id || !( c = get_client_by_id( &client, client_id ) ) ) + { + c = new Client(); + + c->executable_path = strdup( executable ); + + { + char *s = strdup( c->executable_path ); + + c->name = strdup( basename( s ) ); + + free( s ); + } + + if ( client_id ) + c->client_id = strdup( client_id ); + else + c->client_id = generate_client_id( c ); + + client.push_back( c ); + } + + char * url = osc_server->url(); + + int pid; + if ( ! (pid = fork()) ) + { + MESSAGE( "Launching %s\n", executable ); + + char *args[] = { strdup( executable ), NULL }; + + setenv( "NSM_URL", url, 1 ); + + if ( -1 == execvp( executable, args ) ) + { + WARNING( "Error starting process: %s", strerror( errno ) ); + + exit(-1); + } + } + + c->pid = pid; + + MESSAGE( "Process has pid: %i", pid ); + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->name ); + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "launch" ); + } + + return true; +} + +void +command_client_to_save ( Client *c ) +{ + if ( c->active ) + { + MESSAGE( "Telling %s to save", c->name ); + osc_server->send( c->addr, "/nsm/client/save" ); + + c->pending_command = COMMAND_SAVE; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "save" ); + } + else if ( c->is_dumb_client() ) + { + // this is a dumb client... + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "noop" ); + } +} + +void command_client_to_switch ( Client *c, const char *new_client_id ) +{ + char *old_client_id = c->client_id; + + c->client_id = strdup( new_client_id ); + + char *client_project_path = get_client_project_path( session_path, c ); + + MESSAGE( "Commanding %s to switch \"%s\"", c->name, client_project_path ); + + char *full_client_id; + asprintf( &full_client_id, "%s.%s", c->name, c->client_id ); + + osc_server->send( c->addr, "/nsm/client/open", client_project_path, session_name, full_client_id ); + + free( full_client_id ); + free( client_project_path ); + + c->pending_command = COMMAND_OPEN; + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "switch" ); + osc_server->send( gui_addr, "/nsm/gui/client/switch", old_client_id, c->client_id ); + } + + free( old_client_id ); +} + +void +purge_inactive_clients ( ) +{ + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + if ( ! (*i)->active ) + { + delete *i; + i = client.erase( i ); + } + } +} + +/************************/ +/* OSC Message Handlers */ +/************************/ + +OSC_HANDLER( add ) +{ + if ( ! session_path ) + { + osc_server->send( lo_message_get_source( msg ), path, + ERR_NO_SESSION_OPEN, + "Cannot add to session because no session is loaded." ); + + + return 0; + } + + if ( ! launch( &argv[0]->s, NULL ) ) + { + osc_server->send( lo_message_get_source( msg ), path, + ERR_LAUNCH_FAILED, + "Failed to launch process!" ); + } + else + { + osc_server->send( lo_message_get_source( msg ), path, + ERR_OK, + "Launched." ); + } + + return 0; +} + +OSC_HANDLER( announce ) +{ + MESSAGE( "Got announce" ); + + const char *client_name = &argv[0]->s; + const char *capabilities = &argv[1]->s; + const char *executable_path = &argv[2]->s; + int major = argv[3]->i; + int minor = argv[4]->i; + int pid = argv[5]->i; + + bool expected_client = false; + + Client *c = NULL; + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + if ( ! strcmp( (*i)->executable_path, executable_path ) + && ! (*i)->active ) + { + // I think we've found the slot we were looking for. + MESSAGE( "Client was expected." ); + c = *i; + break; + } + } + + if ( ! c ) + { + c = new Client(); + c->executable_path = strdup( executable_path ); + c->client_id = generate_client_id( c ); + } + else + expected_client = true; + + if ( major > NSM_API_VERSION_MAJOR ) + { + DMESSAGE( "Client is using incompatible and more recent API version %i.%i", major, minor ); + + osc_server->send( lo_message_get_source( msg ), "/error", + ERR_INCOMPATIBLE_API, + "Server is using an incompatible API version." ); + + return 0; + } + + c->pid = pid; + c->capabilities = strdup( capabilities ); + c->addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) )); + c->name = strdup( client_name ); + c->active = true; + + MESSAGE( "Process has pid: %i", pid ); + + if ( ! expected_client ) + client.push_back( c ); + + MESSAGE( "The client \"%s\" at \"%s\" informs us it's ready to receive commands.", &argv[0]->s, lo_address_get_url( c->addr ) ); + + osc_server->send( lo_message_get_source( msg ), "/reply", + path, + expected_client ? + "Howdy, what took you so long?" : + "Well hello, stranger. Welcome to the party.", + APP_TITLE, + ":server-control:" ); + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->name ); + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "open" ); + } + + { + char *full_client_id; + asprintf( &full_client_id, "%s.%s", c->name, c->client_id ); + + char *client_project_path = get_client_project_path( session_path, c ); + + osc_server->send( lo_message_get_source( msg ), "/nsm/client/open", client_project_path, session_name, full_client_id ); + + free( full_client_id ); + free( client_project_path ); + } + + return 0; +} + +void +save_session_file ( ) +{ + char *session_file = NULL; + asprintf( &session_file, "%s/session.nsm", session_path ); + + FILE *fp = fopen( session_file, "w+" ); + + /* FIXME: handle errors. */ + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + fprintf( fp, "%s:%s:%s\n", (*i)->name, (*i)->executable_path, (*i)->client_id ); + } + + fclose( fp ); +} + +Client * +client_by_name ( const char *name, + std::list *cl ) +{ + for ( std::list::iterator i = cl->begin(); + i != cl->end(); + ++i ) + { + if ( !strcmp( name, (*i)->name ) ) + return *i; + } + + return NULL; +} + +// capability should be enclosed in colons. I.e. ":switch:" +bool +client_is_capable_of ( Client *c, const char *capability ) +{ + return c->capabilities && + strstr( c->capabilities, capability ); +} + +bool +dumb_clients_are_alive ( ) +{ + std::list *cl = &client; + + for ( std::list::iterator i = cl->begin(); + i != cl->end(); + ++i ) + { + if ( (*i)->is_dumb_client() && (*i)->pid > 0 ) + return true; + } + + return false; +} + +void +wait_for_dumb_clients_to_die ( ) +{ + struct signalfd_siginfo fdsi; + + MESSAGE( "Waiting for any dumb clients to die." ); + + for ( int i = 0; i < 6; i++ ) + { + MESSAGE( "Loop %i", i ); + + if ( ! dumb_clients_are_alive() ) + break; + + ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); + + if (s == sizeof(struct signalfd_siginfo)) + { + if (fdsi.ssi_signo == SIGCHLD) + handle_sigchld(); + } + + usleep( 50000 ); + } + + /* FIXME: give up on remaining clients and purge them */ +} + + +bool +killed_clients_are_alive ( ) +{ + std::list *cl = &client; + + for ( std::list::iterator i = cl->begin(); + i != cl->end(); + ++i ) + { + if ( ( (*i)->pending_command == COMMAND_QUIT || + (*i)->pending_command == COMMAND_KILL ) && + (*i)->pid > 0 ) + return true; + } + + return false; +} + +void +wait_for_killed_clients_to_die ( ) +{ + struct signalfd_siginfo fdsi; + + MESSAGE( "Waiting for killed clients to die." ); + + for ( int i = 0; i < 24; i++ ) + { + MESSAGE( "Loop %i", i ); + + if ( ! killed_clients_are_alive() ) + break; + + ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); + + if (s == sizeof(struct signalfd_siginfo)) + { + if (fdsi.ssi_signo == SIGCHLD) + handle_sigchld(); + } + + usleep( 200 * 1000 ); + } + + if ( killed_clients_are_alive() ) + { + WARNING( "Killed clients are still alive" ); + /* FIXME: give up on remaining clients and purge them */ + } + else + MESSAGE( "All clients have died." ); + +} + + +void +command_all_clients_to_save ( ) +{ + if ( session_path ) + { + MESSAGE( "Commanding attached clients to save." ); + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + command_client_to_save( *i ); + } + + wait_for_replies(); + + save_session_file(); + } +} + + +void +command_client_to_quit ( Client *c ) +{ + MESSAGE( "Commanding %s to quit", c->name ); + + if ( c->active ) + { + c->pending_command = COMMAND_QUIT; + + kill( c->pid, SIGTERM ); + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "quit" ); + } + else if ( c->is_dumb_client() ) + { + if ( c->pid > 0 ) + { + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "kill" ); + + /* should be kill? */ + c->pending_command = COMMAND_KILL; + + // this is a dumb client... try and kill it + kill( c->pid, SIGTERM ); + } + else + { + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "removed" ); + } + } +} + +void +close_all_clients ( ) +{ + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + command_client_to_quit( *i ); + } + + wait_for_killed_clients_to_die(); + + purge_inactive_clients(); + + clear_clients(); + + if ( session_path ) + { + free(session_path); + session_path = NULL; + free(session_name); + session_name = NULL; + } + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/session/name", "" ); + } +} + +void +tell_client_session_is_loaded( Client *c ) +{ + if ( c->active && !c->is_dumb_client() ) + { + osc_server->send( c->addr, "/nsm/client/session_is_loaded" ); + } +} + +void +tell_all_clients_session_is_loaded ( void ) +{ + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + tell_client_session_is_loaded( *i ); + } +} + +bool +load_session_file ( const char * path ) +{ + char *session_file = NULL; + asprintf( &session_file, "%s/session.nsm", path ); + + session_path = strdup( path ); + + set_name( path ); + + FILE *fp; + + if ( ! ( fp = fopen( session_file, "r" ) ) ) + { + return false; + } + + std::list new_clients; + + { + char * client_name = NULL; + char * client_executable = NULL; + char * client_id = NULL; + + // load the client list + while ( fscanf( fp, "%a[^:]:%a[^:]:%a[^:\n]\n", &client_name, &client_executable, &client_id ) > 0 ) + { + Client *c = new Client(); + + c->name = client_name; + c->executable_path = client_executable; + c->client_id = client_id; + + new_clients.push_back( c ); + } + } + + MESSAGE( "Commanding unneeded and dumb clients to quit" ); + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + if ( ! client_is_capable_of( *i, ":switch:" ) + || + ! client_by_name( (*i)->name, &new_clients ) ) + { + command_client_to_quit( *i ); + } + } + +// wait_for_replies(); + + wait_for_killed_clients_to_die(); + +// wait_for_dumb_clients_to_die(); + + purge_inactive_clients(); + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + (*i)->pre_existing = true; + } + + MESSAGE( "Commanding smart clients to switch" ); + + for ( std::list::iterator i = new_clients.begin(); + i != new_clients.end(); + ++i ) + { + Client *c = NULL; + + /* in a duplicated session, clients will have the same + * IDs, so be sure to pick the right one to avoid race + * conditions in JACK name registration. */ + c = get_client_by_name_and_id( &client, (*i)->name, (*i)->client_id ); + + if ( ! c ) + c = client_by_name( (*i)->name, &client ); + + if ( c && c->pre_existing && !c->reply_pending() ) + { + // since we already shutdown clients not capable of 'switch', we can assume that these are. + command_client_to_switch( c, (*i)->client_id ); + } + else + { + /* sleep a little bit because liblo derives its sequence + * of port numbers from the system time (second + * resolution) and if too many clients start at once they + * won't be able to find a free port. */ + usleep( 100 * 1000 ); + + launch( (*i)->executable_path, (*i)->client_id ); + } + } + + wait_for_replies(); + + tell_all_clients_session_is_loaded(); + + MESSAGE( "Loaded." ); + + new_clients.clear(); + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/session/name", session_name ); + } + + return true; +} + +OSC_HANDLER( save ) +{ + if ( ! session_path ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_NO_SESSION_OPEN, + "No session to save."); + + return 0; + } + + command_all_clients_to_save(); + + MESSAGE( "Done." ); + + osc_server->send( lo_message_get_source( msg ), "/reply", path, "Saved." ); + + return 0; +} + +OSC_HANDLER( duplicate ) +{ + if ( ! session_path ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_NO_SESSION_OPEN, + "No session to duplicate."); + + return 0; + } + + if ( ! path_is_valid( &argv[0]->s ) ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_CREATE_FAILED, + "Invalid session name." ); + + return 0; + } + + command_all_clients_to_save(); + + if ( clients_have_errors() ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_GENERAL_ERROR, + "Some clients could not save" ); + + return 0; + } + +// save_session_file(); + + const char *src = session_path; + + char *spath; + asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); + + mkpath( spath, false ); + + /* FIXME: code a recursive copy instead of calling the shell */ + + char *cmd; + asprintf( &cmd, "cp -R \"%s\" \"%s\"", session_path, spath); + + system( cmd ); + + free( cmd ); + + osc_server->send( gui_addr, "/nsm/gui/session/session", &argv[0]->s ); + + MESSAGE( "Attempting to open %s", spath ); + + if ( load_session_file( spath ) ) + { + MESSAGE( "Loaded" ); + osc_server->send( lo_message_get_source( msg ), "/reply", path, + "Loaded." ); + } + else + { + MESSAGE( "Failed" ); + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_NO_SUCH_FILE, + "No such file." ); + } + + free( spath ); + + MESSAGE( "Done" ); + + + osc_server->send( lo_message_get_source( msg ), "/reply", path, "Duplicated." ); + + return 0; +} + +OSC_HANDLER( new ) +{ + if ( ! path_is_valid( &argv[0]->s ) ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_CREATE_FAILED, + "Invalid session name." ); + + return 0; + } + + if ( session_path ) + { + command_all_clients_to_save(); + + close_all_clients(); + } + + MESSAGE( "Creating new session" ); + + + char *spath; + asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); + + if ( mkpath( spath, true ) ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_CREATE_FAILED, + "Could not create the session directory" ); + + free(spath); + + return 0; + } + + session_path = strdup( spath ); + + set_name( session_path ); + + osc_server->send( lo_message_get_source( msg ), "/reply", path, "Created." ); + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/session/session", &argv[0]->s ); + osc_server->send( gui_addr, "/nsm/gui/session/name", &argv[0]->s ); + } + + save_session_file(); + + free( spath ); + + osc_server->send( lo_message_get_source( msg ), "/reply", path, + "Session created" ); + + return 0; +} + +static lo_address list_response_address; + +int +list_file ( const char *fpath, const struct stat *sb, int tflag ) +{ + char *s; + + if ( tflag == FTW_F ) + { + s = strdup( fpath ); + if ( ! strcmp( "session.nsm", basename( s ) ) ) + { + free( s ); + s = strdup( fpath ); + + s = dirname( s ); + + memmove( s, s + strlen( session_root ) + 1, (strlen( s ) - strlen( session_root )) + 1); + + osc_server->send( list_response_address, "/reply", "/nsm/server/list", s ); + + free( s ); + } + else + free( s ); + } + + return 0; +} + +OSC_HANDLER( list ) +{ + MESSAGE( "Listing sessions" ); + + list_response_address = lo_message_get_source( msg ); + + ftw( session_root, list_file, 20 ); + + osc_server->send( lo_message_get_source( msg ), path, + ERR_OK, + "Done." ); + + return 0; +} + +OSC_HANDLER( open ) +{ + DMESSAGE( "Got open" ); + + if ( session_path ) + { + command_all_clients_to_save(); + + if ( clients_have_errors() ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_GENERAL_ERROR, + "Some clients could not save" ); + return 0; + } + +// save_session_file(); + } + + char *spath; + asprintf( &spath, "%s/%s", session_root, &argv[0]->s ); + + MESSAGE( "Attempting to open %s", spath ); + + if ( load_session_file( spath ) ) + { + MESSAGE( "Loaded" ); + osc_server->send( lo_message_get_source( msg ), "/reply", path, + "Loaded." ); + } + else + { + MESSAGE( "Failed" ); + osc_server->send( lo_message_get_source( msg ), "/reply", path, + ERR_NO_SUCH_FILE, + "No such file." ); + } + + free( spath ); + + MESSAGE( "Done" ); + + return 0; +} + + +OSC_HANDLER( quit ) +{ + close_all_clients(); + + exit(0); + + return 0; +} + +OSC_HANDLER( abort ) +{ + if ( ! session_path ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_NO_SESSION_OPEN, + "No session to abort." ); + + return 0; + } + + MESSAGE( "Commanding attached clients to quit." ); + + close_all_clients(); + + osc_server->send( lo_message_get_source( msg ), "/reply", path, + "Aborted." ); + + MESSAGE( "Done" ); + + return 0; +} + +OSC_HANDLER( close ) +{ + if ( ! session_path ) + { + osc_server->send( lo_message_get_source( msg ), "/error", path, + ERR_NO_SESSION_OPEN, + "No session to close." ); + + return 0; + } + + command_all_clients_to_save(); + + MESSAGE( "Commanding attached clients to quit." ); + + close_all_clients(); + + osc_server->send( lo_message_get_source( msg ), "/reply", path, + "Closed." ); + + MESSAGE( "Done" ); + + return 0; +} + + +OSC_HANDLER( broadcast ) +{ + const char *to_path = &argv[0]->s; + + std::list new_args; + + for ( int i = 1; i < argc; ++i ) + { + switch ( types[i] ) + { + case 's': + new_args.push_back( OSC::OSC_String( &argv[i]->s ) ); + break; + case 'i': + new_args.push_back( OSC::OSC_Int( argv[i]->i ) ); + break; + case 'f': + new_args.push_back( OSC::OSC_Float( argv[i]->f ) ); + break; + } + } + + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + if ( (*i)->addr != lo_message_get_source( msg ) ) + { + osc_server->send( (*i)->addr, to_path, new_args ); + } + } + + return 0; +} + + + +/*********************************/ +/* Client Informational Messages */ +/*********************************/ + +OSC_HANDLER( progress ) +{ + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + if ( c ) + { + c->progress = argv[0]->f; + + MESSAGE( "%s progress: %i%%", c->name, (int)(c->progress * 100.0f) ); + + if ( gui_is_active ) + { + osc_server->send( gui_addr, "/nsm/gui/client/progress", c->client_id, (float)c->progress ); + } + } + + return 0; +} + +OSC_HANDLER( is_dirty ) +{ + MESSAGE( "Client sends dirty" ); + + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + c->dirty = 1; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/dirty", c->client_id, c->dirty ); + + return 0; +} + +OSC_HANDLER( is_clean ) +{ + MESSAGE( "Client sends clean" ); + + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + c->dirty = 0; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/dirty", c->client_id, c->dirty ); + + return 0; +} + + +OSC_HANDLER( message ) +{ + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/message", c->client_id, argv[0]->i, &argv[1]->s ); + + return 0; +} + +/**********************/ +/* Response Handlers */ +/**********************/ + + +OSC_HANDLER( error ) +{ + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + const char *rpath = &argv[0]->s; + + int err_code = argv[1]->i; + + const char *message = &argv[2]->s; + + c->set_reply( err_code, message ); + + MESSAGE( "Client \"%s\" replied with error: %s (%i)", c->name, message, err_code ); + c->pending_command = COMMAND_NONE; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "error" ); + + return 0; +} + + +OSC_HANDLER( reply ) +{ + Client *c = get_client_by_address( lo_message_get_source( msg ) ); + + const char *rpath = &argv[0]->s; + const char *message = &argv[1]->s; + + if ( c ) + { + c->set_reply( ERR_OK, message ); + + MESSAGE( "Client \"%s\" replied with: %s", c->name, message ); + + c->pending_command = COMMAND_NONE; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "ready" ); + } + else + MESSAGE( "Reply from unknown client" ); + + return 0; +} + + + +/******************/ +/* GUI operations */ +/******************/ + +OSC_HANDLER( remove ) +{ + Client *c = get_client_by_id( &client, &argv[0]->s ); + + if ( c ) + { + if ( c->pid == 0 && + ! c->active ) + { + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status = "removed" ); + + client.remove( c ); + + delete c; + + if ( gui_is_active ) + osc_server->send( gui_addr, "/reply", "Client removed." ); + } + } + else + { + if ( gui_is_active ) + osc_server->send( gui_addr, "/error", -10, "No such client." ); + } + + + return 0; +} + +OSC_HANDLER( resume ) +{ + Client *c = get_client_by_id( &client, &argv[0]->s ); + + if ( c->pid == 0 && + ! c->active ) + { + + if ( ! launch( c->executable_path, c->client_id ) ) + { + + } + } + + return 0; +} + + +OSC_HANDLER( client_save ) +{ + Client *c = get_client_by_id( &client, &argv[0]->s ); + + if ( c->active ) + { + command_client_to_save( c ); + } + + return 0; +} + +void +announce_gui( const char *url ) +{ + gui_addr = lo_address_new_from_url( url ); + gui_is_active = true; + + osc_server->send( gui_addr, "/nsm/gui/announce", "hi" ); + + for ( std::list::iterator i = client.begin(); + i != client.end(); + ++i ) + { + Client *c = *i; + + osc_server->send( gui_addr, "/nsm/gui/client/new", c->client_id, c->name ); + osc_server->send( gui_addr, "/nsm/gui/client/status", c->client_id, c->status ); + + } + + osc_server->send( gui_addr, "/nsm/gui/session/name", session_name ? session_name : "" ); + + DMESSAGE( "Registered with GUI" ); +} + + +OSC_HANDLER( gui_announce ) +{ + announce_gui( lo_address_get_url( lo_message_get_source( msg ) )); + + return 0; +} + +OSC_HANDLER( ping ) +{ + osc_server->send( lo_message_get_source( msg ), "/reply", path ); + + return 0; +} + + + + +int main(int argc, char *argv[]) +{ + sigset_t mask; + + sigemptyset( &mask ); + sigaddset( &mask, SIGCHLD ); + + sigprocmask(SIG_BLOCK, &mask, NULL ); + + signal_fd = signalfd( -1, &mask, SFD_NONBLOCK ); + + /* generate random seed for client ids */ + { + time_t seconds; + time(&seconds); + + srand( (unsigned int) seconds ); + } + + char *osc_port = "6666"; + const char *gui_url = NULL; + + static struct option long_options[] = + { + { "osc-port", required_argument, 0, 'p' }, + { "gui-url", required_argument, 0, 'g' }, + { "help", no_argument, 0, 'h' }, + { 0, 0, 0, 0 } + }; + + int option_index = 0; + int c = 0; + + while ( ( c = getopt_long_only( argc, argv, "", long_options, &option_index ) ) != -1 ) + { + switch ( c ) + { + case 'p': + DMESSAGE( "Using OSC port %s", optarg ); + osc_port = optarg; + break; + case 'g': + DMESSAGE( "Going to connect to GUI at: %s", optarg ); + gui_url = optarg; + break; + case 'h': + printf( "Usage: nsmd [--osc-port portnum]\n\n" ); + exit(0); + break; + } + } + + asprintf( &session_root, "%s/%s", getenv( "HOME" ), "NSM Sessions" ); + + struct stat st; + + if ( stat( session_root, &st ) ) + { + if ( mkdir( session_root, 0771 ) ) + { + FATAL( "Failed to create session directory: %s", strerror( errno ) ); + } + } + + MESSAGE( "Session root is: %s", session_root ); + + osc_server = new OSC::Endpoint(); + + if ( osc_server->init( osc_port ) ) + { + FATAL( "Failed to create OSC server." ); + } + + printf( "NSM_URL=%s\n", osc_server->url() ); + + if ( gui_url ) + { + announce_gui( gui_url ); + } + + /* */ + osc_server->add_method( "/nsm/server/announce", "sssiii", OSC_NAME( announce ), NULL, "client_name,capabilities,executable,api_version_major,api_version_minor,client_pid" ); + + /* response handlers */ + osc_server->add_method( "/reply", "ss", OSC_NAME( reply ), NULL, "err_code,msg" ); + osc_server->add_method( "/error", "sis", OSC_NAME( error ), NULL, "err_code,msg" ); + + osc_server->add_method( "/nsm/client/progress", "f", OSC_NAME( progress ), NULL, "progress" ); + osc_server->add_method( "/nsm/client/is_dirty", "", OSC_NAME( is_dirty ), NULL, "dirtiness" ); + osc_server->add_method( "/nsm/client/is_clean", "", OSC_NAME( is_clean ), NULL, "dirtiness" ); + osc_server->add_method( "/nsm/client/message", "is", OSC_NAME( message ), NULL, "message" ); + + /* */ + osc_server->add_method( "/nsm/gui/announce", "", OSC_NAME( gui_announce ), NULL, "" ); + osc_server->add_method( "/nsm/gui/client/remove", "s", OSC_NAME( remove ), NULL, "client_id" ); + osc_server->add_method( "/nsm/gui/client/resume", "s", OSC_NAME( resume ), NULL, "client_id" ); + osc_server->add_method( "/nsm/gui/client/save", "s", OSC_NAME( client_save ), NULL, "client_id" ); + + osc_server->add_method( "/osc/ping", "", OSC_NAME( ping ), NULL, "" ); + + osc_server->add_method( "/nsm/server/broadcast", NULL, OSC_NAME( broadcast ), NULL, "" ); + osc_server->add_method( "/nsm/server/duplicate", "s", OSC_NAME( duplicate ), NULL, "" ); + osc_server->add_method( "/nsm/server/abort", "", OSC_NAME( abort ), NULL, "" ); + osc_server->add_method( "/nsm/server/list", "", OSC_NAME( list ), NULL, "" ); + osc_server->add_method( "/nsm/server/add", "s", OSC_NAME( add ), NULL, "commandline" ); + osc_server->add_method( "/nsm/server/new", "s", OSC_NAME( new ), NULL, "name" ); + osc_server->add_method( "/nsm/server/save", "", OSC_NAME( save ), NULL, "" ); + osc_server->add_method( "/nsm/server/open", "s", OSC_NAME( open ), NULL, "name" ); + osc_server->add_method( "/nsm/server/close", "", OSC_NAME( close ), NULL, "" ); + osc_server->add_method( "/nsm/server/quit", "", OSC_NAME( quit ), NULL, "" ); + + struct signalfd_siginfo fdsi; + + /* listen for sigchld signals and process OSC messages forever */ + for ( ;; ) + { + ssize_t s = read(signal_fd, &fdsi, sizeof(struct signalfd_siginfo)); + + if (s == sizeof(struct signalfd_siginfo)) + { + if (fdsi.ssi_signo == SIGCHLD) + handle_sigchld(); + } + + osc_server->wait( 1000 ); + } + +// osc_server->run(); + + return 0; +} diff --git a/session/src/send_osc.C b/session/src/send_osc.C new file mode 100644 index 0000000..76396d7 --- /dev/null +++ b/session/src/send_osc.C @@ -0,0 +1,112 @@ + +/*******************************************************************************/ +/* Copyright (C) 2010 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 +#include +#include +#include + +/* helper macros for defining OSC handlers */ +#define OSC_NAME( name ) osc_ ## name +// #define OSCDMSG() DMESSAGE( "Got OSC message: %s", path ); +#define OSC_HANDLER( name ) static int OSC_NAME( name ) ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + + +static bool got_response = false; + +/************************/ +/* OSC Message Handlers */ +/************************/ + +OSC_HANDLER( reply ) +{ +// OSCDMSG(); + + printf( "Reply: " ); + + for ( int i = 0; i < argc; ++i ) + { + switch ( types[i] ) + { + case 's': + printf( "\"%s\" ", &argv[i]->s ); + break; + case 'f': + printf( "%f ", argv[i]->f ); + break; + case 'i': + printf( "%i ", argv[i]->i ); + break; + } + } + + printf( "\n" ); + + got_response = true; + + return 0; +} + + + +int main(int argc, char *argv[]) +{ + OSC::Endpoint s; + + s.init( NULL ); + + s.add_method( NULL, NULL, OSC_NAME( reply ), 0, ""); + + int r; + + std::list args; + + for ( int i = 3; i < argc; ++i ) + { + const char *s = argv[i]; + + if ( strspn( s, "+-0123456789" ) == strlen( s ) ) + { + args.push_back( OSC::OSC_Int( atol( s ) ) ); + } + else if ( strspn( s, ".+-0123456789" ) == strlen( s ) ) + args.push_back( OSC::OSC_Float( atof( s ) ) ); + else + { + args.push_back( OSC::OSC_String( s ) ); + } + } + + lo_address t = lo_address_new_from_url( argv[1] ); + + fprintf( stderr, "Sending to %s\n", argv[1] ); + + s.send( t, argv[2], args ); + + printf( "Waiting for reply...\n" ); + + while ( ! got_response ) + s.wait( 1000 * 30 ); + + return 0; +} diff --git a/session/src/session-manager.C b/session/src/session-manager.C new file mode 100644 index 0000000..df84b64 --- /dev/null +++ b/session/src/session-manager.C @@ -0,0 +1,763 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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 "OSC/Endpoint.H" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "debug.h" +#include +#include +#include + +#include "FL/Fl_Packscroller.H" + +#include +#include +#include + +#define APP_NAME "Non Session Manager" + +static lo_address nsm_addr = NULL; +static time_t last_ping_response; + +static OSC::Endpoint *osc_endpoint; + +class NSM_Client : public Fl_Group +{ + char *_client_id; + +// Fl_Box *client_name; + Fl_Progress *_progress; + Fl_Light_Button *_dirty; + Fl_Button *_remove_button; + Fl_Button *_restart_button; + +public: + + void + name ( const char *v ) + { + label( strdup( v ) ); + } + + void + client_id ( const char *v ) + { + if ( _client_id ) + free( _client_id ); + + _client_id = strdup( v ); + } + + void + progress ( float f ) + { + _progress->value( f ); + _progress->redraw(); + } + + void + dirty ( bool b ) + { + _dirty->value( b ); + _dirty->redraw(); + } + + void + stopped ( bool b ) + { + if ( b ) + { + _remove_button->show(); + _restart_button->show(); + color( FL_RED ); + redraw(); + } + else + { + _restart_button->hide(); + _remove_button->hide(); + } + + /* _restart_button->redraw(); */ + /* _remove_button->redraw(); */ + } + + void + pending_command ( const char *command ) + { + char *cmd = strdup( command ); + + free( (void*)_progress->label() ); + + _progress->label( cmd ); + + stopped( 0 ); + + if ( ! strcmp( command, "ready" ) ) + { + color( FL_GREEN ); +// _progress->value( 0.0f ); + } + else if ( ! strcmp( command, "quit" ) || + ! strcmp( command, "kill" ) || + ! strcmp( command, "error" ) ) + { + color( FL_RED ); + } + else if ( ! strcmp( command, "stopped" ) ) + { + stopped( 1 ); + } + else + { + color( FL_YELLOW ); + } + + redraw(); + } + + + static void + cb_button ( Fl_Widget *o, void * v ) + { + ((NSM_Client*)v)->cb_button( o ); + } + + void + cb_button ( Fl_Widget *o ) + { + if ( o == _dirty ) + { + MESSAGE( "Sending save."); + osc_endpoint->send( nsm_addr, "/nsm/gui/client/save", _client_id ); + } + if ( o == _remove_button ) + { + MESSAGE( "Sending remove."); + osc_endpoint->send( nsm_addr, "/nsm/gui/client/remove", _client_id ); + } + else if ( o == _restart_button ) + { + MESSAGE( "Sending resume" ); + osc_endpoint->send( nsm_addr, "/nsm/gui/client/resume", _client_id ); + } + } + + + const char * + client_id ( void ) + { return _client_id; } + + NSM_Client ( int X, int Y, int W, int H, const char *L ) : + Fl_Group( X, Y, W, H, L ) + { + + _client_id = NULL; + + align( FL_ALIGN_LEFT | FL_ALIGN_INSIDE ); + color( FL_RED ); + box( FL_UP_BOX ); + + { Fl_Progress *o = _progress = new Fl_Progress( ( X + W ) - ( W / 4) - 20, Y + 5, ( W / 4 ), H - 10, NULL ); + o->label( strdup( "launch" ) ); + o->minimum( 0.0f ); + o->maximum( 1.0f ); + } + { Fl_Light_Button *o = _dirty = new Fl_Light_Button( _progress->x() - 30, Y + 7, 25, 25 ); + o->box( FL_UP_BOX ); + o->type(0); + o->color(); + o->selection_color( FL_YELLOW ); + o->value( 0 ); + o->callback( cb_button, this ); + } + { Fl_Button *o = _remove_button = new Fl_Button( _progress->x() - 60, Y + 7, 25, 25 ); + o->box( FL_UP_BOX ); + o->type(0); + o->color( FL_RED ); + o->value( 0 ); + o->label( "X" ); + o->tooltip( "Remove" ); + o->hide(); + o->callback( cb_button, this ); + } + { Fl_Button *o = _restart_button = new Fl_Button( _progress->x() - 90, Y + 7, 25, 25 ); + o->box( FL_UP_BOX ); + o->type(0); + o->color( FL_GREEN ); + o->value( 0 ); + o->label( "@>" ); + o->tooltip( "Resume" ); + o->hide(); + o->callback( cb_button, this ); + } + + end(); + } +}; + + +class NSM_Controller : public Fl_Group +{ +public: + + Fl_Pack *clients_pack; + Fl_Pack *buttons_pack; + Fl_Button *close_button; + Fl_Button *abort_button; + Fl_Button *save_button; + Fl_Button *open_button; + Fl_Button *new_button; + Fl_Button *add_button; + Fl_Button *duplicate_button; + + Fl_Select_Browser *session_browser; + + static void cb_handle ( Fl_Widget *w, void *v ) + { + ((NSM_Controller*)v)->cb_handle( w ); + + } + + void + cb_handle ( Fl_Widget *w ) + { + if ( w == abort_button ) + { + if ( 0 == fl_choice( "Are you sure you want to abort this session? Unsaved changes will be lost.", "Abort", "Cancel", NULL ) ) + { + MESSAGE( "Sending abort." ); + osc_endpoint->send( nsm_addr, "/nsm/server/abort" ); + } + } + if ( w == close_button ) + { + MESSAGE( "Sending close." ); + osc_endpoint->send( nsm_addr, "/nsm/server/close" ); + } + else if ( w == save_button ) + { + MESSAGE( "Sending save." ); + osc_endpoint->send( nsm_addr, "/nsm/server/save" ); + } + else if ( w == open_button ) + { + const char *name = fl_input( "Open Session", NULL ); + + if ( ! name ) + return; + + MESSAGE( "Sending open for: %s", name ); + osc_endpoint->send( nsm_addr, "/nsm/server/open", name ); + } + else if ( w == duplicate_button ) + { + const char *name = fl_input( "New Session", NULL ); + + if ( ! name ) + return; + + MESSAGE( "Sending duplicate for: %s", name ); + osc_endpoint->send( nsm_addr, "/nsm/server/duplicate", name ); + } + else if ( w == session_browser ) + { + const char *name = session_browser->text( session_browser->value()); + + /* strip out formatting codes */ + + osc_endpoint->send( nsm_addr, "/nsm/server/open", index( name, ' ' ) + 1 ); + } + else if ( w == new_button ) + { + const char *name = fl_input( "New Session", NULL ); + + if ( !name ) + return; + + MESSAGE( "Sending new for: %s", name ); + osc_endpoint->send( nsm_addr, "/nsm/server/new", name ); + } + else if ( w == add_button ) + { + const char *name = fl_input( "Add Client" ); + + if ( !name ) + return; + + MESSAGE( "Sending add for: %s", name ); + osc_endpoint->send( nsm_addr, "/nsm/server/add", name ); + } + } + + void + ForwardSort( Fl_Browser *b ) { + for ( int t=1; t<=b->size(); t++ ) { + for ( int r=t+1; r<=b->size(); r++ ) { + if ( strcmp(b->text(t), b->text(r)) > 0 ) { + b->swap(t,r); + } + } + } + } + + void + sort_sessions ( void ) + { + ForwardSort( session_browser ); + } + + NSM_Client * + client_by_id ( const char *id ) + { + for ( int i = clients_pack->children(); i--; ) + { + NSM_Client *c = (NSM_Client*)clients_pack->child( i ); + + if ( ! strcmp( c->client_id(), id ) ) + { + return c; + } + } + return NULL; + } + + void + session_name ( const char *name ) + { + if ( clients_pack->label() ) + free( (char*)clients_pack->label() ); + + clients_pack->parent()->label( strdup( name ) ); + + redraw(); + } + + void + client_stopped ( const char *client_id ) + { + NSM_Client *c = client_by_id( client_id ); + + if ( c ) + { + c->stopped( 1 ); + } + } + + void + client_quit ( const char *client_id ) + { + NSM_Client *c = client_by_id( client_id ); + + if ( c ) + { + clients_pack->remove( c ); + delete c; + } + + if ( clients_pack->children() == 0 ) + { + ((Fl_Packscroller*)clients_pack->parent())->yposition( 0 ); + } + + parent()->redraw(); + } + + void + client_new ( const char *client_id, const char *client_name ) + { + + NSM_Client *c; + + c = client_by_id( client_id ); + + if ( c ) + { + c->name( client_name ); + return; + } + + c = new NSM_Client( 0, 0, w(), 40, NULL ); + + c->name( client_name ); + c->client_id( client_id ); + c->stopped( 0 ); + + clients_pack->add( c ); + + redraw(); + } + + void client_pending_command ( NSM_Client *c, const char *command ) + { + if ( c ) + { + if ( ! strcmp( command, "removed" ) ) + { + clients_pack->remove( c ); + delete c; + + parent()->redraw(); + } + else + c->pending_command( command ); + } + } + + + void add_session_to_list ( const char *name ) + { + char *s; + asprintf( &s, "@S18@C3 %s", name ); + session_browser->add( s ); + free(s); + } + + + NSM_Controller ( int X, int Y, int W, int H, const char *L ) : + Fl_Group( X, Y, W, H, L ) + { + align( FL_ALIGN_RIGHT | FL_ALIGN_CENTER | FL_ALIGN_INSIDE ); + + { Fl_Pack *o = buttons_pack = new Fl_Pack( X, Y, W, 30 ); + o->type( Fl_Pack::HORIZONTAL ); + o->box( FL_NO_BOX ); + { Fl_Button *o = open_button = new Fl_Button( 0, 0, 80, 50, "Open" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = close_button = new Fl_Button( 0, 0, 80, 50, "Close" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = abort_button = new Fl_Button( 0, 0, 80, 50, "Abort" ); + o->box( FL_UP_BOX ); + o->color( FL_RED ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = save_button = new Fl_Button( 0, 0, 80, 50, "Save" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = new_button = new Fl_Button( 0, 0, 80, 50, "New" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = duplicate_button = new Fl_Button( 0, 0, 100, 50, "Duplicate" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + { Fl_Button *o = add_button = new Fl_Button( 0, 0, 100, 100, "Add Client" ); + o->box( FL_UP_BOX ); + o->callback( cb_handle, (void*)this ); + } + + o->end(); + add(o); + } + { Fl_Tile *o = new Fl_Tile( X, Y + 50, W, H - 50 ); + { + Fl_Select_Browser *o = session_browser = new Fl_Select_Browser( X, Y + 50, W / 3, H - 50 ); + o->callback( cb_handle, (void *)this ); + o->color( fl_darker( FL_GRAY ) ); + o->box( FL_ROUNDED_BOX ); + o->label( "Sessions" ); + } + { + Fl_Packscroller *o = new Fl_Packscroller( X + ( W / 3 ), Y + 50, ( W / 3 ) * 2, H - 50 ); + o->align( FL_ALIGN_TOP ); + o->labeltype( FL_SHADOW_LABEL ); + { + Fl_Pack *o = clients_pack = new Fl_Pack( X + ( W / 3 ), Y + 50, ( W / 3 ) * 2, H - 50 ); + o->align( FL_ALIGN_TOP ); + o->type( Fl_Pack::VERTICAL ); + o->end(); + Fl_Group::current()->resizable( o ); + } + o->end(); + } + o->end(); + } + + Fl_Group::current()->resizable( this ); + + end(); + + deactivate(); + } + + int min_h ( void ) + { + return 500; + } + + void + ping ( void ) + { + if ( nsm_addr ) + { + osc_endpoint->send( nsm_addr, "/osc/ping" ); + } + + if ( last_ping_response ) + { + if ( time(NULL) - last_ping_response > 10 ) + { + if ( active() ) + { + deactivate(); + fl_alert( "Server is not responding..." ); + } + } + } + } + + int init_osc ( void ) + { + osc_endpoint = new OSC::Endpoint(); + + if ( int r = osc_endpoint->init() ) + return r; + + osc_endpoint->owner = this; + + osc_endpoint->url(); + + osc_endpoint->add_method( "/error", "sis", osc_handler, osc_endpoint, "msg" ); + osc_endpoint->add_method( "/reply", "ss", osc_handler, osc_endpoint, "msg" ); + osc_endpoint->add_method( "/reply", "s", osc_handler, osc_endpoint, "" ); + + osc_endpoint->add_method( "/nsm/gui/announce", "s", osc_handler, osc_endpoint, "msg" ); + osc_endpoint->add_method( "/nsm/gui/session/session", "s", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/session/name", "s", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/client/new", "ss", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/client/status", "ss", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/client/switch", "ss", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/client/progress", "sf", osc_handler, osc_endpoint, "path,display_name" ); + osc_endpoint->add_method( "/nsm/gui/client/dirty", "si", osc_handler, osc_endpoint, "path,display_name" ); + + osc_endpoint->start(); + } + + + void announce ( const char *nsm_url ) + { + nsm_addr = lo_address_new_from_url( nsm_url ); + + osc_endpoint->send( nsm_addr, "/nsm/gui/announce" ); + } + +private: + + static int osc_handler ( const char *path, const char *types, lo_arg **argv, int argc, lo_message msg, void *user_data ) + { + OSC_DMSG(); + + NSM_Controller *controller = (NSM_Controller*)((OSC::Endpoint*)user_data)->owner; + + Fl::lock(); + + if ( !strcmp( path, "/nsm/gui/session/session" ) ) + { + controller->add_session_to_list( &argv[0]->s ); + controller->sort_sessions(); + } + else if ( !strcmp( path, "/nsm/gui/announce" ) ) + { + controller->activate(); + + if ( ! nsm_addr ) + nsm_addr = lo_address_new_from_url( lo_address_get_url( lo_message_get_source( msg ) ) ); + + osc_endpoint->send( nsm_addr, "/nsm/server/list" ); + } + else if ( !strcmp( path, "/nsm/gui/session/name" )) + { + controller->session_name( &argv[0]->s ); + } + else if (!strcmp( path, "/error" )) + { + int err = argv[1]->i; + + if ( err != 0 ) + fl_alert( "ERROR: %s failed with: %s", &argv[0]->s, &argv[2]->s ); + } + else if (!strcmp( path, "/reply" )) + { + if ( !strcmp( &argv[0]->s, "/nsm/server/list" ) ) + { + controller->add_session_to_list( &argv[1]->s ); + controller->sort_sessions(); + } + else if ( !strcmp( &argv[0]->s, "/osc/ping" ) ) + { + last_ping_response = time( NULL ); + } + else + MESSAGE( "%s says %s", &argv[0]->s, &argv[1]->s); + } + + if ( !strncmp( path, "/nsm/gui/client/", strlen( "/nsm/gui/client/" ) ) ) + { + if ( !strcmp( path, "/nsm/gui/client/new" )) + { + controller->client_new( &argv[0]->s, &argv[1]->s ); + } + else + { + NSM_Client *c = controller->client_by_id( &argv[0]->s ); + + if ( c ) + { + if ( !strcmp( path, "/nsm/gui/client/status" )) + { + controller->client_pending_command( c, &argv[1]->s ); + } + else if ( !strcmp( path, "/nsm/gui/client/progress" )) + { + c->progress( argv[1]->f ); + } + else if ( !strcmp( path, "/nsm/gui/client/dirty" )) + { + c->dirty( argv[1]->i ); + } + else if ( !strcmp( path, "/nsm/gui/client/switch" ) ) + { + c->client_id( &argv[1]->s ); + } + } + else + MESSAGE( "Got message %s from unknown client", path ); + } + } + + Fl::unlock(); + Fl::awake(); + + return 0; + } +}; + + +static NSM_Controller *controller; + +void +ping ( void *v ) +{ + controller->ping(); + Fl::repeat_timeout( 1.0, ping, NULL ); +} + +int +main (int argc, char **argv ) +{ + + Fl::get_system_colors(); + Fl::scheme( "plastic" ); + Fl::lock(); + + Fl_Double_Window *main_window; + + { + Fl_Double_Window *o = main_window = new Fl_Double_Window( 600, 800, APP_NAME ); + { + main_window->xclass( APP_NAME ); + + Fl_Widget *o = controller = new NSM_Controller( 0, 0, main_window->w(), main_window->h(), NULL ); + Fl_Group::current()->resizable(o); + } + o->end(); + + o->size_range( main_window->w(), controller->min_h(), 0, 0 ); + +// o->callback( (Fl_Callback*)cb_main, main_window ); + o->show( argc, argv ); + // o->show(); + } + + const char *nsm_url = getenv( "NSM_URL" ); + + if ( nsm_url ) + { + MESSAGE( "Found NSM URL of \"%s\" in environment, attempting to connect.", nsm_url ); + + if ( ! controller->init_osc() ) + { + controller->announce( nsm_url ); + } + else + FATAL( "Could not create OSC server" ); + } + else + { + if ( controller->init_osc() ) + FATAL( "Could not create OSC server" ); + + /* start a new daemon... */ + MESSAGE( "Starting daemon..." ); + + char *url = osc_endpoint->url(); + + if ( ! fork() ) + { + char *args[] = { "nsmd", "--gui-url", url, NULL }; + + if ( -1 == execvp( "nsmd", args ) ) + { + FATAL( "Error starting process: %s", strerror( errno ) ); + } + } + } + + Fl::add_timeout( 1.0, ping, NULL ); + Fl::run(); + + if ( ! nsm_url ) + { + MESSAGE( "Telling server to quit" ); + osc_endpoint->send( nsm_addr, "/nsm/server/quit" ); + } + + return 0; +} + + + + + + + + + + + + + + diff --git a/timeline/configure b/timeline/configure index d5301cf..e4fe852 100755 --- a/timeline/configure +++ b/timeline/configure @@ -27,5 +27,8 @@ require_command ar ar require_command makedepend makedepend require_package JACK 0.103.0 jack require_package sndfile 1.0.17 sndfile +require_package liblo 0.23 liblo + +test_version `version_of liblo` 0.26 || warn "Version $(version_of liblo) of liblo is slow to create servers. Consider upgrading to 0.26 or later" end diff --git a/timeline/makefile.inc b/timeline/makefile.inc index f6325b0..4b5f276 100644 --- a/timeline/makefile.inc +++ b/timeline/makefile.inc @@ -10,7 +10,7 @@ Timeline_SRCS:=$(Timeline_SRCS:.fl=.C) Timeline_SRCS:=$(sort $(Timeline_SRCS)) Timeline_OBJS:=$(Timeline_SRCS:.C=.o) -Timeline_LIBS := $(FLTK_LIBS) $(JACK_LIBS) $(SNDFILE_LIBS) +Timeline_LIBS := $(FLTK_LIBS) $(JACK_LIBS) $(SNDFILE_LIBS) $(LIBLO_LIBS) src/timeline: $(Timeline_OBJS) FL/libfl_widgets.a nonlib/libnonlib.a @ echo -n Linking timeline... diff --git a/timeline/src/Engine/Engine.C b/timeline/src/Engine/Engine.C index 2fcdd70..de92bea 100644 --- a/timeline/src/Engine/Engine.C +++ b/timeline/src/Engine/Engine.C @@ -34,6 +34,8 @@ Engine::Engine ( ) : _thread( "RT" ) { _buffers_dropped = 0; + + DMESSAGE( "Creating audio I/O engine" ); } Engine::~Engine ( ) diff --git a/timeline/src/NSM.C b/timeline/src/NSM.C new file mode 100644 index 0000000..aab2033 --- /dev/null +++ b/timeline/src/NSM.C @@ -0,0 +1,83 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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 "const.h" +#include "debug.h" +#include "Timeline.H" +#include "TLE.H" +#include "NSM.H" +#include "Project.H" + +#define OSC_INTERVAL 0.2f + +extern char *instance_name; +extern Timeline *timeline; + +extern NSM_Client *nsm; + +NSM_Client::NSM_Client ( ) +{ +} + +int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ); +int command_save ( char **out_msg ); + +int +NSM_Client::command_save ( char **out_msg ) +{ + if ( timeline->command_save() ) + return ERR_OK; + else + { + *out_msg = strdup( "Failed to save for unknown reason"); + return ERR_GENERAL; + } +} + +int +NSM_Client::command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ) +{ + if ( instance_name ) + free( instance_name ); + + instance_name = strdup( client_id ); + + if ( Project::validate( name ) ) + { + if ( timeline->command_load( name, display_name ) ) + return ERR_OK; + else + { + *out_msg = strdup( "Failed to load for unknown reason" ); + return ERR_GENERAL; + } + } + else + { + if ( timeline->command_new( name, display_name ) ) + return ERR_OK; + else + { + *out_msg = strdup( "Failed to load for unknown reason" ); + return ERR_GENERAL; + } + } + + return 0; +} diff --git a/timeline/src/NSM.H b/timeline/src/NSM.H new file mode 100644 index 0000000..d0e6746 --- /dev/null +++ b/timeline/src/NSM.H @@ -0,0 +1,36 @@ + +/*******************************************************************************/ +/* Copyright (C) 2012 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. */ +/*******************************************************************************/ + +#pragma once + +#include "NSM/Client.H" + +class NSM_Client : public NSM::Client +{ + +public: + + NSM_Client ( ); + ~NSM_Client ( ) { }; + +protected: + + int command_open ( const char *name, const char *display_name, const char *client_id, char **out_msg ); + int command_save ( char **out_msg ); +}; diff --git a/timeline/src/Project.C b/timeline/src/Project.C index 70d8a12..f85e832 100644 --- a/timeline/src/Project.C +++ b/timeline/src/Project.C @@ -44,10 +44,15 @@ #include "file.h" #include "Block_Timer.H" +#include "Transport.H" + +extern Transport *transport; extern TLE *tle; const int PROJECT_VERSION = 1; +extern char *instance_name; + const char *Project::_errstr[] = @@ -200,6 +205,9 @@ Project::close ( void ) release_lock( &_lockfd, ".lock" ); + delete engine; + engine = NULL; + return true; } @@ -233,6 +241,25 @@ Project::validate ( const char *name ) return r; } +void +Project::make_engine ( void ) +{ + if ( engine ) + FATAL( "Engine should be null!" ); + + engine = new Engine; + + if ( ! engine->init( instance_name, JACK::Client::SLOW_SYNC | JACK::Client::TIMEBASE_MASTER )) + FATAL( "Could not connect to JACK!" ); + + timeline->sample_rate( engine->sample_rate() ); + + /* always start stopped (please imagine for me a realistic + * scenario requiring otherwise */ + transport->stop(); +} + + /** Try to open project /name/. Returns 0 if sucsessful, an error code * otherwise */ int @@ -262,13 +289,19 @@ Project::open ( const char *name ) if ( version != PROJECT_VERSION ) return E_VERSION; + /* normally, engine will be NULL after a close or on an initial open, + but 'new' will have already created it to get the sample rate. */ + if ( ! engine ) + make_engine(); + { Block_Timer timer( "Replayed journal" ); if ( ! Loggable::open( "history" ) ) return E_INVALID; } - timeline->sample_rate( rate ); + /* /\* really a good idea? *\/ */ + /* timeline->sample_rate( rate ); */ if ( creation_date ) { @@ -319,6 +352,9 @@ Project::create ( const char *name, const char *template_name ) mkdir( "sources", 0777 ); creat( "history", 0666 ); + if ( ! engine ) + make_engine(); + /* TODO: copy template */ write_info(); diff --git a/timeline/src/Project.H b/timeline/src/Project.H index b3860db..dc09573 100644 --- a/timeline/src/Project.H +++ b/timeline/src/Project.H @@ -33,9 +33,9 @@ class Project static bool write_info ( void ); static bool read_info ( int *version, nframes_t *sample_rate, char **creation_date, char **created_by ); - static void set_name ( const char *name ); static const char *_errstr[]; + static void make_engine ( void ); public: enum @@ -47,6 +47,8 @@ public: E_VERSION = -5 }; + static void set_name ( const char *name ); + static const char *errstr ( int n ) { return _errstr[ ( 0 - n ) - 1 ]; } static const char *name ( void ) { return Project::_name; } diff --git a/timeline/src/TLE.fl b/timeline/src/TLE.fl index 7aa1bba..a823e2d 100644 --- a/timeline/src/TLE.fl +++ b/timeline/src/TLE.fl @@ -67,43 +67,44 @@ decl {\#include "FL/About_Dialog.H"} {} decl {extern char project_display_name[256];} {global } +decl {\#include "NSM.H"} {} + +decl {extern NSM_Client *nsm;} {global +} + decl {extern char *user_config_dir;} {global } class TLE {open } { decl {Fl_Color system_colors[3];} {} + Function {save_options()} {open + } { + code { + + const char options_filename[] = "options"; + // const char state_filename[] = "state"; + + // save options + + char *path; + asprintf( &path, "%s/%s", user_config_dir, options_filename ); + ((Fl_Menu_Settings*)menubar)->dump( menubar->find_item( "&Options" ), path ); + free( path ); +} {} + } Function {save()} {open } { - code {const char options_filename[] = "options"; -// const char state_filename[] = "state"; - -// save options - -char *path; -asprintf( &path, "%s/%s", user_config_dir, options_filename ); -((Fl_Menu_Settings*)menubar)->dump( menubar->find_item( "&Options" ), path ); -free( path );} {} + code { +timeline->command_save();} {} } Function {quit()} {} { - code {Project::close(); - -save(); - -while ( Fl::first_window() ) Fl::first_window()->hide();} {} + code { +timeline->command_quit();} {} } Function {open( const char *name )} {} { - code {if ( ! name ) - return; - -int r = Project::open( name ); - -if ( r < 0 ) -{ - const char *s = Project::errstr( r ); - - fl_alert( "Could not open project \\"%s\\":\\n\\n\\t%s", name, s ); -}} {} + code { + timeline->command_load( name, NULL );} {} } Function {save_timeline_settings()} {open } { @@ -219,7 +220,7 @@ Loggable::progress_callback( &TLE::progress_cb, this );} {} Fl_Window main_window { label {Non DAW : Timeline} callback {if ( Fl::event_key() != FL_Escape ) - o->hide();} open + timeline->command_quit();} open private xywh {705 125 1025 770} type Double resizable xclass Non_DAW visible } { Fl_Menu_Bar menubar {open @@ -808,7 +809,7 @@ p->label( s );} {} update_progress( capture_buffer_progress, cbp, timeline->total_input_buffer_percent() ); update_progress( playback_buffer_progress, pbp, timeline->total_output_buffer_percent() ); -update_progress( cpu_load_progress, clp, engine->cpu_load() ); +update_progress( cpu_load_progress, clp, engine ? engine->cpu_load() : 0 ); update_progress( disk_usage_progress, dup, percent_used( "." ) ); @@ -820,15 +821,22 @@ if ( timeline->total_playback_xruns() ) static char stats[100]; +if ( engine && ! engine->zombified() ) +{ snprintf( stats, sizeof( stats ), "latency: %.1fms, xruns: %d", engine->frames_to_milliseconds( timeline->total_output_latency() ), engine->xruns() ); +} +else +{ + snprintf( stats, sizeof( stats ), "%s", "DISCONNECTED" ); +} stats_box->label( stats ); static bool zombie = false; -if ( engine->zombified() && ! zombie ) +if ( engine && engine->zombified() && ! zombie ) { zombie = true; fl_alert( "Disconnected from JACK!" ); @@ -840,6 +848,13 @@ sm_blinker->value( timeline->session_manager_name() != NULL ); sm_blinker->tooltip( timeline->session_manager_name() ); selected_blinker->value( timeline->nselected() ); seek_blinker->value( timeline->seek_pending() ); + +if ( timeline->session_manager_name() != NULL ) +{ + find_item( menubar, "&Project/&New" )->deactivate(); + find_item( menubar, "&Project/&Open" )->deactivate(); +} + project_name->redraw();} {} } Function {update_cb( void *v )} {open private return_type {static void} @@ -877,6 +892,7 @@ else if ( 0 == p ) static char pat[10]; +nsm->progress( p / 100.0f ); update_progress( progress, pat, p ); progress->redraw(); diff --git a/timeline/src/Timeline.C b/timeline/src/Timeline.C index a058417..0395870 100644 --- a/timeline/src/Timeline.C +++ b/timeline/src/Timeline.C @@ -46,6 +46,14 @@ #include "const.h" #include "debug.h" +/* these headers are just for the NSM support */ +#include "Project.H" +#include "TLE.H" +/* */ + +#include "NSM.H" +extern NSM_Client *nsm; + #ifdef USE_WIDGET_FOR_TIMELINE #define BASE Fl_Group #define redraw_overlay() @@ -74,6 +82,9 @@ bool Timeline::center_playhead = true; const float UPDATE_FREQ = 0.02f; +extern const char *instance_name; +extern TLE *tle; + /** return the combined height of all visible children of (veritcal) @@ -1472,3 +1483,66 @@ Timeline::remove_track ( Track *track ) /* FIXME: why is this necessary? doesn't the above add do DAMAGE_CHILD? */ redraw(); } + +/************/ +/* Commands */ +/************/ + +void +Timeline::command_quit ( ) +{ + Project::close(); + + command_save(); + + while ( Fl::first_window() ) Fl::first_window()->hide(); +} + +bool +Timeline::command_load ( const char *name, const char *display_name ) +{ + if ( ! name ) + return false; + + int r = Project::open( name ); + + if ( r < 0 ) + { + const char *s = Project::errstr( r ); + + fl_alert( "Could not open project \"%s\":\n\n\t%s", name, s ); + + return false; + } + + Project::set_name ( display_name ? display_name : name ); + + return true; +} + +bool +Timeline::command_save ( ) +{ + tle->save_options(); + + return true; +} + +bool +Timeline::command_new ( const char *name, const char *display_name ) +{ + return Project::create( name, NULL ); + + Project::set_name ( display_name ); + + /* FIXME: there's other stuff that needs to be done here! */ + /* tle->update_menu(); */ + + /* tle->main_window->redraw(); */ +} + +const char * +Timeline::session_manager_name ( void ) +{ + return nsm->session_manager_name(); +} diff --git a/timeline/src/Timeline.H b/timeline/src/Timeline.H index f17c854..a7e375b 100644 --- a/timeline/src/Timeline.H +++ b/timeline/src/Timeline.H @@ -31,6 +31,8 @@ #include #include +#include "OSC/Endpoint.H" + class Fl_Scroll; class Fl_Pack; class Fl_Scrollbar; @@ -149,7 +151,7 @@ public: void update_tempomap ( void ); - const char *session_manager_name ( void ) { return NULL; } + const char *session_manager_name ( void ); nframes_t fpp ( void ) const { return 1 << _fpp; } void range ( nframes_t start, nframes_t length ); @@ -221,6 +223,11 @@ public: void wait_for_buffers ( void ); bool seek_pending ( void ); + bool command_load ( const char *name, const char *display_name ); + bool command_new ( const char *name, const char *display_name ); + bool command_save ( void ); + void command_quit ( void ); + private: static void snapshot ( void *v ) { ((Timeline*)v)->snapshot(); } diff --git a/timeline/src/Transport.C b/timeline/src/Transport.C index e1d72f3..d578aa5 100644 --- a/timeline/src/Transport.C +++ b/timeline/src/Transport.C @@ -23,10 +23,6 @@ #include "Engine/Engine.H" -// Transport transport; - -#define client engine->jack_client() - Transport::Transport ( int X, int Y, int W, int H, const char *L ) @@ -36,6 +32,18 @@ Transport::Transport ( int X, int Y, int W, int H, const char *L ) rolling = false; _stop_disables_record = true; + bar = 0; + beat = 0; + tick = 0; + beats_per_minute = 120; + ticks_per_beat = 1920; + beat_type = 4; + beats_per_bar = 4; + next_time = 0; + frame_time =0; + frame_rate = 48000; + frame = 0; + const int bw = W / 3; type( HORIZONTAL ); @@ -156,9 +164,10 @@ Transport::handle ( int m ) void Transport::poll ( void ) { + jack_transport_state_t ts; - ts = jack_transport_query( client, this ); + ts = engine->transport_query( this ); rolling = ts == JackTransportRolling; } @@ -166,9 +175,12 @@ Transport::poll ( void ) void Transport::locate ( nframes_t frame ) { + if ( ! engine ) + return; + if ( ! recording ) // don't allow seeking while record is in progress - jack_transport_locate( client, frame ); + engine->transport_locate( frame ); } @@ -182,7 +194,8 @@ Transport::start ( void ) update_record_state(); } - jack_transport_start( client ); + if ( engine ) + engine->transport_start(); } void @@ -197,7 +210,8 @@ Transport::stop ( void ) update_record_state(); } - jack_transport_stop( client ); + if ( engine ) + engine->transport_stop(); } void diff --git a/timeline/src/main.C b/timeline/src/main.C index 49a1f93..e8719b0 100644 --- a/timeline/src/main.C +++ b/timeline/src/main.C @@ -39,7 +39,7 @@ #include "Track.H" #include "TLE.H" - +#include "Timeline.H" #include "../FL/Boxtypes.H" #include "Project.H" @@ -48,10 +48,15 @@ #include "Thread.H" +#include "NSM.H" + Engine *engine; Timeline *timeline; Transport *transport; TLE *tle; +NSM_Client *nsm; + +char *instance_name = NULL; /* TODO: put these in a header */ #define USER_CONFIG_DIR ".non-daw/" @@ -60,6 +65,8 @@ const char APP_NAME[] = "Non-DAW"; const char APP_TITLE[] = "The Non-DAW"; const char COPYRIGHT[] = "Copyright (C) 2008-2010 Jonathan Moore Liles"; +#define OSC_INTERVAL 0.2f + #define PACKAGE "non" @@ -96,6 +103,34 @@ shift ( char **argv, int *argc, int n ) argc -= n; } +extern Timeline *timeline; + +void +check_osc ( void * v ) +{ + nsm->check(); + Fl::repeat_timeout( OSC_INTERVAL, check_osc, v ); +} + +static int got_sigterm = 0; + +void +sigterm_handler ( int ) +{ + got_sigterm = 1; + Fl::awake(); +} + +void +check_sigterm ( void * ) +{ + if ( got_sigterm ) + { + MESSAGE( "Got SIGTERM, quitting..." ); + timeline->command_quit(); + } +} + int main ( int argc, char **argv ) { @@ -104,6 +139,8 @@ main ( int argc, char **argv ) Thread thread( "UI" ); thread.set(); + signal( SIGTERM, sigterm_handler ); + fl_register_images(); /* welcome to C++ */ @@ -129,32 +166,85 @@ main ( int argc, char **argv ) tle = new TLE; - MESSAGE( "Initializing JACK" ); + instance_name = strdup( APP_NAME ); /* we don't really need a pointer for this */ - engine = new Engine; + // will be created on project new/open + engine = NULL; - const char *jack_name; + nsm = new NSM_Client; - if ( ! ( jack_name = engine->init( APP_NAME, JACK::Client::SLOW_SYNC | JACK::Client::TIMEBASE_MASTER ) ) ) - FATAL( "Could not connect to JACK!" ); + const char *osc_port = NULL; - timeline->sample_rate( engine->sample_rate() ); + { + int r = argc - 1; + int i = 1; + for ( ; i < argc; ++i, --r ) - /* always start stopped (please imagine for me a realistic - * scenario requiring otherwise */ - transport->stop(); + if ( !strcmp( argv[i], "--osc-port" ) ) + { + if ( r > 1 ) + { + MESSAGE( "Using OSC port \"%s\"", argv[i+1] ); + osc_port = argv[i+1]; + --r; + ++i; + } + else + { + FATAL( "Missing OSC port" ); + } + } + - MESSAGE( "Starting GUI" ); + MESSAGE( "Starting GUI" ); - tle->run(); + tle->run(); - if ( argc > 1 ) - tle->open( argv[ 1 ] ); + char *nsm_url = getenv( "NSM_URL" ); + + if ( nsm_url ) + { + if ( ! nsm->init() ); + { + nsm->announce( nsm_url, APP_NAME, ":progress:switch:", argv[0] ); + } + } + else + { + if ( r >= 1 ) + { + MESSAGE( "Loading \"%s\"", argv[i] ); + + tle->open( argv[i] ); + + /* ) */ + /* { */ + /* fl_alert( "Error opening project specified on commandline" ); */ + /* } */ + } + } + + } + + /* poll so we can keep OSC handlers running in the GUI thread and avoid extra sync */ + Fl::add_timeout( OSC_INTERVAL, check_osc, NULL ); + + Fl::add_check( check_sigterm ); Fl::run(); + + if ( engine ) + { + delete engine; + engine = NULL; + } - delete engine; + delete tle; + tle = NULL; + delete nsm; + nsm = NULL; + MESSAGE( "Your fun is over" ); }