/* ------------------------------------------------------------------ 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);