diff --git a/CMakeLists.txt b/CMakeLists.txt index a993e4e..f6fe22e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,16 +5,35 @@ get_filename_component(PROJECT ${FILENAME} NAME_WE) #automatically sets project message(STATUS "Project name is ${PROJECT}") project (${PROJECT}) -find_library(MAPPER_LIB mapper) -if(NOT MAPPER_LIB) - message(FATAL_ERROR "libmapper library not found") -endif() - -find_package(Boost 1.73.0 REQUIRED) -if(NOT Boost_FOUND) - message(FATAL_ERROR "boost library not found") -endif() -include_directories(Boost_INCLUDE_DIRS) +if (WIN32) +# EDIT LIBMAPPER_DIR PATH BELOW BEFORE COMPILING FOR WINDOWS +set(LIBMAPPER_DIR "C:/Users/brady/Documents/Github/libmapper") +set(LIBMAPPER_BUILD_DIR "${LIBMAPPER_DIR}/build/Release") +set(LIBLO_DIR "${LIBMAPPER_DIR}/build/liblo/liblo-master") +set(LIBLO_BUILD_DIR "${LIBLO_DIR}/cmake/build/Release") +set(LIBLO_INCLUDES "${LIBLO_DIR}/cmake/build;${LIBLO_DIR}") + +set(Liblo_LIB ${LIBLO_BUILD_DIR}/liblo.lib) +set(Libmapper_LIB ${LIBMAPPER_BUILD_DIR}/libmapper.lib) +mark_as_advanced(Liblo_LIB) +mark_as_advanced(Libmapper_LIB) +add_definitions( + -D_WINSOCK_DEPRECATED_NO_WARNINGS + -DHAVE_WINSOCK2_H + -DNODEFAULTLIB +) +include_directories( + "${LIBLO_INCLUDES}" + "${LIBMAPPER_DIR}/include" +) +set(MAPPER_LIB ${Liblo_LIB} ${Libmapper_LIB}) +else() + find_library(MAPPER_LIB mapper) + if(NOT MAPPER_LIB) + message(FATAL_ERROR "libmapper library not found") + endif(NOT MAPPER_LIB) + include_directories(/usr/local/include) +endif(WIN32) if (APPLE) set(CMAKE_INSTALL_PREFIX "${CMAKE_BINARY_DIR}") @@ -33,8 +52,6 @@ include_directories(${SC_PATH}/include/plugin_interface) include_directories(${SC_PATH}/include/common) include_directories(${SC_PATH}/common) -include_directories(/usr/local/include) - if (CMAKE_SYSTEM_NAME MATCHES "Linux|.*BSD|DragonFly") set(INSTALL_DESTINATION "lib/SuperCollider/plugins") if (QUARKS) @@ -136,18 +153,25 @@ endif() add_library(${PROJECT} MODULE ${FILENAME}) target_link_libraries(${PROJECT} ${MAPPER_LIB} ${Boost_libraries}) +if(WIN32) + set_target_properties(${PROJECT} PROPERTIES LINK_FLAGS "/NODEFAULTLIB:MSVCRTD") + set_target_properties(${PROJECT} PROPERTIES LINK_FLAGS "/INCREMENTAL:NO") +endif(WIN32) + if(SUPERNOVA) add_library(${PROJECT}_supernova MODULE ${FILENAME}) target_link_libraries(${PROJECT}_supernova ${MAPPER_LIB}) set_property(TARGET ${PROJECT}_supernova PROPERTY COMPILE_DEFINITIONS SUPERNOVA) + if(NOT WIN32) install(TARGETS ${PROJECT}_supernova DESTINATION ${INSTALL_DESTINATION}/${PLUGIN_DIR}) + endif(NOT WIN32) endif() - +if(NOT WIN32) install(TARGETS ${PROJECT} DESTINATION ${INSTALL_DESTINATION}/${PLUGIN_DIR}) - install(DIRECTORY "sc/" DESTINATION "${INSTALL_DESTINATION_DISTRO}" PATTERN "*") +endif(NOT WIN32) \ No newline at end of file diff --git a/Mapper.cpp b/Mapper.cpp index 659fbbf..ead4b15 100644 --- a/Mapper.cpp +++ b/Mapper.cpp @@ -22,7 +22,6 @@ #include #include -#include #include #include #include @@ -34,12 +33,8 @@ static mpr_dev dev; static std::atomic isReady; static std::thread* libmapperThreadHandle; -typedef std::function Task; -static boost::lockfree::queue taskQueue(128); - -void libmapperThread() { - // TODO(mb): Add option for specifying device name with Mapper.enable(name) - dev = mpr_dev_new("SuperCollider", 0); +void libmapperThread(char* deviceName) { + dev = mpr_dev_new(deviceName, 0); while (!mpr_dev_get_is_ready(dev)) { mpr_dev_poll(dev, 10); } @@ -47,28 +42,32 @@ void libmapperThread() { Print("Mapper: libmapper ready!\n"); while (dev) { mpr_dev_poll(dev, 1); - // Execute tasks - Task* f; - if (taskQueue.pop(f)) { - (*f)(); - delete f; - } } } -struct MapperUnit : public Unit { +struct MapperSignalUnit : public Unit { int signalNameSize; char* signalName; - mpr_sig sig; + mpr_sig sig = nullptr; float sigMin = 0; float sigMax = 1; float val = 0; }; -struct MapIn : public MapperUnit {}; -struct MapOut : public MapperUnit {}; -struct MapperEnabler : public Unit {}; +struct MapperDeviceUnit : public Unit { + int deviceNameSize; + char* deviceName; +}; + +struct MapperIsReadyUnit : public Unit { + bool shouldSendNeg = true; // Send -1 the first frame to trigger free when ready +}; + +struct MapIn : public MapperSignalUnit {}; +struct MapOut : public MapperSignalUnit {}; +struct MapperEnabler : public MapperDeviceUnit {}; struct MapperDisabler : public Unit {}; +struct MapperIsReady : public MapperIsReadyUnit {}; // Empty DSP function static void Unit_next_nop(Unit* unit, int inNumSamples) {} @@ -84,36 +83,24 @@ static void MapOut_next(MapOut* unit, int inNumSamples); static void MapperEnabler_Ctor(MapperEnabler* unit); static void MapperDisabler_Ctor(MapperDisabler* unit); -static void MapIn_signalUpdate(mpr_sig sig, mpr_sig_evt evt, mpr_id inst, - int length, mpr_type type, const void* value, - mpr_time time) { - MapIn* m = (MapIn*)mpr_obj_get_prop_as_ptr(sig, MPR_PROP_DATA, 0); - if (m) { - m->val = *reinterpret_cast(value); - } -} +static void MapperIsReady_Ctor(MapperIsReady* unit); +static void MapperIsReady_Dtor(MapperIsReady* unit); +static void MapperIsReady_next(MapperIsReady* unit, int inNumSamples); -static void MapperUnit_bindToSignal(MapperUnit* unit, mpr_dir direction) { +static void MapperSignalUnit_bindToSignal(MapperSignalUnit* unit, mpr_dir direction) { // Search for existing output signal with same name mpr_list sigs = mpr_dev_get_sigs(dev, direction); sigs = mpr_list_filter(sigs, MPR_PROP_NAME, 0, 1, MPR_STR, unit->signalName, MPR_OP_EQ); - mpr_sig_handler* handler = direction == MPR_DIR_IN ? MapIn_signalUpdate : 0; - int flags = direction == MPR_DIR_IN ? MPR_SIG_UPDATE : 0; - if (sigs) { // Signal exists, bind unit to signal unit->sig = *sigs; - // Update pointer for signal update callback - mpr_obj_set_prop(unit->sig, MPR_PROP_DATA, 0, 1, MPR_PTR, unit, 0); - mpr_sig_set_cb(unit->sig, handler, flags); - // Update signal metadata if signal range has changed - // mpr_obj_set_prop(unit->sig, MPR_PROP_MIN, 0, 1, MPR_FLT, &unit->sigMin, - // 1); mpr_obj_set_prop(unit->sig, MPR_PROP_MAX, 0, 1, MPR_FLT, - // &unit->sigMax, 1); + mpr_obj_set_prop(unit->sig, MPR_PROP_MIN, 0, 1, MPR_FLT, &unit->sigMin, 1); + mpr_obj_set_prop(unit->sig, MPR_PROP_MAX, 0, 1, MPR_FLT, &unit->sigMax, 1); + mpr_obj_push(unit->sig); // Update maps containing signal // mpr_list maps = mpr_sig_get_maps(unit->sig, MPR_DIR_ANY); @@ -135,10 +122,9 @@ static void MapperUnit_bindToSignal(MapperUnit* unit, mpr_dir direction) { // } } else { // Signal doesn't exist, create new - Print("Creating signal '%s'\n", unit->signalName); + Print("Mapper: Creating signal '%s'\n", unit->signalName); unit->sig = mpr_sig_new(dev, direction, unit->signalName, 1, MPR_FLT, 0, - &unit->sigMin, &unit->sigMax, 0, handler, flags); - mpr_obj_set_prop(unit->sig, MPR_PROP_DATA, 0, 1, MPR_PTR, unit, 0); + &unit->sigMin, &unit->sigMax, 0, 0, 0); } } @@ -151,6 +137,7 @@ void MapIn_Ctor(MapIn* unit) { unit->sigMin = IN0(0); unit->sigMax = IN0(1); + // Set signal name unit->signalNameSize = IN0(2); const int signalNameAllocSize = (unit->signalNameSize + 1) * sizeof(char); @@ -167,9 +154,9 @@ void MapIn_Ctor(MapIn* unit) { } unit->signalName[unit->signalNameSize] = 0; + // Bind to signal if (dev) { - taskQueue.push( - new Task([unit]() { MapperUnit_bindToSignal(unit, MPR_DIR_IN); })); + MapperSignalUnit_bindToSignal(unit, MPR_DIR_IN); } else { Print("MapIn: libmapper not enabled\n"); } @@ -181,7 +168,23 @@ void MapIn_Dtor(MapIn* unit) { RTFree(unit->mWorld, unit->signalName); } void MapIn_next(MapIn* unit, int inNumSamples) { float* out = OUT(0); - *out = unit->val; + + // Signal is not created yet + if (!unit->sig) { + *out = 0.f; + } + // Get signal value pointer + const float* val = + static_cast(mpr_sig_get_value(unit->sig, 0, 0)); + + // Signal doesn't have a value yet + if (!val) { + *out = 0.f; + return; + } + + // Set out value to signal value + *out = *val; } // MapOut @@ -190,6 +193,7 @@ void MapOut_Ctor(MapOut* unit) { unit->sigMin = IN0(1); unit->sigMax = IN0(2); + // Set signal name unit->signalNameSize = IN0(3); const int signalNameAllocSize = (unit->signalNameSize + 1) * sizeof(char); @@ -208,8 +212,7 @@ void MapOut_Ctor(MapOut* unit) { unit->signalName[unit->signalNameSize] = 0; if (isReady) { - taskQueue.push( - new Task([unit]() { MapperUnit_bindToSignal(unit, MPR_DIR_OUT); })); + MapperSignalUnit_bindToSignal(unit, MPR_DIR_OUT); } else { Print("MapOut: libmapper not enabled\n"); SETCALC(Unit_next_nop); @@ -222,17 +225,37 @@ void MapOut_Ctor(MapOut* unit) { void MapOut_Dtor(MapOut* unit) { RTFree(unit->mWorld, unit->signalName); } void MapOut_next(MapOut* unit, int inNumSamples) { + // Signal is not ready yet + if (!unit->sig) { + return; + } + + // Set output signal value float val = IN0(0); - taskQueue.push( - new Task([=]() { mpr_sig_set_value(unit->sig, 0, 1, MPR_FLT, &val); })); + mpr_sig_set_value(unit->sig, 0, 1, MPR_FLT, &val); } // MapperEnabler void MapperEnabler_Ctor(MapperEnabler* unit) { if (!dev) { - // dev = mpr_dev_new("SuperCollider", 0); - libmapperThreadHandle = new std::thread(libmapperThread); + // Set device name + unit->deviceNameSize = IN0(0); + const int deviceNameAllocSize = (unit->deviceNameSize + 1) * sizeof(char); + + void* chunk = RTAlloc(unit->mWorld, deviceNameAllocSize); + if (!chunk) { + Print("MapOut: RT memory allocation failed\n"); + SETCALC(Unit_next_nop); + return; + } + + unit->deviceName = reinterpret_cast(chunk); + for (int i = 0; i < unit->deviceNameSize; i++) { + unit->deviceName[i] = static_cast(IN0(1 + i)); + } + unit->deviceName[unit->deviceNameSize] = 0; + libmapperThreadHandle = new std::thread(libmapperThread, unit->deviceName); } else { Print("Mapper: libmapper already enabled.\n"); } @@ -250,10 +273,28 @@ void MapperDisabler_Ctor(MapperDisabler* unit) { SETCALC(Unit_next_nop); } +// MapperIsReady + +void MapperIsReady_Ctor(MapperIsReady* unit) { + SETCALC(MapperIsReady_next); + MapperIsReady_next(unit, 1); +} +void MapperIsReady_Dtor(MapperIsReady* unit) {} +void MapperIsReady_next(MapperIsReady* unit, int inNumSamples) { + float* out = OUT(0); + if (unit->shouldSendNeg) { + *out = -1.0f; + unit->shouldSendNeg = false; // Sent -1 once, send regular value now + } else { + *out = (isReady) ? 1.0f : -1.0f; + } +} + PluginLoad(MapperUGens) { ft = inTable; DefineDtorUnit(MapIn); DefineDtorUnit(MapOut); DefineSimpleUnit(MapperEnabler); DefineSimpleUnit(MapperDisabler); + DefineDtorUnit(MapperIsReady); } diff --git a/README.md b/README.md index 22c312f..118d282 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ Please follow the [libmapper](https://github.com/libmapper/libmapper) documentat ## Compilation from source +### GNU/Linux or macOS + ``` git clone --recursive https://github.com/IDMIL/MapperUGen.git cd MapperUGen @@ -30,3 +32,31 @@ mkdir -p build cmake -B ./build -DSUPERNOVA=ON cmake --build build --target install ``` + +### Windows + +Note: when cloning supercollider below, if the cloning hangs just execute `git config --global url."https://".insteadOf git://` to fix the issue. It's a known issue with older versions of the supercollider SDK. + +``` +git clone https://github.com/libmapper/MapperUGen.git +cd MapperUGen +git config --global url."https://".insteadOf git:// +git submodule update --init --recursive +``` + +Now edit the CMakeLists.txt LIBMAPPER_DIR and LIBLO_DIR paths to match with your local libmapper paths. For libmapper installation help, consult the instructions [here](https://github.com/libmapper/libmapper/blob/main/doc/how_to_compile_and_run.md). Finally: + +``` +mkdir build +cd build +cmake -DSUPERNOVA=ON .. +cmake --build . +``` + +#### Windows manual installation after compiling + +1. Evaluate `Platform.userExtensionDir` in supercollider and create a "Mapper" folder there +2. Copy everything inside the ./sc folder to the Mapper folder +3. Create a "plugins" folder in the Mapper directory +4. Copy the build outputs from ./build/Debug and your libmapper, liblo and zlib .dlls to the plugins folder +5. Reboot the supercollider interpreter \ No newline at end of file diff --git a/sc/HelpSource/Classes/Mapper.schelp b/sc/HelpSource/Classes/Mapper.schelp index a8a2d29..893efdb 100644 --- a/sc/HelpSource/Classes/Mapper.schelp +++ b/sc/HelpSource/Classes/Mapper.schelp @@ -3,7 +3,7 @@ summary:: UGen for using libmapper with SuperCollider server categories:: UGens>Synth control DESCRIPTION:: -Creates a libmapper devices on the SuperCollider server. Input and output signals can be created with the MapIn and MapOut. The signals stay active util the libmapper device is destroyed either by rebooting the server or calling Mapper.disable. +Creates a libmapper devices on the SuperCollider server. Input and output signals can be created with the MapIn and MapOut, or a mappable Bus can be created for an input signal with Mapper.makeInSignalBus(). The signals stay active util the libmapper device is destroyed either by rebooting the server or calling Mapper.disable. CLASSMETHODS:: private:: categories @@ -13,24 +13,35 @@ METHOD:: enable, disable EXAMPLES:: code:: ( -s.waitForBoot({ - Mapper.enable; -}); +// Just a rudimentary SynthDef we'll use below +SynthDef(\SineGenerator, { |out=0, freq=200, gain=0| + Out.ar( out, SinOsc.ar(freq, 0, gain) ) + }).add; ) -// wait for libmapper ready message - -// Output signal ( -{ - MapOut.kr(SinOsc.kr(1), \sine, -1, 1); -}.play; +s.waitForBoot({ + // Create and optionally name your libmapper device + Mapper.enable("MySuperColliderDevice"); + + // Wait for libmapper to fully boot and create/map signals + Mapper.waitForBoot({ + // Send output signal from SuperCollider + { MapOut.kr(SinOsc.kr(1), \sine, -1, 1) }.play; + + // Receive input signal to SuperCollider + { RLPF.ar(Saw.ar(50), MapIn.kr(\ffreq, 20, 20000), 0.2).dup * 0.2 }.play; + + // Or, use makeInSignalBus to map an input signal directly to a Synth argument using a Bus + a = Synth(\SineGenerator); + ~freqBus = Mapper.makeInSignalBus(s, "ffreq2", 10, 10000); + a.map(\freq, ~freqBus); + }); +}); ) -// Input signal ( -{ - RLPF.ar(Saw.ar(50), MapIn.kr(\ffreq, 20, 20000), 0.2).dup * 0.2; -}.play +// Disable when we're done +Mapper.disable; ) :: diff --git a/sc/classes/Mapper.sc b/sc/classes/Mapper.sc index 32ca5ac..9776d0c 100644 --- a/sc/classes/Mapper.sc +++ b/sc/classes/Mapper.sc @@ -1,7 +1,7 @@ Mapper : UGen { - *enable { + *enable { arg name = "SuperCollider"; play { - MapperEnabler.kr; + MapperEnabler.kr(name); FreeSelf.kr(Impulse.kr(1)); } } @@ -12,6 +12,20 @@ Mapper : UGen { FreeSelf.kr(Impulse.kr(1)); } } + + *waitForBoot { arg onComplete; + fork { + { FreeSelf.kr(MapperIsReady.kr) }.play.waitForFree; + onComplete.value; + }; + } + + *makeInSignalBus { + arg server, name, min, max; + var bus = Bus.control(server); + {Out.kr(bus.index, MapIn.kr(name, min, max))}.play; + ^bus; + } } MapIn : UGen { @@ -30,8 +44,9 @@ MapOut : UGen { } MapperEnabler : UGen { - *kr { - this.new1('control'); + *kr { arg name; + var ascii = name.ascii; + this.new1('control', *[ascii.size].addAll(ascii)); ^0.0; } } @@ -42,3 +57,9 @@ MapperDisabler : UGen { ^0.0; } } + +MapperIsReady : UGen { + *kr { + ^this.new1('control'); + } +}