Files
vimix/Mixer.cpp
Bruno Herbelin 1cb448c42e Output session fading fixed for OSC and animation.
Linear interpolation (instead of dichotomy converge) for fading at Session update. Mixing View update reads value of session fading to animate the cursor (which was preventing other manipulation of fading). Cleanup fading in OSC controller, with animation options and fade-in and fade-out controls.
2021-12-26 00:41:02 +01:00

1421 lines
40 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, bool with_version)
{
// capture a snapshot of current version if requested
if (with_version)
Action::manager().snapshot( SystemToolkit::date_time_string());
// 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;
}
// 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"));
// notify creation of source
Log::Notify("Added source '%s' with %s", s->name().c_str(), s->info().c_str());
MediaSource *ms = dynamic_cast<MediaSource *>(s);
if (ms)
Settings::application.recentImport.push(ms->path());
// 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());
Log::Notify("Added source '%s' with %s", sessiongroup->name().c_str(), sessiongroup->info().c_str());
// give the hand to the user
Mixer::manager().setCurrentSource(sessiongroup);
}
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) );
}
Source *Mixer::sourceAtIndex (int index)
{
SourceList::iterator s = session_->at(index);
if (s!=session_->end())
return *s;
return nullptr;
}
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() const
{
return current_source_index_;
}
int Mixer::count() const
{
return (int) session_->numSource();
}
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(bool with_version)
{
if (!session_->filename().empty())
saveas(session_->filename(), with_version);
}
void Mixer::saveas(const std::string& filename, bool with_version)
{
// 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_, with_version).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)->call( new 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->call( new SetAlpha(s->alpha() * source->alpha()));
// set depth (proportional to depth of s, adjusted by needed space)
s->call( new 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_->setFadingTarget( MAX(back_session_->fadingTarget(), session_->fadingTarget()));
// 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_;
}