diff --git a/BUGS b/BUGS new file mode 100644 index 0000000..ef8ad2c --- /dev/null +++ b/BUGS @@ -0,0 +1,6 @@ +LibreMapping known bugs +======================= + + +* segfaults if OSC receiving port is busy + diff --git a/DestinationGLCanvas.h b/DestinationGLCanvas.h index c64e04d..ebd620e 100644 --- a/DestinationGLCanvas.h +++ b/DestinationGLCanvas.h @@ -2,6 +2,7 @@ * DestinationGLCanvas.h * * (c) 2013 Sofian Audry -- info(@)sofianaudry(.)com + * (c) 2013 Alexandre Quessy -- alexandre(@)quessy(.)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 diff --git a/MainWindow.cpp b/MainWindow.cpp index 90b1cfd..56b4918 100644 --- a/MainWindow.cpp +++ b/MainWindow.cpp @@ -37,6 +37,7 @@ MainWindow::MainWindow() createStatusBar(); readSettings(); + startOscReceiver(); //setWindowIcon(QIcon(":/images/icon.png")); setCurrentFile(""); @@ -164,7 +165,7 @@ void MainWindow::import() QString fileName = QFileDialog::getOpenFileName(this, tr("Import resource"), "."); if (!fileName.isEmpty()) - importFile(fileName); + importMediaFile(fileName); } } @@ -448,6 +449,7 @@ void MainWindow::createContextMenu() void MainWindow::createToolBars() { fileToolBar = addToolBar(tr("&File")); + fileToolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); fileToolBar->addAction(importAction); fileToolBar->addAction(newAction); fileToolBar->addAction(openAction); @@ -492,6 +494,7 @@ void MainWindow::readSettings() mainSplitter->restoreState(settings.value("mainSplitter").toByteArray()); sourceSplitter->restoreState(settings.value("sourceSplitter").toByteArray()); canvasSplitter->restoreState(settings.value("canvasSplitter").toByteArray()); + config_osc_receive_port = settings.value("osc_receive_port", 12345).toInt(); } void MainWindow::writeSettings() @@ -502,6 +505,7 @@ void MainWindow::writeSettings() settings.setValue("mainSplitter", mainSplitter->saveState()); settings.setValue("sourceSplitter", sourceSplitter->saveState()); settings.setValue("canvasSplitter", canvasSplitter->saveState()); + settings.setValue("osc_receive_port", config_osc_receive_port); } bool MainWindow::okToContinue() @@ -591,7 +595,12 @@ void MainWindow::setCurrentFile(const QString &fileName) setWindowTitle(tr("%1[*] - %2").arg(shownName).arg(tr("LibreMapping Project"))); } -bool MainWindow::importFile(const QString &fileName) +// TODO +// bool MainWindow::updateMediaFile(const QString &source_name, const QString &fileName) +// { +// } + +bool MainWindow::importMediaFile(const QString &fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { @@ -666,3 +675,82 @@ QString MainWindow::strippedName(const QString &fullFileName) { return QFileInfo(fullFileName).fileName(); } + +void MainWindow::startOscReceiver() +{ +#ifdef HAVE_OSC + int port = config_osc_receive_port; + std::ostringstream os; + os << port; + osc_interface.reset(new OscInterface(this, os.str())); + if (port != 0) + { + osc_interface->start(); + } + osc_timer = new QTimer(this); // FIXME: memleak? + connect(osc_timer, SIGNAL(timeout()), this, SLOT(pollOscInterface())); + osc_timer->start(); +#endif +} + +void MainWindow::pollOscInterface() +{ +#ifdef HAVE_OSC + osc_interface->consume_commands(); +#endif +} + +void MainWindow::applyOscCommand(QVariantList & command) +{ + bool VERBOSE = true; + if (VERBOSE) + { + std::cout << "Receive OSC: "; + for (int i = 0; i < command.size(); ++i) + { + if (command.at(i).type() == QVariant::Int) + { + std::cout << command.at(i).toInt() << " "; + } + else if (command.at(i).type() == QVariant::Double) + { + std::cout << command.at(i).toDouble() << " "; + } + else if (command.at(i).type() == QVariant::String) + { + std::cout << command.at(i).toString().toStdString() << " "; + } + else + { + std::cout << "??? "; + } + } + std::cout << std::endl; + std::cout.flush(); + } + + if (command.size() < 2) + return; + if (command.at(0).type() != QVariant::String) + return; + if (command.at(1).type() != QVariant::String) + return; + std::string path = command.at(0).toString().toStdString(); + std::string typetags = command.at(1).toString().toStdString(); + + // Handle all OSC messages here + if (path == "/image/uri" && typetags == "s") + { + std::string image_uri = command.at(2).toString().toStdString(); + std::cout << "TODO load /image/uri " << image_uri << std::endl; + } + else if (path == "/add/quad") + addQuad(); + else if (path == "/add/triangle") + addTriangle(); + else if (path == "/project/save") + save(); + else if (path == "/project/open") + open(); +} + diff --git a/MainWindow.h b/MainWindow.h index 2168e25..e12add5 100644 --- a/MainWindow.h +++ b/MainWindow.h @@ -2,6 +2,7 @@ * MainWindow.h * * (c) 2013 Sofian Audry -- info(@)sofianaudry(.)com + * (c) 2013 Alexandre Quessy -- alexandre(@)quessy(.)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 @@ -21,10 +22,13 @@ #define MAIN_WINDOW_H_ #include - +#include +#include #include "SourceGLCanvas.h" +#ifdef HAVE_OSC +#include "OscInterface.h" +#endif #include "DestinationGLCanvas.h" - #include "MappingManager.h" #define LIBREMAPPING_VERSION "0.1" @@ -36,6 +40,7 @@ Q_OBJECT public: ~MainWindow(); static MainWindow& getInstance(); + void applyOscCommand(QVariantList & command); private: MainWindow(); @@ -52,6 +57,9 @@ private slots: void open(); bool save(); bool saveAs(); + /** + * Action that will call importMediaFile. + */ void import(); void about(); void updateStatusBar(); @@ -62,6 +70,7 @@ private slots: void addTriangle(); void windowModified(); + void pollOscInterface(); private: // Methods. @@ -73,11 +82,12 @@ private: void createStatusBar(); void readSettings(); void writeSettings(); + void startOscReceiver(); bool okToContinue(); bool loadFile(const QString &fileName); bool saveFile(const QString &fileName); void setCurrentFile(const QString &fileName); - bool importFile(const QString &fileName); + bool importMediaFile(const QString &fileName); void addMappingItem(int mappingId); void clearWindow(); QString strippedName(const QString &fullFileName); @@ -121,6 +131,12 @@ private: QSplitter* sourceSplitter; QSplitter* canvasSplitter; +#ifdef HAVE_OSC + OscInterface::ptr osc_interface; +#endif + int config_osc_receive_port; + QTimer *osc_timer; + private: // Model. MappingManager* mappingManager; diff --git a/OscInterface.cpp b/OscInterface.cpp new file mode 100644 index 0000000..32fc26a --- /dev/null +++ b/OscInterface.cpp @@ -0,0 +1,176 @@ +/* + * OscInterface.cpp + * + * Copyright (c) 2010 Alexandre Quessy + * Copyright (c) 2010 Tristan Matthews + * (c) 2013 Sofian Audry -- info(@)sofianaudry(.)com + * (c) 2013 Alexandre Quessy -- alexandre(@)quessy(.)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 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 . + */ + +#include "OscInterface.h" +#include "MainWindow.h" +#include + +OscInterface::OscInterface( + MainWindow* owner, + const std::string &listen_port) : + receiver_(listen_port), + owner_(owner), + messaging_queue_() +{ + //if (listen_port != OSC_PORT_NONE) + receiving_enabled_ = true; + if (receiving_enabled_) + { + std::cout << "Listening osc_udp://localhost:" << listen_port << std::endl; + // receiver_.addHandler("/ping", "", ping_cb, this); + // receiver_.addHandler("/pong", "", pong_cb, this); + // receiver_.addHandler("/image/path", "s", image_path_cb, this); + receiver_.addHandler(NULL, NULL, genericHandler, this); + } +} + +/** + * Handles /pong. Does nothing. + */ +int OscInterface::pong_cb( + const char *path, + const char * /*types*/, + lo_arg ** /*argv*/, + int /*argc*/, + void * /*data*/, + void *user_data) +{ + OscInterface* context = static_cast(user_data); + if (context->is_verbose()) + std::cout << "Got " << path << std::endl; + return 0; +} + +/** + * Handles /ping. Does nothing. + */ +int OscInterface::ping_cb( + const char *path, + const char * /*types*/, + lo_arg ** /*argv*/, + int /*argc*/, + void * /*data*/, + void *user_data) +{ + OscInterface* context = static_cast(user_data); + if (context->is_verbose()) + std::cout << "Got " << path << std::endl; + return 0; +} + +int OscInterface::image_path_cb( + const char *path, + const char * /*types*/, + lo_arg **argv, + int /*argc*/, + void * /*data*/, + void *user_data) +{ + OscInterface* context = static_cast(user_data); + std::string file_name(static_cast(&argv[0]->s)); + //if (context->is_verbose()) + std::cout << "Got " << path << " " << file_name << std::endl; + QVariantList message; + message.append(QVariant(QString("image/path"))); + message.append(QVariant(QString(file_name.c_str()))); + context->push_command(message); + return 0; +} + +void OscInterface::push_command(QVariantList command) +{ + messaging_queue_.push(command); +} +/** + * Takes action! + * + * Should be called when it's time to take action, before rendering a frame, for example. + */ +void OscInterface::consume_commands() +{ + bool success = true; + while (success) + { + QVariantList command; + success = messaging_queue_.try_pop(command); + if (success) + { + //if (is_verbose()) + // std::cout << __FUNCTION__ << ": apply " << + // command.first().toString().toStdString() << std::endl; + owner_->applyOscCommand(command); + } + } +} + +/** Starts listening if enabled + */ +void OscInterface::start() +{ + if (receiving_enabled_) + { + // start a thread to try and subscribe us + receiver_.listen(); // start listening in separate thread + } +} + +/* catch any incoming messages and display them. returning 1 means that the + * * message has not been fully handled and the server should try other methods */ +int OscInterface::genericHandler(const char *path, + const char *types, lo_arg **argv, + int argc, void * /*data*/, void * user_data) +{ + OscInterface* context = static_cast(user_data); + QVariantList message; + + message.append(QVariant(QString(path))); + message.append(QVariant(QString(types))); + + for (int i = 0; i < argc; ++i) + { + switch (types[i]) + { + case 'i': + message.append(QVariant(argv[i]->i)); + break; + case 'f': + message.append(QVariant((double) argv[i]->f)); + break; + case 's': + message.append(QVariant(QString(static_cast(&argv[i]->s)))); + break; + case 'd': + message.append(QVariant((double) argv[i]->d)); + break; + case 'T': + message.append(QVariant(true)); + break; + case 'F': + message.append(QVariant(false)); + break; + default: + break; + } + context->push_command(message); + } + return 0; // handled +} diff --git a/OscInterface.h b/OscInterface.h new file mode 100644 index 0000000..4c19a87 --- /dev/null +++ b/OscInterface.h @@ -0,0 +1,71 @@ +/* + * OscInterface.h + * + * Copyright (c) 2010 Alexandre Quessy + * Copyright (c) 2010 Tristan Matthews + * (c) 2013 Sofian Audry -- info(@)sofianaudry(.)com + * (c) 2013 Alexandre Quessy -- alexandre(@)quessy(.)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 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 . + */ + +#ifndef OSC_INTERFACE_H_ +#define OSC_INTERFACE_H_ + +#include +#include "concurrentqueue.h" +#include "OscReceiver.h" +#include + +class MainWindow; // forward decl + +/** + * Open Sound Control sending and receiving for LibreMapping. + */ +class OscInterface +{ + public: + typedef std::tr1::shared_ptr ptr; + OscInterface( + MainWindow* owner, + const std::string &listen_port); + ~OscInterface() {} + void start(); + void consume_commands(); + bool is_verbose() { return false; } + private: + void push_command(QVariantList command); + /** + * OSC callback + */ + static int image_path_cb(const char *path, + const char *types, lo_arg **argv, + int argc, void *data, void *user_data); + static int ping_cb(const char *path, + const char *types, lo_arg **argv, + int argc, void *data, void *user_data); + static int pong_cb(const char *path, + const char *types, lo_arg **argv, + int argc, void *data, void *user_data); + static int genericHandler(const char *path, + const char *types, lo_arg **argv, + int argc, void *data, void *user_data); + + bool receiving_enabled_; + OscReceiver receiver_; + MainWindow* owner_; + ConcurrentQueue messaging_queue_; +}; + +#endif /* include guard */ diff --git a/OscReceiver.cpp b/OscReceiver.cpp new file mode 100644 index 0000000..7f19119 --- /dev/null +++ b/OscReceiver.cpp @@ -0,0 +1,71 @@ +#include "OscReceiver.h" +#include +#include + +OscReceiver::OscReceiver(const std::string &port) : + port_(port), + server_(lo_server_thread_new(port_.c_str(), error)) +{ +// #ifdef CONFIG_DEBUG +// /* add method that will match any path and args */ +// lo_server_thread_add_method(server_, NULL, NULL, genericHandler, this); +// #endif +} + +OscReceiver::~OscReceiver() +{ +// std::cout << "Freeing OSC server thread\n"; + lo_server_thread_free(server_); +} + +void OscReceiver::addHandler(const char *path, const char *types, lo_method_handler handler, void *userData) +{ + lo_server_thread_add_method(server_, path, types, handler, userData); +} + +void OscReceiver::listen() +{ + int lo_fd = lo_server_get_socket_fd(server_); + if (lo_fd == 0) + { + std::cout << "OSC port " << port_ << " is already in use." << std::endl; + } + else + { + std::cout << "Listening on port " << port_ << std::endl; + lo_server_thread_start(server_); + } +} + +void OscReceiver::error(int num, const char *msg, const char *path) +{ + std::cerr << "liblo server error " << num << " in path " << path + << ": " << msg << std::endl; +} + +// #ifdef CONFIG_DEBUG +// /* catch any incoming messages and display them. returning 1 means that the +// * * message has not been fully handled and the server should try other methods */ +// int OscReceiver::genericHandler(const char *path, +// const char *types, lo_arg **argv, +// int argc, void * /*data*/, void * /*user_data*/) +// { +// //OscReceiver *context = static_cast(user_data); +// printf("path: <%s>\n", path); +// for (int i = 0; i < argc; ++i) +// { +// printf("arg %d '%c' ", i, types[i]); +// lo_arg_pp(static_cast(types[i]), argv[i]); +// printf("\n"); +// } +// printf("\n"); +// fflush(stdout); +// +// return 1; +// } +// #endif // CONFIG_DEBUG + +std::string OscReceiver::toString() const +{ + return "port:" + port_; +} diff --git a/OscReceiver.h b/OscReceiver.h new file mode 100644 index 0000000..fc28720 --- /dev/null +++ b/OscReceiver.h @@ -0,0 +1,32 @@ +#ifndef _OSC_RECEIVER_H_ +#define _OSC_RECEIVER_H_ + +#include "lo/lo.h" +#include +#define CONFIG_DEBUG + +/** General-purpose wrapper around liblo to receive OSC messages. + */ +class OscReceiver { + public: + OscReceiver(const std::string &port); + ~OscReceiver(); + void listen(); + const char * port() const { return port_.c_str(); } + void addHandler(const char *path, + const char *types, lo_method_handler handler, + void *user_data); + private: + std::string toString() const; + std::string port_; + lo_server_thread server_; +#ifdef CONFIG_DEBUG + static int genericHandler(const char *path, + const char *types, lo_arg **argv, + int argc, void *data, void *user_data); +#endif + static void error(int num, const char *msg, const char *path); +}; + +#endif // _OSC_RECEIVER_H_ + diff --git a/ProjectReader.cpp b/ProjectReader.cpp index b3d18cb..8ef1fee 100644 --- a/ProjectReader.cpp +++ b/ProjectReader.cpp @@ -94,13 +94,13 @@ void ProjectReader::readMapping() { // FIXME: we assume an Image mapping Q_ASSERT(_xml.isStartElement() && _xml.name() == "mapping"); - const QString *paintIdAttrValue; + const QString *paint_id_attr; QXmlStreamAttributes attributes = _xml.attributes(); if (attributes.hasAttribute("", "paint_id")) - paintIdAttrValue = attributes.value("", "paint_id").string(); + paint_id_attr = attributes.value("", "paint_id").string(); - std::cout << " * " << "with paint ID " << paintIdAttrValue << std::endl; + std::cout << " * " << "with paint ID " << paint_id_attr << std::endl; //QString title = _xml.readElementText(); //item->setText(0, title); @@ -110,17 +110,18 @@ void ProjectReader::readPaint() { // FIXME: we assume an Image mapping Q_ASSERT(_xml.isStartElement() && _xml.name() == "paint"); - const QString *paintIdAttrValue; + const QString *paint_id_attr; + const QString *uri_attr; const QString *typeAttrValue; QXmlStreamAttributes attributes = _xml.attributes(); if (attributes.hasAttribute("", "name")) - paintIdAttrValue = attributes.value("", "name").string(); + paint_id_attr = attributes.value("", "name").string(); if (attributes.hasAttribute("", "type")) typeAttrValue = attributes.value("", "type").string(); std::cout << "Found " << typeAttrValue->toStdString() << - " paint " << paintIdAttrValue->toStdString() << std::endl; + " paint " << paint_id_attr->toStdString() << std::endl; /* Next element... */ _xml.readNext(); @@ -145,7 +146,8 @@ void ProjectReader::readPaint() { // pass } - std::cout << "uri " << _xml.text().toString().toStdString() << std::endl; + //uri_attr = _xml.text().toString(); + //std::cout << "uri " << uri_attr.toStdString() << std::endl; } else if (_xml.name() == "width") { diff --git a/README b/README index e2158ad..1b9c83a 100644 --- a/README +++ b/README @@ -19,7 +19,7 @@ Build ----- Dependencies: - * libwxgtk2.8-dev + * Qt * qt4-default * qt4-qmake diff --git a/concurrentqueue.h b/concurrentqueue.h new file mode 100644 index 0000000..18f3f68 --- /dev/null +++ b/concurrentqueue.h @@ -0,0 +1,86 @@ +/* + * Toonloop + * + * Copyright (c) 2010 Alexandre Quessy + * Copyright (c) 2010 Tristan Matthews + * + * Toonloop 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. + * + * Toonloop 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 Toonloop. If not, see . + */ + +// Written by Anthony Williams, 2008 +// Public domain based on his comment: +// "Yes, you can just copy the code presented here and use it for whatever you +// like. There won't be any licensing issues. I'm glad you find it helpful." +// Reference: +// http://www.justsoftwaresolutions.co.uk/threading/implementing-a-thread-safe-queue-using-condition-variables.html + +#ifndef _CONCURRENT_QUEUE_H_ +#define _CONCURRENT_QUEUE_H_ + +#include +#include +#include + +template +class ConcurrentQueue +{ + private: + std::queue queue_; + mutable boost::mutex mutex_; + boost::condition_variable condition_; + public: + ConcurrentQueue() : queue_(), mutex_(), condition_() + {} + + void push(Data const& data) + { + boost::mutex::scoped_lock lock(mutex_); + queue_.push(data); + lock.unlock(); + condition_.notify_one(); + } + + bool empty() const + { + boost::mutex::scoped_lock lock(mutex_); + return queue_.empty(); + } + + bool try_pop(Data& popped_value) + { + boost::mutex::scoped_lock lock(mutex_); + if(queue_.empty()) + { + return false; + } + + popped_value = queue_.front(); + queue_.pop(); + return true; + } + + void wait_and_pop(Data& popped_value) + { + boost::mutex::scoped_lock lock(mutex_); + while(queue_.empty()) + { + condition_.wait(lock); + } + + popped_value = queue_.front(); + queue_.pop(); + } +}; + +#endif // _CONCURRENT_QUEUE_H_ diff --git a/libremapping.pro b/libremapping.pro index ff844d2..ffad944 100644 --- a/libremapping.pro +++ b/libremapping.pro @@ -15,3 +15,13 @@ macx:QMAKE_CXXFLAGS += -D__MACOSX_CORE__ # not mac !macx:LIBS += -lglut -lGLU + +# detect osc +# unix { +system(pkg-config --exists liblo) { + CONFIG += link_pkgconfig + PKGCONFIG += liblo + DEFINES += HAVE_OSC + SOURCES += OscInterface.cpp OscReceiver.cpp + HEADERS += OscInterface.h OscReceiver.h concurrentqueue.h +} diff --git a/main.cpp b/main.cpp index 6820146..f87cb6f 100644 --- a/main.cpp +++ b/main.cpp @@ -10,6 +10,7 @@ int main(int argc, char *argv[]) { + // TODO: avoid segfaults when OSC port is busy QApplication app(argc, argv); if (!QGLFormat::hasOpenGL())