No-op: remove all trailing whitespace.
[dcpomatic.git] / src / lib / film.cc
index a7be8aea7857265fb0b6b7aff82c9d8726075569..66f1de868bd8c6307eb208337e92f478afeff605 100644 (file)
@@ -32,7 +32,6 @@
 #include "exceptions.h"
 #include "examine_content_job.h"
 #include "config.h"
-#include "ui_signaller.h"
 #include "playlist.h"
 #include "player.h"
 #include "dcp_content_type.h"
@@ -42,6 +41,8 @@
 #include "safe_stringstream.h"
 #include "environment_info.h"
 #include "raw_convert.h"
+#include "audio_processor.h"
+#include "md5_digester.h"
 #include <libcxml/cxml.h>
 #include <dcp/cpl.h>
 #include <dcp/signer.h>
@@ -71,6 +72,7 @@ using std::map;
 using std::vector;
 using std::setfill;
 using std::min;
+using std::max;
 using std::make_pair;
 using std::endl;
 using std::cout;
@@ -127,18 +129,19 @@ Film::Film (boost::filesystem::path dir, bool log)
        , _sequence_video (true)
        , _interop (false)
        , _burn_subtitles (false)
+       , _audio_processor (0)
        , _state_version (current_state_version)
        , _dirty (false)
 {
        set_isdcf_date_today ();
 
        _playlist_changed_connection = _playlist->Changed.connect (bind (&Film::playlist_changed, this));
-       _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2));
-       
+       _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2, _3));
+
        /* Make state.directory a complete path without ..s (where possible)
           (Code swiped from Adam Bowen on stackoverflow)
        */
-       
+
        boost::filesystem::path p (boost::filesystem::system_complete (dir));
        boost::filesystem::path result;
        for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) {
@@ -168,7 +171,7 @@ Film::~Film ()
        for (list<boost::signals2::connection>::const_iterator i = _job_connections.begin(); i != _job_connections.end(); ++i) {
                i->disconnect ();
        }
-}      
+}
 
 string
 Film::video_identifier () const
@@ -177,7 +180,7 @@ Film::video_identifier () const
 
        SafeStringStream s;
        s.imbue (std::locale::classic ());
-       
+
        s << container()->id()
          << "_" << resolution_to_string (_resolution)
          << "_" << _playlist->video_identifier()
@@ -206,47 +209,29 @@ Film::video_identifier () const
 
        return s.str ();
 }
-         
-/** @return The path to the directory to write video frame info files to */
+
+/** @return The file to write video frame info to */
 boost::filesystem::path
-Film::info_dir () const
+Film::info_file () const
 {
        boost::filesystem::path p;
        p /= "info";
        p /= video_identifier ();
-       return dir (p);
+       return file (p);
 }
 
 boost::filesystem::path
-Film::internal_video_mxf_dir () const
+Film::internal_video_asset_dir () const
 {
        return dir ("video");
 }
 
 boost::filesystem::path
-Film::internal_video_mxf_filename () const
+Film::internal_video_asset_filename () const
 {
        return video_identifier() + ".mxf";
 }
 
-boost::filesystem::path
-Film::video_mxf_filename () const
-{
-       return filename_safe_name() + "_video.mxf";
-}
-
-boost::filesystem::path
-Film::audio_mxf_filename () const
-{
-       return filename_safe_name() + "_audio.mxf";
-}
-
-boost::filesystem::path
-Film::subtitle_xml_filename () const
-{
-       return filename_safe_name() + "_subtitle.xml";
-}
-
 string
 Film::filename_safe_name () const
 {
@@ -264,9 +249,28 @@ Film::filename_safe_name () const
 }
 
 boost::filesystem::path
-Film::audio_analysis_dir () const
+Film::audio_analysis_path () const
 {
-       return dir ("analysis");
+       boost::filesystem::path p = dir ("analysis");
+
+       MD5Digester digester;
+       BOOST_FOREACH (shared_ptr<Content> i, content ()) {
+               shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (i);
+               if (!ac) {
+                       continue;
+               }
+
+               digester.add (ac->digest ());
+               digester.add (ac->audio_mapping().digest ());
+               digester.add (ac->audio_gain ());
+       }
+
+       if (audio_processor ()) {
+               digester.add (audio_processor()->id ());
+       }
+
+       p /= digester.get ();
+       return p;
 }
 
 /** Add suitable Jobs to the JobManager to create a DCP for this Film */
@@ -274,7 +278,7 @@ void
 Film::make_dcp ()
 {
        set_isdcf_date_today ();
-       
+
        if (dcp_name().find ("/") != string::npos) {
                throw BadSettingError (_("name"), _("cannot contain slashes"));
        }
@@ -288,7 +292,7 @@ Film::make_dcp ()
        LOG_GENERAL ("DCP video rate %1 fps", video_frame_rate());
        LOG_GENERAL ("%1 threads", Config::instance()->num_local_encoding_threads());
        LOG_GENERAL ("J2K bandwidth %1", j2k_bandwidth());
-       
+
        if (container() == 0) {
                throw MissingSettingError (_("container"));
        }
@@ -316,25 +320,6 @@ Film::send_dcp_to_tms ()
        JobManager::instance()->add (j);
 }
 
-/** Count the number of frames that have been encoded for this film.
- *  @return frame count.
- */
-int
-Film::encoded_frames () const
-{
-       if (container() == 0) {
-               return 0;
-       }
-
-       int N = 0;
-       for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (info_dir ()); i != boost::filesystem::directory_iterator(); ++i) {
-               ++N;
-               boost::this_thread::interruption_point ();
-       }
-
-       return N;
-}
-
 shared_ptr<xmlpp::Document>
 Film::metadata () const
 {
@@ -366,6 +351,9 @@ Film::metadata () const
        root->add_child("Signed")->add_child_text (_signed ? "1" : "0");
        root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0");
        root->add_child("Key")->add_child_text (_key.hex ());
+       if (_audio_processor) {
+               root->add_child("AudioProcessor")->add_child_text (_audio_processor->id ());
+       }
        _playlist->as_xml (root->add_child ("Playlist"));
 
        return doc;
@@ -398,7 +386,7 @@ Film::read_metadata ()
        if (_state_version > current_state_version) {
                throw StringError (_("This film was created with a newer version of DCP-o-matic, and it cannot be loaded into this version.  Sorry!"));
        }
-       
+
        _name = f.string_child ("Name");
        if (_state_version >= 9) {
                _use_isdcf_name = f.bool_child ("UseISDCFName");
@@ -446,6 +434,12 @@ Film::read_metadata ()
        }
        _key = dcp::Key (f.string_child ("Key"));
 
+       if (f.optional_string_child ("AudioProcessor")) {
+               _audio_processor = AudioProcessor::from_id (f.string_child ("AudioProcessor"));
+       } else {
+               _audio_processor = 0;
+       }
+
        list<string> notes;
        /* This method is the only one that can return notes (so far) */
        _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version, notes);
@@ -466,9 +460,9 @@ Film::dir (boost::filesystem::path d) const
        boost::filesystem::path p;
        p /= _directory;
        p /= d;
-       
+
        boost::filesystem::create_directories (p);
-       
+
        return p;
 }
 
@@ -483,7 +477,7 @@ Film::file (boost::filesystem::path f) const
        p /= f;
 
        boost::filesystem::create_directories (p.parent_path ());
-       
+
        return p;
 }
 
@@ -500,7 +494,7 @@ Film::isdcf_name (bool if_created_now) const
        split (words, raw_name, is_any_of (" "));
 
        string fixed_name;
-       
+
        /* Add each word to fixed_name */
        for (vector<string>::const_iterator i = words.begin(); i != words.end(); ++i) {
                string w = *i;
@@ -515,7 +509,7 @@ Film::isdcf_name (bool if_created_now) const
                                ++caps;
                        }
                }
-               
+
                /* If w is all caps make the rest of it lower case, otherwise
                   leave it alone.
                */
@@ -546,15 +540,15 @@ Film::isdcf_name (bool if_created_now) const
        if (dm.temp_version) {
                d << "-Temp";
        }
-       
+
        if (dm.pre_release) {
                d << "-Pre";
        }
-       
+
        if (dm.red_band) {
                d << "-RedBand";
        }
-       
+
        if (!dm.chain.empty ()) {
                d << "-" << dm.chain;
        }
@@ -574,13 +568,13 @@ Film::isdcf_name (bool if_created_now) const
        if (video_frame_rate() != 24) {
                d << "-" << video_frame_rate();
        }
-       
+
        if (container()) {
                d << "_" << container()->isdcf_name();
        }
 
        ContentList cl = content ();
-       
+
        /* XXX: this uses the first bit of content only */
 
        /* The standard says we don't do this for trailers, for some strange reason */
@@ -598,7 +592,7 @@ Film::isdcf_name (bool if_created_now) const
                                break;
                        }
                }
-               
+
                if (content_ratio && content_ratio != container()) {
                        d << "-" << content_ratio->isdcf_name();
                }
@@ -622,32 +616,44 @@ Film::isdcf_name (bool if_created_now) const
 
        /* Find all mapped channels */
 
-       list<dcp::Channel> mapped;
-       for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) {
-               shared_ptr<const AudioContent> ac = dynamic_pointer_cast<const AudioContent> (*i);
-               if (ac) {
-                       list<dcp::Channel> c = ac->audio_mapping().mapped_dcp_channels ();
-                       copy (c.begin(), c.end(), back_inserter (mapped));
-               }
-       }
-
-       mapped.sort ();
-       mapped.unique ();
-       
-       /* Count them */
-                       
        int non_lfe = 0;
        int lfe = 0;
-       for (list<dcp::Channel>::const_iterator i = mapped.begin(); i != mapped.end(); ++i) {
-               if (static_cast<int> (*i) >= audio_channels()) {
-                       /* This channel is mapped but is not included in the DCP */
-                       continue;
-               }
-               
-               if ((*i) == dcp::LFE) {
+
+       if (audio_processor ()) {
+               /* Processors are mapped 1:1 to DCP outputs so we can guess the number of LFE/
+                  non-LFE from the channel counts.
+               */
+               non_lfe = audio_processor()->out_channels ();
+               if (non_lfe >= 4) {
+                       --non_lfe;
                        ++lfe;
-               } else {
-                       ++non_lfe;
+               }
+       } else {
+               list<int> mapped;
+               for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) {
+                       shared_ptr<const AudioContent> ac = dynamic_pointer_cast<const AudioContent> (*i);
+                       if (ac) {
+                               list<int> c = ac->audio_mapping().mapped_output_channels ();
+                               copy (c.begin(), c.end(), back_inserter (mapped));
+                       }
+               }
+
+               mapped.sort ();
+               mapped.unique ();
+
+               /* Count them */
+
+               for (list<int>::const_iterator i = mapped.begin(); i != mapped.end(); ++i) {
+                       if (*i >= audio_channels()) {
+                               /* This channel is mapped but is not included in the DCP */
+                               continue;
+                       }
+
+                       if (static_cast<dcp::Channel> (*i) == dcp::LFE) {
+                               ++lfe;
+                       } else {
+                               ++non_lfe;
+                       }
                }
        }
 
@@ -658,7 +664,7 @@ Film::isdcf_name (bool if_created_now) const
        /* XXX: HI/VI */
 
        d << "_" << resolution_to_string (_resolution);
-       
+
        if (!dm.studio.empty ()) {
                d << "_" << dm.studio;
        }
@@ -678,7 +684,7 @@ Film::isdcf_name (bool if_created_now) const
        } else {
                d << "_SMPTE";
        }
-       
+
        if (three_d ()) {
                d << "-3D";
        }
@@ -694,11 +700,26 @@ Film::isdcf_name (bool if_created_now) const
 string
 Film::dcp_name (bool if_created_now) const
 {
+       string unfiltered;
        if (use_isdcf_name()) {
-               return isdcf_name (if_created_now);
+               unfiltered = isdcf_name (if_created_now);
+       } else {
+               unfiltered = name ();
+       }
+
+       /* Filter out `bad' characters which cause problems with some systems.
+          There's no apparent list of what really is allowed, so this is a guess.
+       */
+
+       string filtered;
+       string const allowed = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
+       for (size_t i = 0; i < unfiltered.size(); ++i) {
+               if (allowed.find (unfiltered[i]) != string::npos) {
+                       filtered += unfiltered[i];
+               }
        }
 
-       return name();
+       return filtered;
 }
 
 void
@@ -792,6 +813,14 @@ Film::set_burn_subtitles (bool b)
        signal_changed (BURN_SUBTITLES);
 }
 
+void
+Film::set_audio_processor (AudioProcessor const * processor)
+{
+       _audio_processor = processor;
+       signal_changed (AUDIO_PROCESSOR);
+       signal_changed (AUDIO_CHANNELS);
+}
+
 void
 Film::signal_changed (Property p)
 {
@@ -809,9 +838,7 @@ Film::signal_changed (Property p)
                break;
        }
 
-       if (ui_signaller) {
-               ui_signaller->emit (boost::bind (boost::ref (Changed), p));
-       }
+       emit (boost::bind (boost::ref (Changed), p));
 }
 
 void
@@ -820,32 +847,6 @@ Film::set_isdcf_date_today ()
        _isdcf_date = boost::gregorian::day_clock::local_day ();
 }
 
-boost::filesystem::path
-Film::info_path (int f, Eyes e) const
-{
-       boost::filesystem::path p;
-       p /= info_dir ();
-
-       SafeStringStream s;
-       s.width (8);
-       s << setfill('0') << f;
-
-       if (e == EYES_LEFT) {
-               s << ".L";
-       } else if (e == EYES_RIGHT) {
-               s << ".R";
-       }
-
-       s << ".md5";
-       
-       p /= s.str();
-
-       /* info_dir() will already have added any initial bit of the path,
-          so don't call file() on this.
-       */
-       return p;
-}
-
 boost::filesystem::path
 Film::j2c_path (int f, Eyes e, bool t) const
 {
@@ -862,7 +863,7 @@ Film::j2c_path (int f, Eyes e, bool t) const
        } else if (e == EYES_RIGHT) {
                s << ".R";
        }
-       
+
        s << ".j2c";
 
        if (t) {
@@ -878,7 +879,7 @@ vector<CPLSummary>
 Film::cpls () const
 {
        vector<CPLSummary> out;
-       
+
        boost::filesystem::path const dir = directory ();
        for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator(dir); i != boost::filesystem::directory_iterator(); ++i) {
                if (
@@ -902,14 +903,8 @@ Film::cpls () const
                        }
                }
        }
-       
-       return out;
-}
 
-shared_ptr<Player>
-Film::make_player () const
-{
-       return shared_ptr<Player> (new Player (shared_from_this (), _playlist));
+       return out;
 }
 
 void
@@ -926,10 +921,11 @@ Film::set_encrypted (bool e)
        signal_changed (ENCRYPTED);
 }
 
-shared_ptr<Playlist>
-Film::playlist () const
+void
+Film::set_key (dcp::Key key)
 {
-       return _playlist;
+       _key = key;
+       signal_changed (KEY);
 }
 
 ContentList
@@ -951,13 +947,13 @@ Film::examine_and_add_content (shared_ptr<Content> c)
        if (dynamic_pointer_cast<FFmpegContent> (c)) {
                run_ffprobe (c->path(0), file ("ffprobe.log"), _log);
        }
-                       
+
        shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
 
        _job_connections.push_back (
                j->Finished.connect (bind (&Film::maybe_add_content, this, boost::weak_ptr<Job> (j), boost::weak_ptr<Content> (c)))
                );
-       
+
        JobManager::instance()->add (j);
 }
 
@@ -968,7 +964,7 @@ Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c)
        if (!job || !job->finished_ok ()) {
                return;
        }
-       
+
        shared_ptr<Content> content = c.lock ();
        if (content) {
                add_content (content);
@@ -1023,19 +1019,15 @@ Film::active_frame_rate_change (DCPTime t) const
 }
 
 void
-Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
+Film::playlist_content_changed (boost::weak_ptr<Content> c, int p, bool frequent)
 {
        if (p == VideoContentProperty::VIDEO_FRAME_RATE) {
                set_video_frame_rate (_playlist->best_dcp_frame_rate ());
-       } else if (
-               p == AudioContentProperty::AUDIO_MAPPING ||
-               p == AudioContentProperty::AUDIO_CHANNELS) {
+       } else if (p == AudioContentProperty::AUDIO_STREAMS) {
                signal_changed (NAME);
        }
 
-       if (ui_signaller) {
-               ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p));
-       }
+       emit (boost::bind (boost::ref (ContentChanged), c, p, frequent));
 }
 
 void
@@ -1043,7 +1035,7 @@ Film::playlist_changed ()
 {
        signal_changed (CONTENT);
        signal_changed (NAME);
-}      
+}
 
 int
 Film::audio_frame_rate () const
@@ -1079,7 +1071,7 @@ Film::full_frame () const
 dcp::Size
 Film::frame_size () const
 {
-       return fit_ratio_within (container()->ratio(), full_frame (), 1);
+       return fit_ratio_within (container()->ratio(), full_frame ());
 }
 
 dcp::EncryptedKDM
@@ -1096,7 +1088,7 @@ Film::make_kdm (
        if (!signer->valid ()) {
                throw InvalidSignerError ();
        }
-       
+
        return dcp::DecryptedKDM (
                cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string()
                ).encrypt (signer, target, formulation);
@@ -1139,10 +1131,29 @@ Film::required_disk_space () const
  *  Note: the decision made by this method isn't, of course, 100% reliable.
  */
 bool
-Film::should_be_enough_disk_space (double& required, double& available) const
-{
-       boost::filesystem::space_info s = boost::filesystem::space (internal_video_mxf_dir ());
+Film::should_be_enough_disk_space (double& required, double& available, bool& can_hard_link) const
+{
+       /* Create a test file and see if we can hard-link it */
+       boost::filesystem::path test = internal_video_asset_dir() / "test";
+       boost::filesystem::path test2 = internal_video_asset_dir() / "test2";
+       can_hard_link = true;
+       FILE* f = fopen_boost (test, "w");
+       if (f) {
+               fclose (f);
+               boost::system::error_code ec;
+               boost::filesystem::create_hard_link (test, test2, ec);
+               if (ec) {
+                       can_hard_link = false;
+               }
+               boost::filesystem::remove (test);
+               boost::filesystem::remove (test2);
+       }
+
+       boost::filesystem::space_info s = boost::filesystem::space (internal_video_asset_dir ());
        required = double (required_disk_space ()) / 1073741824.0f;
+       if (!can_hard_link) {
+               required *= 2;
+       }
        available = double (s.available) / 1073741824.0f;
        return (available - required) > 1;
 }
@@ -1151,7 +1162,7 @@ string
 Film::subtitle_language () const
 {
        set<string> languages;
-       
+
        ContentList cl = content ();
        BOOST_FOREACH (shared_ptr<Content>& c, cl) {
                shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (c);
@@ -1171,3 +1182,65 @@ Film::subtitle_language () const
 
        return all;
 }
+
+/** Change the gains of the supplied AudioMapping to make it a default
+ *  for this film.  The defaults are guessed based on what processor (if any)
+ *  is in use and the number of input channels.
+ */
+void
+Film::make_audio_mapping_default (AudioMapping& mapping) const
+{
+       if (audio_processor ()) {
+               audio_processor()->make_audio_mapping_default (mapping);
+       } else {
+               mapping.make_zero ();
+               if (mapping.input_channels() == 1) {
+                       /* Mono -> Centre */
+                       mapping.set (0, static_cast<int> (dcp::CENTRE), 1);
+               } else {
+                       /* 1:1 mapping */
+                       for (int i = 0; i < min (mapping.input_channels(), mapping.output_channels()); ++i) {
+                               mapping.set (i, i, 1);
+                       }
+               }
+       }
+}
+
+/** @return The names of the channels that audio contents' outputs are passed into;
+ *  this is either the DCP or a AudioProcessor.
+ */
+vector<string>
+Film::audio_output_names () const
+{
+       if (audio_processor ()) {
+               return audio_processor()->input_names ();
+       }
+
+       vector<string> n;
+       n.push_back (_("L"));
+       n.push_back (_("R"));
+       n.push_back (_("C"));
+       n.push_back (_("Lfe"));
+       n.push_back (_("Ls"));
+       n.push_back (_("Rs"));
+       n.push_back (_("HI"));
+       n.push_back (_("VI"));
+       n.push_back (_("Lc"));
+       n.push_back (_("Rc"));
+       n.push_back (_("BsL"));
+       n.push_back (_("BsR"));
+
+       return vector<string> (n.begin(), n.begin() + audio_channels ());
+}
+
+void
+Film::repeat_content (ContentList c, int n)
+{
+       _playlist->repeat (c, n);
+}
+
+void
+Film::remove_content (ContentList c)
+{
+       _playlist->remove (c);
+}