diff --git a/CMakeLists.txt b/CMakeLists.txt index 912c081..55c2e50 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,17 +18,35 @@ if(NOT SC_PATH) message(FATAL_ERROR "SuperCollider source path is not set") endif() -set(FLUID_PATH "" CACHE PATH "Optional path to the flucoma-core repo") +set(FLUID_PATH "" CACHE PATH "Optional path to the Fluid Decomposition repo") if (APPLE) - set(CMAKE_OSX_ARCHITECTURES x86_64) set(CMAKE_XCODE_GENERATE_SCHEME ON) - set(CMAKE_OSX_DEPLOYMENT_TARGET 10.8) + set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "") + set(CMAKE_OSX_DEPLOYMENT_TARGET "10.8" CACHE STRING "") + #A consequence of targetting 10.8. Needs to be set globally from 10.15 onwards in order for the test program to compile successfully during configure + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") endif() ################################################################################ # Main project project (flucoma-sc LANGUAGES CXX) + +if(NOT MSVC) + add_compile_options(-fdiagnostics-color=always) +endif() + +#set correct std lib linking for Windows (in CMake 3.15 this has a native function) +if(MSVC) #needs to be after project() + foreach(flag_var + CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE + CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO) + if(${flag_var} MATCHES "/MD") + string(REGEX REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}") + endif() + endforeach() +endif() + set_property(GLOBAL PROPERTY USE_FOLDERS ON) set(CMAKE_XCODE_GENERATE_TOP_LEVEL_PROJECT_ONLY ON) @@ -74,6 +92,8 @@ set_if_toplevel(VAR CMAKE_LIBRARY_OUTPUT_DIRECTORY set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_TEST "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}") set(CMAKE_SHARED_MODULE_PREFIX "") if(APPLE OR WIN32) @@ -83,7 +103,36 @@ endif() #needed for complaint-free static linking with GCC if(CMAKE_COMPILER_IS_GNUCXX) target_compile_options( HISSTools_FFT PUBLIC -fPIC ) -ENDIF() +endif() + +#sandbox regrettable dependency on SC internals for SendReply() +add_library(FLUID_SC_COPYREPLYADDR STATIC + "${CMAKE_SOURCE_DIR}/include/wrapper/CopyReplyAddress.cpp" + "${SC_PATH}/common/SC_Reply.cpp" + "${SC_PATH}/external_libraries/boost/libs/system/src/error_code.cpp" +) + +target_include_directories(FLUID_SC_COPYREPLYADDR SYSTEM PRIVATE + "${SC_PATH}/include/plugin_interface" + "${SC_PATH}/include/common" + "${SC_PATH}/common" + "${SC_PATH}/external_libraries/boost" +) +set_target_properties(FLUID_SC_COPYREPLYADDR PROPERTIES + CXX_STANDARD 14 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO +) + +if(CMAKE_COMPILER_IS_GNUCXX) + target_compile_options(FLUID_SC_COPYREPLYADDR PUBLIC -fPIC ) +endif() + +if(APPLE) + target_compile_options(FLUID_SC_COPYREPLYADDR PRIVATE -stdlib=libc++) +endif() + +target_compile_definitions(FLUID_SC_COPYREPLYADDR PRIVATE BOOST_ALL_NO_LIB BOOST_CONFIG_SUPPRESS_OUTDATED_MESSAGE) add_library(FLUID_SC_WRAPPER INTERFACE) target_include_directories(FLUID_SC_WRAPPER @@ -97,6 +146,8 @@ target_sources(FLUID_SC_WRAPPER "${CMAKE_CURRENT_SOURCE_DIR}/include/SCBufferAdaptor.hpp" ) +target_link_libraries(FLUID_SC_WRAPPER INTERFACE FLUID_SC_COPYREPLYADDR) + SUBDIRLIST(PROJECT_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/src") foreach (project_dir ${PROJECT_DIRS}) if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/src/${project_dir}/CMakeLists.txt") @@ -105,6 +156,8 @@ foreach (project_dir ${PROJECT_DIRS}) endif () endforeach () + + #install bits. set(SC_INSTALL_PREFIX "." CACHE PATH "Prefix for assembling SC packages") diff --git a/include/FluidSCWrapper.hpp b/include/FluidSCWrapper.hpp index 0df8bc3..226bddb 100644 --- a/include/FluidSCWrapper.hpp +++ b/include/FluidSCWrapper.hpp @@ -11,541 +11,42 @@ under the European Union’s Horizon 2020 research and innovation programme #pragma once #include "SCBufferAdaptor.hpp" -#include -#include -#include -#include +#include "wrapper/DeriveBaseClass.hpp" +#include "wrapper/NonRealtime.hpp" +#include "wrapper/Realtime.hpp" #include -#include -#include -#include -#include -#include -#include -#include namespace fluid { namespace client { -template -class FluidSCWrapper; - -namespace impl { - -// Iterate over kr/ir inputs via callbacks from params object -struct FloatControlsIter -{ - FloatControlsIter(float** vals, index N) : mValues(vals), mSize(N) {} - - float next() { return mCount >= mSize ? 0 : *mValues[mCount++]; } - - void reset(float** vals) - { - mValues = vals; - mCount = 0; - } - - index size() const noexcept { return mSize; } - -private: - float** mValues; - index mSize; - index mCount{0}; -}; - -//////////////////////////////////////////////////////////////////////////////// - -// Real Time Processor - -template -class RealTime : public SCUnit -{ - using HostVector = FluidTensorView; - using ParamSetType = typename Client::ParamSetType; - -public: - static void setup(InterfaceTable* ft, const char* name) - { - registerUnit(ft, name); - ft->fDefineUnitCmd(name, "latency", doLatency); - } - - static void doLatency(Unit* unit, sc_msg_iter*) - { - float l[]{ - static_cast(static_cast(unit)->mClient.latency())}; - auto ft = Wrapper::getInterfaceTable(); - - std::stringstream ss; - ss << '/' << Wrapper::getName() << "_latency"; - std::cout << ss.str() << std::endl; - ft->fSendNodeReply(&unit->mParent->mNode, -1, ss.str().c_str(), 1, l); - } - - RealTime() - : mControlsIterator{mInBuf + mSpecialIndex + 1, - static_cast(mNumInputs) - mSpecialIndex - 1}, - mParams{Wrapper::Client::getParameterDescriptors()}, - mClient{Wrapper::setParams(mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator, true)} - {} - - void init() - { - assert( - !(mClient.audioChannelsOut() > 0 && mClient.controlChannelsOut() > 0) && - "Client can't have both audio and control outputs"); - - // If we don't the number of arguments we expect, the language side code is - // probably the wrong version set plugin to no-op, squawk, and bail; - if (mControlsIterator.size() != Client::getParameterDescriptors().count()) - { - mCalcFunc = Wrapper::getInterfaceTable()->fClearUnitOutputs; - std::cout - << "ERROR: " << Wrapper::getName() - << " wrong number of arguments. Expected " - << Client::getParameterDescriptors().count() << ", got " - << mControlsIterator.size() - << ". Your .sc file and binary plugin might be different versions." - << std::endl; - return; - } - - mClient.sampleRate(fullSampleRate()); - mInputConnections.reserve(asUnsigned(mClient.audioChannelsIn())); - mOutputConnections.reserve(asUnsigned(mClient.audioChannelsOut())); - mAudioInputs.reserve(asUnsigned(mClient.audioChannelsIn())); - mOutputs.reserve(asUnsigned( - std::max(mClient.audioChannelsOut(), mClient.controlChannelsOut()))); - - for (index i = 0; i < mClient.audioChannelsIn(); ++i) - { - mInputConnections.emplace_back(isAudioRateIn(static_cast(i))); - mAudioInputs.emplace_back(nullptr, 0, 0); - } - - for (index i = 0; i < mClient.audioChannelsOut(); ++i) - { - mOutputConnections.emplace_back(true); - mOutputs.emplace_back(nullptr, 0, 0); - } - - for (index i = 0; i < mClient.controlChannelsOut(); ++i) - { mOutputs.emplace_back(nullptr, 0, 0); } - - mCalcFunc = make_calc_function(); - Wrapper::getInterfaceTable()->fClearUnitOutputs(this, 1); - } - - void next(int) - { - mControlsIterator.reset(mInBuf + 1); // mClient.audioChannelsIn()); - Wrapper::setParams( - mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator); // forward on inputs N + audio inputs as params - mParams.constrainParameterValues(); - const Unit* unit = this; - for (index i = 0; i < mClient.audioChannelsIn(); ++i) - { - if (mInputConnections[asUnsigned(i)]) - { mAudioInputs[asUnsigned(i)].reset(IN(i), 0, fullBufferSize()); } - } - for (index i = 0; i < mClient.audioChannelsOut(); ++i) - { - assert(i <= std::numeric_limits::max()); - if (mOutputConnections[asUnsigned(i)]) - mOutputs[asUnsigned(i)].reset(out(static_cast(i)), 0, - fullBufferSize()); - } - for (index i = 0; i < mClient.controlChannelsOut(); ++i) - { - assert(i <= std::numeric_limits::max()); - mOutputs[asUnsigned(i)].reset(out(static_cast(i)), 0, 1); - } - mClient.process(mAudioInputs, mOutputs, mContext); - } - -private: - std::vector mInputConnections; - std::vector mOutputConnections; - std::vector mAudioInputs; - std::vector mOutputs; - FloatControlsIter mControlsIterator; - FluidContext mContext; - -protected: - ParamSetType mParams; - Client mClient; -}; - -//////////////////////////////////////////////////////////////////////////////// - -/// Non Real Time Processor -/// This is also a UGen, but the main action is delegated off to a worker -/// thread, via the NRT thread. The RT bit is there to allow us (a) to poll our -/// thread and (b) emit a kr progress update -template -class NonRealTime : public SCUnit -{ - using ParamSetType = typename Client::ParamSetType; - -public: - static void setup(InterfaceTable* ft, const char* name) - { - registerUnit(ft, name); - ft->fDefineUnitCmd(name, "cancel", doCancel); - ft->fDefineUnitCmd( - name, "queue_enabled", [](struct Unit* unit, struct sc_msg_iter* args) { - auto w = static_cast(unit); - w->mQueueEnabled = args->geti(0); - w->mFifoMsg.Set( - w->mWorld, - [](FifoMsg* f) { - auto w = static_cast(f->mData); - w->mClient.setQueueEnabled(w->mQueueEnabled); - }, - nullptr, w); - Wrapper::getInterfaceTable()->fSendMsgFromRT(w->mWorld, w->mFifoMsg); - }); - ft->fDefineUnitCmd( - name, "synchronous", [](struct Unit* unit, struct sc_msg_iter* args) { - auto w = static_cast(unit); - w->mSynchronous = args->geti(0); - w->mFifoMsg.Set( - w->mWorld, - [](FifoMsg* f) { - auto w = static_cast(f->mData); - w->mClient.setSynchronous(w->mSynchronous); - }, - nullptr, w); - Wrapper::getInterfaceTable()->fSendMsgFromRT(w->mWorld, w->mFifoMsg); - }); - } - - /// Penultimate input is the doneAction, final is blocking mode. Neither are - /// params, so we skip them in the controlsIterator - NonRealTime() - : mControlsIterator{mInBuf, index(mNumInputs) - mSpecialIndex - 2}, - mParams{Wrapper::Client::getParameterDescriptors()}, - mClient{Wrapper::setParams(mParams, mWorld->mVerbosity > 0, mWorld, - mControlsIterator, true)}, - mSynchronous{mNumInputs > 2 ? (in0(int(mNumInputs) - 1) > 0) : false} - {} - - ~NonRealTime() - { - if (mClient.state() == ProcessState::kProcessing) - { - std::cout << Wrapper::getName() << ": Processing cancelled" << std::endl; - Wrapper::getInterfaceTable()->fSendNodeReply(&mParent->mNode, 1, "/done", - 0, nullptr); - } - // processing will be cancelled in ~NRTThreadAdaptor() - } - - - /// No option of not using a worker thread for now - /// init() sets up the NRT process via the SC NRT thread, and then sets our - /// UGen calc function going - void init() - { - mFifoMsg.Set(mWorld, initNRTJob, nullptr, this); - mWorld->ft->fSendMsgFromRT(mWorld, mFifoMsg); - // we want to poll thread roughly every 20ms - checkThreadInterval = static_cast(0.02 / controlDur()); - set_calc_function(); - }; - - /// The calc function. Checks to see if we've cancelled, spits out progress, - /// launches tidy up when complete - void poll(int) - { - out0(0) = mDone ? 1.0f : static_cast(mClient.progress()); - - if (0 == pollCounter++ && !mCheckingForDone) - { - mCheckingForDone = true; - mWorld->ft->fDoAsynchronousCommand(mWorld, nullptr, Wrapper::getName(), - this, postProcess, exchangeBuffers, - tidyUp, destroy, 0, nullptr); - } - pollCounter %= checkThreadInterval; - } - - - /// To be called on NRT thread. Validate parameters and commence processing in - /// new thread - static void initNRTJob(FifoMsg* f) - { - auto w = static_cast(f->mData); - w->mDone = false; - w->mCancelled = false; - - Result result = validateParameters(w); - - if (!result.ok()) - { - std::cout << "ERROR: " << Wrapper::getName() << ": " - << result.message().c_str() << std::endl; - return; - } - w->mClient.setSynchronous(w->mSynchronous); - w->mClient.enqueue(w->mParams); - w->mResult = w->mClient.process(); - } - - /// Check result and report if bad - static bool postProcess(World*, void* data) - { - auto w = static_cast(data); - Result r; - ProcessState s = w->mClient.checkProgress(r); - - if(w->mSynchronous) r = w->mResult; - - if ((s == ProcessState::kDone || s == ProcessState::kDoneStillProcessing) || - (w->mSynchronous && - s == ProcessState::kNoProcess)) // I think this hinges on the fact that - // when mSynchrous = true, this call - // will always be behind process() on - // the command FIFO, so we can assume - // that if the state is kNoProcess, it - // has run (vs never having run) - { - // Given that cancellation from the language now always happens by freeing - // the synth, this block isn't reached normally. HOwever, if someone - // cancels using u_cmd, this is what will fire - if (r.status() == Result::Status::kCancelled) - { - std::cout << Wrapper::getName() << ": Processing cancelled" << std::endl; - w->mCancelled = true; - return false; - } - - if (!r.ok()) - { - std::cout << "ERROR: " << Wrapper::getName() << ": " - << r.message().c_str() << std::endl; - return false; - } - - w->mDone = true; - return true; - } - return false; - } - - /// swap NRT buffers back to RT-land - static bool exchangeBuffers(World* world, void* data) - { - return static_cast(data)->exchangeBuffers(world); - } - /// Tidy up any temporary buffers - static bool tidyUp(World* world, void* data) - { - return static_cast(data)->tidyUp(world); - } - - /// Now we're actually properly done, call the UGen's done action (possibly - /// destroying this instance) - static void destroy(World* world, void* data) - { - auto w = static_cast(data); - if (w->mDone && - w->mNumInputs > - 2) // don't check for doneAction if UGen has no ins (there should be - // 3 minimum -> sig, doneAction, blocking mode) - { - int doneAction = static_cast( - w->in0(int(w->mNumInputs) - - 2)); // doneAction is penultimate input; THIS IS THE LAW - world->ft->fDoneAction(doneAction, w); - return; - } - w->mCheckingForDone = false; - } - - static void doCancel(Unit* unit, sc_msg_iter*) - { - static_cast(unit)->mClient.cancel(); - } - -private: - static Result validateParameters(NonRealTime* w) - { - auto results = w->mParams.constrainParameterValues(); - for (auto& r : results) - { - if (!r.ok()) return r; - } - return {}; - } - - bool exchangeBuffers(World* world) // RT thread - { - mParams.template forEachParamType(world); - // At this point, we can see if we're finished and let the language know (or - // it can wait for the doneAction, but that takes extra time) use replyID to - // convey status (0 = normal completion, 1 = cancelled) - if (mDone) - world->ft->fSendNodeReply(&mParent->mNode, 0, "/done", 0, nullptr); - if (mCancelled) - world->ft->fSendNodeReply(&mParent->mNode, 1, "/done", 0, nullptr); - return true; - } - - bool tidyUp(World*) // NRT thread - { - mParams.template forEachParamType(); - return true; - } - - template - struct AssignBuffer - { - void operator()(const typename BufferT::type& p, World* w) - { - if (auto b = static_cast(p.get())) b->assignToRT(w); - } - }; - - template - struct CleanUpBuffer - { - void operator()(const typename BufferT::type& p) - { - if (auto b = static_cast(p.get())) b->cleanUp(); - } - }; - - FloatControlsIter mControlsIterator; - FifoMsg mFifoMsg; - char* mCompletionMessage = nullptr; - void* mReplyAddr = nullptr; - const char* mName = nullptr; - index checkThreadInterval; - index pollCounter{0}; - -protected: - ParamSetType mParams; - Client mClient; - bool mSynchronous{true}; - bool mQueueEnabled{false}; - bool mCheckingForDone{false}; // only write to this from RT thread kthx - bool mCancelled{false}; - Result mResult; -}; - -//////////////////////////////////////////////////////////////////////////////// - -/// An impossible monstrosty -template -class NonRealTimeAndRealTime : public RealTime, - public NonRealTime -{ - static void setup(InterfaceTable* ft, const char* name) - { - RealTime::setup(ft, name); - NonRealTime::setup(ft, name); - } -}; - -//////////////////////////////////////////////////////////////////////////////// - -// Template Specialisations for NRT/RT - -template -class FluidSCWrapperImpl; - -template -class FluidSCWrapperImpl - : public NonRealTime -{}; - -template -class FluidSCWrapperImpl - : public RealTime -{}; - -//////////////////////////////////////////////////////////////////////////////// - -// Make base class(es), full of CRTP mixin goodness -template -using FluidSCWrapperBase = - FluidSCWrapperImpl, isNonRealTime, - isRealTime>; - -} // namespace impl - -//////////////////////////////////////////////////////////////////////////////// - /// The main wrapper template class FluidSCWrapper : public impl::FluidSCWrapperBase { - using FloatControlsIter = impl::FloatControlsIter; - - // Iterate over arguments via callbacks from params object - template - struct Setter - { - static constexpr index argSize = - C::getParameterDescriptors().template get().fixedSize; - - auto fromArgs(World*, FloatControlsIter& args, LongT::type, int) - { - return args.next(); - } - auto fromArgs(World*, FloatControlsIter& args, FloatT::type, int) - { - return args.next(); - } - - auto fromArgs(World* w, ArgType args, BufferT::type, int) - { - typename LongT::type bufnum = - static_cast(fromArgs(w, args, LongT::type(), -1)); - return BufferT::type(bufnum >= 0 ? new SCBufferAdaptor(bufnum, w) - : nullptr); - } - - auto fromArgs(World* w, ArgType args, InputBufferT::type, int) - { - typename LongT::type bufnum = - static_cast(fromArgs(w, args, LongT::type(), -1)); - return InputBufferT::type(bufnum >= 0 ? new SCBufferAdaptor(bufnum, w) - : nullptr); - } - - typename T::type operator()(World* w, ArgType args) - { - ParamLiteralConvertor a; - using LiteralType = - typename ParamLiteralConvertor::LiteralType; - - for (index i = 0; i < argSize; i++) - a[i] = static_cast(fromArgs(w, args, a[0], 0)); - - return a.value(); - } - }; - - template - using ControlSetter = Setter; + + //I would like to template these to something more scaleable, but baby steps + friend class impl::RealTime; + friend class impl::NonRealTime; static void doVersion(Unit*, sc_msg_iter*) { - std::cout << "Fluid Corpus Manipualtion Toolkit version " << fluidVersion() + std::cout << "Fluid Corpus Manipulation Toolkit: version " << fluidVersion() << std::endl; } + bool mInit{false}; public: - using Client = C; - using ParameterSetType = typename C::ParamSetType; - FluidSCWrapper() { impl::FluidSCWrapperBase::init(); } + template + using ArgumentSetter = typename ClientParams::template Setter; + + template + using ControlSetter = typename ClientParams::template Setter; + + using Client = C; + using ParamSetType = typename C::ParamSetType; static const char* getName(const char* setName = nullptr) { @@ -565,33 +66,79 @@ public: getInterfaceTable(ft); impl::FluidSCWrapperBase::setup(ft, name); ft->fDefineUnitCmd(name, "version", doVersion); + + std::string commandName("/"); + commandName += getName(); + commandName += "/version"; + ft->fDefinePlugInCmd(commandName.c_str(), + [](World*, void*, sc_msg_iter*, void*){ doVersion(nullptr,nullptr); }, + nullptr); + + } + + static auto& setParams(Unit* x, ParamSetType& p, + FloatControlsIter& inputs, bool constrain = false, bool initialized = true) + { + bool verbose = x->mWorld->mVerbosity > 0; + + using Reportage = decltype(static_cast(x)->mReportage); + + Reportage* reportage = initialized ? &(static_cast(x)->mReportage) : new Reportage(); + + p.template setParameterValuesRT(verbose ? reportage: nullptr , x, inputs); + if (constrain) p.constrainParameterValuesRT(verbose ? reportage : nullptr); + if(verbose) + { + for(auto& r:*reportage) + { + if(!r.ok()) printResult(x->mParent->mNode.mWorld, r); + } + } + if(!initialized) delete reportage; + return p; } - static auto& setParams(ParameterSetType& p, bool verbose, World* world, - FloatControlsIter& inputs, bool constrain = false) +// static void printResult(SharedState& x, Result& r) +// { +// if (!x.get() || !x->mNodeAlive) return; +// FluidSCWrapper::printResult(x->mNode->mWorld, r); +// } + + static void printResult(World* w,Result& r) { - // We won't even try and set params if the arguments don't match - if (inputs.size() == C::getParameterDescriptors().count()) - { - p.template setParameterValues(verbose, world, inputs); - if (constrain) p.constrainParameterValues(); - } - else + + switch (r.status()) { - std::cout << "ERROR: " << getName() - << ": parameter count mismatch. Perhaps your binary plugins " - "and SC sources are different versions" - << std::endl; - // TODO: work out how to bring any further work to a halt - } - return p; + case Result::Status::kWarning: + { + if (!w || w->mVerbosity > 0) + std::cout << "WARNING: " << getName() << " - " << r.message().c_str() << '\n'; + break; + } + case Result::Status::kError: + { + std::cout << "ERROR: " << getName() << " - " << r.message().c_str() << '\n'; + break; + } + case Result::Status::kCancelled: + { + std::cout << getName() << ": Task cancelled\n" << '\n'; + break; + } + default: + { + } + } } + +private: + std::array mReportage; }; -template