/*
------------------------------------------------------------------
Python Plugin
Copyright (C) 2016 FP Battaglia
based on
Open Ephys GUI
Copyright (C) 2013, 2015 Open Ephys
------------------------------------------------------------------
v
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 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
/*
==============================================================================
PythonPlugin.cpp
Created: 13 Jun 2014 5:56:17pm
Author: fpbatta
==============================================================================
*/
#include "PythonPlugin.h"
#include "PythonEditor.h"
#ifdef _WIN32
#include
#else
#include
#endif
#include
#include
#include
#ifdef DEBUG
#define PYTHON_DEBUG
#endif
#ifdef PYTHON_DEBUG
#if defined(__linux__)
#include
#include
#elif !defined(_WIN32)
#include
#endif
#endif
// debug logs when entering function
#ifdef PYTHON_DEBUG
#if defined(__linux__)
#define GET_TID pid_t tid = syscall(SYS_gettid)
#elif defined(_WIN32)
#define GET_TID DWORD tid = GetCurrentThreadId()
#else
#define GET_TID uint64_t tid; pthread_threadid_np(NULL, &tid)
#endif
#define DEBUG_LOG(str) JUCE_BLOCK_WITH_FORCED_SEMICOLON(std::cout << str << std::endl;)
#define LOG_ENTER(fname) \
GET_TID; \
std::cout << "in " << fname << " pthread_threadid_np()=" << tid << std::endl
#else // not debugging
#define DEBUG_LOG(str)
#define LOG_ENTER(fname)
#endif
PythonPlugin::PythonPlugin(const String &processorName)
: GenericProcessor(processorName) //, threshold(200.0), state(true)
{
LOG_ENTER("constructor");
}
void PythonPlugin::createEventChannels()
{
EventChannel* ev = new EventChannel(EventChannel::TTL, 8, 1, CoreServices::getGlobalSampleRate(), this);
ev->setName("Python events");
ev->setDescription("Events generated by a Python plugin");
String identifier = "dataderived.python";
ev->setIdentifier(identifier);
MetaDataDescriptor md(MetaDataDescriptor::CHAR, 25, "Plugin type", "Description of the plugin", "channelInfo.extra");
MetaDataValue mv(md);
mv.setValue("Python plugin");
ev->addMetaData(md, mv);
eventChannelArray.add(ev);
ttlChannel = ev;
std::cout << "Python event channel created" << std::endl;
}
AudioProcessorEditor* PythonPlugin::createEditor()
{
editor = new PythonEditor(this, true);
return editor;
}
bool PythonPlugin::isReady()
{
LOG_ENTER("isReady");
if (plugin.getNativeHandle() == nullptr)
{
CoreServices::sendStatusMessage ("No plugin selected in Python Plugin.");
return false;
}
else
{
const PythonLock pyLock;
if (pluginIsReady && !(*pluginIsReady)())
{
CoreServices::sendStatusMessage("Python Plugin is not ready");
return false;
}
return true;
}
}
void PythonPlugin::process(AudioSampleBuffer& buffer)
{
LOG_ENTER("process");
PythonEvent *pyEvents = (PythonEvent *)calloc(1, sizeof(PythonEvent));
pyEvents->type = 0; // this marks an empty event
{
const PythonLock pyLock;
(*pluginFunction)(*(buffer.getArrayOfWritePointers()), buffer.getNumChannels(), buffer.getNumSamples(), getNumSamples(0), pyEvents);
}
if(wasTriggered)
{
uint8 ttlData = 0;
// std::cout << "in Python plugin resetting channel: " << lastChan << std::endl;
TTLEventPtr event = TTLEvent::createTTLEvent(ttlChannel, getTimestamp(0),
&ttlData, sizeof(uint8), lastChan);
addEvent(ttlChannel, event, 0);
wasTriggered = false;
}
if(pyEvents->type != 0)
{
lastChan = (uint16)pyEvents->eventId;
uint8 ttlData = 1 << lastChan;
// std::cout << "in Python plugin ts is " << getTimestamp(0) + pyEvents->sampleNum << " and sampleNum is " <<
// pyEvents->sampleNum << " eventId: " << uint16(pyEvents->eventId) << " ttlData: " << int(ttlData) << std::endl;
// FIXME now we set the ts at the first samble in the block
TTLEventPtr event = TTLEvent::createTTLEvent(ttlChannel, getTimestamp(0) + pyEvents->sampleNum,
&ttlData, sizeof(uint8), lastChan);
addEvent(ttlChannel, event, pyEvents->sampleNum);
PythonEvent *lastEvent = pyEvents;
PythonEvent *nextEvent = lastEvent->nextEvent;
free((void *)lastEvent);
wasTriggered = true;
// std::cout << "lastChan is " << lastChan << std::endl;
while (nextEvent) {
lastChan = (uint16)nextEvent->eventId;
uint8 ttlData = 1 << lastChan;
// std::cout << "in Python plugin ts is " << getTimestamp(0) << " and sampleNum is " <<
// nextEvent->sampleNum << " ttlData: " << ttlData << std::endl;
TTLEventPtr event = TTLEvent::createTTLEvent(ttlChannel, getTimestamp(0) + nextEvent->sampleNum,
&ttlData, sizeof(uint8), lastChan);
addEvent(ttlChannel, event, nextEvent->sampleNum);
lastEvent = nextEvent;
nextEvent = nextEvent->nextEvent;
free((void *)lastEvent);
wasTriggered = true;
}
}
}
/** START CJB ADDED **/
void PythonPlugin::handleEvent(const EventChannel* eventInfo, const MidiMessage& event, int sampleNum){
int eventType;
int sourceID;
int subProcessorIdx;
double timestamp;
int sourceIndex;
const void* ptr;
if (eventInfo->getChannelType() == EventChannel::TTL)
{
TTLEventPtr ttl = TTLEvent::deserializeFromMessage(event, eventInfo);
eventType = int(ttl->getEventType());
sourceID = int(ttl->getSourceID());
subProcessorIdx = int(ttl->getSubProcessorIdx());
timestamp = double(ttl->getTimestamp());
sourceIndex = int(ttl->getSourceIndex());
//ptr = ttl->getRawDataPointer();
sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex);
}
else if (eventInfo->getChannelType() == EventChannel::TEXT)
{
TextEventPtr txt = TextEvent::deserializeFromMessage(event, eventInfo);
eventType = int(txt->getEventType());
sourceID = int(txt->getSourceID());
subProcessorIdx = int(txt->getSubProcessorIdx());
timestamp = double(txt->getTimestamp());
sourceIndex = int(txt->getSourceIndex());
ptr = txt->getRawDataPointer();
sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex);
}
else if (eventInfo->getChannelType() == EventChannel::TEXT)
{
BinaryEventPtr bi = BinaryEvent::deserializeFromMessage(event, eventInfo);
eventType = int(bi->getEventType());
sourceID = int(bi->getSourceID());
subProcessorIdx = int(bi->getSubProcessorIdx());
timestamp = double(bi->getTimestamp());
sourceIndex = int(bi->getSourceIndex());
//ptr = bi->getRawDataPointer();
sendEventPlugin(eventType, sourceID, subProcessorIdx, timestamp, sourceIndex);
}
}
void PythonPlugin::sendEventPlugin(int eventType, int sourceID, int subProcessorIdx, double timestamp, int sourceIndex)
{
LOG_ENTER("sendEventPlugin");
const PythonLock pyLock;
(*eventFunction)(eventType, sourceID, subProcessorIdx,timestamp,sourceIndex);
}
void PythonPlugin::handleSpike(const SpikeChannel* spikeInfo, const MidiMessage& event, int samplePosition)
{
LOG_ENTER("handleSpike");
SpikeEventPtr newSpike = SpikeEvent::deserializeFromMessage(event, spikeInfo);
const float* dataPtr = newSpike->getDataPointer();
float spikeBuf[18];
for(int i = 0 ;i < 18;i++){
spikeBuf[i] = dataPtr[i];
}
//juce::uint16
int sortedID = int(newSpike->getSortedID());
int electrode = getSpikeChannelIndex(newSpike);
const PythonLock pyLock;
(*spikeFunction)(electrode, sortedID, spikeBuf);
}
/** END CJB ADDED **/
/* The complete API that the Cython plugin has to expose is
void pluginStartup(void): a function to initialize the plugin data structures prior to start ACQ
int isReady(void): a boolean function telling the processor whether the plugin is ready to receive data
int getParamNum(void) get the number of parameters that the plugin takes
ParamConfig *getParamConfig(void) this will allow generating the editor GUI TODO
void setIntParameter(char *name, int value) set integer parameter
void set FloatParameter(char *name, float value) set float parameter
*/
String lastError()
{
String message;
#ifdef _WIN32
/*Get the error message, if any.*/
DWORD errorMessageID = ::GetLastError();
if (errorMessageID != 0) // Error message has been recorded
{
LPSTR messageBuffer = nullptr;
size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
message = String(messageBuffer, size);
//Free the buffer.
LocalFree(messageBuffer);
}
#else
message = String(dlerror());
#endif
return message;
}
void PythonPlugin::setFile(String fullpath)
{
LOG_ENTER("setFile");
filePath = fullpath;
if (!plugin.open(filePath))
{
std::cout << "Can't open plugin "
<< '"' << filePath << '"' << std::endl
<< lastError() << std::endl;
return;
}
String pluginName = File(filePath).getFileName().upToFirstOccurrenceOf(".", false, true);
#if PY_MAJOR_VERSION>=3
String initPluginName = String("PyInit_");
#else
String initPluginName = String("init");
#endif
initPluginName.append(pluginName, 200);
std::cout << "init function is: " << initPluginName << std::endl;
void *initializer = plugin.getFunction(initPluginName);
DEBUG_LOG("initializer: " << initializer);
if (!initializer)
{
std::cout << "Can't find init function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
initfunc_t initF = (initfunc_t)initializer;
void *cfunc = plugin.getFunction("pluginisready");
if (!cfunc)
{
std::cout << "Can't find ready function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
pluginIsReady = (isreadyfunc_t)cfunc;
cfunc = plugin.getFunction("pluginStartup");
if (!cfunc)
{
std::cout << "Can't find startup function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
pluginStartupFunction = (startupfunc_t)cfunc;
std::cout << "loaded pluginStartup \n \n \n \n \n ";
cfunc = plugin.getFunction("getParamNum");
if (!cfunc)
{
std::cout << "Can't find getParamNum function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
getParamNumFunction = (getparamnumfunc_t)cfunc;
cfunc = plugin.getFunction("getParamConfig");
if (!cfunc)
{
std::cout << "Can't find getParamConfig function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
getParamConfigFunction = (getparamconfigfunc_t)cfunc;
cfunc = plugin.getFunction("pluginFunction");
if (!cfunc)
{
std::cout << "Can't find plugin function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
pluginFunction = (pluginfunc_t)cfunc;
// CJB added start
cfunc = plugin.getFunction("eventFunction");
if (!cfunc)
{
std::cout << "Can't find event function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
eventFunction = (eventfunc_t)cfunc;
cfunc = plugin.getFunction("spikeFunction");
if (!cfunc)
{
std::cout << "Can't find spike function in plugin "
<< '"' << pluginName << '"' << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
spikeFunction = (spikefunc_t)cfunc;
// CJB added end
cfunc = plugin.getFunction("updateSettings");
if (!cfunc)
{
std::cout << "Can't find updateSettings function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
updateSettingsFunction = (updatefunc_t)cfunc;
cfunc = plugin.getFunction("channelChanged");
if (!cfunc)
{
std::cout << "Can't find channelChanged function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
channelChangedFunction = (chanchangefunc_t)cfunc;
cfunc = plugin.getFunction("setIntParam");
if (!cfunc)
{
std::cout << "Can't find setIntParam function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
setIntParamFunction = (setintparamfunc_t)cfunc;
cfunc = plugin.getFunction("setFloatParam");
if (!cfunc)
{
std::cout << "Can't find setFloatParam function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
setFloatParamFunction = (setfloatparamfunc_t)cfunc;
cfunc = plugin.getFunction("getIntParam");
if (!cfunc)
{
std::cout << "Can't find getIntParam function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
getIntParamFunction = (getintparamfunc_t)cfunc;
cfunc = plugin.getFunction("getFloatParam");
if (!cfunc)
{
std::cout << "Can't find getFloatParam function in plugin "
<< '"' << pluginName << "\"" << std::endl
<< lastError() << std::endl;
resetPlugin();
return;
}
getFloatParamFunction = (getfloatparamfunc_t)cfunc;
// now the API should be fully loaded
const PythonLock pyLock;
// initialize the plugin
DEBUG_LOG("before initplugin");
(*initF)();
DEBUG_LOG("after initplugin");
(*pluginStartupFunction)(nChans, dataSampleRate, chanEnabled.getRawDataPointer());
// load the parameter configuration
numPythonParams = (*getParamNumFunction)();
DEBUG_LOG("the plugin wants " << numPythonParams << " parameters");
params = (ParamConfig *)calloc(numPythonParams, sizeof(ParamConfig));
paramsControl = (Component **)calloc(numPythonParams, sizeof(Component *));
(*getParamConfigFunction)(params);
DEBUG_LOG("release paramconfig");
auto ed = static_cast(getEditor());
for(int i = 0; i < numPythonParams; i++)
{
DEBUG_LOG("param " << i << " is a " << params[i].type);
DEBUG_LOG("it is named: " << params[i].name << std::endl);
switch (params[i].type) {
case TOGGLE:
paramsControl[i] = ed->addToggleButton(String(params[i].name), params[i].isEnabled);
break;
case INT_SET:
paramsControl[i] = ed->addComboBox(String(params[i].name), params[i].nEntries, params[i].entries);
break;
case FLOAT_RANGE:
paramsControl[i] = ed->addSlider(String(params[i].name), params[i].rangeMin, params[i].rangeMax, params[i].startValue);
break;
default:
break;
}
}
}
String PythonPlugin::getFile()
{
return filePath;
}
void PythonPlugin::updateSettings()
{
// update the sample rate...
// if you have input data channels, we'll use the first one's sample rate (sane?)
// otherwise, we'll just use the superclass' implementation if getSampleRate()
if (getNumInputs() > 0) {
dataSampleRate = getDataChannel(0)->getSampleRate();
} else {
dataSampleRate = GenericProcessor::getSampleRate();
}
// update number of channels
int prevChans = nChans;
nChans = getNumInputs();
chanEnabled.resize(nChans);
for (int c = prevChans; c < nChans; ++c)
{
// new channels are enabled by default
chanEnabled.set(c, true);
}
if (updateSettingsFunction)
{
const PythonLock pyLock;
(*updateSettingsFunction)(nChans, dataSampleRate);
}
}
void PythonPlugin::channelChanged(int chan, bool state)
{
jassert(chan >= 0 && chan < chanEnabled.size());
chanEnabled.set(chan, state);
if (channelChangedFunction)
{
const PythonLock pyLock;
(*channelChangedFunction)(chan, state);
}
}
void PythonPlugin::setIntPythonParameter(String name, int value)
{
LOG_ENTER("setIntPythonParameter");
const PythonLock pyLock;
(*setIntParamFunction)(name.getCharPointer().getAddress(), value);
}
void PythonPlugin::setFloatPythonParameter(String name, float value)
{
LOG_ENTER("setFloatPythonParameter");
const PythonLock pyLock;
(*setFloatParamFunction)(name.getCharPointer().getAddress(), value);
}
int PythonPlugin::getIntPythonParameter(String name)
{
LOG_ENTER("getIntPythonParameter");
int value;
const PythonLock pyLock;
value = (*getIntParamFunction)(name.getCharPointer().getAddress());
return value;
}
float PythonPlugin::getFloatPythonParameter(String name)
{
LOG_ENTER("getFloatPythonParameter");
float value;
const PythonLock pyLock;
value = (*getFloatParamFunction)(name.getCharPointer().getAddress());
return value;
}
void PythonPlugin::resetPlugin()
{
pluginFunction = nullptr;
pluginIsReady = nullptr;
pluginStartupFunction = nullptr;
getParamNumFunction = nullptr;
getParamConfigFunction = nullptr;
updateSettingsFunction = nullptr;
channelChangedFunction = nullptr;
setIntParamFunction = nullptr;
setFloatParamFunction = nullptr;
getIntParamFunction = nullptr;
getFloatParamFunction = nullptr;
eventFunction = nullptr;
spikeFunction = nullptr;
plugin.close();
}
// PythonLock
PythonPlugin::PythonLock::PythonLock()
: pgss(PyGILState_Ensure())
{
// if current state is not the mainState or saved threadState, need to save it
PyThreadState* currState = PyThreadState_Get();
if (currState != mainState && currState != threadState)
{
// abusing the API a little - call ...Ensure again to increment the counter
// and prevent it from being deleted automatically when the lock is released
PyGILState_Ensure();
// delete the old thread state, if any
if (threadState)
{
PyThreadState_Clear(threadState);
PyThreadState_Delete(threadState);
}
threadState = currState;
}
}
PythonPlugin::PythonLock::~PythonLock()
{
PyGILState_Release(pgss);
}
static PyThreadState* startInterpreter()
{
// if on windows, PYTHON_HOME_NAME is set by PythonEnv.props (corresponds to CONDA_HOME environment variable)
#ifndef _WIN32
#define QUOTE(name) #name
#define STR(macro) QUOTE(macro)
#define PYTHON_HOME_NAME STR(PYTHON_HOME)
#endif
char * old_python_home = getenv("PYTHONHOME");
if (old_python_home == NULL || strcmp(old_python_home, PYTHON_HOME_NAME) != 0)
{
#ifdef PYTHON_DEBUG
std::cout << "setting PYTHONHOME" << std::endl;
#endif
#ifdef _WIN32
_putenv_s("PYTHONHOME", PYTHON_HOME_NAME);
#else
setenv("PYTHONHOME", PYTHON_HOME_NAME, 1);
#endif
}
#ifdef PYTHON_DEBUG
std::cout << "PYTHONHOME: " << getenv("PYTHONHOME") << std::endl;
#endif
#ifdef _WIN32
// set PYTHONPATH to avoid error described here: https://stackoverflow.com/questions/5694706/py-initialize-fails-unable-to-load-the-file-system-codec
_putenv_s("PYTHONPATH", PYTHON_HOME_NAME "\\DLLs;" PYTHON_HOME_NAME "\\Lib;" PYTHON_HOME_NAME "\\Lib\\site-packages");
#endif
#if PY_MAJOR_VERSION==3
Py_SetProgramName((wchar_t *)"PythonPlugin");
#else
Py_SetProgramName((char *)"PythonPlugin");
#endif
Py_Initialize();
PyEval_InitThreads();
PyRun_SimpleString("import sys");
PyRun_SimpleString("sys.setcheckinterval(10000)");
#ifdef PYTHON_DEBUG
std::cout << Py_GetPrefix() << std::endl;
std::cout << Py_GetVersion() << std::endl;
#endif
return PyEval_SaveThread();
}
const PyThreadState* PythonPlugin::PythonLock::mainState(startInterpreter());
PyThreadState* PythonPlugin::PythonLock::threadState(nullptr);