added remotePlugin-framework allowing to easily write plugins which actually run as external process

git-svn-id: https://lmms.svn.sf.net/svnroot/lmms/trunk/lmms@1518 0778d3d1-df1d-0410-868b-ea421aaaa00d
This commit is contained in:
Tobias Doerffel
2008-08-30 00:03:58 +00:00
parent d30a530482
commit d3763240b3
2 changed files with 1027 additions and 0 deletions

657
include/remote_plugin.h Executable file
View File

@@ -0,0 +1,657 @@
/*
* remote_plugin.h - base class providing RPC like mechanisms
*
* Copyright (c) 2008 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*
* This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
*
* 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 COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef _REMOTE_PLUGIN_H
#define _REMOTE_PLUGIN_H
#include "lmmsconfig.h"
#include "mixer.h"
#include "midi.h"
#ifdef LMMS_HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <vector>
#include <string>
#ifdef LMMS_HOST_X86_64
#define USE_NATIVE_SHM_API
#else
#if QT_VERSION >= 0x040400
#include <QtCore/QSharedMemory>
#else
#define USE_NATIVE_SHM_API
#endif
#endif
#ifdef USE_NATIVE_SHM_API
#ifdef LMMS_HAVE_SYS_IPC_H
#include <sys/ipc.h>
#endif
#ifdef LMMS_HAVE_SYS_SHM_H
#include <sys/shm.h>
#endif
#endif
#ifdef BUILD_REMOTE_PLUGIN_CLIENT
#define COMPILE_REMOTE_PLUGIN_BASE
#endif
inline int32_t readInt( int _fd )
{
int32_t i;
if( read( _fd, &i, sizeof( i ) ) == sizeof( i ) )
{
return( i );
}
return( 0 );
}
inline bool writeInt( const int32_t & _i, int _fd )
{
if( write( _fd, &_i, sizeof( _i ) ) != sizeof( _i ) )
{
return false;
}
return true;
}
static inline std::string readString( int _fd )
{
const int len = readInt( _fd );
if( len )
{
char * sc = new char[len + 1];
if( read( _fd, sc, len ) == len )
{
sc[len] = 0;
std::string s( sc );
delete[] sc;
return( s );
}
delete[] sc;
}
return std::string();
}
static inline bool writeString( const std::string & _s, int _fd )
{
const int len = _s.size();
writeInt( len, _fd );
if( write( _fd, _s.c_str(), len ) != len )
{
return false;
}
return true;
}
enum RemoteMessageIDs
{
IdUndefined,
IdGeneralFailure,
IdInitDone,
IdClosePlugin,
IdDebugMessage,
IdSampleRateInformation,
IdBufferSizeInformation,
IdMidiEvent,
IdStartProcessing,
IdProcessingDone,
IdChangeSharedMemoryKey,
IdChangeInputCount,
IdChangeOutputCount,
IdShowUI,
IdHideUI,
IdSaveSettingsToString,
IdSaveSettingsToFile,
IdLoadSettingsFromString,
IdLoadSettingsFromFile,
IdLoadPresetFromFile,
IdUserBase = 64
} ;
class remotePluginBase
{
public:
struct message
{
message() :
id( IdUndefined ),
data()
{
}
message( const message & _m ) :
id( _m.id ),
data( _m.data )
{
}
message( int _id ) :
id( _id ),
data()
{
}
void addInt( int _i )
{
char buf[128];
buf[0] = 0;
sprintf( buf, "%d", _i );
data.push_back( std::string( buf ) );
}
int getInt( int _p ) const
{
return( atoi( data[_p].c_str() ) );
}
bool operator==( const message & _m )
{
return( id == _m.id );
}
int id;
std::vector<std::string> data;
} ;
remotePluginBase( void );
virtual ~remotePluginBase();
void sendMessage( const message & _m );
message receiveMessage( void );
bool messagesLeft( void ) const;
message waitForMessage( const message & _m,
bool _busy_waiting = FALSE );
inline message fetchAndProcessNextMessage( void )
{
message m = receiveMessage();
processMessage( m );
return m;
}
inline bool fetchAndProcessAllMessages( void )
{
while( messagesLeft() )
{
fetchAndProcessNextMessage();
}
}
virtual bool processMessage( const message & _m ) = 0;
protected:
void setInFD( int _fd )
{
m_inFD = _fd;
}
void setOutFD( int _fd )
{
m_outFD = _fd;
}
private:
int m_inFD;
int m_outFD;
} ;
#ifndef BUILD_REMOTE_PLUGIN_CLIENT
class remotePlugin : public remotePluginBase
{
public:
remotePlugin( const QString & _plugin_executable );
virtual ~remotePlugin();
virtual bool processMessage( const message & _m );
bool process( const sampleFrame * _in_buf,
sampleFrame * _out_buf, bool _wait );
bool waitForProcessingFinished( sampleFrame * _out_buf );
void processMidiEvent( const midiEvent &, const f_cnt_t _offset );
void updateSampleRate( sample_rate_t _sr )
{
message m( IdSampleRateInformation );
m.addInt( _sr );
sendMessage( m );
}
void showUI( void )
{
sendMessage( IdShowUI );
}
void hideUI( void )
{
sendMessage( IdHideUI );
}
protected:
inline void lock( void )
{
m_commMutex.lock();
}
inline void unlock( void )
{
m_commMutex.unlock();
}
private:
void resizeSharedMemory( void );
bool m_initialized;
bool m_failed;
int m_pluginPID;
int m_pipes[2][2];
QMutex m_commMutex;
#ifdef USE_NATIVE_SHM_API
int m_shmID;
#else
QSharedMemory m_shmObj;
#endif
size_t m_shmSize;
float * m_shm;
int m_inputCount;
int m_outputCount;
} ;
#endif
#ifdef BUILD_REMOTE_PLUGIN_CLIENT
class remotePluginClient : public remotePluginBase
{
public:
remotePluginClient( void );
virtual ~remotePluginClient();
virtual bool processMessage( const message & _m );
virtual bool process( const sampleFrame * _in_buf,
sampleFrame * _out_buf ) = 0;
virtual void processMidiEvent( const midiEvent &,
const f_cnt_t /* _offset */ )
{
}
inline float * sharedMemory( void )
{
return( m_shm );
}
virtual void updateSampleRate( sample_rate_t )
{
}
virtual void updateBufferSize( fpp_t )
{
}
inline fpp_t bufferSize( void ) const
{
return( m_bufferSize );
}
void debugMessage( const char * _fmt, ... )
{
va_list ap;
char buffer[512];
va_start( ap, _fmt );
buffer[0] = 0;
vsnprintf( buffer, sizeof( buffer ), _fmt, ap );
message m( IdDebugMessage );
m.data.push_back( std::string( buffer ) );
sendMessage( m );
va_end( ap );
}
void setInputCount( int _i )
{
m_inputCount = _i;
message m( IdChangeInputCount );
m.addInt( _i );
sendMessage( m );
}
void setOutputCount( int _i )
{
m_outputCount = _i;
message m( IdChangeOutputCount );
m.addInt( _i );
sendMessage( m );
}
private:
void setShmKey( int _key, int _size );
void doProcessing( void );
#ifndef USE_NATIVE_SHM_API
QSharedMemory m_shmObj;
#endif
float * m_shm;
int m_inputCount;
int m_outputCount;
fpp_t m_bufferSize;
} ;
#endif
#ifdef COMPILE_REMOTE_PLUGIN_BASE
#ifndef BUILD_REMOTE_PLUGIN_CLIENT
#include <QtCore/QCoreApplication>
#endif
remotePluginBase::remotePluginBase( void ) :
m_inFD( -1 ),
m_outFD( -1 )
{
}
remotePluginBase::~remotePluginBase()
{
}
void remotePluginBase::sendMessage( const message & _m )
{
writeInt( _m.id, m_outFD );
writeInt( _m.data.size(), m_outFD );
for( int i = 0; i < _m.data.size(); ++i )
{
writeString( _m.data[i], m_outFD );
}
}
remotePluginBase::message remotePluginBase::receiveMessage( void )
{
message m;
m.id = readInt( m_inFD );
const int s = readInt( m_inFD );
for( int i = 0; i < s; ++i )
{
m.data.push_back( readString( m_inFD ) );
}
return( m );
}
inline bool remotePluginBase::messagesLeft( void ) const
{
fd_set rfds;
FD_ZERO( &rfds );
FD_SET( m_inFD, &rfds );
timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 1; // can we use 0 here?
return( select( m_inFD + 1, &rfds, NULL, NULL, &tv ) > 0 );
}
remotePluginBase::message remotePluginBase::waitForMessage(
const message & _wm,
bool _busy_waiting )
{
while( 1 )
{
if( messagesLeft() )
{
message m;
m = receiveMessage();
processMessage( m );
if( m.id == _wm.id )
{
return( m );
}
else if( m.id == IdGeneralFailure )
{
return( m );
}
}
#ifndef BUILD_REMOTE_PLUGIN_CLIENT
if( _busy_waiting )
{
QCoreApplication::processEvents(
QEventLoop::AllEvents, 50 );
}
else
{
usleep( 10 );
}
#else
usleep( 10 );
#endif
}
}
#endif
#ifdef BUILD_REMOTE_PLUGIN_CLIENT
remotePluginClient::remotePluginClient( void ) :
remotePluginBase(),
#ifndef USE_NATIVE_SHM_API
m_shmObj(),
#endif
m_shm( NULL ),
m_inputCount( DEFAULT_CHANNELS ),
m_outputCount( DEFAULT_CHANNELS ),
m_bufferSize( DEFAULT_BUFFER_SIZE )
{
setInFD( 0 );
setOutFD( 1 );
sendMessage( IdSampleRateInformation );
sendMessage( IdBufferSizeInformation );
}
remotePluginClient::~remotePluginClient()
{
sendMessage( IdClosePlugin );
#ifdef USE_NATIVE_SHM_API
shmdt( m_shm );
#endif
}
bool remotePluginClient::processMessage( const message & _m )
{
message reply_message( _m.id );
bool reply = false;
switch( _m.id )
{
case IdGeneralFailure:
return( false );
case IdSampleRateInformation:
updateSampleRate( _m.getInt( 0 ) );
break;
case IdBufferSizeInformation:
m_bufferSize = _m.getInt( 0 );
updateBufferSize( m_bufferSize );
break;
case IdClosePlugin:
return( false );
case IdMidiEvent:
processMidiEvent(
midiEvent( static_cast<MidiEventTypes>(
_m.getInt( 0 ) ),
_m.getInt( 1 ),
_m.getInt( 2 ),
_m.getInt( 3 ) ),
_m.getInt( 4 ) );
break;
case IdStartProcessing:
doProcessing();
reply_message.id = IdProcessingDone;
reply = true;
break;
case IdChangeSharedMemoryKey:
setShmKey( _m.getInt( 0 ), _m.getInt( 1 ) );
break;
case IdUndefined:
default:
break;
}
if( reply )
{
sendMessage( reply_message );
}
return( true );
}
void remotePluginClient::setShmKey( int _key, int _size )
{
#ifdef USE_NATIVE_SHM_API
if( m_shm != NULL )
{
shmdt( m_shm );
m_shm = NULL;
}
// only called for detaching SHM?
if( _key == 0 )
{
return;
}
int shm_id = shmget( _key, _size, 0 );
if( shm_id == -1 )
{
debugMessage( "failed getting shared memory" );
}
else
{
m_shm = (float *) shmat( shm_id, 0, 0 );
}
#else
m_shmObj.setKey( QString::number( _key ) );
if( m_shmObj.attach() )
{
m_shm = (float *) m_shmObj.data();
}
else
{
debugMessage( "failed getting shared memory" );
}
#endif
}
void remotePluginClient::doProcessing( void )
{
if( m_shm != NULL )
{
process( (sampleFrame *)( m_inputCount > 0 ? m_shm : NULL ),
(sampleFrame *)( m_shm +
( m_inputCount*m_bufferSize ) ) );
}
}
#endif
#endif

370
src/core/remote_plugin.cpp Normal file
View File

@@ -0,0 +1,370 @@
/*
* remote_plugin.cpp - base class providing RPC like mechanisms
*
* Copyright (c) 2008 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*
* This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
*
* 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 COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#define COMPILE_REMOTE_PLUGIN_BASE
#include "remote_plugin.h"
#include "lmmsconfig.h"
#include "engine.h"
#include "config_mgr.h"
#include <QtCore/QDir>
#include <QtCore/QTime>
#ifdef LMMS_HAVE_UNISTD_H
#include <unistd.h>
#endif
#ifdef LMMS_HAVE_SIGNAL_H
#include <signal.h>
#endif
#include <cstdio>
#ifdef LMMS_HAVE_SYS_SELECT_H
#include <sys/select.h>
#endif
#ifdef LMMS_HAVE_SYS_TIME_H
#include <sys/time.h>
#endif
#ifdef LMMS_HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
remotePlugin::remotePlugin( const QString & _plugin_executable ) :
remotePluginBase(),
m_initialized( false ),
m_failed( true ),
m_pluginPID( -1 ),
m_commMutex( QMutex::Recursive ),
#ifdef USE_NATIVE_SHM_API
m_shmID( 0 ),
#else
m_shmObj(),
#endif
m_shmSize( 0 ),
m_shm( NULL ),
m_inputCount( DEFAULT_CHANNELS ),
m_outputCount( DEFAULT_CHANNELS )
{
QString fe = configManager::inst()->pluginDir() +
QDir::separator() + _plugin_executable;
if( pipe( m_pipes[0] ) || pipe( m_pipes[1] ) )
{
printf( "error while creating pipes!\n" );
}
if( ( m_pluginPID = fork() ) < 0 )
{
printf( "fork() failed!\n" );
return;
}
else if( m_pluginPID == 0 )
{
dup2( m_pipes[0][0], 0 );
dup2( m_pipes[1][1], 1 );
execlp( fe.toAscii().constData(), fe.toAscii().constData(),
NULL );
exit( 0 );
}
setInFD( m_pipes[1][0] );
setOutFD( m_pipes[0][1] );
resizeSharedMemory();
lock();
m_failed = waitForMessage( IdInitDone ).id != IdInitDone;
unlock();
}
remotePlugin::~remotePlugin()
{
if( m_failed == false )
{
sendMessage( IdClosePlugin );
// wait for acknowledge
QTime t;
t.start();
while( t.elapsed() < 1000 )
{
if( messagesLeft() &&
receiveMessage().id == IdClosePlugin )
{
//m_pluginPID = 0;
break;
}
usleep( 10 );
}
// timeout?
/* if( m_pluginPID != 0 )
{*/
kill( m_pluginPID, SIGTERM );
kill( m_pluginPID, SIGKILL );
//}
// remove process from PCB
waitpid( m_pluginPID, NULL, 0 );
// close all sides of our pipes
close( m_pipes[0][0] );
close( m_pipes[0][1] );
close( m_pipes[1][0] );
close( m_pipes[1][1] );
#ifdef USE_NATIVE_SHM_API
shmdt( m_shm );
shmctl( m_shmID, IPC_RMID, NULL );
#endif
}
}
bool remotePlugin::process( const sampleFrame * _in_buf,
sampleFrame * _out_buf, bool _wait )
{
const fpp_t frames = engine::getMixer()->framesPerPeriod();
if( m_shm == NULL )
{
// m_shm being zero means we didn't initialize everything so
// far so process one message each time (and hope we get
// information like SHM-key etc.) until we process messages
// in a later stage of this procedure
if( m_shmSize == 0 )
{
lock();
fetchAndProcessAllMessages();
unlock();
}
if( _out_buf != NULL )
{
engine::getMixer()->clearAudioBuffer( _out_buf,
frames );
}
return( false );
}
memset( m_shm, 0, m_shmSize );
ch_cnt_t inputs = tMax<ch_cnt_t>( m_inputCount, DEFAULT_CHANNELS );
if( _in_buf != NULL && inputs > 0 )
{
for( ch_cnt_t ch = 0; ch < inputs; ++ch )
{
for( fpp_t frame = 0; frame < frames; ++frame )
{
m_shm[ch * frames + frame] = _in_buf[frame][ch];
}
}
}
lock();
sendMessage( IdStartProcessing );
m_initialized = TRUE;
if( _wait )
{
waitForProcessingFinished( _out_buf );
}
unlock();
return( TRUE );
}
bool remotePlugin::waitForProcessingFinished( sampleFrame * _out_buf )
{
if( !m_initialized || _out_buf == NULL || m_outputCount == 0 )
{
return( false );
}
lock();
waitForMessage( IdProcessingDone );
unlock();
const fpp_t frames = engine::getMixer()->framesPerPeriod();
const ch_cnt_t outputs = tMax<ch_cnt_t>( m_outputCount,
DEFAULT_CHANNELS );
if( outputs != DEFAULT_CHANNELS )
{
// clear buffer, if plugin didn't fill up both channels
engine::getMixer()->clearAudioBuffer( _out_buf, frames );
}
sampleFrame * o = (sampleFrame *) ( m_shm + m_inputCount*frames );
for( ch_cnt_t ch = 0; ch < outputs; ++ch )
{
for( fpp_t frame = 0; frame < frames; ++frame )
{
_out_buf[frame][ch] = o[frame][ch];
}
}
return( TRUE );
}
void remotePlugin::processMidiEvent( const midiEvent & _e,
const f_cnt_t _offset )
{
message m( IdMidiEvent );
m.addInt( _e.m_type );
m.addInt( _e.m_channel );
m.addInt( _e.m_data.m_param[0] );
m.addInt( _e.m_data.m_param[1] );
m.addInt( _offset );
lock();
sendMessage( m );
unlock();
}
void remotePlugin::resizeSharedMemory( void )
{
const size_t s = ( m_inputCount+m_outputCount ) *
engine::getMixer()->framesPerPeriod() *
sizeof( float );
if( m_shm != NULL )
{
#ifdef USE_NATIVE_SHM_API
shmdt( m_shm );
shmctl( m_shmID, IPC_RMID, NULL );
#else
m_shmObj.detach();
#endif
}
int shm_key = 0;
#ifdef USE_NATIVE_SHM_API
while( ( m_shmID = shmget( ++shm_key, s, IPC_CREAT | IPC_EXCL |
0600 ) ) == -1 )
{
}
m_shm = (float *) shmat( m_shmID, 0, 0 );
#else
do
{
m_shmObj.setKey( QString( "%1" ).arg( ++shm_key ) );
m_shmObj.create( s );
} while( m_shmObj.error() != QSharedMemory::NoError );
m_shm = (float *) m_shmObj.data();
#endif
m_shmSize = s;
message m( IdChangeSharedMemoryKey );
m.addInt( shm_key );
m.addInt( m_shmSize );
sendMessage( m );
}
bool remotePlugin::processMessage( const message & _m )
{
lock();
message reply_message( _m.id );
bool reply = false;
switch( _m.id )
{
case IdGeneralFailure:
return( false );
case IdDebugMessage:
printf( "debug message from client: %s\n",
_m.data[0].c_str() );
break;
case IdInitDone:
reply = true;
break;
case IdSampleRateInformation:
reply = true;
reply_message.addInt(
engine::getMixer()->processingSampleRate() );
break;
case IdBufferSizeInformation:
reply = true;
reply_message.addInt(
engine::getMixer()->framesPerPeriod() );
break;
case IdChangeInputCount:
m_inputCount = _m.getInt( 0 );
resizeSharedMemory();
break;
case IdChangeOutputCount:
m_outputCount = _m.getInt( 0 );
resizeSharedMemory();
break;
case IdProcessingDone:
case IdClosePlugin:
case IdUndefined:
default:
break;
}
if( reply )
{
sendMessage( reply_message );
}
unlock();
return( true );
}