mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-08 00:40:02 +01:00
967 lines
37 KiB
C++
967 lines
37 KiB
C++
/*
|
|
* This file is part of vimix - video live mixer
|
|
*
|
|
* **Copyright** (C) 2019-2022 Bruno Herbelin <bruno.herbelin@gmail.com>
|
|
*
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
**/
|
|
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
#include <algorithm>
|
|
#include <iomanip>
|
|
|
|
#include "osc/OscOutboundPacketStream.h"
|
|
|
|
#include "defines.h"
|
|
#include "Log.h"
|
|
#include "Settings.h"
|
|
#include "BaseToolkit.h"
|
|
#include "Mixer.h"
|
|
#include "Source.h"
|
|
#include "SourceCallback.h"
|
|
#include "ActionManager.h"
|
|
#include "SystemToolkit.h"
|
|
#include "tinyxml2Toolkit.h"
|
|
#include "Metronome.h"
|
|
|
|
#include "UserInterfaceManager.h"
|
|
#include "RenderingManager.h"
|
|
#include <GLFW/glfw3.h>
|
|
|
|
#include "ControlManager.h"
|
|
|
|
#ifndef NDEBUG
|
|
#define CONTROL_DEBUG
|
|
#endif
|
|
|
|
#define CONTROL_OSC_MSG "OSC: "
|
|
|
|
//bool Control::input_active[INPUT_MAX]{};
|
|
//float Control::input_values[INPUT_MAX]{};
|
|
//std::mutex Control::input_access_;
|
|
|
|
|
|
void Control::RequestListener::ProcessMessage( const osc::ReceivedMessage& m,
|
|
const IpEndpointName& remoteEndpoint )
|
|
{
|
|
char sender[IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH];
|
|
remoteEndpoint.AddressAndPortAsString(sender);
|
|
|
|
try{
|
|
#ifdef CONTROL_DEBUG
|
|
Log::Info(CONTROL_OSC_MSG "received '%s' from %s", FullMessage(m).c_str(), sender);
|
|
#endif
|
|
// Preprocessing with Translator
|
|
std::string address_pattern = Control::manager().translate(m.AddressPattern());
|
|
|
|
// structured OSC address
|
|
std::list<std::string> address = BaseToolkit::splitted(address_pattern, OSC_SEPARATOR);
|
|
//
|
|
// A wellformed OSC address is in the form '/vimix/target/attribute {arguments}'
|
|
// First test: should have 3 elements and start with APP_NAME ('vimix')
|
|
//
|
|
if (address.size() > 2 && address.front().compare(OSC_PREFIX) == 0 ){
|
|
// done with the first part of the OSC address
|
|
address.pop_front();
|
|
// next part of the OSC message is the target
|
|
std::string target = address.front();
|
|
// next part of the OSC message is the attribute
|
|
address.pop_front();
|
|
std::string attribute = address.front();
|
|
// Log target: just print text in log window
|
|
if ( target.compare(OSC_INFO) == 0 )
|
|
{
|
|
if ( attribute.compare(OSC_INFO_NOTIFY) == 0) {
|
|
Log::Notify(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
|
|
}
|
|
else if ( attribute.compare(OSC_INFO_LOG) == 0) {
|
|
Log::Info(CONTROL_OSC_MSG "Received '%s' from %s", FullMessage(m).c_str(), sender);
|
|
}
|
|
}
|
|
// Output target: concerns attributes of the rendering output
|
|
else if ( target.compare(OSC_OUTPUT) == 0 )
|
|
{
|
|
if ( Control::manager().receiveOutputAttribute(attribute, m.ArgumentStream())) {
|
|
// send the global status
|
|
Control::manager().sendOutputStatus(remoteEndpoint);
|
|
}
|
|
}
|
|
// Multitouch target: user input on 'Multitouch' tab
|
|
else if ( target.compare(OSC_MULTITOUCH) == 0 )
|
|
{
|
|
Control::manager().receiveMultitouchAttribute(attribute, m.ArgumentStream());
|
|
}
|
|
// Session target: concerns attributes of the session
|
|
else if ( target.compare(OSC_SESSION) == 0 )
|
|
{
|
|
if ( Control::manager().receiveSessionAttribute(attribute, m.ArgumentStream()) ) {
|
|
// send the global status
|
|
Control::manager().sendOutputStatus(remoteEndpoint);
|
|
// send the status of all sources
|
|
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
|
}
|
|
}
|
|
// ALL sources target: apply attribute to all sources of the session
|
|
else if ( target.compare(OSC_ALL) == 0 )
|
|
{
|
|
// Loop over selected sources
|
|
for (SourceList::iterator it = Mixer::manager().session()->begin(); it != Mixer::manager().session()->end(); ++it) {
|
|
// apply attributes
|
|
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
|
|
// and send back feedback if needed
|
|
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
|
}
|
|
}
|
|
// Selected sources target: apply attribute to all sources of the selection
|
|
else if ( target.compare(OSC_SELECTED) == 0 )
|
|
{
|
|
// Loop over selected sources
|
|
for (SourceList::iterator it = Mixer::selection().begin(); it != Mixer::selection().end(); ++it) {
|
|
// apply attributes
|
|
if ( Control::manager().receiveSourceAttribute( *it, attribute, m.ArgumentStream()) && Mixer::manager().currentSource() == *it)
|
|
// and send back feedback if needed
|
|
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
|
}
|
|
}
|
|
// Current source target: apply attribute to the current sources
|
|
else if ( target.compare(OSC_CURRENT) == 0 )
|
|
{
|
|
int sourceid = -1;
|
|
if ( attribute.compare(OSC_SYNC) == 0) {
|
|
// send the status of all sources
|
|
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
|
}
|
|
else if ( attribute.compare(OSC_NEXT) == 0) {
|
|
// set current to NEXT
|
|
Mixer::manager().setCurrentNext();
|
|
// send the status of all sources
|
|
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
|
}
|
|
else if ( attribute.compare(OSC_PREVIOUS) == 0) {
|
|
// set current to PREVIOUS
|
|
Mixer::manager().setCurrentPrevious();
|
|
// send the status of all sources
|
|
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
|
}
|
|
else if ( BaseToolkit::is_a_number( attribute.substr(1), &sourceid) ){
|
|
// set current to given INDEX
|
|
Mixer::manager().setCurrentIndex(sourceid);
|
|
// send the status of all sources
|
|
Control::manager().sendSourcesStatus(remoteEndpoint, m.ArgumentStream());
|
|
}
|
|
// all other attributes operate on current source
|
|
else {
|
|
// apply attributes to current source
|
|
if ( Control::manager().receiveSourceAttribute( Mixer::manager().currentSource(), attribute, m.ArgumentStream()) )
|
|
// and send back feedback if needed
|
|
Control::manager().sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
|
}
|
|
}
|
|
// General case: try to identify the target
|
|
else {
|
|
// try to find source by index
|
|
Source *s = nullptr;
|
|
int sourceid = -1;
|
|
if ( BaseToolkit::is_a_number(target.substr(1), &sourceid) )
|
|
s = Mixer::manager().sourceAtIndex(sourceid);
|
|
|
|
// if failed, try to find source by name
|
|
if (s == nullptr)
|
|
s = Mixer::manager().findSource(target.substr(1));
|
|
|
|
// if a source with the given target name or index was found
|
|
if (s) {
|
|
// apply attributes to source
|
|
if ( Control::manager().receiveSourceAttribute(s, attribute, m.ArgumentStream()) )
|
|
// and send back feedback if needed
|
|
Control::manager().sendSourceAttibutes(remoteEndpoint, target, s);
|
|
}
|
|
else
|
|
Log::Info(CONTROL_OSC_MSG "Unknown target '%s' requested by %s.", target.c_str(), sender);
|
|
}
|
|
}
|
|
else {
|
|
Log::Info(CONTROL_OSC_MSG "Unknown osc message '%s' sent by %s.", m.AddressPattern(), sender);
|
|
}
|
|
}
|
|
catch( osc::Exception& e ){
|
|
// any parsing errors such as unexpected argument types, or
|
|
// missing arguments get thrown as exceptions.
|
|
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s' from %s : %s", m.AddressPattern(), sender, e.what());
|
|
}
|
|
}
|
|
|
|
|
|
std::string Control::RequestListener::FullMessage( const osc::ReceivedMessage& m )
|
|
{
|
|
// build a string with the address pattern of the message
|
|
std::ostringstream message;
|
|
message << m.AddressPattern() << " ";
|
|
|
|
// try to fill the string with the arguments
|
|
std::ostringstream arguments;
|
|
try{
|
|
// loop over all arguments
|
|
osc::ReceivedMessage::const_iterator arg = m.ArgumentsBegin();
|
|
while (arg != m.ArgumentsEnd()) {
|
|
if( arg->IsBool() ){
|
|
bool a = (arg++)->AsBoolUnchecked();
|
|
message << (a ? "T" : "F");
|
|
}
|
|
else if( arg->IsInt32() ){
|
|
int a = (arg++)->AsInt32Unchecked();
|
|
message << "i";
|
|
arguments << " " << a;
|
|
}
|
|
else if( arg->IsFloat() ){
|
|
float a = (arg++)->AsFloatUnchecked();
|
|
message << "f";
|
|
arguments << " " << std::fixed << std::setprecision(2) << a;
|
|
}
|
|
else if( arg->IsString() ){
|
|
const char *a = (arg++)->AsStringUnchecked();
|
|
message << "s";
|
|
arguments << " " << a;
|
|
}
|
|
}
|
|
}
|
|
catch( osc::Exception& e ){
|
|
// any parsing errors such as unexpected argument types, or
|
|
// missing arguments get thrown as exceptions.
|
|
Log::Info(CONTROL_OSC_MSG "Ignoring error in message '%s': %s", m.AddressPattern(), e.what());
|
|
}
|
|
|
|
// append list of arguments to the message string
|
|
message << arguments.str();
|
|
|
|
// returns the full message
|
|
return message.str();
|
|
}
|
|
|
|
|
|
Control::Control() : receiver_(nullptr)
|
|
{
|
|
for (size_t i = 0; i < INPUT_MULTITOUCH_COUNT; ++i) {
|
|
multitouch_active[i] = false;
|
|
multitouch_values[i] = glm::vec2(0.f);
|
|
}
|
|
|
|
for (size_t i = 0; i < INPUT_MAX; ++i) {
|
|
input_active[i] = false;
|
|
input_values[i] = 0.f;
|
|
}
|
|
}
|
|
|
|
Control::~Control()
|
|
{
|
|
terminate();
|
|
}
|
|
|
|
std::string Control::translate (std::string addresspattern)
|
|
{
|
|
auto it_translation = translation_.find(addresspattern);
|
|
if ( it_translation != translation_.end() )
|
|
return it_translation->second;
|
|
else
|
|
return addresspattern;
|
|
}
|
|
|
|
void Control::loadOscConfig()
|
|
{
|
|
// reset translations
|
|
translation_.clear();
|
|
|
|
// load osc config file
|
|
tinyxml2::XMLDocument xmlDoc;
|
|
tinyxml2::XMLError eResult = xmlDoc.LoadFile(Settings::application.control.osc_filename.c_str());
|
|
|
|
// the only reason to return false is if the file does not exist or is empty
|
|
if (eResult == tinyxml2::XML_ERROR_FILE_NOT_FOUND
|
|
| eResult == tinyxml2::XML_ERROR_FILE_COULD_NOT_BE_OPENED
|
|
| eResult == tinyxml2::XML_ERROR_FILE_READ_ERROR
|
|
| eResult == tinyxml2::XML_ERROR_EMPTY_DOCUMENT )
|
|
resetOscConfig();
|
|
|
|
// found the file, could open and read it
|
|
else if (eResult != tinyxml2::XML_SUCCESS)
|
|
Log::Warning(CONTROL_OSC_MSG "Error while parsing Translator: %s", xmlDoc.ErrorIDToName(eResult));
|
|
|
|
// no XML parsing error
|
|
else {
|
|
// parse all entries 'osc'
|
|
tinyxml2::XMLElement* osc = xmlDoc.FirstChildElement("osc");
|
|
for( ; osc ; osc=osc->NextSiblingElement()) {
|
|
// get the 'from' entry
|
|
tinyxml2::XMLElement* from = osc->FirstChildElement("from");
|
|
if (from) {
|
|
const char *str_from = from->GetText();
|
|
if (str_from) {
|
|
// get the 'to' entry
|
|
tinyxml2::XMLElement* to = osc->FirstChildElement("to");
|
|
if (to) {
|
|
const char *str_to = to->GetText();
|
|
// if could get both; add to translator
|
|
if (str_to)
|
|
translation_[str_from] = str_to;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Log::Info(CONTROL_OSC_MSG "Loaded %d translation%s.", translation_.size(), translation_.size()>1?"s":"");
|
|
}
|
|
|
|
void Control::resetOscConfig()
|
|
{
|
|
// generate a template xml translation dictionary
|
|
tinyxml2::XMLDocument xmlDoc;
|
|
tinyxml2::XMLDeclaration *pDec = xmlDoc.NewDeclaration();
|
|
xmlDoc.InsertFirstChild(pDec);
|
|
tinyxml2::XMLComment *pComment = xmlDoc.NewComment("The OSC translator converts OSC address patterns into other ones.\n"
|
|
"Complete the dictionary by adding as many <osc> translations as you want.\n"
|
|
"Each <osc> should contain a <from> pattern to translate into a <to> pattern.\n"
|
|
"More at https://github.com/brunoherbelin/vimix/wiki/Open-Sound-Control-API.");
|
|
xmlDoc.InsertEndChild(pComment);
|
|
tinyxml2::XMLElement *from = xmlDoc.NewElement( "from" );
|
|
from->InsertFirstChild( xmlDoc.NewText("/example/osc/message") );
|
|
tinyxml2::XMLElement *to = xmlDoc.NewElement( "to" );
|
|
to->InsertFirstChild( xmlDoc.NewText("/vimix/info/log") );
|
|
tinyxml2::XMLElement *osc = xmlDoc.NewElement("osc");
|
|
osc->InsertEndChild(from);
|
|
osc->InsertEndChild(to);
|
|
xmlDoc.InsertEndChild(osc);
|
|
|
|
// save xml in osc config file
|
|
xmlDoc.SaveFile(Settings::application.control.osc_filename.c_str());
|
|
|
|
// reset and fill translation with default example
|
|
translation_.clear();
|
|
translation_["/example/osc/message"] = "/vimix/info/log";
|
|
}
|
|
|
|
bool Control::init()
|
|
{
|
|
//
|
|
// terminate before init (allows calling init() multiple times)
|
|
//
|
|
terminate();
|
|
|
|
//
|
|
// set keyboard callback
|
|
//
|
|
GLFWwindow *main = Rendering::manager().mainWindow().window();
|
|
GLFWwindow *output = Rendering::manager().outputWindow().window();
|
|
glfwSetKeyCallback( main, Control::keyboardCalback);
|
|
glfwSetKeyCallback( output, Control::keyboardCalback);
|
|
|
|
//
|
|
// load OSC Translator
|
|
//
|
|
loadOscConfig();
|
|
|
|
//
|
|
// launch OSC listener
|
|
//
|
|
try {
|
|
// try to create listenning socket
|
|
// through exception runtime if fails
|
|
receiver_ = new UdpListeningReceiveSocket( IpEndpointName( IpEndpointName::ANY_ADDRESS,
|
|
Settings::application.control.osc_port_receive ), &listener_ );
|
|
// listen for answers in a separate thread
|
|
std::thread(listen).detach();
|
|
|
|
// inform user
|
|
IpEndpointName ip = receiver_->LocalEndpointFor( IpEndpointName( NetworkToolkit::hostname().c_str(),
|
|
Settings::application.control.osc_port_receive ));
|
|
static char *addresseip = (char *)malloc(IpEndpointName::ADDRESS_AND_PORT_STRING_LENGTH);
|
|
ip.AddressAndPortAsString(addresseip);
|
|
Log::Info(CONTROL_OSC_MSG "Listening to UDP messages sent to %s", addresseip);
|
|
}
|
|
catch (const std::runtime_error &e) {
|
|
// arg, the receiver could not be initialized
|
|
// (often because the port was not available)
|
|
receiver_ = nullptr;
|
|
Log::Warning(CONTROL_OSC_MSG "The port %d is already used by another program; %s", Settings::application.control.osc_port_receive, e.what());
|
|
}
|
|
|
|
return receiver_ != nullptr;
|
|
}
|
|
|
|
void Control::update()
|
|
{
|
|
// read joystick buttons
|
|
int num_buttons = 0;
|
|
const unsigned char *state_buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &num_buttons );
|
|
// map to Control input array
|
|
for (int b = 0; b < num_buttons; ++b) {
|
|
input_access_.lock();
|
|
input_active[INPUT_JOYSTICK_FIRST_BUTTON + b] = state_buttons[b] == GLFW_PRESS;
|
|
input_values[INPUT_JOYSTICK_FIRST_BUTTON + b] = state_buttons[b] == GLFW_PRESS ? 1.f : 0.f;
|
|
input_access_.unlock();
|
|
}
|
|
|
|
// read joystick axis
|
|
int num_axis = 0;
|
|
const float *state_axis = glfwGetJoystickAxes(GLFW_JOYSTICK_1, &num_axis );
|
|
for (int a = 0; a < num_axis; ++a) {
|
|
input_access_.lock();
|
|
input_active[INPUT_JOYSTICK_FIRST_AXIS + a] = ABS(state_axis[a]) > 0.02 ? true : false;
|
|
input_values[INPUT_JOYSTICK_FIRST_AXIS + a] = state_axis[a];
|
|
input_access_.unlock();
|
|
}
|
|
|
|
// multitouch input needs to be cleared when no more OSC input comes in
|
|
for (int m = 0; m < INPUT_MULTITOUCH_COUNT; ++m) {
|
|
if ( multitouch_active[m] > 0 )
|
|
multitouch_active[m] -= 1;
|
|
else {
|
|
input_access_.lock();
|
|
input_active[INPUT_MULTITOUCH_FIRST + m] = false;
|
|
input_values[INPUT_MULTITOUCH_FIRST + m] = 0.f;
|
|
multitouch_values[m] = glm::vec2(0.f);
|
|
input_access_.unlock();
|
|
}
|
|
}
|
|
|
|
// draft : react to metronome
|
|
// int p = (int) Metronome::manager().phase();
|
|
// static bool bip = false;
|
|
// static int t = 2;
|
|
// if (!bip) {
|
|
// if (p + 1 == t){
|
|
// g_print("bip");
|
|
// bip = true;
|
|
// }
|
|
// }
|
|
// else {
|
|
// if (p + 1 != t){
|
|
// bip = false;
|
|
// }
|
|
// }
|
|
}
|
|
|
|
void Control::listen()
|
|
{
|
|
if (Control::manager().receiver_)
|
|
Control::manager().receiver_->Run();
|
|
|
|
Control::manager().receiver_end_.notify_all();
|
|
}
|
|
|
|
void Control::terminate()
|
|
{
|
|
if ( receiver_ != nullptr ) {
|
|
|
|
// request termination of receiver
|
|
receiver_->AsynchronousBreak();
|
|
|
|
// wait for the receiver_end_ notification
|
|
std::mutex mtx;
|
|
std::unique_lock<std::mutex> lck(mtx);
|
|
// if waited more than 2 seconds, its dead :(
|
|
if ( receiver_end_.wait_for(lck,std::chrono::seconds(2)) == std::cv_status::timeout)
|
|
Log::Warning(CONTROL_OSC_MSG "Failed to terminate; try again.");
|
|
|
|
// delete receiver and ready to initialize
|
|
delete receiver_;
|
|
receiver_ = nullptr;
|
|
}
|
|
|
|
}
|
|
|
|
|
|
bool Control::receiveOutputAttribute(const std::string &attribute,
|
|
osc::ReceivedMessageArgumentStream arguments)
|
|
{
|
|
bool need_feedback = false;
|
|
|
|
try {
|
|
if ( attribute.compare(OSC_SYNC) == 0) {
|
|
need_feedback = true;
|
|
}
|
|
/// e.g. '/vimix/output/enable' or '/vimix/output/enable 1.0' or '/vimix/output/enable 0.0'
|
|
else if ( attribute.compare(OSC_OUTPUT_ENABLE) == 0) {
|
|
float on = 1.f;
|
|
if ( !arguments.Eos()) {
|
|
arguments >> on >> osc::EndMessage;
|
|
}
|
|
Settings::application.render.disabled = on < 0.5f;
|
|
}
|
|
/// e.g. '/vimix/output/disable' or '/vimix/output/disable 1.0' or '/vimix/output/disable 0.0'
|
|
else if ( attribute.compare(OSC_OUTPUT_DISABLE) == 0) {
|
|
float on = 1.f;
|
|
if ( !arguments.Eos()) {
|
|
arguments >> on >> osc::EndMessage;
|
|
}
|
|
Settings::application.render.disabled = on > 0.5f;
|
|
}
|
|
/// e.g. '/vimix/output/fading f 0.2' or '/vimix/output/fading ff 1.0 300.f'
|
|
else if ( attribute.compare(OSC_OUTPUT_FADING) == 0) {
|
|
float f = 0.f, d = 0.f;
|
|
// first argument is fading value
|
|
arguments >> f;
|
|
if (arguments.Eos())
|
|
arguments >> osc::EndMessage;
|
|
// if a second argument is given, it is a duration
|
|
else
|
|
arguments >> d >> osc::EndMessage;
|
|
Mixer::manager().session()->setFadingTarget(f, d);
|
|
}
|
|
/// e.g. '/vimix/output/fadein' or '/vimix/output/fadein f 300.f'
|
|
else if ( attribute.compare(OSC_OUTPUT_FADE_IN) == 0) {
|
|
float f = 0.f;
|
|
// if argument is given, it is a duration
|
|
if (!arguments.Eos())
|
|
arguments >> f >> osc::EndMessage;
|
|
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() - f * 0.01);
|
|
need_feedback = true;
|
|
}
|
|
else if ( attribute.compare(OSC_OUTPUT_FADE_OUT) == 0) {
|
|
float f = 0.f;
|
|
// if argument is given, it is a duration
|
|
if (!arguments.Eos())
|
|
arguments >> f >> osc::EndMessage;
|
|
Mixer::manager().session()->setFadingTarget( Mixer::manager().session()->fading() + f * 0.01);
|
|
need_feedback = true;
|
|
}
|
|
#ifdef CONTROL_DEBUG
|
|
else {
|
|
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'output'", attribute.c_str());
|
|
}
|
|
#endif
|
|
|
|
}
|
|
catch (osc::MissingArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'output'", attribute.c_str());
|
|
}
|
|
catch (osc::ExcessArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'output'", attribute.c_str());
|
|
}
|
|
catch (osc::WrongArgumentTypeException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'output'", attribute.c_str());
|
|
}
|
|
|
|
return need_feedback;
|
|
}
|
|
|
|
bool Control::receiveSourceAttribute(Source *target, const std::string &attribute,
|
|
osc::ReceivedMessageArgumentStream arguments)
|
|
{
|
|
bool send_feedback = false;
|
|
|
|
if (target == nullptr)
|
|
return send_feedback;
|
|
|
|
try {
|
|
/// e.g. '/vimix/current/play' or '/vimix/current/play T' or '/vimix/current/play F'
|
|
if ( attribute.compare(OSC_SOURCE_PLAY) == 0) {
|
|
float on = 1.f;
|
|
if ( !arguments.Eos()) {
|
|
arguments >> on >> osc::EndMessage;
|
|
}
|
|
target->call( new Play(on > 0.5f) );
|
|
}
|
|
/// e.g. '/vimix/current/pause' or '/vimix/current/pause T' or '/vimix/current/pause F'
|
|
else if ( attribute.compare(OSC_SOURCE_PAUSE) == 0) {
|
|
float on = 1.f;
|
|
if ( !arguments.Eos()) {
|
|
arguments >> on >> osc::EndMessage;
|
|
}
|
|
target->call( new Play(on < 0.5f) );
|
|
}
|
|
/// e.g. '/vimix/current/replay'
|
|
else if ( attribute.compare(OSC_SOURCE_REPLAY) == 0) {
|
|
target->call( new RePlay() );
|
|
}
|
|
/// e.g. '/vimix/current/alpha f 0.3'
|
|
else if ( attribute.compare(OSC_SOURCE_LOCK) == 0) {
|
|
float x = 1.f;
|
|
arguments >> x >> osc::EndMessage;
|
|
target->call( new Lock(x > 0.5f ? true : false) );
|
|
}
|
|
/// e.g. '/vimix/current/alpha f 0.3'
|
|
else if ( attribute.compare(OSC_SOURCE_ALPHA) == 0) {
|
|
float x = 1.f;
|
|
arguments >> x >> osc::EndMessage;
|
|
target->call( new SetAlpha(x), true );
|
|
}
|
|
/// e.g. '/vimix/current/alpha f 0.3'
|
|
else if ( attribute.compare(OSC_SOURCE_LOOM) == 0) {
|
|
float x = 1.f;
|
|
arguments >> x >> osc::EndMessage;
|
|
target->call( new Loom(x, 0.f), true );
|
|
// this will require to send feedback status about source
|
|
send_feedback = true;
|
|
}
|
|
/// e.g. '/vimix/current/transparency f 0.7'
|
|
else if ( attribute.compare(OSC_SOURCE_TRANSPARENCY) == 0) {
|
|
float x = 0.f;
|
|
arguments >> x >> osc::EndMessage;
|
|
target->call( new SetAlpha(1.f - x), true );
|
|
}
|
|
/// e.g. '/vimix/current/depth f 5.0'
|
|
else if ( attribute.compare(OSC_SOURCE_DEPTH) == 0) {
|
|
float x = 0.f;
|
|
arguments >> x >> osc::EndMessage;
|
|
target->call( new SetDepth(x), true );
|
|
}
|
|
/// e.g. '/vimix/current/translation ff 10.0 2.2'
|
|
else if ( attribute.compare(OSC_SOURCE_GRAB) == 0) {
|
|
float x = 0.f, y = 0.f;
|
|
arguments >> x >> y >> osc::EndMessage;
|
|
target->call( new Grab( x, y, 0.f), true );
|
|
}
|
|
/// e.g. '/vimix/current/scale ff 10.0 2.2'
|
|
else if ( attribute.compare(OSC_SOURCE_RESIZE) == 0) {
|
|
float x = 0.f, y = 0.f;
|
|
arguments >> x >> y >> osc::EndMessage;
|
|
target->call( new Resize( x, y, 0.f), true );
|
|
}
|
|
/// e.g. '/vimix/current/turn f 1.0'
|
|
else if ( attribute.compare(OSC_SOURCE_TURN) == 0) {
|
|
float x = 0.f, y = 0.f;
|
|
arguments >> x;
|
|
if (arguments.Eos())
|
|
arguments >> osc::EndMessage;
|
|
else // ignore second argument
|
|
arguments >> y >> osc::EndMessage;
|
|
target->call( new Turn( x, 0.f), true );
|
|
}
|
|
/// e.g. '/vimix/current/reset'
|
|
else if ( attribute.compare(OSC_SOURCE_RESET) == 0) {
|
|
target->call( new ResetGeometry(), true );
|
|
}
|
|
#ifdef CONTROL_DEBUG
|
|
else {
|
|
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
|
}
|
|
#endif
|
|
|
|
// overwrite value if source locked
|
|
if (target->locked())
|
|
send_feedback = true;
|
|
|
|
}
|
|
catch (osc::MissingArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
|
}
|
|
catch (osc::ExcessArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
|
}
|
|
catch (osc::WrongArgumentTypeException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target %s.", attribute.c_str(), target->name().c_str());
|
|
}
|
|
|
|
return send_feedback;
|
|
}
|
|
|
|
bool Control::receiveSessionAttribute(const std::string &attribute,
|
|
osc::ReceivedMessageArgumentStream arguments)
|
|
{
|
|
bool send_feedback = false;
|
|
|
|
try {
|
|
if ( attribute.compare(OSC_SYNC) == 0) {
|
|
send_feedback = true;
|
|
}
|
|
else if ( attribute.compare(OSC_SESSION_VERSION) == 0) {
|
|
float v = 0.f;
|
|
arguments >> v >> osc::EndMessage;
|
|
size_t id = (int) ceil(v);
|
|
std::list<uint64_t> snapshots = Action::manager().snapshots();
|
|
if ( id < snapshots.size() ) {
|
|
for (size_t i = 0; i < id; ++i)
|
|
snapshots.pop_back();
|
|
uint64_t snap = snapshots.back();
|
|
Action::manager().restore(snap);
|
|
}
|
|
send_feedback = true;
|
|
}
|
|
#ifdef CONTROL_DEBUG
|
|
else {
|
|
Log::Info(CONTROL_OSC_MSG "Ignoring attribute '%s' for target 'session'", attribute.c_str());
|
|
}
|
|
#endif
|
|
|
|
}
|
|
catch (osc::MissingArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target 'session'", attribute.c_str());
|
|
}
|
|
catch (osc::ExcessArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target 'session'", attribute.c_str());
|
|
}
|
|
catch (osc::WrongArgumentTypeException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target 'session'", attribute.c_str());
|
|
}
|
|
|
|
return send_feedback;
|
|
}
|
|
|
|
void Control::receiveMultitouchAttribute(const std::string &attribute,
|
|
osc::ReceivedMessageArgumentStream arguments)
|
|
{
|
|
try {
|
|
// address should be in the form /vimix/multitouch/i
|
|
int t = -1;
|
|
if ( BaseToolkit::is_a_number(attribute.substr(1), &t) && t >= 0 && t < INPUT_MULTITOUCH_COUNT )
|
|
{
|
|
// get value inputs
|
|
float x = 0.f, y = 0.f;
|
|
if ( !arguments.Eos())
|
|
arguments >> x >> y >> osc::EndMessage;
|
|
|
|
input_access_.lock();
|
|
|
|
// if the touch was already pressed
|
|
if ( multitouch_active[t] > 0 ) {
|
|
// active value decreases with the distance from original press position
|
|
input_values[INPUT_MULTITOUCH_FIRST + t] = 1.f - glm::distance(Control::multitouch_values[t], glm::vec2(x, y)) / M_SQRT2;
|
|
}
|
|
// first time touch is pressed
|
|
else {
|
|
// store original press position
|
|
multitouch_values[t] = glm::vec2(x, y);
|
|
// active value is 1.f at first press (full)
|
|
input_values[INPUT_MULTITOUCH_FIRST + t] = 1.f;
|
|
}
|
|
// keep track of button press
|
|
multitouch_active[t] = 3;
|
|
// set array of active input
|
|
input_active[INPUT_MULTITOUCH_FIRST + t] = true;
|
|
|
|
input_access_.unlock();
|
|
}
|
|
}
|
|
catch (osc::MissingArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Missing argument for attribute '%s' for target %s.", attribute.c_str(), OSC_MULTITOUCH);
|
|
}
|
|
catch (osc::ExcessArgumentException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Too many arguments for attribute '%s' for target %s.", attribute.c_str(), OSC_MULTITOUCH);
|
|
}
|
|
catch (osc::WrongArgumentTypeException &e) {
|
|
Log::Info(CONTROL_OSC_MSG "Invalid argument for attribute '%s' for target %s.", attribute.c_str(), OSC_MULTITOUCH);
|
|
}
|
|
|
|
}
|
|
|
|
void Control::sendSourceAttibutes(const IpEndpointName &remoteEndpoint, std::string target, Source *s)
|
|
{
|
|
// default values
|
|
char name[21] = {"\0"};
|
|
float lock = 0.f;
|
|
float play = 0.f;
|
|
float depth = 0.f;
|
|
float alpha = 0.f;
|
|
|
|
// get source or current source
|
|
Source *_s = s;
|
|
if ( target.compare(OSC_CURRENT) == 0 )
|
|
_s = Mixer::manager().currentSource();
|
|
|
|
// fill values if the source is valid
|
|
if (_s!=nullptr) {
|
|
strncpy(name, _s->name().c_str(), 20);
|
|
lock = _s->locked() ? 1.f : 0.f;
|
|
play = _s->playing() ? 1.f : 0.f;
|
|
depth = _s->depth();
|
|
alpha = _s->alpha();
|
|
}
|
|
|
|
// build socket to send message to indicated endpoint
|
|
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
|
|
|
// build messages packet
|
|
char buffer[IP_MTU_SIZE];
|
|
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
|
|
|
// create bundle
|
|
p.Clear();
|
|
p << osc::BeginBundle();
|
|
|
|
/// name
|
|
std::string address = std::string(OSC_PREFIX) + target + OSC_SOURCE_NAME;
|
|
p << osc::BeginMessage( address.c_str() ) << name << osc::EndMessage;
|
|
/// Play status
|
|
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_LOCK;
|
|
p << osc::BeginMessage( address.c_str() ) << lock << osc::EndMessage;
|
|
/// Play status
|
|
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_PLAY;
|
|
p << osc::BeginMessage( address.c_str() ) << play << osc::EndMessage;
|
|
/// Depth
|
|
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_DEPTH;
|
|
p << osc::BeginMessage( address.c_str() ) << depth << osc::EndMessage;
|
|
/// Alpha
|
|
address = std::string(OSC_PREFIX) + target + OSC_SOURCE_ALPHA;
|
|
p << osc::BeginMessage( address.c_str() ) << alpha << osc::EndMessage;
|
|
|
|
// send bundle
|
|
p << osc::EndBundle;
|
|
socket.Send( p.Data(), p.Size() );
|
|
}
|
|
|
|
|
|
void Control::sendSourcesStatus(const IpEndpointName &remoteEndpoint, osc::ReceivedMessageArgumentStream arguments)
|
|
{
|
|
// (if an argument is given, it indicates the number of sources to update)
|
|
float N = 0.f;
|
|
if ( !arguments.Eos())
|
|
arguments >> N >> osc::EndMessage;
|
|
|
|
// build socket to send message to indicated endpoint
|
|
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
|
|
|
// build messages packet
|
|
char buffer[IP_MTU_SIZE];
|
|
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
|
|
|
p.Clear();
|
|
p << osc::BeginBundle();
|
|
|
|
int i = 0;
|
|
char oscaddr[128];
|
|
int index_current = Mixer::manager().indexCurrentSource();
|
|
for (; i < Mixer::manager().numSource(); ++i) {
|
|
// send status of currently selected
|
|
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
|
p << osc::BeginMessage( oscaddr ) << (index_current == i ? 1.f : 0.f) << osc::EndMessage;
|
|
|
|
// send status of alpha
|
|
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
|
p << osc::BeginMessage( oscaddr ) << Mixer::manager().sourceAtIndex(i)->alpha() << osc::EndMessage;
|
|
}
|
|
|
|
for (; i < (int) N ; ++i) {
|
|
// reset status of currently selected
|
|
sprintf(oscaddr, OSC_PREFIX OSC_CURRENT "/%d", i);
|
|
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
|
|
|
// reset status of alpha
|
|
sprintf(oscaddr, OSC_PREFIX "/%d" OSC_SOURCE_ALPHA, i);
|
|
p << osc::BeginMessage( oscaddr ) << 0.f << osc::EndMessage;
|
|
}
|
|
|
|
p << osc::EndBundle;
|
|
socket.Send( p.Data(), p.Size() );
|
|
|
|
// send status of current source
|
|
sendSourceAttibutes(remoteEndpoint, OSC_CURRENT);
|
|
}
|
|
|
|
|
|
void Control::sendOutputStatus(const IpEndpointName &remoteEndpoint)
|
|
{
|
|
// build socket to send message to indicated endpoint
|
|
UdpTransmitSocket socket( IpEndpointName( remoteEndpoint.address, Settings::application.control.osc_port_send ) );
|
|
|
|
// build messages packet
|
|
char buffer[IP_MTU_SIZE];
|
|
osc::OutboundPacketStream p( buffer, IP_MTU_SIZE );
|
|
|
|
p.Clear();
|
|
p << osc::BeginBundle();
|
|
|
|
/// output attributes
|
|
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_ENABLE );
|
|
p << (Settings::application.render.disabled ? 0.f : 1.f);
|
|
p << osc::EndMessage;
|
|
p << osc::BeginMessage( OSC_PREFIX OSC_OUTPUT OSC_OUTPUT_FADING );
|
|
p << Mixer::manager().session()->fading();
|
|
p << osc::EndMessage;
|
|
|
|
p << osc::EndBundle;
|
|
socket.Send( p.Data(), p.Size() );
|
|
}
|
|
|
|
|
|
void Control::keyboardCalback(GLFWwindow* window, int key, int, int action, int mods)
|
|
{
|
|
if (UserInterface::manager().keyboardAvailable() && !mods )
|
|
{
|
|
Control::manager().input_access_.lock();
|
|
if (key >= GLFW_KEY_A && key <= GLFW_KEY_Z) {
|
|
Control::manager().input_active[INPUT_KEYBOARD_FIRST + key - GLFW_KEY_A] = action > GLFW_RELEASE;
|
|
Control::manager().input_values[INPUT_KEYBOARD_FIRST + key - GLFW_KEY_A] = action > GLFW_RELEASE ? 1.f : 0.f;
|
|
}
|
|
else if (key >= GLFW_KEY_KP_0 && key <= GLFW_KEY_KP_EQUAL) {
|
|
Control::manager().input_active[INPUT_NUMPAD_FIRST + key - GLFW_KEY_KP_0] = action > GLFW_RELEASE;
|
|
Control::manager().input_values[INPUT_NUMPAD_FIRST + key - GLFW_KEY_KP_0] = action > GLFW_RELEASE ? 1.f : 0.f;
|
|
}
|
|
else if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS )
|
|
{
|
|
static GLFWwindow *output = Rendering::manager().outputWindow().window();
|
|
if (window==output)
|
|
Rendering::manager().outputWindow().exitFullscreen();
|
|
}
|
|
Control::manager().input_access_.unlock();
|
|
}
|
|
}
|
|
|
|
bool Control::inputActive (uint id)
|
|
{
|
|
input_access_.lock();
|
|
const bool ret = input_active[MIN(id,INPUT_MAX)];
|
|
input_access_.unlock();
|
|
|
|
return ret && !Settings::application.mapping.disabled;
|
|
}
|
|
|
|
float Control::inputValue (uint id)
|
|
{
|
|
input_access_.lock();
|
|
const float ret = input_values[MIN(id,INPUT_MAX)];
|
|
input_access_.unlock();
|
|
|
|
return ret;
|
|
}
|
|
|
|
std::string Control::inputLabel(uint id)
|
|
{
|
|
std::string label;
|
|
|
|
if ( id >= INPUT_KEYBOARD_FIRST && id <= INPUT_KEYBOARD_LAST )
|
|
{
|
|
label = std::string( 1, 'A' + (char) (id -INPUT_KEYBOARD_FIRST) );
|
|
}
|
|
else if ( id >= INPUT_NUMPAD_FIRST && id <= INPUT_NUMPAD_LAST )
|
|
{
|
|
static std::vector< std::string > pad_labels = { "0", "1", "2", "3", "4", "5",
|
|
"6", "7", "8", "9", ".", "/", "*", "-", "+"
|
|
};
|
|
label = pad_labels[id -INPUT_NUMPAD_FIRST];
|
|
}
|
|
else if ( id >= INPUT_JOYSTICK_FIRST && id <= INPUT_JOYSTICK_LAST )
|
|
{
|
|
static std::vector< std::string > joystick_labels = { "Button A", "Button B", "Button X", "Button Y",
|
|
"Left bumper", "Right bumper", "Back", "Start",
|
|
"Guide", "Left thumb", "Right thumb",
|
|
"Up", "Right", "Down", "Left",
|
|
"Left Axis X", "Left Axis Y", "Left Trigger",
|
|
"Right Axis X", "Right Axis Y", "Right Trigger" };
|
|
label = joystick_labels[id - INPUT_JOYSTICK_FIRST];
|
|
}
|
|
else if ( id >= INPUT_MULTITOUCH_FIRST && id <= INPUT_MULTITOUCH_LAST )
|
|
{
|
|
label = std::string( "Multitouch ") + std::to_string(id - INPUT_MULTITOUCH_FIRST);
|
|
}
|
|
else if ( id >= INPUT_CUSTOM_FIRST && id <= INPUT_CUSTOM_LAST )
|
|
{
|
|
label = std::string( "Custom ") + std::to_string(id - INPUT_CUSTOM_FIRST);
|
|
}
|
|
|
|
return label;
|
|
}
|