mirror of
https://github.com/brunoherbelin/vimix.git
synced 2025-12-05 15:30:00 +01:00
1402 lines
39 KiB
C++
1402 lines
39 KiB
C++
/*
|
|
* This file is part of vimix - video live mixer
|
|
*
|
|
* **Copyright** (C) 2020-2021 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 <algorithm>
|
|
#include <thread>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <vector>
|
|
#include <chrono>
|
|
#include <future>
|
|
#include <sstream>
|
|
|
|
// GStreamer
|
|
#include <gst/gst.h>
|
|
|
|
#include <tinyxml2.h>
|
|
#include "tinyxml2Toolkit.h"
|
|
|
|
#include "defines.h"
|
|
#include "Settings.h"
|
|
#include "Log.h"
|
|
#include "View.h"
|
|
#include "ImageShader.h"
|
|
#include "BaseToolkit.h"
|
|
#include "SystemToolkit.h"
|
|
#include "SessionCreator.h"
|
|
#include "SessionVisitor.h"
|
|
#include "SessionSource.h"
|
|
#include "MediaSource.h"
|
|
#include "PatternSource.h"
|
|
#include "DeviceSource.h"
|
|
#include "MultiFileSource.h"
|
|
#include "StreamSource.h"
|
|
#include "NetworkSource.h"
|
|
#include "ActionManager.h"
|
|
#include "MixingGroup.h"
|
|
#include "Streamer.h"
|
|
|
|
#include "Mixer.h"
|
|
|
|
#define THREADED_LOADING
|
|
std::vector< std::future<Session *> > sessionLoaders_;
|
|
std::vector< std::future<Session *> > sessionImporters_;
|
|
std::vector< SessionSource * > sessionSourceToImport_;
|
|
const std::chrono::milliseconds timeout_ = std::chrono::milliseconds(4);
|
|
|
|
|
|
// static multithreaded session saving
|
|
static void saveSession(const std::string& filename, Session *session)
|
|
{
|
|
// lock access while saving
|
|
session->lock();
|
|
|
|
// save file to disk
|
|
if ( SessionVisitor::saveSession(filename, session) ) {
|
|
// all ok
|
|
// set session filename
|
|
session->setFilename(filename);
|
|
// cosmetics saved ok
|
|
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(filename));
|
|
Settings::application.recentSessions.push(filename);
|
|
Log::Notify("Session %s saved.", filename.c_str());
|
|
|
|
}
|
|
else {
|
|
// error
|
|
Log::Warning("Failed to save Session file %s.", filename.c_str());
|
|
}
|
|
|
|
session->unlock();
|
|
}
|
|
|
|
Mixer::Mixer() : session_(nullptr), back_session_(nullptr), sessionSwapRequested_(false),
|
|
current_view_(nullptr), dt_(16.f), dt__(16.f)
|
|
{
|
|
// unsused initial empty session
|
|
session_ = new Session;
|
|
current_source_ = session_->end();
|
|
current_source_index_ = -1;
|
|
|
|
// auto load if Settings ask to
|
|
if ( Settings::application.recentSessions.load_at_start &&
|
|
Settings::application.recentSessions.front_is_valid &&
|
|
Settings::application.recentSessions.filenames.size() > 0 &&
|
|
Settings::application.fresh_start)
|
|
load( Settings::application.recentSessions.filenames.front() );
|
|
else
|
|
// initializes with a new empty session
|
|
clear();
|
|
|
|
// this initializes with the current view
|
|
setView( (View::Mode) Settings::application.current_view );
|
|
}
|
|
|
|
void Mixer::update()
|
|
{
|
|
// sort-of garbage collector : just wait for 1 iteration
|
|
// before deleting the previous session: this way, the sources
|
|
// had time to end properly
|
|
if (garbage_.size()>0) {
|
|
delete garbage_.back();
|
|
garbage_.pop_back();
|
|
}
|
|
|
|
#ifdef THREADED_LOADING
|
|
// if there is a session importer pending
|
|
if (!sessionImporters_.empty()) {
|
|
// check status of loader: did it finish ?
|
|
if (sessionImporters_.back().wait_for(timeout_) == std::future_status::ready ) {
|
|
// get the session loaded by this loader
|
|
merge( sessionImporters_.back().get() );
|
|
// FIXME: shouldn't we delete the imported session?
|
|
// done with this session loader
|
|
sessionImporters_.pop_back();
|
|
}
|
|
}
|
|
|
|
// if there is a session loader pending
|
|
if (!sessionLoaders_.empty()) {
|
|
// check status of loader: did it finish ?
|
|
if (sessionLoaders_.back().wait_for(timeout_) == std::future_status::ready ) {
|
|
// get the session loaded by this loader
|
|
if (sessionLoaders_.back().valid())
|
|
set( sessionLoaders_.back().get() );
|
|
// done with this session loader
|
|
sessionLoaders_.pop_back();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// if there is a session source to import
|
|
if (!sessionSourceToImport_.empty()) {
|
|
// get the session source to be imported
|
|
SessionSource *source = sessionSourceToImport_.back();
|
|
// merge (&delete) the session inside this session source
|
|
merge( source );
|
|
// done with this session source
|
|
sessionSourceToImport_.pop_back();
|
|
}
|
|
|
|
// if a change of session is requested
|
|
if (sessionSwapRequested_) {
|
|
sessionSwapRequested_ = false;
|
|
// sanity check
|
|
if ( back_session_ ) {
|
|
// swap front and back sessions
|
|
swap();
|
|
++View::need_deep_update_;
|
|
// inform new session filename
|
|
if (session_->filename().empty()) {
|
|
Rendering::manager().setMainWindowTitle(Settings::application.windows[0].name);
|
|
} else {
|
|
Rendering::manager().setMainWindowTitle(SystemToolkit::filename(session_->filename()));
|
|
Settings::application.recentSessions.push(session_->filename());
|
|
}
|
|
}
|
|
}
|
|
|
|
// if there is a source candidate for this session
|
|
if (candidate_sources_.size() > 0) {
|
|
// NB: only make the last candidate the current source in Mixing view
|
|
insertSource(candidate_sources_.front(), candidate_sources_.size() > 1 ? View::INVALID : View::MIXING);
|
|
candidate_sources_.pop_front();
|
|
}
|
|
|
|
// compute dt
|
|
static GTimer *timer = g_timer_new ();
|
|
dt_ = g_timer_elapsed (timer, NULL) * 1000.0;
|
|
g_timer_start(timer);
|
|
|
|
// compute stabilized dt__
|
|
dt__ = 0.05f * dt_ + 0.95f * dt__;
|
|
|
|
// update session and associated sources
|
|
session_->update(dt_);
|
|
|
|
// grab frames to recorders & streamers
|
|
FrameGrabbing::manager().grabFrame(session_->frame());
|
|
|
|
// delete sources which failed update (one by one)
|
|
Source *failure = session()->failedSource();
|
|
if (failure != nullptr) {
|
|
// failed media: remove it from the list of imports
|
|
MediaSource *failedMedia = dynamic_cast<MediaSource *>(failure);
|
|
if (failedMedia != nullptr) {
|
|
Settings::application.recentImport.remove( failedMedia->path() );
|
|
}
|
|
// failed Render loopback: replace it with one matching the current session
|
|
RenderSource *failedRender = dynamic_cast<RenderSource *>(failure);
|
|
if (failedRender != nullptr) {
|
|
if ( recreateSource(failedRender) )
|
|
failure = nullptr; // prevent delete (already done in recreateSource)
|
|
}
|
|
// delete the source
|
|
deleteSource(failure);
|
|
}
|
|
|
|
// update views
|
|
mixing_.update(dt_);
|
|
geometry_.update(dt_);
|
|
layer_.update(dt_);
|
|
appearance_.update(dt_);
|
|
transition_.update(dt_);
|
|
|
|
// deep update was performed
|
|
if (View::need_deep_update_ > 0)
|
|
--View::need_deep_update_;
|
|
}
|
|
|
|
void Mixer::draw()
|
|
{
|
|
// draw the current view in the window
|
|
current_view_->draw();
|
|
|
|
}
|
|
|
|
// manangement of sources
|
|
Source * Mixer::createSourceFile(const std::string &path)
|
|
{
|
|
// ready to create a source
|
|
Source *s = nullptr;
|
|
|
|
// sanity check
|
|
if ( SystemToolkit::file_exists( path ) ) {
|
|
|
|
// test type of file by extension
|
|
std::string ext = SystemToolkit::extension_filename(path);
|
|
if ( ext == "mix" )
|
|
{
|
|
// create a session source
|
|
SessionFileSource *ss = new SessionFileSource;
|
|
ss->load(path);
|
|
s = ss;
|
|
}
|
|
else {
|
|
// (try to) create media source by default
|
|
MediaSource *ms = new MediaSource;
|
|
ms->setPath(path);
|
|
s = ms;
|
|
}
|
|
|
|
// remember in recent media
|
|
Settings::application.recentImport.push(path);
|
|
|
|
// propose a new name based on uri
|
|
s->setName(SystemToolkit::base_filename(path));
|
|
|
|
}
|
|
else {
|
|
Settings::application.recentImport.remove(path);
|
|
Log::Notify("File %s does not exist.", path.c_str());
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourceMultifile(const std::list<std::string> &list_files, uint fps)
|
|
{
|
|
// ready to create a source
|
|
Source *s = nullptr;
|
|
|
|
if ( list_files.size() >0 ) {
|
|
|
|
// validate the creation of a sequence from the list
|
|
MultiFileSequence sequence(list_files);
|
|
|
|
if ( sequence.valid() ) {
|
|
|
|
// try to create a sequence
|
|
MultiFileSource *mfs = new MultiFileSource;
|
|
mfs->setSequence(sequence, fps);
|
|
s = mfs;
|
|
|
|
// propose a new name
|
|
s->setName( SystemToolkit::base_filename( BaseToolkit::common_prefix(list_files) ) );
|
|
}
|
|
else {
|
|
Log::Notify("Could not find a sequence of consecutively numbered files.");
|
|
}
|
|
}
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourceRender()
|
|
{
|
|
// ready to create a source
|
|
RenderSource *s = new RenderSource;
|
|
s->setSession(session_);
|
|
|
|
// propose a new name based on session name
|
|
if ( !session_->filename().empty() )
|
|
s->setName(SystemToolkit::base_filename(session_->filename()));
|
|
else
|
|
s->setName("Output");
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourceStream(const std::string &gstreamerpipeline)
|
|
{
|
|
// ready to create a source
|
|
GenericStreamSource *s = new GenericStreamSource;
|
|
s->setDescription(gstreamerpipeline);
|
|
|
|
// propose a new name based on pattern name
|
|
s->setName( gstreamerpipeline.substr(0, gstreamerpipeline.find(" ")) );
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourcePattern(uint pattern, glm::ivec2 res)
|
|
{
|
|
// ready to create a source
|
|
PatternSource *s = new PatternSource;
|
|
s->setPattern(pattern, res);
|
|
|
|
// propose a new name based on pattern name
|
|
std::string name = Pattern::get(pattern).label;
|
|
name = name.substr(0, name.find(" "));
|
|
s->setName(name);
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourceDevice(const std::string &namedevice)
|
|
{
|
|
// ready to create a source
|
|
Source *s = Device::manager().createSource(namedevice);
|
|
|
|
// propose a new name based on pattern name
|
|
s->setName( namedevice.substr(0, namedevice.find(" ")) );
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
Source * Mixer::createSourceNetwork(const std::string &nameconnection)
|
|
{
|
|
// ready to create a source
|
|
NetworkSource *s = new NetworkSource;
|
|
s->setConnection(nameconnection);
|
|
|
|
// propose a new name based on address
|
|
s->setName(nameconnection);
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
Source * Mixer::createSourceGroup()
|
|
{
|
|
SessionGroupSource *s = new SessionGroupSource;
|
|
s->setResolution( session_->frame()->resolution() );
|
|
|
|
// propose a new name
|
|
s->setName("Group");
|
|
|
|
return s;
|
|
}
|
|
|
|
Source * Mixer::createSourceClone(const std::string &namesource)
|
|
{
|
|
// ready to create a source
|
|
Source *s = nullptr;
|
|
|
|
// origin to clone is either the given name or the current
|
|
SourceList::iterator origin = session_->end();
|
|
if ( !namesource.empty() )
|
|
origin =session_->find(namesource);
|
|
else if (current_source_ != session_->end())
|
|
origin = current_source_;
|
|
|
|
// have an origin, can clone it
|
|
if (origin != session_->end())
|
|
// create a source
|
|
s = (*origin)->clone();
|
|
|
|
return s;
|
|
}
|
|
|
|
void Mixer::addSource(Source *s)
|
|
{
|
|
if (s != nullptr)
|
|
candidate_sources_.push_back(s);
|
|
}
|
|
|
|
void Mixer::insertSource(Source *s, View::Mode m)
|
|
{
|
|
if ( s != nullptr )
|
|
{
|
|
// avoid duplicate name
|
|
renameSource(s);
|
|
|
|
// Add source to Session (ignored if source already in)
|
|
SourceList::iterator sit = session_->addSource(s);
|
|
|
|
// set a default depth to the new source
|
|
layer_.setDepth(s);
|
|
|
|
// set a default alpha to the new source
|
|
mixing_.setAlpha(s);
|
|
|
|
// add sources Nodes to all views
|
|
attach(s);
|
|
|
|
// new state in history manager
|
|
Action::manager().store(s->name() + std::string(": source inserted"));
|
|
|
|
// if requested to show the source in a given view
|
|
// (known to work for View::MIXING et TRANSITION: other views untested)
|
|
if (m != View::INVALID) {
|
|
|
|
// switch to this view to show source created
|
|
setView(m);
|
|
current_view_->update(0.f);
|
|
current_view_->centerSource(s);
|
|
|
|
// set this new source as current
|
|
setCurrentSource( sit );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
bool Mixer::replaceSource(Source *from, Source *to)
|
|
{
|
|
if ( from == nullptr || to == nullptr)
|
|
return false;
|
|
|
|
// rename
|
|
renameSource(to, from->name());
|
|
|
|
// remove source Nodes from all views
|
|
detach(from);
|
|
|
|
// copy all transforms
|
|
to->group(View::MIXING)->copyTransform( from->group(View::MIXING) );
|
|
to->group(View::GEOMETRY)->copyTransform( from->group(View::GEOMETRY) );
|
|
to->group(View::LAYER)->copyTransform( from->group(View::LAYER) );
|
|
to->group(View::MIXING)->copyTransform( from->group(View::MIXING) );
|
|
|
|
// TODO copy all filters
|
|
|
|
|
|
// add source Nodes to all views
|
|
attach(to);
|
|
|
|
// add source
|
|
session_->addSource(to);
|
|
|
|
// delete source
|
|
session_->deleteSource(from);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool Mixer::recreateSource(Source *s)
|
|
{
|
|
if ( s == nullptr )
|
|
return false;
|
|
|
|
// get the xml description from this source, and exit if not wellformed
|
|
tinyxml2::XMLDocument xmlDoc;
|
|
tinyxml2::XMLElement* sourceNode = SessionLoader::firstSourceElement(SessionVisitor::getClipboard(s), xmlDoc);
|
|
if ( sourceNode == nullptr )
|
|
return false;
|
|
|
|
// actually create the source with SessionLoader using xml description
|
|
SessionLoader loader( session_ );
|
|
Source *replacement = loader.createSource(sourceNode, SessionLoader::DUPLICATE); // not clone
|
|
if (replacement == nullptr)
|
|
return false;
|
|
|
|
// remove source Nodes from all views
|
|
detach(s);
|
|
|
|
// delete source
|
|
session_->deleteSource(s);
|
|
|
|
// add sources Nodes to all views
|
|
attach(replacement);
|
|
|
|
// add source
|
|
session_->addSource(replacement);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Mixer::deleteSource(Source *s)
|
|
{
|
|
if ( s != nullptr )
|
|
{
|
|
// keep name for log
|
|
std::string name = s->name();
|
|
|
|
// remove source Nodes from all views
|
|
detach(s);
|
|
|
|
// delete source
|
|
session_->deleteSource(s);
|
|
|
|
// log
|
|
Log::Notify("Source %s deleted.", name.c_str());
|
|
}
|
|
|
|
// cancel transition source in TRANSITION view
|
|
if ( current_view_ == &transition_ ) {
|
|
// cancel attachment
|
|
transition_.attach(nullptr);
|
|
// revert to mixing view
|
|
setView(View::MIXING);
|
|
}
|
|
}
|
|
|
|
|
|
void Mixer::attach(Source *s)
|
|
{
|
|
if ( s != nullptr )
|
|
{
|
|
// force update
|
|
s->touch();
|
|
// attach to views
|
|
mixing_.scene.ws()->attach( s->group(View::MIXING) );
|
|
geometry_.scene.ws()->attach( s->group(View::GEOMETRY) );
|
|
layer_.scene.ws()->attach( s->group(View::LAYER) );
|
|
appearance_.scene.ws()->attach( s->group(View::TEXTURE) );
|
|
}
|
|
}
|
|
|
|
void Mixer::detach(Source *s)
|
|
{
|
|
if ( s != nullptr )
|
|
{
|
|
// in case it was the current source...
|
|
unsetCurrentSource();
|
|
// in case it was selected..
|
|
selection().remove(s);
|
|
// detach from views
|
|
mixing_.scene.ws()->detach( s->group(View::MIXING) );
|
|
geometry_.scene.ws()->detach( s->group(View::GEOMETRY) );
|
|
layer_.scene.ws()->detach( s->group(View::LAYER) );
|
|
appearance_.scene.ws()->detach( s->group(View::TEXTURE) );
|
|
transition_.scene.ws()->detach( s->group(View::TRANSITION) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
bool Mixer::concealed(Source *s)
|
|
{
|
|
SourceList::iterator it = std::find(stash_.begin(), stash_.end(), s);
|
|
return it != stash_.end();
|
|
}
|
|
|
|
void Mixer::conceal(Source *s)
|
|
{
|
|
if ( !concealed(s) ) {
|
|
// in case it was the current source...
|
|
unsetCurrentSource();
|
|
|
|
// in case it was selected..
|
|
selection().remove(s);
|
|
|
|
// store to stash
|
|
stash_.push_front(s);
|
|
|
|
// remove from session
|
|
session_->removeSource(s);
|
|
|
|
// detach from scene workspace, and put only in mixing background
|
|
detach(s);
|
|
mixing_.scene.bg()->attach( s->group(View::MIXING) );
|
|
}
|
|
}
|
|
|
|
void Mixer::uncover(Source *s)
|
|
{
|
|
SourceList::iterator it = std::find(stash_.begin(), stash_.end(), s);
|
|
if ( it != stash_.end() ) {
|
|
stash_.erase(it);
|
|
|
|
mixing_.scene.bg()->detach( s->group(View::MIXING) );
|
|
attach(s);
|
|
session_->addSource(s);
|
|
}
|
|
}
|
|
|
|
|
|
void Mixer::deselect(Source *s)
|
|
{
|
|
if ( s != nullptr ) {
|
|
if ( s == *current_source_)
|
|
unsetCurrentSource();
|
|
Mixer::selection().remove(s);
|
|
}
|
|
}
|
|
|
|
void Mixer::deleteSelection()
|
|
{
|
|
// number of sources in selection
|
|
int N = selection().size();
|
|
// ignore if selection empty
|
|
if (N > 0) {
|
|
|
|
// adapt Action::manager undo info depending on case
|
|
std::ostringstream info;
|
|
if (N > 1)
|
|
info << N << " sources deleted";
|
|
else
|
|
info << selection().front()->name() << ": deleted";
|
|
|
|
// get clones first : this way we store the history of deletion in the right order
|
|
SourceList selection_clones_;
|
|
for ( auto sit = selection().begin(); sit != selection().end(); sit++ ) {
|
|
CloneSource *clone = dynamic_cast<CloneSource *>(*sit);
|
|
if (clone)
|
|
selection_clones_.push_back(clone);
|
|
}
|
|
// delete all clones
|
|
while ( !selection_clones_.empty() ) {
|
|
deleteSource( selection_clones_.front());// this also removes element from selection()
|
|
selection_clones_.pop_front();
|
|
}
|
|
// empty the selection
|
|
while ( !selection().empty() )
|
|
deleteSource( selection().front() ); // this also removes element from selection()
|
|
|
|
Action::manager().store(info.str());
|
|
}
|
|
}
|
|
|
|
void Mixer::groupSelection()
|
|
{
|
|
if (selection().empty())
|
|
return;
|
|
|
|
SessionGroupSource *sessiongroup = new SessionGroupSource;
|
|
sessiongroup->setResolution( session_->frame()->resolution() );
|
|
|
|
// prepare for new session group attributes
|
|
std::string name;
|
|
float d = selection().front()->depth();
|
|
|
|
// empty the selection
|
|
while ( !selection().empty() ) {
|
|
|
|
Source *s = selection().front();
|
|
d = MIN(s->depth(), d);
|
|
|
|
// import source into group
|
|
if ( sessiongroup->import(s) ) {
|
|
name += s->initials();
|
|
// detach & remove element from selection()
|
|
detach (s);
|
|
// remove source from session
|
|
session_->removeSource(s);
|
|
}
|
|
else
|
|
selection().pop_front();
|
|
|
|
}
|
|
|
|
// set depth at given location
|
|
sessiongroup->group(View::LAYER)->translation_.z = d;
|
|
|
|
// set alpha to full opacity
|
|
sessiongroup->group(View::MIXING)->translation_.x = 0.f;
|
|
sessiongroup->group(View::MIXING)->translation_.y = 0.f;
|
|
|
|
// Add source to Session
|
|
session_->addSource(sessiongroup);
|
|
|
|
// Attach source to Mixer
|
|
attach(sessiongroup);
|
|
|
|
// rename and avoid name duplicates
|
|
renameSource(sessiongroup, name);
|
|
|
|
// store in action manager
|
|
std::ostringstream info;
|
|
info << sessiongroup->name() << " inserted: " << sessiongroup->session()->numSource() << " sources flatten.";
|
|
Action::manager().store(info.str());
|
|
|
|
// give the hand to the user
|
|
Mixer::manager().setCurrentSource(sessiongroup);
|
|
Log::Notify(info.str().c_str());
|
|
}
|
|
|
|
void Mixer::renameSource(Source *s, const std::string &newname)
|
|
{
|
|
if ( s != nullptr )
|
|
{
|
|
// tentative new name
|
|
std::string tentativename = s->name();
|
|
|
|
// try the given new name if valid
|
|
if ( !newname.empty() )
|
|
tentativename = newname;
|
|
|
|
tentativename = BaseToolkit::uniqueName(tentativename, session_->getNameList(s->id()));
|
|
|
|
// ok to rename
|
|
s->setName(tentativename);
|
|
}
|
|
}
|
|
|
|
void Mixer::setCurrentSource(SourceList::iterator it)
|
|
{
|
|
// nothing to do if already current
|
|
if ( current_source_ == it )
|
|
return;
|
|
|
|
// clear current (even if 'it' is invalid)
|
|
unsetCurrentSource();
|
|
|
|
// change current if 'it' is valid
|
|
if ( it != session_->end() /*&& (*it)->mode() > Source::UNINITIALIZED */) {
|
|
current_source_ = it;
|
|
current_source_index_ = session_->index(current_source_);
|
|
|
|
// set selection for this only source if not already part of a selection
|
|
if (!selection().contains(*current_source_))
|
|
selection().set(*current_source_);
|
|
|
|
// show status as current
|
|
(*current_source_)->setMode(Source::CURRENT);
|
|
|
|
if (current_view_ == &mixing_)
|
|
(*current_source_)->group(View::MIXING)->update_callbacks_.push_back(new BounceScaleCallback);
|
|
else if (current_view_ == &layer_)
|
|
(*current_source_)->group(View::LAYER)->update_callbacks_.push_back(new BounceScaleCallback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Source * Mixer::findSource (Node *node)
|
|
{
|
|
SourceList::iterator it = session_->find(node);
|
|
if (it != session_->end())
|
|
return *it;
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
Source * Mixer::findSource (std::string namesource)
|
|
{
|
|
SourceList::iterator it = session_->find(namesource);
|
|
if (it != session_->end())
|
|
return *it;
|
|
return nullptr;
|
|
}
|
|
|
|
Source * Mixer::findSource (uint64_t id)
|
|
{
|
|
SourceList::iterator it = session_->find(id);
|
|
if (it != session_->end())
|
|
return *it;
|
|
return nullptr;
|
|
}
|
|
|
|
SourceList Mixer::findSources (float depth_from, float depth_to)
|
|
{
|
|
SourceList found;
|
|
SourceList dsl = session_->getDepthSortedList();
|
|
SourceList::iterator it = dsl.begin();
|
|
for (; it != dsl.end(); ++it) {
|
|
if ( (*it)->depth() > depth_to )
|
|
break;
|
|
if ( (*it)->depth() >= depth_from )
|
|
found.push_back(*it);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
SourceList Mixer::validate (const SourceList &list)
|
|
{
|
|
SourceList sl;
|
|
for( auto sit = list.begin(); sit != list.end(); ++sit) {
|
|
SourceList::iterator it = session_->find( *sit );
|
|
if (it != session_->end())
|
|
sl.push_back(*sit);
|
|
}
|
|
return sl;
|
|
}
|
|
|
|
void Mixer::setCurrentSource(uint64_t id)
|
|
{
|
|
setCurrentSource( session_->find(id) );
|
|
}
|
|
|
|
void Mixer::setCurrentSource(Node *node)
|
|
{
|
|
if (node!=nullptr)
|
|
setCurrentSource( session_->find(node) );
|
|
}
|
|
|
|
void Mixer::setCurrentSource(std::string namesource)
|
|
{
|
|
setCurrentSource( session_->find(namesource) );
|
|
}
|
|
|
|
void Mixer::setCurrentSource(Source *s)
|
|
{
|
|
if (s!=nullptr)
|
|
setCurrentSource( session_->find(s) );
|
|
}
|
|
|
|
void Mixer::setCurrentIndex(int index)
|
|
{
|
|
setCurrentSource( session_->at(index) );
|
|
}
|
|
|
|
void Mixer::moveIndex (int current_index, int target_index)
|
|
{
|
|
// remember ptr to current source
|
|
Source *previous_current_source_ = currentSource();
|
|
|
|
// change order
|
|
session_->move(current_index, target_index);
|
|
|
|
// restore current
|
|
unsetCurrentSource();
|
|
setCurrentSource(previous_current_source_);
|
|
}
|
|
|
|
void Mixer::setCurrentNext()
|
|
{
|
|
if (session_->numSource() > 0) {
|
|
|
|
SourceList::iterator it = current_source_;
|
|
++it;
|
|
|
|
if (it == session_->end()) {
|
|
it = session_->begin();
|
|
}
|
|
|
|
setCurrentSource( it );
|
|
}
|
|
}
|
|
|
|
void Mixer::setCurrentPrevious()
|
|
{
|
|
if (session_->numSource() > 0) {
|
|
|
|
SourceList::iterator it = current_source_;
|
|
|
|
if (it == session_->begin()) {
|
|
it = session_->end();
|
|
}
|
|
|
|
--it;
|
|
setCurrentSource( it );
|
|
}
|
|
}
|
|
|
|
void Mixer::unsetCurrentSource()
|
|
{
|
|
// discard overlay for previously current source
|
|
if ( current_source_ != session_->end() ) {
|
|
|
|
// current source is part of a selection, just change status
|
|
if (selection().size() > 1) {
|
|
(*current_source_)->setMode(Source::SELECTED);
|
|
}
|
|
// current source is the only selected source, unselect too
|
|
else
|
|
{
|
|
// remove from selection
|
|
selection().remove( *current_source_ );
|
|
}
|
|
|
|
// deselect current source
|
|
current_source_ = session_->end();
|
|
current_source_index_ = -1;
|
|
}
|
|
}
|
|
|
|
int Mixer::indexCurrentSource()
|
|
{
|
|
return current_source_index_;
|
|
}
|
|
|
|
Source *Mixer::currentSource()
|
|
{
|
|
if ( current_source_ != session_->end() )
|
|
return (*current_source_);
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
// management of view
|
|
void Mixer::setView(View::Mode m)
|
|
{
|
|
// special case when leaving transition view
|
|
if ( current_view_ == &transition_ ) {
|
|
// get the session detached from the transition view and set it as current session
|
|
// NB: detatch() can return nullptr, which is then ignored.
|
|
Session *se = transition_.detach();
|
|
if ( se != nullptr )
|
|
set ( se );
|
|
else
|
|
Log::Info("Transition interrupted.");
|
|
}
|
|
|
|
switch (m) {
|
|
case View::TRANSITION:
|
|
current_view_ = &transition_;
|
|
break;
|
|
case View::GEOMETRY:
|
|
current_view_ = &geometry_;
|
|
break;
|
|
case View::LAYER:
|
|
current_view_ = &layer_;
|
|
break;
|
|
case View::TEXTURE:
|
|
current_view_ = &appearance_;
|
|
break;
|
|
case View::MIXING:
|
|
default:
|
|
current_view_ = &mixing_;
|
|
break;
|
|
}
|
|
|
|
// setttings
|
|
Settings::application.current_view = (int) m;
|
|
|
|
// selection might have to change
|
|
for (auto sit = session_->begin(); sit != session_->end(); sit++) {
|
|
if ( !current_view_->canSelect(*sit) )
|
|
deselect( *sit );
|
|
}
|
|
|
|
// need to deeply update view to apply eventual changes
|
|
++View::need_deep_update_;
|
|
}
|
|
|
|
View *Mixer::view(View::Mode m)
|
|
{
|
|
switch (m) {
|
|
case View::TRANSITION:
|
|
return &transition_;
|
|
case View::GEOMETRY:
|
|
return &geometry_;
|
|
case View::LAYER:
|
|
return &layer_;
|
|
case View::TEXTURE:
|
|
return &appearance_;
|
|
case View::MIXING:
|
|
return &mixing_;
|
|
default:
|
|
return current_view_;
|
|
}
|
|
}
|
|
|
|
void Mixer::save()
|
|
{
|
|
if (!session_->filename().empty())
|
|
saveas(session_->filename());
|
|
}
|
|
|
|
void Mixer::saveas(const std::string& filename)
|
|
{
|
|
// optional copy of views config
|
|
session_->config(View::MIXING)->copyTransform( mixing_.scene.root() );
|
|
session_->config(View::GEOMETRY)->copyTransform( geometry_.scene.root() );
|
|
session_->config(View::LAYER)->copyTransform( layer_.scene.root() );
|
|
session_->config(View::TEXTURE)->copyTransform( appearance_.scene.root() );
|
|
|
|
// launch a thread to save the session
|
|
std::thread (saveSession, filename, session_).detach();
|
|
}
|
|
|
|
void Mixer::load(const std::string& filename)
|
|
{
|
|
if (filename.empty())
|
|
return;
|
|
|
|
#ifdef THREADED_LOADING
|
|
// load only one at a time
|
|
if (sessionLoaders_.empty()) {
|
|
// Start async thread for loading the session
|
|
// Will be obtained in the future in update()
|
|
sessionLoaders_.emplace_back( std::async(std::launch::async, Session::load, filename, 0) );
|
|
}
|
|
#else
|
|
set( Session::load(filename) );
|
|
#endif
|
|
}
|
|
|
|
void Mixer::open(const std::string& filename, bool smooth)
|
|
{
|
|
if (smooth)
|
|
{
|
|
// create special SessionSource to be used for the smooth transition
|
|
SessionFileSource *ts = new SessionFileSource;
|
|
|
|
// open filename if specified
|
|
if (!filename.empty())
|
|
{
|
|
Log::Info("\nStarting transition to session %s", filename.c_str());
|
|
ts->load(filename);
|
|
// propose a new name based on uri
|
|
ts->setName(SystemToolkit::base_filename(filename));
|
|
}
|
|
|
|
// attach the SessionSource to the transition view
|
|
transition_.attach(ts);
|
|
|
|
// insert source and switch to transition view
|
|
insertSource(ts, View::TRANSITION);
|
|
|
|
}
|
|
else
|
|
load(filename);
|
|
}
|
|
|
|
void Mixer::import(const std::string& filename)
|
|
{
|
|
#ifdef THREADED_LOADING
|
|
// import only one at a time
|
|
if (sessionImporters_.empty()) {
|
|
// Start async thread for loading the session
|
|
// Will be obtained in the future in update()
|
|
sessionImporters_.emplace_back( std::async(std::launch::async, Session::load, filename, 0) );
|
|
}
|
|
#else
|
|
merge( Session::load(filename) );
|
|
#endif
|
|
}
|
|
|
|
void Mixer::import(SessionSource *source)
|
|
{
|
|
sessionSourceToImport_.push_back( source );
|
|
}
|
|
|
|
|
|
void Mixer::merge(Session *session)
|
|
{
|
|
if ( session == nullptr ) {
|
|
Log::Warning("Failed to import Session.");
|
|
return;
|
|
}
|
|
|
|
// import every sources
|
|
std::ostringstream info;
|
|
info << session->numSource() << " sources imported from:" << session->filename();
|
|
for ( Source *s = session->popSource(); s != nullptr; s = session->popSource()) {
|
|
// avoid name duplicates
|
|
renameSource(s);
|
|
|
|
// Add source to Session
|
|
session_->addSource(s);
|
|
|
|
// Attach source to Mixer
|
|
attach(s);
|
|
}
|
|
|
|
// import and attach session's mixing groups
|
|
auto group_iter = session->beginMixingGroup();
|
|
while ( group_iter != session->endMixingGroup() ){
|
|
session_->link((*group_iter)->getCopy(), mixing_.scene.fg());
|
|
group_iter = session->deleteMixingGroup(group_iter);
|
|
}
|
|
|
|
// needs to update !
|
|
++View::need_deep_update_;
|
|
|
|
// avoid display issues
|
|
current_view_->update(0.f);
|
|
|
|
// new state in history manager
|
|
Action::manager().store(info.str());
|
|
|
|
}
|
|
|
|
void Mixer::merge(SessionSource *source)
|
|
{
|
|
if ( source == nullptr ) {
|
|
Log::Warning("Failed to import Session Source.");
|
|
return;
|
|
}
|
|
|
|
// detach session from SessionSource (source will fail and be deleted later)
|
|
Session *session = source->detach();
|
|
|
|
// prepare Action manager info
|
|
std::ostringstream info;
|
|
info << source->name().c_str() << " expanded:" << session->numSource() << " sources imported";
|
|
|
|
// import sources of the session (if not empty)
|
|
if ( !session->empty() ) {
|
|
|
|
// where to put the sources imported in depth?
|
|
float target_depth = source->depth();
|
|
|
|
// get how much space we need from there
|
|
SourceList dsl = session->getDepthSortedList();
|
|
float start_depth = dsl.front()->depth();
|
|
float end_depth = dsl.back()->depth();
|
|
float need_depth = MAX( end_depth - start_depth, LAYER_STEP);
|
|
|
|
// make room if there is not enough space
|
|
SourceList to_be_moved = findSources(target_depth, MAX_DEPTH);
|
|
if (!to_be_moved.empty()){
|
|
float next_depth = to_be_moved.front()->depth();
|
|
if ( next_depth < target_depth + need_depth) {
|
|
SourceList::iterator it = to_be_moved.begin();
|
|
for (; it != to_be_moved.end(); ++it) {
|
|
float scale_depth = (MAX_DEPTH-(*it)->depth()) / (MAX_DEPTH-next_depth);
|
|
(*it)->setDepth( (*it)->depth() + scale_depth );
|
|
}
|
|
}
|
|
}
|
|
|
|
// import every sources
|
|
for ( Source *s = session->popSource(); s != nullptr; s = session->popSource()) {
|
|
|
|
// avoid name duplicates
|
|
renameSource(s);
|
|
|
|
// scale alpha
|
|
s->setAlpha( s->alpha() * source->alpha() );
|
|
|
|
// set depth (proportional to depth of s, adjusted by needed space)
|
|
s->setDepth( target_depth + ( (s->depth()-start_depth)/ need_depth) );
|
|
|
|
// set location
|
|
// a. transform of node to import
|
|
Group *sNode = s->group(View::GEOMETRY);
|
|
glm::mat4 sTransform = GlmToolkit::transform(sNode->translation_, sNode->rotation_, sNode->scale_);
|
|
// b. transform of session source
|
|
Group *sourceNode = source->group(View::GEOMETRY);
|
|
glm::mat4 sourceTransform = GlmToolkit::transform(sourceNode->translation_, sourceNode->rotation_, sourceNode->scale_);
|
|
// c. combined transform of source and session source
|
|
sourceTransform *= sTransform;
|
|
GlmToolkit::inverse_transform(sourceTransform, sNode->translation_, sNode->rotation_, sNode->scale_);
|
|
|
|
// Add source to Session
|
|
session_->addSource(s);
|
|
|
|
// Attach source to Mixer
|
|
attach(s);
|
|
}
|
|
|
|
// import and attach session's mixing groups
|
|
auto group_iter = session->beginMixingGroup();
|
|
while ( group_iter != session->endMixingGroup() ){
|
|
session_->link((*group_iter)->getCopy(), mixing_.scene.fg());
|
|
group_iter = session->deleteMixingGroup(group_iter);
|
|
}
|
|
|
|
// needs to update !
|
|
++View::need_deep_update_;
|
|
|
|
}
|
|
|
|
// imported source itself should be removed
|
|
detach(source);
|
|
session_->deleteSource(source);
|
|
|
|
// avoid display issues
|
|
current_view_->update(0.f);
|
|
|
|
// new state in history manager
|
|
Action::manager().store(info.str());
|
|
}
|
|
|
|
void Mixer::swap()
|
|
{
|
|
if (!back_session_ || !session_)
|
|
return;
|
|
|
|
if (session_) {
|
|
// clear selection
|
|
selection().clear();
|
|
// detatch current session's nodes from views
|
|
for (auto source_iter = session_->begin(); source_iter != session_->end(); source_iter++)
|
|
detach(*source_iter);
|
|
}
|
|
|
|
// swap back and front
|
|
Session *tmp = session_;
|
|
session_ = back_session_;
|
|
back_session_ = tmp;
|
|
|
|
// attach new session's nodes to views
|
|
for (auto source_iter = session_->begin(); source_iter != session_->end(); source_iter++)
|
|
attach(*source_iter);
|
|
|
|
// optional copy of views config
|
|
mixing_.scene.root()->copyTransform( session_->config(View::MIXING) );
|
|
geometry_.scene.root()->copyTransform( session_->config(View::GEOMETRY) );
|
|
layer_.scene.root()->copyTransform( session_->config(View::LAYER) );
|
|
appearance_.scene.root()->copyTransform( session_->config(View::TEXTURE) );
|
|
|
|
// attach new session's mixing group to mixingview
|
|
for (auto group_iter = session_->beginMixingGroup(); group_iter != session_->endMixingGroup(); group_iter++)
|
|
(*group_iter)->attachTo( mixing_.scene.fg() );
|
|
|
|
// set resolution
|
|
session_->setResolution( session_->config(View::RENDERING)->scale_ );
|
|
|
|
// transfer fading
|
|
session_->setFading( MAX(back_session_->fading(), session_->fading()), true );
|
|
|
|
// no current source
|
|
current_source_ = session_->end();
|
|
current_source_index_ = -1;
|
|
|
|
// delete back (former front session)
|
|
garbage_.push_back(back_session_);
|
|
back_session_ = nullptr;
|
|
|
|
// reset History manager
|
|
Action::manager().init();
|
|
|
|
// notification
|
|
Log::Notify("Session %s loaded. %d source(s) created.", session_->filename().c_str(), session_->numSource());
|
|
}
|
|
|
|
void Mixer::close(bool smooth)
|
|
{
|
|
if (smooth)
|
|
{
|
|
// create empty SessionSource to be used for the smooth transition
|
|
SessionFileSource *ts = new SessionFileSource;
|
|
|
|
// insert source and switch to transition view
|
|
insertSource(ts, View::TRANSITION);
|
|
|
|
// attach the SessionSource to the transition view
|
|
transition_.attach(ts);
|
|
}
|
|
else
|
|
clear();
|
|
}
|
|
|
|
void Mixer::clear()
|
|
{
|
|
// delete previous back session if needed
|
|
if (back_session_)
|
|
garbage_.push_back(back_session_);
|
|
|
|
// create empty session
|
|
back_session_ = new Session;
|
|
|
|
// swap current with empty
|
|
sessionSwapRequested_ = true;
|
|
|
|
// need to deeply update view to apply eventual changes
|
|
++View::need_deep_update_;
|
|
|
|
Settings::application.recentSessions.front_is_valid = false;
|
|
Log::Info("New session ready.");
|
|
}
|
|
|
|
void Mixer::set(Session *s)
|
|
{
|
|
if ( s == nullptr )
|
|
return;
|
|
|
|
// delete previous back session if needed
|
|
if (back_session_)
|
|
garbage_.push_back(back_session_);
|
|
|
|
// set to new given session
|
|
back_session_ = s;
|
|
|
|
// swap current with given session
|
|
sessionSwapRequested_ = true;
|
|
}
|
|
|
|
void Mixer::setResolution(glm::vec3 res)
|
|
{
|
|
if (session_) {
|
|
session_->setResolution(res);
|
|
++View::need_deep_update_;
|
|
std::ostringstream info;
|
|
info << "Session resolution changed to " << res.x << "x" << res.y;
|
|
Log::Info("%s", info.str().c_str());
|
|
}
|
|
}
|
|
|
|
void Mixer::paste(const std::string& clipboard)
|
|
{
|
|
tinyxml2::XMLDocument xmlDoc;
|
|
tinyxml2::XMLElement* sourceNode = SessionLoader::firstSourceElement(clipboard, xmlDoc);
|
|
if (sourceNode) {
|
|
|
|
SessionLoader loader( session_ );
|
|
|
|
for( ; sourceNode ; sourceNode = sourceNode->NextSiblingElement())
|
|
{
|
|
Source *s = loader.createSource(sourceNode);
|
|
if (s) {
|
|
// Add source to Session
|
|
session_->addSource(s);
|
|
// Add source to Mixer
|
|
addSource(s);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void Mixer::restore(tinyxml2::XMLElement *sessionNode)
|
|
{
|
|
//
|
|
// source lists
|
|
//
|
|
|
|
// sessionsources contains list of ids of all sources currently in the session (before loading)
|
|
SourceIdList session_sources = session_->getIdList();
|
|
// for( auto it = sessionsources.begin(); it != sessionsources.end(); it++)
|
|
// Log::Info("sessionsources id %s", std::to_string(*it).c_str());
|
|
|
|
// load history status:
|
|
// - if a source exists, its attributes are updated, and that's all
|
|
// - if a source does not exists (in current session), it is created inside the session
|
|
SessionLoader loader( session_ );
|
|
loader.load( sessionNode );
|
|
|
|
// loaded_sources contains map of xml ids of all sources treated by loader
|
|
std::map< uint64_t, Source* > loaded_sources = loader.getSources();
|
|
|
|
// remove intersect of both lists (sources were updated by SessionLoader)
|
|
for( auto lsit = loaded_sources.begin(); lsit != loaded_sources.end(); ){
|
|
auto ssit = std::find(session_sources.begin(), session_sources.end(), (*lsit).first);
|
|
if ( ssit != session_sources.end() ) {
|
|
lsit = loaded_sources.erase(lsit);
|
|
session_sources.erase(ssit);
|
|
}
|
|
else
|
|
lsit++;
|
|
}
|
|
|
|
// remaining ids in list sessionsources : to remove
|
|
while ( !session_sources.empty() ){
|
|
Source *s = Mixer::manager().findSource( session_sources.front() );
|
|
if (s!=nullptr) {
|
|
#ifdef ACTION_DEBUG
|
|
Log::Info("Delete id %s\n", std::to_string(session_sources.front() ).c_str());
|
|
#endif
|
|
// remove the source from the mixer
|
|
detach( s );
|
|
// delete source from session
|
|
session_->deleteSource( s );
|
|
}
|
|
session_sources.pop_front();
|
|
}
|
|
|
|
// remaining sources in list loaded_sources : to add
|
|
for ( auto lsit = loaded_sources.begin(); lsit != loaded_sources.end(); lsit++)
|
|
{
|
|
#ifdef ACTION_DEBUG
|
|
Log::Info("Recreate id %s to %s\n", std::to_string((*lsit).first).c_str(), std::to_string((*lsit).second->id()).c_str());
|
|
#endif
|
|
// attach created source
|
|
attach( (*lsit).second );
|
|
}
|
|
|
|
//
|
|
// mixing groups
|
|
//
|
|
|
|
// Get the list of mixing groups in the xml loader
|
|
std::list< SourceList > loadergroups = loader.getMixingGroups();
|
|
|
|
// clear all session groups
|
|
auto group_iter = session_->beginMixingGroup();
|
|
while ( group_iter != session_->endMixingGroup() )
|
|
group_iter = session_->deleteMixingGroup(group_iter);
|
|
|
|
// apply all changes creating or modifying groups in the session
|
|
// (after this, new groups are created and existing groups are adjusted)
|
|
for (auto group_loader_it = loadergroups.begin(); group_loader_it != loadergroups.end(); group_loader_it++)
|
|
session_->link( *group_loader_it, view(View::MIXING)->scene.fg() );
|
|
|
|
|
|
++View::need_deep_update_;
|
|
}
|
|
|