Merge master.
authorCarl Hetherington <cth@carlh.net>
Tue, 1 Apr 2014 21:51:54 +0000 (22:51 +0100)
committerCarl Hetherington <cth@carlh.net>
Tue, 1 Apr 2014 21:51:54 +0000 (22:51 +0100)
29 files changed:
1  2 
ChangeLog
src/lib/config.cc
src/lib/config.h
src/lib/dcp_video_frame.cc
src/lib/encoder.cc
src/lib/film.cc
src/lib/playlist.cc
src/lib/sndfile_content.cc
src/lib/types.h
src/lib/util.cc
src/lib/util.h
src/lib/video_content.cc
src/lib/video_content.h
src/lib/video_decoder.cc
src/lib/writer.cc
src/lib/writer.h
src/lib/wscript
src/wx/config_dialog.cc
src/wx/film_editor.cc
src/wx/properties_dialog.cc
src/wx/screen_dialog.cc
src/wx/screen_dialog.h
src/wx/timecode.cc
src/wx/timeline.cc
src/wx/timing_panel.cc
src/wx/video_panel.cc
src/wx/wscript
src/wx/wx_util.cc
wscript

diff --combined ChangeLog
index 065a10ae324ca38809de65a83408ab822f3367df,22fddbeb1fbca95c3271bc0f2e08ab4065c856a5..1d3ee28fa067027dbc1a5e236160be18151cd498
+++ b/ChangeLog
@@@ -1,7 -1,56 +1,61 @@@
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-04-01  Carl Hetherington  <cth@carlh.net>
+       * Basic support for separate left/right-eye files or directories
+       for 3D.
+ 2014-03-30  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.9 released.
+ 2014-03-30  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.8 released.
+       * nl_NL translation from Theo Kooijmans.
+ 2014-03-27  Carl Hetherington  <cth@carlh.net>
+       * Auto-save film metadata before starting DCP encode.
+ 2014-03-25  Carl Hetherington  <cth@carlh.net>
+       * Add support for downloading Doremi server certificates.
+ 2014-03-24  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.7 released.
+ 2014-03-24  Carl Hetherington  <cth@carlh.net>
+       * Fix error on creating DCPs without audio.
+ 2014-03-23  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.6 released.
+ 2014-03-23  Carl Hetherington  <cth@carlh.net>
+       * Attempt to fix format string specifier error on Windows.
+       * Version 1.66.5 released.
+ 2014-03-22  Carl Hetherington  <cth@carlh.net>
+       * Version 1.66.4 released.
+ 2014-03-22  Carl Hetherington  <cth@carlh.net>
+       * Allow specification of the video frame rate that a sound file
+       was prepared for.
+       * Another attempt to fix colour conversion dialog strange behaviour
+       on OS X.
++>>>>>>> master
  2014-03-18  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.66.3 released.
diff --combined src/lib/config.cc
index ad1408cff89d4f07553ff2056175a3f44f18f780,eda56416d20a380f4e0ca654540593ba1ce1ae64..ca8d0bc53c407182c8b39dc0770648574f1d9e51
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -23,7 -23,7 +23,7 @@@
  #include <glib.h>
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include <libcxml/cxml.h>
  #include "config.h"
  #include "server.h"
@@@ -70,6 -70,7 +70,7 @@@ Config::Config (
                )
        , _check_for_updates (false)
        , _check_for_test_updates (false)
+       , _maximum_j2k_bandwidth (250000000)
  {
        _allowed_dcp_frame_rates.push_back (24);
        _allowed_dcp_frame_rates.push_back (25);
@@@ -78,9 -79,9 +79,9 @@@
        _allowed_dcp_frame_rates.push_back (50);
        _allowed_dcp_frame_rates.push_back (60);
  
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, libdcp::colour_matrix::srgb_to_xyz, 2.6));
 -      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, dcp::colour_matrix::srgb_to_xyz, 2.6));
 +      _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
  }
  
  void
@@@ -164,7 -165,7 +165,7 @@@ Config::read (
                /* Loading version 0 (before Rec. 709 was added as a preset).
                   Add it in.
                */
 -              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6));
 +              _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6));
        }
  
        list<cxml::NodePtr> cin = f.node_children ("Cinema");
  
        _check_for_updates = f.optional_bool_child("CheckForUpdates").get_value_or (false);
        _check_for_test_updates = f.optional_bool_child("CheckForTestUpdates").get_value_or (false);
+       _maximum_j2k_bandwidth = f.optional_number_child<int> ("MaximumJ2KBandwidth").get_value_or (250000000);
  }
  
  void
@@@ -362,6 -365,8 +365,8 @@@ Config::write () cons
        root->add_child("CheckForUpdates")->add_child_text (_check_for_updates ? "1" : "0");
        root->add_child("CheckForTestUpdates")->add_child_text (_check_for_test_updates ? "1" : "0");
  
+       root->add_child("MaximumJ2KBandwidth")->add_child_text (lexical_cast<string> (_maximum_j2k_bandwidth));
        doc.write_to_file_formatted (file(false).string ());
  }
  
@@@ -387,3 -392,10 +392,10 @@@ Config::drop (
        delete _instance;
        _instance = 0;
  }
+ void
+ Config::changed ()
+ {
+       write ();
+       Changed ();
+ }
diff --combined src/lib/config.h
index 68aae7414a009206f5984a46630f7a5b3c56895c,a40e3680aace969bcf3fcfaa00d8dc206bbdc18a..ee11dcadbefb071cf606a07e9114275a5974c469
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -28,7 -28,7 +28,7 @@@
  #include <boost/shared_ptr.hpp>
  #include <boost/signals2.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/metadata.h>
 +#include <dcp/metadata.h>
  #include "dci_metadata.h"
  #include "colour_conversion.h"
  #include "server.h"
@@@ -66,7 -66,7 +66,7 @@@ public
  
        void set_use_any_servers (bool u) {
                _use_any_servers = u;
-               write ();
+               changed ();
        }
  
        bool use_any_servers () const {
@@@ -76,7 -76,7 +76,7 @@@
        /** @param s New list of servers */
        void set_servers (std::vector<std::string> s) {
                _servers = s;
-               write ();
+               changed ();
        }
  
        /** @return Host names / IP addresses of J2K encoding servers that should definitely be used */
@@@ -89,7 -89,7 +89,7 @@@
                return _tms_ip;
        }
        
-       /** @return The path on a TMS that we should write DCPs to */
+       /** @return The path on a TMS that we should changed DCPs to */
        std::string tms_path () const {
                return _tms_path;
        }
                return _default_dcp_content_type;
        }
  
 -      libdcp::XMLMetadata dcp_metadata () const {
 +      dcp::XMLMetadata dcp_metadata () const {
                return _dcp_metadata;
        }
  
        bool check_for_test_updates () const {
                return _check_for_test_updates;
        }
+       int maximum_j2k_bandwidth () const {
+               return _maximum_j2k_bandwidth;
+       }
        
        /** @param n New number of local encoding threads */
        void set_num_local_encoding_threads (int n) {
                _num_local_encoding_threads = n;
-               write ();
+               changed ();
        }
  
        void set_default_directory (boost::filesystem::path d) {
                _default_directory = d;
-               write ();
+               changed ();
        }
  
        /** @param p New server port */
        void set_server_port_base (int p) {
                _server_port_base = p;
-               write ();
+               changed ();
        }
  
        /** @param i IP address of a TMS that we can copy DCPs to */
        void set_tms_ip (std::string i) {
                _tms_ip = i;
-               write ();
+               changed ();
        }
  
-       /** @param p Path on a TMS that we should write DCPs to */
+       /** @param p Path on a TMS that we should changed DCPs to */
        void set_tms_path (std::string p) {
                _tms_path = p;
-               write ();
+               changed ();
        }
  
        /** @param u User name to log into the TMS with */
        void set_tms_user (std::string u) {
                _tms_user = u;
-               write ();
+               changed ();
        }
  
        /** @param p Password to log into the TMS with */
        void set_tms_password (std::string p) {
                _tms_password = p;
-               write ();
+               changed ();
        }
  
        void add_cinema (boost::shared_ptr<Cinema> c) {
                _cinemas.push_back (c);
+               changed ();
        }
  
        void remove_cinema (boost::shared_ptr<Cinema> c) {
                _cinemas.remove (c);
+               changed ();
        }
  
        void set_allowed_dcp_frame_rates (std::list<int> const & r) {
                _allowed_dcp_frame_rates = r;
-               write ();
+               changed ();
        }
  
        void set_default_dci_metadata (DCIMetadata d) {
                _default_dci_metadata = d;
-               write ();
+               changed ();
        }
  
        void set_language (std::string l) {
                _language = l;
-               write ();
+               changed ();
        }
  
        void unset_language () {
                _language = boost::none;
-               write ();
+               changed ();
        }
  
        void set_default_still_length (int s) {
                _default_still_length = s;
-               write ();
+               changed ();
        }
  
        void set_default_container (Ratio const * c) {
                _default_container = c;
-               write ();
+               changed ();
        }
  
        void set_default_dcp_content_type (DCPContentType const * t) {
                _default_dcp_content_type = t;
-               write ();
+               changed ();
        }
  
 -      void set_dcp_metadata (libdcp::XMLMetadata m) {
 +      void set_dcp_metadata (dcp::XMLMetadata m) {
                _dcp_metadata = m;
-               write ();
+               changed ();
        }
  
        void set_default_j2k_bandwidth (int b) {
                _default_j2k_bandwidth = b;
-               write ();
+               changed ();
        }
  
        void set_default_audio_delay (int d) {
                _default_audio_delay = d;
-               write ();
+               changed ();
        }
  
        void set_colour_conversions (std::vector<PresetColourConversion> const & c) {
                _colour_conversions = c;
-               write ();
+               changed ();
        }
  
        void set_mail_server (std::string s) {
                _mail_server = s;
-               write ();
+               changed ();
        }
  
        void set_mail_user (std::string u) {
                _mail_user = u;
-               write ();
+               changed ();
        }
  
        void set_mail_password (std::string p) {
                _mail_password = p;
-               write ();
+               changed ();
        }
  
        void set_kdm_from (std::string f) {
                _kdm_from = f;
-               write ();
+               changed ();
        }
  
        void set_kdm_email (std::string e) {
                _kdm_email = e;
-               write ();
+               changed ();
        }
  
        void set_check_for_updates (bool c) {
                _check_for_updates = c;
-               write ();
+               changed ();
        }
  
        void set_check_for_test_updates (bool c) {
                _check_for_test_updates = c;
-               write ();
+               changed ();
        }
-       
-       void write () const;
  
+       void set_maximum_j2k_bandwidth (int b) {
+               _maximum_j2k_bandwidth = b;
+               changed ();
+       }
+       
        boost::filesystem::path signer_chain_directory () const;
  
+       void changed ();
+       boost::signals2::signal<void ()> Changed;
        static Config* instance ();
        static void drop ();
  
@@@ -332,6 -344,7 +344,7 @@@ private
        boost::filesystem::path file (bool) const;
        void read ();
        void read_old_metadata ();
+       void write () const;
  
        /** number of threads to use for J2K encoding on the local machine */
        int _num_local_encoding_threads;
        int _default_still_length;
        Ratio const * _default_container;
        DCPContentType const * _default_dcp_content_type;
 -      libdcp::XMLMetadata _dcp_metadata;
 +      dcp::XMLMetadata _dcp_metadata;
        int _default_j2k_bandwidth;
        int _default_audio_delay;
        std::vector<PresetColourConversion> _colour_conversions;
        /** true to check for updates on startup */
        bool _check_for_updates;
        bool _check_for_test_updates;
+       /** maximum allowed J2K bandwidth in bits per second */
+       int _maximum_j2k_bandwidth;
  
        /** Singleton instance, or 0 */
        static Config* _instance;
index 3044efd8d504a28d3fa517b121c1c1edea0f56a2,54531a0f9947e0eca275f78b36e8b4f6d71d6387..2b7a2e18f6b2ff712ba6baf47b9e074f44681676
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
      Taken from code Copyright (C) 2010-2011 Terrence Meiczinger
  
      This program is free software; you can redistribute it and/or modify
  #include <boost/asio.hpp>
  #include <boost/filesystem.hpp>
  #include <boost/lexical_cast.hpp>
 -#include <libdcp/rec709_linearised_gamma_lut.h>
 -#include <libdcp/srgb_linearised_gamma_lut.h>
 -#include <libdcp/gamma_lut.h>
 -#include <libdcp/xyz_frame.h>
 -#include <libdcp/rgb_xyz.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/gamma_lut.h>
 +#include <dcp/xyz_frame.h>
 +#include <dcp/rgb_xyz.h>
 +#include <dcp/colour_matrix.h>
  #include <libcxml/cxml.h>
  #include "film.h"
  #include "dcp_video_frame.h"
@@@ -66,7 -68,7 +66,7 @@@ using std::stringstream
  using std::cout;
  using boost::shared_ptr;
  using boost::lexical_cast;
 -using libdcp::Size;
 +using dcp::Size;
  
  #define DCI_COEFFICENT (48.0 / 52.37)
  
@@@ -118,8 -120,12 +118,8 @@@ DCPVideoFrame::DCPVideoFrame (shared_pt
  shared_ptr<EncodedData>
  DCPVideoFrame::encode_locally ()
  {
 -      shared_ptr<libdcp::LUT> in_lut;
 -      if (_conversion.input_gamma_linearised) {
 -              in_lut = libdcp::SRGBLinearisedGammaLUT::cache.get (12, _conversion.input_gamma);
 -      } else {
 -              in_lut = libdcp::GammaLUT::cache.get (12, _conversion.input_gamma);
 -      }
 +      shared_ptr<dcp::GammaLUT> in_lut;
 +      in_lut = dcp::GammaLUT::cache.get (12, _conversion.input_gamma, _conversion.input_gamma_linearised);
  
        /* XXX: libdcp should probably use boost */
        
                }
        }
        
 -      shared_ptr<libdcp::XYZFrame> xyz = libdcp::rgb_to_xyz (
 +      shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz (
                _image,
                in_lut,
 -              libdcp::GammaLUT::cache.get (16, 1 / _conversion.output_gamma),
 +              dcp::GammaLUT::cache.get (16, 1 / _conversion.output_gamma, false),
                matrix
                );
                
@@@ -288,12 -294,7 +288,7 @@@ DCPVideoFrame::encode_remotely (ServerD
        stringstream xml;
        doc.write_to_stream (xml, "UTF-8");
  
-       _log->log (String::compose (
-                          N_("Sending to remote; pixel format %1, components %2, lines (%3,%4,%5), line sizes (%6,%7,%8)"),
-                          _image->pixel_format(), _image->components(),
-                          _image->lines(0), _image->lines(1), _image->lines(2),
-                          _image->line_size()[0], _image->line_size()[1], _image->line_size()[2]
-                          ));
+       _log->log (String::compose (N_("Sending frame %1 to remote"), _frame));
  
        socket->write (xml.str().length() + 1);
        socket->write ((uint8_t *) xml.str().c_str(), xml.str().length() + 1);
@@@ -391,7 -392,7 +386,7 @@@ EncodedData::write (shared_ptr<const Fi
  }
  
  void
 -EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, libdcp::FrameInfo fin) const
 +EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, dcp::FrameInfo fin) const
  {
        boost::filesystem::path const info = film->info_path (frame, eyes);
        FILE* h = fopen_boost (info, "w");
diff --combined src/lib/encoder.cc
index 6b1c6a20041c3ac30eb5252145ec6169a9f04433,8e8da6229099002dc1cb6cfb61d5914532c46b58..8f31b596eb8f4d972f38eb95c3b9e84c8e9d921f
@@@ -35,7 -35,6 +35,7 @@@
  #include "writer.h"
  #include "server_finder.h"
  #include "player.h"
 +#include "dcp_video.h"
  
  #include "i18n.h"
  
@@@ -61,7 -60,9 +61,7 @@@ Encoder::Encoder (shared_ptr<const Film
        , _video_frames_out (0)
        , _terminate (false)
  {
 -      _have_a_real_frame[EYES_BOTH] = false;
 -      _have_a_real_frame[EYES_LEFT] = false;
 -      _have_a_real_frame[EYES_RIGHT] = false;
 +
  }
  
  Encoder::~Encoder ()
@@@ -76,6 -77,7 +76,7 @@@
  void
  Encoder::add_worker_threads (ServerDescription d)
  {
+       _film->log()->log (String::compose (N_("Adding %1 worker threads for remote %2"), d.host_name ()));
        for (int i = 0; i < d.threads(); ++i) {
                _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, d)));
        }
@@@ -177,7 -179,7 +178,7 @@@ Encoder::frame_done (
  }
  
  void
 -Encoder::process_video (shared_ptr<PlayerImage> image, Eyes eyes, ColourConversion conversion, bool same)
 +Encoder::process_video (shared_ptr<DCPVideo> frame)
  {
        _waker.nudge ();
        
        rethrow ();
  
        if (_writer->can_fake_write (_video_frames_out)) {
 -              _writer->fake_write (_video_frames_out, eyes);
 -              _have_a_real_frame[eyes] = false;
 -              frame_done ();
 -      } else if (same && _have_a_real_frame[eyes]) {
 -              /* Use the last frame that we encoded. */
 -              _writer->repeat (_video_frames_out, eyes);
 +              _writer->fake_write (_video_frames_out, frame->eyes ());
                frame_done ();
        } else {
                /* Queue this new frame for encoding */
                TIMING ("adding to queue of %1", _queue.size ());
                _queue.push_back (shared_ptr<DCPVideoFrame> (
                                          new DCPVideoFrame (
 -                                                image->image(), _video_frames_out, eyes, conversion, _film->video_frame_rate(),
 -                                                _film->j2k_bandwidth(), _film->resolution(), _film->log()
 +                                                frame->image(PIX_FMT_RGB24, false),
 +                                                _video_frames_out,
 +                                                frame->eyes(),
 +                                                frame->conversion(),
 +                                                _film->video_frame_rate(),
 +                                                _film->j2k_bandwidth(),
 +                                                _film->resolution(),
 +                                                _film->log()
                                                  )
                                          ));
                
                _condition.notify_all ();
 -              _have_a_real_frame[eyes] = true;
        }
  
 -      if (eyes != EYES_LEFT) {
 +      if (frame->eyes() != EYES_LEFT) {
                ++_video_frames_out;
        }
  }
diff --combined src/lib/film.cc
index 79833b3669ab7a6c41e8e6d7b9c0c821f11af186,8bececf4f3f8e777e7ecd6e0b86b5f65587b35e9..267138ce63b74d51054d695b52777bd405a6ebc9
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  #include <boost/filesystem.hpp>
  #include <boost/algorithm/string.hpp>
  #include <boost/lexical_cast.hpp>
 -#include <boost/date_time.hpp>
  #include <libxml++/libxml++.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/cpl.h>
 -#include <libdcp/signer.h>
 -#include <libdcp/util.h>
 -#include <libdcp/kdm.h>
 +#include <dcp/signer_chain.h>
 +#include <dcp/cpl.h>
 +#include <dcp/signer.h>
 +#include <dcp/util.h>
 +#include <dcp/local_time.h>
  #include "film.h"
  #include "job.h"
  #include "util.h"
@@@ -77,8 -78,8 +77,8 @@@ using boost::to_upper_copy
  using boost::ends_with;
  using boost::starts_with;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::Signer;
 +using dcp::Size;
 +using dcp::Signer;
  
  /* 5 -> 6
   * AudioMapping XML changed.
@@@ -245,7 -246,12 +245,12 @@@ Film::make_dcp (
        if (dcp_name().find ("/") != string::npos) {
                throw BadSettingError (_("name"), _("cannot contain slashes"));
        }
-       
+       /* It seems to make sense to auto-save metadata here, since the make DCP may last
+          a long time, and crashes/power failures are moderately likely.
+        */
+       write_metadata ();
        log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary()));
  
        {
@@@ -430,7 -436,7 +435,7 @@@ Film::read_metadata (
        _sequence_video = f.bool_child ("SequenceVideo");
        _three_d = f.bool_child ("ThreeD");
        _interop = f.bool_child ("Interop");
 -      _key = libdcp::Key (f.string_child ("Key"));
 +      _key = dcp::Key (f.string_child ("Key"));
  
        list<string> notes;
        /* This method is the only one that can return notes (so far) */
@@@ -760,7 -766,7 +765,7 @@@ Film::j2c_path (int f, Eyes e, bool t) 
        return file (p);
  }
  
 -/** @return List of subdirectories (not full paths) containing DCPs that can be successfully libdcp::DCP::read() */
 +/** @return List of subdirectories (not full paths) containing DCPs that can be successfully dcp::DCP::read() */
  list<boost::filesystem::path>
  Film::dcps () const
  {
                        ) {
  
                        try {
 -                              libdcp::DCP dcp (*i);
 +                              dcp::DCP dcp (*i);
                                dcp.read ();
                                out.push_back (i->path().leaf ());
                        } catch (...) {
@@@ -869,7 -875,7 +874,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -881,18 -887,12 +886,18 @@@ Film::has_subtitles () cons
        return _playlist->has_subtitles ();
  }
  
 -OutputVideoFrame
 +int
  Film::best_video_frame_rate () const
  {
        return _playlist->best_dcp_frame_rate ();
  }
  
 +FrameRateChange
 +Film::active_frame_rate_change (DCPTime t) const
 +{
 +      return _playlist->active_frame_rate_change (t, video_frame_rate ());
 +}
 +
  void
  Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
  {
@@@ -911,7 -911,31 +916,7 @@@ Film::playlist_changed (
        signal_changed (CONTENT);
  }     
  
 -OutputAudioFrame
 -Film::time_to_audio_frames (Time t) const
 -{
 -      return divide_with_round (t * audio_frame_rate (), TIME_HZ);
 -}
 -
 -OutputVideoFrame
 -Film::time_to_video_frames (Time t) const
 -{
 -      return divide_with_round (t * video_frame_rate (), TIME_HZ);
 -}
 -
 -Time
 -Film::audio_frames_to_time (OutputAudioFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, audio_frame_rate ());
 -}
 -
 -Time
 -Film::video_frames_to_time (OutputVideoFrame f) const
 -{
 -      return divide_with_round (f * TIME_HZ, video_frame_rate ());
 -}
 -
 -OutputAudioFrame
 +int
  Film::audio_frame_rate () const
  {
        /* XXX */
@@@ -927,38 -951,38 +932,38 @@@ Film::set_sequence_video (bool s
  }
  
  /** @return Size of the largest possible image in whatever resolution we are using */
 -libdcp::Size
 +dcp::Size
  Film::full_frame () const
  {
        switch (_resolution) {
        case RESOLUTION_2K:
 -              return libdcp::Size (2048, 1080);
 +              return dcp::Size (2048, 1080);
        case RESOLUTION_4K:
 -              return libdcp::Size (4096, 2160);
 +              return dcp::Size (4096, 2160);
        }
  
        assert (false);
 -      return libdcp::Size ();
 +      return dcp::Size ();
  }
  
  /** @return Size of the frame */
 -libdcp::Size
 +dcp::Size
  Film::frame_size () const
  {
        return fit_ratio_within (container()->ratio(), full_frame ());
  }
  
 -libdcp::KDM
 +dcp::EncryptedKDM
  Film::make_kdm (
 -      shared_ptr<libdcp::Certificate> target,
 +      shared_ptr<dcp::Certificate> target,
        boost::filesystem::path dcp_dir,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until
 +      dcp::LocalTime from,
 +      dcp::LocalTime until
        ) const
  {
        shared_ptr<const Signer> signer = make_signer ();
  
 -      libdcp::DCP dcp (dir (dcp_dir.string ()));
 +      dcp::DCP dcp (dir (dcp_dir.string ()));
        
        try {
                dcp.read ();
                throw KDMError (_("Could not read DCP to make KDM for"));
        }
        
 -      time_t now = time (0);
 -      struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 -      
        dcp.cpls().front()->set_mxf_keys (key ());
        
 -      return libdcp::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
 +      return dcp::DecryptedKDM (
 +              dcp.cpls().front(), from, until, "DCP-o-matic", dcp.cpls().front()->content_title_text(), dcp::LocalTime().as_string()
 +              ).encrypt (signer, target);
  }
  
 -list<libdcp::KDM>
 +list<dcp::EncryptedKDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
 -      boost::posix_time::ptime from,
 -      boost::posix_time::ptime until
 +      dcp::LocalTime from,
 +      dcp::LocalTime until
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::EncryptedKDM> kdms;
  
        for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) {
                kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until));
  uint64_t
  Film::required_disk_space () const
  {
 -      return uint64_t (j2k_bandwidth() / 8) * length() / TIME_HZ;
 +      return uint64_t (j2k_bandwidth() / 8) * length().seconds();
  }
  
  /** This method checks the disk that the Film is on and tries to decide whether or not
diff --combined src/lib/playlist.cc
index c46e65d8bfface99d07ef8d5dc00e2475c374f31,a2bec83bb3266948802c14f4c9fe4436e3b4e649..1e8a3319c54ed0569f0d555902d973eb61155d81
@@@ -64,7 -64,7 +64,7 @@@ Playlist::~Playlist (
  void
  Playlist::content_changed (weak_ptr<Content> content, int property, bool frequent)
  {
-       if (property == ContentProperty::LENGTH) {
+       if (property == ContentProperty::LENGTH || property == VideoContentProperty::VIDEO_FRAME_TYPE) {
                maybe_sequence_video ();
        }
        
@@@ -81,14 -81,21 +81,21 @@@ Playlist::maybe_sequence_video (
        _sequencing_video = true;
        
        ContentList cl = _content;
-       DCPTime next;
 -      Time next_left = 0;
 -      Time next_right = 0;
++      DCPTime next_left;
++      DCPTime next_right;
        for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) {
-               if (!dynamic_pointer_cast<VideoContent> (*i)) {
+               shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
+               if (!vc) {
                        continue;
                }
 -      
 +              
-               (*i)->set_position (next);
-               next = (*i)->end() + DCPTime::delta ();
+               if (vc->video_frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT) {
+                       vc->set_position (next_right);
 -                      next_right = vc->end() + 1;
++                      next_right = vc->end() + DCPTime::delta ();
+               } else {
+                       vc->set_position (next_left);
 -                      next_left = vc->end() + 1;
++                      next_left = vc->end() + DCPTime::delta ();
+               }
        }
  
        /* This won't change order, so it does not need a sort */
@@@ -254,12 -261,12 +261,12 @@@ Playlist::best_dcp_frame_rate () cons
        return best->dcp;
  }
  
 -Time
 +DCPTime
  Playlist::length () const
  {
 -      Time len = 0;
 +      DCPTime len;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              len = max (len, (*i)->end() + 1);
 +              len = max (len, (*i)->end() + DCPTime::delta ());
        }
  
        return len;
@@@ -279,10 -286,10 +286,10 @@@ Playlist::reconnect (
        }
  }
  
 -Time
 +DCPTime
  Playlist::video_end () const
  {
 -      Time end = 0;
 +      DCPTime end;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
                if (dynamic_pointer_cast<const VideoContent> (*i)) {
                        end = max (end, (*i)->end ());
        return end;
  }
  
 +FrameRateChange
 +Playlist::active_frame_rate_change (DCPTime t, int dcp_video_frame_rate) const
 +{
 +      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 +              shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
 +              if (!vc) {
 +                      break;
 +              }
 +
 +              if (vc->position() >= t && t < vc->end()) {
 +                      return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
 +              }
 +      }
 +
 +      return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
 +}
 +
  void
  Playlist::set_sequence_video (bool s)
  {
@@@ -331,7 -321,7 +338,7 @@@ Playlist::content () cons
  void
  Playlist::repeat (ContentList c, int n)
  {
 -      pair<Time, Time> range (TIME_MAX, 0);
 +      pair<DCPTime, DCPTime> range (DCPTime::max (), DCPTime ());
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                range.first = min (range.first, (*i)->position ());
                range.second = max (range.second, (*i)->position ());
                range.second = max (range.second, (*i)->end ());
        }
  
 -      Time pos = range.second;
 +      DCPTime pos = range.second;
        for (int i = 0; i < n; ++i) {
                for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                        shared_ptr<Content> copy = (*i)->clone ();
@@@ -372,7 -362,7 +379,7 @@@ Playlist::move_earlier (shared_ptr<Cont
                return;
        }
        
 -      Time const p = (*previous)->position ();
 +      DCPTime const p = (*previous)->position ();
        (*previous)->set_position (p + c->length_after_trim ());
        c->set_position (p);
        sort (_content.begin(), _content.end(), ContentSorter ());
@@@ -399,7 -389,7 +406,7 @@@ Playlist::move_later (shared_ptr<Conten
                return;
        }
  
 -      Time const p = (*next)->position ();
 +      DCPTime const p = (*next)->position ();
        (*next)->set_position (c->position ());
        c->set_position (p + c->length_after_trim ());
        sort (_content.begin(), _content.end(), ContentSorter ());
index 71b549d51a056b89e6bde4dac439053d8575d4a5,a8388ab77b24cb1441e1c19a0905e01ec775a193..2d7fa1c1cf9446e0a26cedf13db62bd911cc6df6
@@@ -1,5 -1,5 +1,5 @@@
  /*
--    Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
++    Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -33,6 -33,8 +33,6 @@@ using std::cout
  using boost::shared_ptr;
  using boost::lexical_cast;
  
 -int const SndfileContentProperty::VIDEO_FRAME_RATE = 600;
 -
  SndfileContent::SndfileContent (shared_ptr<const Film> f, boost::filesystem::path p)
        : Content (f, p)
        , AudioContent (f, p)
@@@ -49,7 -51,7 +49,7 @@@ SndfileContent::SndfileContent (shared_
        , _audio_mapping (node->node_child ("AudioMapping"), version)
  {
        _audio_channels = node->number_child<int> ("AudioChannels");
 -      _audio_length = node->number_child<AudioContent::Frame> ("AudioLength");
 +      _audio_length = ContentTime (node->number_child<int64_t> ("AudioLength"));
        _audio_frame_rate = node->number_child<int> ("AudioFrameRate");
  }
  
@@@ -81,7 -83,7 +81,7 @@@ SndfileContent::information () cons
                _("%1 channels, %2kHz, %3 samples"),
                audio_channels(),
                content_audio_frame_rate() / 1000.0,
 -              audio_length()
 +              audio_length().frames (content_audio_frame_rate ())
                );
        
        return s.str ();
@@@ -102,7 -104,10 +102,7 @@@ SndfileContent::examine (shared_ptr<Job
        job->set_progress_unknown ();
        Content::examine (job);
  
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -
 -      SndfileDecoder dec (film, shared_from_this());
 +      SndfileDecoder dec (shared_from_this());
  
        {
                boost::mutex::scoped_lock lm (_mutex);
@@@ -133,17 -138,24 +133,17 @@@ SndfileContent::as_xml (xmlpp::Node* no
        AudioContent::as_xml (node);
  
        node->add_child("AudioChannels")->add_child_text (lexical_cast<string> (audio_channels ()));
 -      node->add_child("AudioLength")->add_child_text (lexical_cast<string> (audio_length ()));
 +      node->add_child("AudioLength")->add_child_text (lexical_cast<string> (audio_length().get ()));
        node->add_child("AudioFrameRate")->add_child_text (lexical_cast<string> (content_audio_frame_rate ()));
        _audio_mapping.as_xml (node->add_child("AudioMapping"));
  }
  
 -Time
 +DCPTime
  SndfileContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -
 -      float const rate = _video_frame_rate.get_value_or (film->video_frame_rate ());
 -      OutputAudioFrame const len = divide_with_round (
 -              audio_length() * output_audio_frame_rate() * rate,
 -              content_audio_frame_rate() * film->video_frame_rate()
 -              );
 -      
 -      return film->audio_frames_to_time (len);
 +      return DCPTime (audio_length(), film->active_frame_rate_change (position ()));
  }
  
  int
@@@ -165,3 -177,18 +165,4 @@@ SndfileContent::set_audio_mapping (Audi
  
        signal_changed (AudioContentProperty::AUDIO_MAPPING);
  }
 -float
 -SndfileContent::video_frame_rate () const
 -{
 -      {
 -              boost::mutex::scoped_lock lm (_mutex);
 -              if (_video_frame_rate) {
 -                      return _video_frame_rate.get ();
 -              }
 -      }
 -
 -      shared_ptr<const Film> film = _film.lock ();
 -      assert (film);
 -      return film->video_frame_rate ();
 -}
diff --combined src/lib/types.h
index 8c89ecd17f9e1c5fcbfebcf975020310fe560030,c255bd0d8a43acd6c80a9a5a38f4040fa0e82dca..35c7a91f9785865e1dada8d2449fa725f4149ae4
@@@ -23,9 -23,7 +23,9 @@@
  #include <vector>
  #include <stdint.h>
  #include <boost/shared_ptr.hpp>
 -#include <libdcp/util.h>
 +#include <dcp/util.h>
 +#include "dcpomatic_time.h"
 +#include "position.h"
  
  class Content;
  class VideoContent;
@@@ -40,29 -38,31 +40,29 @@@ class AudioBuffers
   */
  #define SERVER_LINK_VERSION 1
  
 -typedef int64_t Time;
 -#define TIME_MAX INT64_MAX
 -#define TIME_HZ        ((Time) 96000)
 -typedef int64_t OutputAudioFrame;
 -typedef int   OutputVideoFrame;
  typedef std::vector<boost::shared_ptr<Content> > ContentList;
  typedef std::vector<boost::shared_ptr<VideoContent> > VideoContentList;
  typedef std::vector<boost::shared_ptr<AudioContent> > AudioContentList;
  typedef std::vector<boost::shared_ptr<SubtitleContent> > SubtitleContentList;
  typedef std::vector<boost::shared_ptr<FFmpegContent> > FFmpegContentList;
  
 -template<class T>
 +typedef int64_t VideoFrame;
 +typedef int64_t AudioFrame;
 +
 +/* XXX -> DCPAudio */
  struct TimedAudioBuffers
  {
        TimedAudioBuffers ()
                : time (0)
        {}
        
 -      TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, T t)
 +      TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, DCPTime t)
                : audio (a)
                , time (t)
        {}
        
        boost::shared_ptr<AudioBuffers> audio;
 -      T time;
 +      DCPTime time;
  };
  
  enum VideoFrameType
        VIDEO_FRAME_TYPE_2D,
        VIDEO_FRAME_TYPE_3D_LEFT_RIGHT,
        VIDEO_FRAME_TYPE_3D_TOP_BOTTOM,
-       VIDEO_FRAME_TYPE_3D_ALTERNATE
+       VIDEO_FRAME_TYPE_3D_ALTERNATE,
+       /** This content is all the left frames of some 3D */
+       VIDEO_FRAME_TYPE_3D_LEFT,
+       /** This content is all the right frames of some 3D */
+       VIDEO_FRAME_TYPE_3D_RIGHT
  };
  
  enum Eyes
@@@ -98,7 -102,7 +102,7 @@@ struct Cro
        /** Number of pixels to remove from the bottom */
        int bottom;
  
 -      libdcp::Size apply (libdcp::Size s, int minimum = 4) const {
 +      dcp::Size apply (dcp::Size s, int minimum = 4) const {
                s.width -= left + right;
                s.height -= top + bottom;
  
diff --combined src/lib/util.cc
index 1339be73d1044cf58192454984dc5a3de2a9f2c7,7a19790eb4ec261d1131ebd5e015565324f562a6..40e9d9c2eed730795a8f8476533ae16290c403c0
@@@ -1,5 -1,5 +1,5 @@@
  /*
--    Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
++    Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
      Copyright (C) 2000-2007 Paul Davis
  
      This program is free software; you can redistribute it and/or modify
  #include <glib.h>
  #include <openjpeg.h>
  #include <openssl/md5.h>
 +#include <pangomm/init.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
 -#include <libdcp/version.h>
 -#include <libdcp/util.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/signer.h>
 +#include <dcp/version.h>
 +#include <dcp/util.h>
 +#include <dcp/signer_chain.h>
 +#include <dcp/signer.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
@@@ -71,7 -70,6 +71,7 @@@
  #include "job.h"
  #include "cross.h"
  #include "video_content.h"
 +#include "rect.h"
  #ifdef DCPOMATIC_WINDOWS
  #include "stack.hpp"
  #endif
@@@ -103,7 -101,7 +103,7 @@@ using boost::shared_ptr
  using boost::thread;
  using boost::lexical_cast;
  using boost::optional;
 -using libdcp::Size;
 +using dcp::Size;
  
  static boost::thread::id ui_thread;
  static boost::filesystem::path backtrace_file;
@@@ -239,6 -237,24 +239,6 @@@ ffmpeg_version_to_string (int v
        return s.str ();
  }
  
 -/** Return a user-readable string summarising the versions of our dependencies */
 -string
 -dependency_version_summary ()
 -{
 -      stringstream s;
 -      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 -        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 -        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 -        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 -        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 -        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 -        << MagickVersion << N_(", ")
 -        << N_("libssh ") << ssh_version (0) << N_(", ")
 -        << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit;
 -
 -      return s.str ();
 -}
 -
  double
  seconds (struct timeval t)
  {
@@@ -325,8 -341,7 +325,8 @@@ dcpomatic_setup (
  
        set_terminate (terminate);
  
 -      libdcp::init ();
 +      Pango::init ();
 +      dcp::init ();
        
        Ratio::setup_ratios ();
        VideoContentScale::setup_scales ();
@@@ -475,6 -490,33 +475,6 @@@ md5_digest (vector<boost::filesystem::p
        return s.str ();
  }
  
 -static bool
 -about_equal (float a, float b)
 -{
 -      /* A film of F seconds at f FPS will be Ff frames;
 -         Consider some delta FPS d, so if we run the same
 -         film at (f + d) FPS it will last F(f + d) seconds.
 -
 -         Hence the difference in length over the length of the film will
 -         be F(f + d) - Ff frames
 -          = Ff + Fd - Ff frames
 -          = Fd frames
 -          = Fd/f seconds
 - 
 -         So if we accept a difference of 1 frame, ie 1/f seconds, we can
 -         say that
 -
 -         1/f = Fd/f
 -      ie 1 = Fd
 -      ie d = 1/F
 - 
 -         So for a 3hr film, ie F = 3 * 60 * 60 = 10800, the acceptable
 -         FPS error is 1/F ~= 0.0001 ~= 10-e4
 -      */
 -
 -      return (fabs (a - b) < 1e-4);
 -}
 -
  /** @param An arbitrary audio frame rate.
   *  @return The appropriate DCP-approved frame rate (48kHz or 96kHz).
   */
@@@ -734,6 -776,17 +734,6 @@@ ensure_ui_thread (
        assert (boost::this_thread::get_id() == ui_thread);
  }
  
 -/** @param v Content video frame.
 - *  @param audio_sample_rate Source audio sample rate.
 - *  @param frames_per_second Number of video frames per second.
 - *  @return Equivalent number of audio frames for `v'.
 - */
 -int64_t
 -video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second)
 -{
 -      return ((int64_t) v * audio_sample_rate / frames_per_second);
 -}
 -
  string
  audio_channel_name (int c)
  {
        return channels[c];
  }
  
 -FrameRateConversion::FrameRateConversion (float source, int dcp)
 -      : skip (false)
 -      , repeat (1)
 -      , change_speed (false)
 -{
 -      if (fabs (source / 2.0 - dcp) < fabs (source - dcp)) {
 -              /* The difference between source and DCP frame rate will be lower
 -                 (i.e. better) if we skip.
 -              */
 -              skip = true;
 -      } else if (fabs (source * 2 - dcp) < fabs (source - dcp)) {
 -              /* The difference between source and DCP frame rate would be better
 -                 if we repeated each frame once; it may be better still if we
 -                 repeated more than once.  Work out the required repeat.
 -              */
 -              repeat = round (dcp / source);
 -      }
 -
 -      change_speed = !about_equal (source * factor(), dcp);
 -
 -      if (!skip && repeat == 1 && !change_speed) {
 -              description = _("Content and DCP have the same rate.\n");
 -      } else {
 -              if (skip) {
 -                      description = _("DCP will use every other frame of the content.\n");
 -              } else if (repeat == 2) {
 -                      description = _("Each content frame will be doubled in the DCP.\n");
 -              } else if (repeat > 2) {
 -                      description = String::compose (_("Each content frame will be repeated %1 more times in the DCP.\n"), repeat - 1);
 -              }
 -
 -              if (change_speed) {
 -                      float const pc = dcp * 100 / (source * factor());
 -                      description += String::compose (_("DCP will run at %1%% of the content speed.\n"), pc);
 -              }
 -      }
 -}
 -
  LocaleGuard::LocaleGuard ()
        : _old (0)
  {
@@@ -803,7 -894,7 +803,7 @@@ tidy_for_filename (string f
        return t;
  }
  
 -shared_ptr<const libdcp::Signer>
 +shared_ptr<const dcp::Signer>
  make_signer ()
  {
        boost::filesystem::path const sd = Config::instance()->signer_chain_directory ();
                if (!boost::filesystem::exists (p)) {
                        boost::filesystem::remove_all (sd);
                        boost::filesystem::create_directories (sd);
 -                      libdcp::make_signer_chain (sd, openssl_path ());
 +                      dcp::make_signer_chain (sd, openssl_path ());
                        break;
                }
  
                ++i;
        }
        
 -      libdcp::CertificateChain chain;
 +      dcp::CertificateChain chain;
  
        {
                boost::filesystem::path p (sd);
                p /= "ca.self-signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        {
                boost::filesystem::path p (sd);
                p /= "intermediate.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        {
                boost::filesystem::path p (sd);
                p /= "leaf.signed.pem";
 -              chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p)));
 +              chain.add (shared_ptr<dcp::Certificate> (new dcp::Certificate (p)));
        }
  
        boost::filesystem::path signer_key (sd);
        signer_key /= "leaf.key";
  
 -      return shared_ptr<const libdcp::Signer> (new libdcp::Signer (chain, signer_key));
 +      return shared_ptr<const dcp::Signer> (new dcp::Signer (chain, signer_key));
  }
  
  map<string, string>
@@@ -902,14 -993,14 +902,14 @@@ split_get_request (string url
        return r;
  }
  
 -libdcp::Size
 -fit_ratio_within (float ratio, libdcp::Size full_frame)
 +dcp::Size
 +fit_ratio_within (float ratio, dcp::Size full_frame)
  {
        if (ratio < full_frame.ratio ()) {
 -              return libdcp::Size (rint (full_frame.height * ratio), full_frame.height);
 +              return dcp::Size (rint (full_frame.height * ratio), full_frame.height);
        }
        
 -      return libdcp::Size (full_frame.width, rint (full_frame.width / ratio));
 +      return dcp::Size (full_frame.width, rint (full_frame.width / ratio));
  }
  
  void *
@@@ -940,20 -1031,37 +940,55 @@@ divide_with_round (int64_t a, int64_t b
        }
  }
  
 +/** Return a user-readable string summarising the versions of our dependencies */
 +string
 +dependency_version_summary ()
 +{
 +      stringstream s;
 +      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 +        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 +        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 +        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 +        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 +        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 +        << MagickVersion << N_(", ")
 +        << N_("libssh ") << ssh_version (0) << N_(", ")
 +        << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit;
 +
 +      return s.str ();
 +}
++
+ ScopedTemporary::ScopedTemporary ()
+       : _open (0)
+ {
+       _file = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
+ }
+ ScopedTemporary::~ScopedTemporary ()
+ {
+       close ();       
+       boost::system::error_code ec;
+       boost::filesystem::remove (_file, ec);
+ }
+ char const *
+ ScopedTemporary::c_str () const
+ {
+       return _file.string().c_str ();
+ }
+ FILE*
+ ScopedTemporary::open (char const * params)
+ {
+       _open = fopen (c_str(), params);
+       return _open;
+ }
+ void
+ ScopedTemporary::close ()
+ {
+       if (_open) {
+               fclose (_open);
+               _open = 0;
+       }
+ }
diff --combined src/lib/util.h
index 2ae97814cf77726377a4f187c6e2e32d9c186435,0bbab83058644a1e833f60834672a52db90d933f..579b1c231bf144874af2aa6fcc78b239e419dbe3
@@@ -31,8 -31,7 +31,8 @@@
  #include <boost/asio.hpp>
  #include <boost/optional.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/util.h>
 +#include <dcp/util.h>
 +#include <dcp/signer.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavfilter/avfilter.h>
@@@ -77,10 -76,44 +77,10 @@@ extern bool valid_image_file (boost::fi
  extern boost::filesystem::path mo_path ();
  #endif
  extern std::string tidy_for_filename (std::string);
 -extern boost::shared_ptr<const libdcp::Signer> make_signer ();
 -extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size);
 +extern boost::shared_ptr<const dcp::Signer> make_signer ();
 +extern dcp::Size fit_ratio_within (float ratio, dcp::Size);
  extern std::string entities_to_text (std::string e);
  extern std::map<std::string, std::string> split_get_request (std::string url);
 -
 -struct FrameRateConversion
 -{
 -      FrameRateConversion (float, int);
 -
 -      /** @return factor by which to multiply a source frame rate
 -          to get the effective rate after any skip or repeat has happened.
 -      */
 -      float factor () const {
 -              if (skip) {
 -                      return 0.5;
 -              }
 -
 -              return repeat;
 -      }
 -
 -      /** true to skip every other frame */
 -      bool skip;
 -      /** number of times to use each frame (e.g. 1 is normal, 2 means repeat each frame once, and so on) */
 -      int repeat;
 -      /** true if this DCP will run its video faster or slower than the source
 -       *  without taking into account `repeat' nor `skip'.
 -       *  (e.g. change_speed will be true if
 -       *          source is 29.97fps, DCP is 30fps
 -       *          source is 14.50fps, DCP is 30fps
 -       *  but not if
 -       *          source is 15.00fps, DCP is 30fps
 -       *          source is 12.50fps, DCP is 25fps)
 -       */
 -      bool change_speed;
 -
 -      std::string description;
 -};
 -
  extern int dcp_audio_frame_rate (int);
  extern int stride_round_up (int, int const *, int);
  extern std::multimap<std::string, std::string> read_key_value (std::istream& s);
@@@ -131,6 -164,8 +131,6 @@@ private
        int _timeout;
  };
  
 -extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second);
 -
  class LocaleGuard
  {
  public:
@@@ -141,6 -176,24 +141,24 @@@ private
        char* _old;
  };
  
+ class ScopedTemporary
+ {
+ public:
+       ScopedTemporary ();
+       ~ScopedTemporary ();
+       boost::filesystem::path file () const {
+               return _file;
+       }
+       
+       char const * c_str () const;
+       FILE* open (char const *);
+       void close ();
+ private:
+       boost::filesystem::path _file;
+       FILE* _open;
+ };
  
  #endif
  
diff --combined src/lib/video_content.cc
index 5864342a265c0fc873c302490acb3191f8f251bd,b704b64477b1ceaef7208dafd5ae2e0b775fcd89..9edbc104a92d3daa77a0a19b46dd18d55317c14e
@@@ -19,7 -19,7 +19,7 @@@
  
  #include <iomanip>
  #include <libcxml/cxml.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include "video_content.h"
  #include "video_examiner.h"
  #include "compose.hpp"
@@@ -61,7 -61,7 +61,7 @@@ VideoContent::VideoContent (shared_ptr<
        setup_default_colour_conversion ();
  }
  
 -VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Frame len)
 +VideoContent::VideoContent (shared_ptr<const Film> f, DCPTime s, ContentTime len)
        : Content (f, s)
        , _video_length (len)
        , _video_frame_rate (0)
@@@ -84,7 -84,7 +84,7 @@@ VideoContent::VideoContent (shared_ptr<
  VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
        : Content (f, node)
  {
 -      _video_length = node->number_child<VideoContent::Frame> ("VideoLength");
 +      _video_length = ContentTime (node->number_child<int64_t> ("VideoLength"));
        _video_size.width = node->number_child<int> ("VideoWidth");
        _video_size.height = node->number_child<int> ("VideoHeight");
        _video_frame_rate = node->number_child<float> ("VideoFrameRate");
@@@ -155,7 -155,7 +155,7 @@@ voi
  VideoContent::as_xml (xmlpp::Node* node) const
  {
        boost::mutex::scoped_lock lm (_mutex);
 -      node->add_child("VideoLength")->add_child_text (lexical_cast<string> (_video_length));
 +      node->add_child("VideoLength")->add_child_text (lexical_cast<string> (_video_length.get ()));
        node->add_child("VideoWidth")->add_child_text (lexical_cast<string> (_video_size.width));
        node->add_child("VideoHeight")->add_child_text (lexical_cast<string> (_video_size.height));
        node->add_child("VideoFrameRate")->add_child_text (lexical_cast<string> (_video_frame_rate));
  void
  VideoContent::setup_default_colour_conversion ()
  {
 -      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
 +      _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6).conversion;
  }
  
  void
  VideoContent::take_from_video_examiner (shared_ptr<VideoExaminer> d)
  {
        /* These examiner calls could call other content methods which take a lock on the mutex */
 -      libdcp::Size const vs = d->video_size ();
 +      dcp::Size const vs = d->video_size ();
        float const vfr = d->video_frame_rate ();
        
        {
@@@ -319,25 -319,24 +319,27 @@@ VideoContent::technical_summary () cons
  {
        return String::compose (
                "video: length %1, size %2x%3, rate %4",
 -              video_length_after_3d_combine(), video_size().width, video_size().height, video_frame_rate()
 +              video_length_after_3d_combine().seconds(),
 +              video_size().width,
 +              video_size().height,
 +              video_frame_rate()
                );
  }
  
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_3d_split () const
  {
 -      libdcp::Size const s = video_size ();
 +      dcp::Size const s = video_size ();
        switch (video_frame_type ()) {
        case VIDEO_FRAME_TYPE_2D:
        case VIDEO_FRAME_TYPE_3D_ALTERNATE:
+       case VIDEO_FRAME_TYPE_3D_LEFT:
+       case VIDEO_FRAME_TYPE_3D_RIGHT:
                return s;
        case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT:
 -              return libdcp::Size (s.width / 2, s.height);
 +              return dcp::Size (s.width / 2, s.height);
        case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM:
 -              return libdcp::Size (s.width, s.height / 2);
 +              return dcp::Size (s.width, s.height / 2);
        }
  
        assert (false);
@@@ -355,21 -354,28 +357,21 @@@ VideoContent::set_colour_conversion (Co
  }
  
  /** @return Video size after 3D split and crop */
 -libdcp::Size
 +dcp::Size
  VideoContent::video_size_after_crop () const
  {
        return crop().apply (video_size_after_3d_split ());
  }
  
  /** @param t A time offset from the start of this piece of content.
 - *  @return Corresponding frame index.
 + *  @return Corresponding time with respect to the content.
   */
 -VideoContent::Frame
 -VideoContent::time_to_content_video_frames (Time t) const
 +ContentTime
 +VideoContent::dcp_time_to_content_time (DCPTime t) const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 -
 -      /* Here we are converting from time (in the DCP) to a frame number in the content.
 -         Hence we need to use the DCP's frame rate and the double/skip correction, not
 -         the source's rate.
 -      */
 -      return t * film->video_frame_rate() / (frc.factor() * TIME_HZ);
 +      return ContentTime (t, FrameRateChange (video_frame_rate(), film->video_frame_rate()));
  }
  
  VideoContentScale::VideoContentScale (Ratio const * r)
@@@ -446,14 -452,14 +448,14 @@@ VideoContentScale::name () cons
  /** @param display_container Size of the container that we are displaying this content in.
   *  @param film_container The size of the film's image.
   */
 -libdcp::Size
 -VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size display_container, libdcp::Size film_container) const
 +dcp::Size
 +VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size display_container, dcp::Size film_container) const
  {
        if (_ratio) {
                return fit_ratio_within (_ratio->ratio (), display_container);
        }
  
 -      libdcp::Size const ac = c->video_size_after_crop ();
 +      dcp::Size const ac = c->video_size_after_crop ();
  
        /* Force scale if the film_container is smaller than the content's image */
        if (_scale || film_container.width < ac.width || film_container.height < ac.height) {
        /* Scale the image so that it will be in the right place in film_container, even if display_container is a
           different size.
        */
 -      return libdcp::Size (
 +      return dcp::Size (
                c->video_size().width  * float(display_container.width)  / film_container.width,
                c->video_size().height * float(display_container.height) / film_container.height
                );
diff --combined src/lib/video_content.h
index eeb49cfa543a3ed4a746ae7c01093d7e65eb6f79,fbba9c09da21ea8b015ad441c92e446e4bb9d499..6b91997c9f4076c91ffdd9d536ee21106bc0e9d0
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -45,7 -45,7 +45,7 @@@ public
        VideoContentScale (bool);
        VideoContentScale (boost::shared_ptr<cxml::Node>);
  
 -      libdcp::Size size (boost::shared_ptr<const VideoContent>, libdcp::Size, libdcp::Size) const;
 +      dcp::Size size (boost::shared_ptr<const VideoContent>, dcp::Size, dcp::Size) const;
        std::string id () const;
        std::string name () const;
        void as_xml (xmlpp::Node *) const;
@@@ -81,7 -81,7 +81,7 @@@ public
        typedef int Frame;
  
        VideoContent (boost::shared_ptr<const Film>);
 -      VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame);
 +      VideoContent (boost::shared_ptr<const Film>, DCPTime, ContentTime);
        VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path);
        VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
        VideoContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >);
        virtual std::string information () const;
        virtual std::string identifier () const;
  
 -      VideoContent::Frame video_length () const {
 +      ContentTime video_length () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_length;
        }
  
 -      VideoContent::Frame video_length_after_3d_combine () const {
 +      ContentTime video_length_after_3d_combine () const {
                boost::mutex::scoped_lock lm (_mutex);
                if (_video_frame_type == VIDEO_FRAME_TYPE_3D_ALTERNATE) {
 -                      return _video_length / 2;
 +                      return ContentTime (_video_length.get() / 2);
                }
                
                return _video_length;
        }
  
 -      libdcp::Size video_size () const {
 +      dcp::Size video_size () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_size;
        }
                return _colour_conversion;
        }
  
 -      libdcp::Size video_size_after_3d_split () const;
 -      libdcp::Size video_size_after_crop () const;
 +      dcp::Size video_size_after_3d_split () const;
 +      dcp::Size video_size_after_crop () const;
  
 -      VideoContent::Frame time_to_content_video_frames (Time) const;
 +      ContentTime dcp_time_to_content_time (DCPTime) const;
  
  protected:
        void take_from_video_examiner (boost::shared_ptr<VideoExaminer>);
  
 -      VideoContent::Frame _video_length;
 +      ContentTime _video_length;
        float _video_frame_rate;
  
  private:
  
        void setup_default_colour_conversion ();
        
 -      libdcp::Size _video_size;
 +      dcp::Size _video_size;
        VideoFrameType _video_frame_type;
        Crop _crop;
        VideoContentScale _scale;
diff --combined src/lib/video_decoder.cc
index 34299bd3c1c87d781312e2e80853e3c260d8cb4f,2a33a8c3a260d78bc2697f80f217e78d635ff1ff..6a7a62b742bad5655beb6915db7de5ec0dff60b1
  
  #include "video_decoder.h"
  #include "image.h"
 +#include "content_video.h"
  
  #include "i18n.h"
  
  using std::cout;
 +using std::list;
  using boost::shared_ptr;
 +using boost::optional;
  
 -VideoDecoder::VideoDecoder (shared_ptr<const Film> f, shared_ptr<const VideoContent> c)
 -      : Decoder (f)
 -      , _video_content (c)
 -      , _video_position (0)
 +VideoDecoder::VideoDecoder (shared_ptr<const VideoContent> c)
 +      : _video_content (c)
  {
  
  }
  
 +optional<ContentVideo>
 +VideoDecoder::decoded_video (VideoFrame frame)
 +{
 +      for (list<ContentVideo>::const_iterator i = _decoded_video.begin(); i != _decoded_video.end(); ++i) {
 +              if (i->frame == frame) {
 +                      return *i;
 +              }
 +      }
 +
 +      return optional<ContentVideo> ();
 +}
 +
 +optional<ContentVideo>
 +VideoDecoder::get_video (VideoFrame frame, bool accurate)
 +{
 +      if (_decoded_video.empty() || (frame < _decoded_video.front().frame || frame > (_decoded_video.back().frame + 1))) {
 +              /* Either we have no decoded data, or what we do have is a long way from what we want: seek */
 +              seek (ContentTime::from_frames (frame, _video_content->video_frame_rate()), accurate);
 +      }
 +
 +      optional<ContentVideo> dec;
 +
 +      /* Now enough pass() calls will either:
 +       *  (a) give us what we want, or
 +       *  (b) hit the end of the decoder.
 +       */
 +      if (accurate) {
 +              /* We are being accurate, so we want the right frame */
 +              while (!decoded_video (frame) && !pass ()) {}
 +              dec = decoded_video (frame);
 +      } else {
 +              /* Any frame will do: use the first one that comes out of pass() */
 +              while (_decoded_video.empty() && !pass ()) {}
 +              if (!_decoded_video.empty ()) {
 +                      dec = _decoded_video.front ();
 +              }
 +      }
 +
 +      /* Clean up decoded_video */
 +      while (!_decoded_video.empty() && _decoded_video.front().frame < (frame - 1)) {
 +              _decoded_video.pop_front ();
 +      }
 +
 +      return dec;
 +}
 +
 +
 +/** Called by subclasses when they have a video frame ready */
  void
 -VideoDecoder::video (shared_ptr<const Image> image, bool same, VideoContent::Frame frame)
 +VideoDecoder::video (shared_ptr<const Image> image, VideoFrame frame)
  {
 +      /* Fill in gaps */
 +      /* XXX: 3D */
 +      while (!_decoded_video.empty () && (_decoded_video.back().frame + 1) < frame) {
 +              _decoded_video.push_back (
 +                      ContentVideo (
 +                              _decoded_video.back().image,
 +                              _decoded_video.back().eyes,
 +                              _decoded_video.back().frame + 1
 +                              )
 +                      );
 +      }
 +      
        switch (_video_content->video_frame_type ()) {
        case VIDEO_FRAME_TYPE_2D:
 -              Video (image, EYES_BOTH, same, frame);
 +              _decoded_video.push_back (ContentVideo (image, EYES_BOTH, frame));
                break;
        case VIDEO_FRAME_TYPE_3D_ALTERNATE:
 -              Video (image, (frame % 2) ? EYES_RIGHT : EYES_LEFT, same, frame / 2);
 +              _decoded_video.push_back (ContentVideo (image, (frame % 2) ? EYES_RIGHT : EYES_LEFT, frame));
                break;
        case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT:
        {
                int const half = image->size().width / 2;
 -              Video (image->crop (Crop (0, half, 0, 0), true), EYES_LEFT, same, frame);
 -              Video (image->crop (Crop (half, 0, 0, 0), true), EYES_RIGHT, same, frame);
 +              _decoded_video.push_back (ContentVideo (image->crop (Crop (0, half, 0, 0), true), EYES_LEFT, frame));
 +              _decoded_video.push_back (ContentVideo (image->crop (Crop (half, 0, 0, 0), true), EYES_RIGHT, frame));
                break;
        }
        case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM:
        {
                int const half = image->size().height / 2;
 -              Video (image->crop (Crop (0, 0, 0, half), true), EYES_LEFT, same, frame);
 -              Video (image->crop (Crop (0, 0, half, 0), true), EYES_RIGHT, same, frame);
 +              _decoded_video.push_back (ContentVideo (image->crop (Crop (0, 0, 0, half), true), EYES_LEFT, frame));
 +              _decoded_video.push_back (ContentVideo (image->crop (Crop (0, 0, half, 0), true), EYES_RIGHT, frame));
                break;
        }
 -              Video (image, EYES_LEFT, same, frame);
+       case VIDEO_FRAME_TYPE_3D_LEFT:
 -              Video (image, EYES_RIGHT, same, frame);
++              _decoded_video.push_back (ContentVideo (image, EYES_LEFT, frame));
+               break;
+       case VIDEO_FRAME_TYPE_3D_RIGHT:
++              _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, frame));
+               break;
 +      default:
 +              assert (false);
        }
 -      
 -      _video_position = frame + 1;
 +}
 +
 +void
 +VideoDecoder::seek (ContentTime, bool)
 +{
 +      _decoded_video.clear ();
  }
  
diff --combined src/lib/writer.cc
index 202ec9a0f082f134996cad3e795f3feb1000548f,6396851492be8fc2322b13b7d43387bfc81adf2e..8508a07163218a8e16b446640007e9431956fc18
  
  #include <fstream>
  #include <cerrno>
 -#include <libdcp/mono_picture_asset.h>
 -#include <libdcp/stereo_picture_asset.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/reel.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/cpl.h>
 +#include <dcp/mono_picture_mxf.h>
 +#include <dcp/stereo_picture_mxf.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_mxf_writer.h>
 +#include <dcp/reel.h>
 +#include <dcp/reel_mono_picture_asset.h>
 +#include <dcp/reel_stereo_picture_asset.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/dcp.h>
 +#include <dcp/cpl.h>
  #include "writer.h"
  #include "compose.hpp"
  #include "film.h"
@@@ -41,7 -37,6 +41,7 @@@
  #include "config.h"
  #include "job.h"
  #include "cross.h"
 +#include "audio_buffers.h"
  
  #include "i18n.h"
  
@@@ -56,7 -51,6 +56,7 @@@ using std::cout
  using std::stringstream;
  using boost::shared_ptr;
  using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
  
  int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4;
  
@@@ -71,6 -65,7 +71,6 @@@ Writer::Writer (shared_ptr<const Film> 
        , _last_written_eyes (EYES_RIGHT)
        , _full_written (0)
        , _fake_written (0)
 -      , _repeat_written (0)
        , _pushed_to_disk (0)
  {
        /* Remove any old DCP */
        */
  
        if (_film->three_d ()) {
 -              _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        } else {
 -              _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        }
  
 -      _picture_asset->set_edit_rate (_film->video_frame_rate ());
 -      _picture_asset->set_size (_film->frame_size ());
 -      _picture_asset->set_interop (_film->interop ());
 +      _picture_mxf->set_size (_film->frame_size ());
  
        if (_film->encrypted ()) {
 -              _picture_asset->set_key (_film->key ());
 +              _picture_mxf->set_key (_film->key ());
        }
        
 -      _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
 +      _picture_mxf_writer = _picture_mxf->start_write (
 +              _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(),
 +              _film->interop() ? dcp::INTEROP : dcp::SMPTE,
 +              _first_nonexistant_frame > 0
 +              );
  
-       _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
+       if (_film->audio_channels ()) {
 -              _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ()));
 -              _sound_asset->set_edit_rate (_film->video_frame_rate ());
 -              _sound_asset->set_channels (_film->audio_channels ());
 -              _sound_asset->set_sampling_rate (_film->audio_frame_rate ());
 -              _sound_asset->set_interop (_film->interop ());
++              _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
  
-       if (_film->encrypted ()) {
-               _sound_mxf->set_key (_film->key ());
-       }
+               if (_film->encrypted ()) {
 -                      _sound_asset->set_key (_film->key ());
++                      _sound_mxf->set_key (_film->key ());
+               }
 -              
 -              /* Write the sound asset into the film directory so that we leave the creation
 +      
-       /* Write the sound MXF into the film directory so that we leave the creation
-          of the DCP directory until the last minute.
-       */
-       _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
++              /* Write the sound MXF into the film directory so that we leave the creation
+                  of the DCP directory until the last minute.
+               */
 -              _sound_asset_writer = _sound_asset->start_write ();
++              _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
+       }
  
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
  
@@@ -167,7 -166,7 +169,7 @@@ Writer::fake_write (int frame, Eyes eye
        }
        
        FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r");
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        
        QueueItem qi;
  void
  Writer::write (shared_ptr<const AudioBuffers> audio)
  {
-       _sound_mxf_writer->write (audio->data(), audio->frames());
 -      if (_sound_asset) {
 -              _sound_asset_writer->write (audio->data(), audio->frames());
++      if (_sound_mxf_writer) {
++              _sound_mxf_writer->write (audio->data(), audio->frames());
+       }
  }
  
  /** This must be called from Writer::thread() with an appropriate lock held */
@@@ -264,7 -265,7 +268,7 @@@ tr
                                        qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false)));
                                }
  
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size());
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size());
                                qi.encoded->write_info (_film, qi.frame, qi.eyes, fin);
                                _last_written[qi.eyes] = qi.encoded;
                                ++_full_written;
                        }
                        case QueueItem::FAKE:
                                _film->log()->log (String::compose (N_("Writer FAKE-writes %1 to MXF"), qi.frame));
 -                              _picture_asset_writer->fake_write (qi.size);
 +                              _picture_mxf_writer->fake_write (qi.size);
                                _last_written[qi.eyes].reset ();
                                ++_fake_written;
                                break;
 -                      case QueueItem::REPEAT:
 -                      {
 -                              _film->log()->log (String::compose (N_("Writer REPEAT-writes %1 to MXF"), qi.frame));
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (
 -                                      _last_written[qi.eyes]->data(),
 -                                      _last_written[qi.eyes]->size()
 -                                      );
 -                              
 -                              _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin);
 -                              ++_repeat_written;
 -                              break;
 -                      }
                        }
                        lock.lock ();
  
                        _last_written_frame = qi.frame;
                        _last_written_eyes = qi.eyes;
                        
 -                      if (_film->length()) {
 -                              shared_ptr<Job> job = _job.lock ();
 -                              assert (job);
 -                              int total = _film->time_to_video_frames (_film->length ());
 -                              if (_film->three_d ()) {
 -                                      /* _full_written and so on are incremented for each eye, so we need to double the total
 -                                         frames to get the correct progress.
 -                                      */
 -                                      total *= 2;
 -                              }
 -                              job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
 +                      shared_ptr<Job> job = _job.lock ();
 +                      assert (job);
 +                      int64_t total = _film->length().frames (_film->video_frame_rate ());
 +                      if (_film->three_d ()) {
 +                              /* _full_written and so on are incremented for each eye, so we need to double the total
 +                                 frames to get the correct progress.
 +                              */
 +                              total *= 2;
 +                      }
 +                      if (total) {
 +                              job->set_progress (float (_full_written + _fake_written) / total);
                        }
                }
  
@@@ -367,9 -380,15 +371,11 @@@ Writer::finish (
        
        terminate_thread (true);
  
 -      _picture_asset_writer->finalize ();
 -      if (_sound_asset_writer) {
 -              _sound_asset_writer->finalize ();
 +      _picture_mxf_writer->finalize ();
-       _sound_mxf_writer->finalize ();
++      if (_sound_mxf_writer) {
++              _sound_mxf_writer->finalize ();
+       }
        
 -      int const frames = _last_written_frame + 1;
 -
 -      _picture_asset->set_duration (frames);
 -
        /* Hard-link the video MXF into the DCP */
        boost::filesystem::path video_from;
        video_from /= _film->internal_video_mxf_dir();
                _film->log()->log ("Hard-link failed; fell back to copying");
        }
  
 -      /* And update the asset */
 -
 -      _picture_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _picture_asset->set_file_name (_film->video_mxf_filename ());
 -
        /* Move the audio MXF into the DCP */
  
-       boost::filesystem::path audio_to;
-       audio_to /= _film->dir (_film->dcp_name ());
-       audio_to /= _film->audio_mxf_filename ();
-       
-       boost::filesystem::rename (_film->file (_film->audio_mxf_filename ()), audio_to, ec);
-       if (ec) {
-               throw FileError (
-                       String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ())
-                       );
 -      if (_sound_asset) {
++      if (_sound_mxf) {
+               boost::filesystem::path audio_to;
+               audio_to /= _film->dir (_film->dcp_name ());
+               audio_to /= _film->audio_mxf_filename ();
+               
+               boost::filesystem::rename (_film->file (_film->audio_mxf_filename ()), audio_to, ec);
+               if (ec) {
+                       throw FileError (
+                               String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ())
+                               );
+               }
 -              
 -              _sound_asset->set_directory (_film->dir (_film->dcp_name ()));
 -              _sound_asset->set_duration (frames);
        }
 -      
 -      libdcp::DCP dcp (_film->dir (_film->dcp_name()));
  
 -      shared_ptr<libdcp::CPL> cpl (
 -              new libdcp::CPL (
 -                      _film->dir (_film->dcp_name()),
 +      dcp::DCP dcp (_film->dir (_film->dcp_name()));
 +
 +      shared_ptr<dcp::CPL> cpl (
 +              new dcp::CPL (
                        _film->dcp_name(),
 -                      _film->dcp_content_type()->libdcp_kind (),
 -                      frames,
 -                      _film->video_frame_rate ()
 +                      _film->dcp_content_type()->libdcp_kind ()
                        )
                );
        
 -      dcp.add_cpl (cpl);
 +      dcp.add (cpl);
 +
 +      shared_ptr<dcp::Reel> reel (new dcp::Reel ());
  
 -      cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (
 -                                                       _picture_asset,
 -                                                       _sound_asset,
 -                                                       shared_ptr<libdcp::SubtitleAsset> ()
 -                                                       )
 -                             ));
 +      shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf);
 +      if (mono) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0)));
 +      }
 +
 +      shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf);
 +      if (stereo) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0)));
 +      }
 +
 +      reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0)));
 +      
 +      cpl->add (reel);
  
        shared_ptr<Job> job = _job.lock ();
        assert (job);
  
        job->sub (_("Computing image digest"));
 -      _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
-       job->sub (_("Computing audio digest"));
-       _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
 -      if (_sound_asset) {
++      if (_sound_mxf) {
+               job->sub (_("Computing audio digest"));
 -              _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
++              _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
+       }
  
 -      libdcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
 +      dcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
        meta.set_issue_date_now ();
 -      dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ());
 +      dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, _film->is_signed() ? make_signer () : shared_ptr<const dcp::Signer> ());
  
        _film->log()->log (
 -              String::compose (N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk)
 +              String::compose (N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk)
                );
  }
  
 -/** Tell the writer that frame `f' should be a repeat of the frame before it */
 -void
 -Writer::repeat (int f, Eyes e)
 -{
 -      boost::mutex::scoped_lock lock (_mutex);
 -
 -      while (_queued_full_in_memory > _maximum_frames_in_memory) {
 -              _full_condition.wait (lock);
 -      }
 -      
 -      QueueItem qi;
 -      qi.type = QueueItem::REPEAT;
 -      qi.frame = f;
 -      if (_film->three_d() && e == EYES_BOTH) {
 -              qi.eyes = EYES_LEFT;
 -              _queue.push_back (qi);
 -              qi.eyes = EYES_RIGHT;
 -              _queue.push_back (qi);
 -      } else {
 -              qi.eyes = e;
 -              _queue.push_back (qi);
 -      }
 -
 -      _empty_condition.notify_all ();
 -}
 -
  bool
  Writer::check_existing_picture_mxf_frame (FILE* mxf, int f, Eyes eyes)
  {
                return false;
        }
        
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        if (info.size == 0) {
                _film->log()->log (String::compose ("Existing frame %1 has no info file", f));
diff --combined src/lib/writer.h
index af535c004ca05f2260bb0e35f2f7fe4a4cbeb031,7af79a417ed193780931e805eda92d5ca1d58515..e817b1ca205beadc9d7ebbfe256ba2d04183a963
  
  */
  
 +/** @file  src/lib/writer.h
 + *  @brief Writer class.
 + */
 +
  #include <list>
  #include <boost/shared_ptr.hpp>
  #include <boost/thread.hpp>
@@@ -33,15 -29,15 +33,15 @@@ class EncodedData
  class AudioBuffers;
  class Job;
  
 -namespace libdcp {
 -      class MonoPictureAsset;
 -      class MonoPictureAssetWriter;
 -      class StereoPictureAsset;
 -      class StereoPictureAssetWriter;
 -      class PictureAsset;
 -      class PictureAssetWriter;
 -      class SoundAsset;
 -      class SoundAssetWriter;
 +namespace dcp {
 +      class MonoPictureMXF;
 +      class MonoPictureMXFWriter;
 +      class StereoPictureMXF;
 +      class StereoPictureMXFWriter;
 +      class PictureMXF;
 +      class PictureMXFWriter;
 +      class SoundMXF;
 +      class SoundMXFWriter;
  }
  
  struct QueueItem
@@@ -55,8 -51,8 +55,6 @@@ public
                    state but we use the data that is already on disk.
                */
                FAKE,
--              /** this is a repeat of the last frame to be written */
--              REPEAT
        } type;
  
        /** encoded data for FULL */
  bool operator< (QueueItem const & a, QueueItem const & b);
  bool operator== (QueueItem const & a, QueueItem const & b);
  
 +/** @class Writer
 + *  @brief Class to manage writing JPEG2000 and audio data to MXFs on disk.
 + *
 + *  This class creates sound and picture MXFs, then takes EncodedData
 + *  or AudioBuffers objects (containing image or sound data respectively)
 + *  and writes them to the MXFs.
 + *
 + *  ::write() for EncodedData can be called out of order, and the Writer
 + *  will sort it out.  write() for AudioBuffers must be called in order.
 + */
 +
  class Writer : public ExceptionStore, public boost::noncopyable
  {
  public:
@@@ -138,13 -123,15 +136,13 @@@ private
        int _full_written;
        /** number of FAKE written frames */
        int _fake_written;
 -      /** number of REPEAT written frames */
 -      int _repeat_written;
        /** number of frames pushed to disk and then recovered
            due to the limit of frames to be held in memory.
        */
        int _pushed_to_disk;
        
 -      boost::shared_ptr<libdcp::PictureAsset> _picture_asset;
 -      boost::shared_ptr<libdcp::PictureAssetWriter> _picture_asset_writer;
 -      boost::shared_ptr<libdcp::SoundAsset> _sound_asset;
 -      boost::shared_ptr<libdcp::SoundAssetWriter> _sound_asset_writer;
 +      boost::shared_ptr<dcp::PictureMXF> _picture_mxf;
 +      boost::shared_ptr<dcp::PictureMXFWriter> _picture_mxf_writer;
 +      boost::shared_ptr<dcp::SoundMXF> _sound_mxf;
 +      boost::shared_ptr<dcp::SoundMXFWriter> _sound_mxf_writer;
  };
diff --combined src/lib/wscript
index 00ed171a37a32e5938f175a6925a708982b3ccef,a50216f6d500755cf338c00fd8308ca075215633..433f50b3fdca4565902baf3b4b7e7246a59514d4
@@@ -13,13 -13,11 +13,13 @@@ sources = ""
            config.cc
            content.cc
            content_factory.cc
 +          content_subtitle.cc
            cross.cc
            dci_metadata.cc
            dcp_content_type.cc
 +          dcp_video.cc
            dcp_video_frame.cc
 -          decoder.cc
 +          dcpomatic_time.cc
            dolby_cp750.cc
            encoder.cc
            examine_content_job.cc
@@@ -32,7 -30,7 +32,8 @@@
            ffmpeg_examiner.cc
            film.cc
            filter.cc
 +          frame_rate_change.cc
+           internet.cc
            image.cc
            image_content.cc
            image_decoder.cc
@@@ -45,7 -43,6 +46,7 @@@
            player.cc
            playlist.cc
            ratio.cc
 +          render_subtitles.cc
            resampler.cc
            scp_dcp_job.cc
            scaler.cc
@@@ -55,9 -52,6 +56,9 @@@
            sndfile_content.cc
            sndfile_decoder.cc
            sound_processor.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -84,7 -78,7 +85,7 @@@ def build(bld)
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
                   SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 -                 CURL ZIP QUICKMAIL
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM
                   """
  
      if bld.env.TARGET_OSX:
diff --combined src/wx/config_dialog.cc
index a0aedaf47db04cebced346945cf152266672e2b9,e7133a534a7dc5cc808066ebce4dbaba5eb5dcf0..799067920d0fbed4aad634dc484eb4c53ce1645b
@@@ -28,7 -28,7 +28,7 @@@
  #include <wx/preferences.h>
  #include <wx/filepicker.h>
  #include <wx/spinctrl.h>
 -#include <libdcp/colour_matrix.h>
 +#include <dcp/colour_matrix.h>
  #include "lib/config.h"
  #include "lib/ratio.h"
  #include "lib/scaler.h"
@@@ -86,12 -86,13 +86,13 @@@ public
                _set_language = new wxCheckBox (panel, wxID_ANY, _("Set language"));
                table->Add (_set_language, 1);
                _language = new wxChoice (panel, wxID_ANY);
+               _language->Append (wxT ("Deutsch"));
                _language->Append (wxT ("English"));
+               _language->Append (wxT ("Español"));
                _language->Append (wxT ("Français"));
                _language->Append (wxT ("Italiano"));
-               _language->Append (wxT ("Español"));
+               _language->Append (wxT ("Nederlands"));
                _language->Append (wxT ("Svenska"));
-               _language->Append (wxT ("Deutsch"));
                table->Add (_language);
                
                wxStaticText* restart = add_label_to_sizer (table, panel, _("(restart DCP-o-matic to see language changes)"), false);
                add_label_to_sizer (table, panel, _("Threads to use for encoding on this host"), true);
                _num_local_encoding_threads = new wxSpinCtrl (panel);
                table->Add (_num_local_encoding_threads, 1);
+               add_label_to_sizer (table, panel, _("Maximum JPEG2000 bandwidth"), true);
+               _maximum_j2k_bandwidth = new wxSpinCtrl (panel);
+               table->Add (_maximum_j2k_bandwidth, 1);
                
                add_label_to_sizer (table, panel, _("Outgoing mail server"), true);
                _mail_server = new wxTextCtrl (panel, wxID_ANY);
                _set_language->SetValue (config->language ());
                
                if (config->language().get_value_or ("") == "fr") {
-                       _language->SetSelection (1);
+                       _language->SetSelection (3);
                } else if (config->language().get_value_or ("") == "it") {
-               _language->SetSelection (2);
+                       _language->SetSelection (4);
                } else if (config->language().get_value_or ("") == "es") {
-                       _language->SetSelection (3);
+                       _language->SetSelection (2);
                } else if (config->language().get_value_or ("") == "sv") {
-                       _language->SetSelection (4);
+                       _language->SetSelection (6);
                } else if (config->language().get_value_or ("") == "de") {
+                       _language->SetSelection (0);
+               } else if (config->language().get_value_or ("") == "nl") {
                        _language->SetSelection (5);
                } else {
-                       _language->SetSelection (0);
+                       _language->SetSelection (1);
                }
                
                setup_language_sensitivity ();
                _num_local_encoding_threads->SetRange (1, 128);
                _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ());
                _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::num_local_encoding_threads_changed, this));
+               _maximum_j2k_bandwidth->SetRange (1, 500);
+               _maximum_j2k_bandwidth->SetValue (config->maximum_j2k_bandwidth() / 1000000);
+               _maximum_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&GeneralPage::maximum_j2k_bandwidth_changed, this));
                
                _mail_server->SetValue (std_to_wx (config->mail_server ()));
                _mail_server->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&GeneralPage::mail_server_changed, this));
@@@ -196,22 -207,25 +207,25 @@@ private
        {
                switch (_language->GetSelection ()) {
                case 0:
-                       Config::instance()->set_language ("en");
+                       Config::instance()->set_language ("de");
                        break;
                case 1:
-                       Config::instance()->set_language ("fr");
+                       Config::instance()->set_language ("en");
                        break;
                case 2:
-                       Config::instance()->set_language ("it");
+                       Config::instance()->set_language ("es");
                        break;
                case 3:
-                       Config::instance()->set_language ("es");
+                       Config::instance()->set_language ("fr");
                        break;
                case 4:
-                       Config::instance()->set_language ("sv");
+                       Config::instance()->set_language ("it");
                        break;
                case 5:
-                       Config::instance()->set_language ("de");
+                       Config::instance()->set_language ("nl");
+                       break;
+               case 6:
+                       Config::instance()->set_language ("sv");
                        break;
                }
        }
        {
                Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ());
        }
+       void maximum_j2k_bandwidth_changed ()
+       {
+               Config::instance()->set_maximum_j2k_bandwidth (_maximum_j2k_bandwidth->GetValue() * 1000000);
+       }
        
        wxCheckBox* _set_language;
        wxChoice* _language;
        wxSpinCtrl* _num_local_encoding_threads;
+       wxSpinCtrl* _maximum_j2k_bandwidth;
        wxTextCtrl* _mail_server;
        wxTextCtrl* _mail_user;
        wxTextCtrl* _mail_password;
@@@ -440,14 -460,14 +460,14 @@@ private
  
        void issuer_changed ()
        {
 -              libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
 +              dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
                m.issuer = wx_to_std (_issuer->GetValue ());
                Config::instance()->set_dcp_metadata (m);
        }
        
        void creator_changed ()
        {
 -              libdcp::XMLMetadata m = Config::instance()->dcp_metadata ();
 +              dcp::XMLMetadata m = Config::instance()->dcp_metadata ();
                m.creator = wx_to_std (_creator->GetValue ());
                Config::instance()->set_dcp_metadata (m);
        }
@@@ -723,7 -743,10 +743,10 @@@ create_config_dialog (
        wxSize ps = wxSize (480, -1);
        int const border = 16;
  #else
-       wxSize ps = wxDefaultSize;
+       /* We seem to need to specify height here, otherwise the general panel
+          is too short (at least on Linux).
+        */
+       wxSize ps = wxSize (-1, 400);
        int const border = 8;
  #endif
        
diff --combined src/wx/film_editor.cc
index 78a5c440cfe24cbc8dedf2aab491ef7f5eb8edb1,31b9b8368b0097e006c974c20aef437fa4db1111..7fa34c516c60875e0208b0f907adf6d332078b94
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -92,6 -92,8 +92,8 @@@ FilmEditor::FilmEditor (shared_ptr<Film
        JobManager::instance()->ActiveJobsChanged.connect (
                bind (&FilmEditor::active_jobs_changed, this, _1)
                );
+       Config::instance()->Changed.connect (boost::bind (&FilmEditor::config_changed, this));
        
        SetSizerAndFit (s);
  }
@@@ -213,7 -215,7 +215,7 @@@ FilmEditor::make_dcp_panel (
        }
  
        _audio_channels->SetRange (0, MAX_AUDIO_CHANNELS);
-       _j2k_bandwidth->SetRange (1, 250);
+       _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
  
        _resolution->Append (_("2K"));
        _resolution->Append (_("4K"));
@@@ -268,19 -270,25 +270,25 @@@ FilmEditor::make_content_panel (
                _content->InsertColumn (0, wxT(""));
                _content->SetColumnWidth (0, 512);
  
+ #ifdef DCPOMATIC_OSX
+               int const pad = 2;
+ #else
+               int const pad = 0;
+ #endif                
                wxBoxSizer* b = new wxBoxSizer (wxVERTICAL);
                _content_add_file = new wxButton (_content_panel, wxID_ANY, _("Add file(s)..."));
-               b->Add (_content_add_file, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               b->Add (_content_add_file, 1, wxEXPAND | wxALL, pad);
                _content_add_folder = new wxButton (_content_panel, wxID_ANY, _("Add folder..."));
-               b->Add (_content_add_folder, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               b->Add (_content_add_folder, 1, wxEXPAND | wxALL, pad);
                _content_remove = new wxButton (_content_panel, wxID_ANY, _("Remove"));
-               b->Add (_content_remove, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               b->Add (_content_remove, 1, wxEXPAND | wxALL, pad);
                _content_earlier = new wxButton (_content_panel, wxID_ANY, _("Up"));
-               b->Add (_content_earlier, 1, wxEXPAND);
+               b->Add (_content_earlier, 1, wxEXPAND | wxALL, pad);
                _content_later = new wxButton (_content_panel, wxID_ANY, _("Down"));
-               b->Add (_content_later, 1, wxEXPAND);
+               b->Add (_content_later, 1, wxEXPAND | wxALL, pad);
                _content_timeline = new wxButton (_content_panel, wxID_ANY, _("Timeline..."));
-               b->Add (_content_timeline, 1, wxEXPAND | wxLEFT | wxRIGHT);
+               b->Add (_content_timeline, 1, wxEXPAND | wxALL, pad);
  
                s->Add (b, 0, wxALL, 4);
  
@@@ -841,7 -849,7 +849,7 @@@ FilmEditor::setup_content_sensitivity (
  
        _video_panel->Enable    (video_selection.size() > 0 && _generally_sensitive);
        _audio_panel->Enable    (audio_selection.size() > 0 && _generally_sensitive);
 -      _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<FFmpegContent> (selection.front()) && _generally_sensitive);
 +      _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<SubtitleContent> (selection.front()) && _generally_sensitive);
        _timing_panel->Enable   (selection.size() == 1 && _generally_sensitive);
  }
  
@@@ -998,3 -1006,9 +1006,9 @@@ FilmEditor::content_later_clicked (
                content_selection_changed ();
        }
  }
+ void
+ FilmEditor::config_changed ()
+ {
+       _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000);
+ }
index 8c976f53ae7df64c34df2d13dd55e132ae5ba250,28247bc33eb3c62b9ef85a7dea83785be7b4fa54..801996efa7bcb611111285a847c41848591f69b7
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -33,47 -33,26 +33,25 @@@ using boost::shared_ptr
  using boost::lexical_cast;
  
  PropertiesDialog::PropertiesDialog (wxWindow* parent, shared_ptr<Film> film)
-       : wxDialog (parent, wxID_ANY, _("Film Properties"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
+       : TableDialog (parent, _("Film Properties"), 2, false)
        , _film (film)
  {
-       _table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP);
+       add (_("Frames"), true);
+       _frames = add (new wxStaticText (this, wxID_ANY, wxT ("")));
  
-       add_label_to_sizer (_table, this, _("Frames"), true);
-       _frames = new wxStaticText (this, wxID_ANY, wxT (""));
-       _table->Add (_frames, 1, wxALIGN_CENTER_VERTICAL);
+       add (_("Disk space required"), true);
+       _disk = add (new wxStaticText (this, wxID_ANY, wxT ("")));
  
-       add_label_to_sizer (_table, this, _("Disk space required"), true);
-       _disk = new wxStaticText (this, wxID_ANY, wxT (""));
-       _table->Add (_disk, 1, wxALIGN_CENTER_VERTICAL);
-       add_label_to_sizer (_table, this, _("Frames already encoded"), true);
-       _encoded = new ThreadedStaticText (this, _("counting..."), boost::bind (&PropertiesDialog::frames_already_encoded, this));
+       add (_("Frames already encoded"), true);
+       _encoded = add (new ThreadedStaticText (this, _("counting..."), boost::bind (&PropertiesDialog::frames_already_encoded, this)));
        _encoded->Finished.connect (boost::bind (&PropertiesDialog::layout, this));
-       _table->Add (_encoded, 1, wxALIGN_CENTER_VERTICAL);
 -      
 -      _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->time_to_video_frames (_film->length()))));
 +      _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->length().frames (_film->video_frame_rate ()))));
        double const disk = double (_film->required_disk_space()) / 1073741824.0f;
        stringstream s;
        s << fixed << setprecision (1) << disk << wx_to_std (_("Gb"));
        _disk->SetLabel (std_to_wx (s.str ()));
  
-       wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
-       overall_sizer->Add (_table, 0, wxALL, DCPOMATIC_DIALOG_BORDER);
-       
-       wxSizer* buttons = CreateSeparatedButtonSizer (wxOK);
-       if (buttons) {
-               overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
-       }
-       SetSizer (overall_sizer);
-       overall_sizer->SetSizeHints (this);
- }
- void
- PropertiesDialog::layout ()
- {
-       _table->Layout ();
-       Fit ();
+       layout ();
  }
  
  string
@@@ -85,11 -64,10 +63,11 @@@ PropertiesDialog::frames_already_encode
        } catch (boost::thread_interrupted &) {
                return "";
        }
 -      
 -      if (_film->length()) {
 +
 +      uint64_t const frames = _film->length().frames (_film->video_frame_rate ());
 +      if (frames) {
                /* XXX: encoded_frames() should check which frames have been encoded */
 -              u << " (" << (_film->encoded_frames() * 100 / _film->time_to_video_frames (_film->length())) << "%)";
 +              u << " (" << (_film->encoded_frames() * 100 / frames) << "%)";
        }
        return u.str ();
  }
diff --combined src/wx/screen_dialog.cc
index 89249645a5153009c2b0e26530561a4ce7e837d8,b702ae0ad8188be5892108c5b8ebef9afee3157e..0d46a46ec215dfc5f7de8cdaf89821d955bf3fe2
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  #include <wx/filepicker.h>
  #include <wx/validate.h>
 -#include <libdcp/exceptions.h>
 +#include <dcp/exceptions.h>
  #include "lib/compose.hpp"
+ #include "lib/util.h"
  #include "screen_dialog.h"
  #include "wx_util.h"
+ #include "doremi_certificate_dialog.h"
+ #include "dolby_certificate_dialog.h"
  
  using std::string;
  using std::cout;
  using boost::shared_ptr;
  
 -ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, shared_ptr<libdcp::Certificate> certificate)
 +ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, shared_ptr<dcp::Certificate> certificate)
-       : wxDialog (parent, wxID_ANY, std_to_wx (title))
+       : TableDialog (parent, std_to_wx (title), 2, true)
        , _certificate (certificate)
  {
-       wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6);
-       table->AddGrowableCol (1, 1);
+       add ("Name", true);
+       _name = add (new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (320, -1)));
  
-       add_label_to_sizer (table, this, "Name", true);
-       _name = new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (320, -1));
-       table->Add (_name, 1, wxEXPAND);
+       add ("Server manufacturer", true);
+       _manufacturer = add (new wxChoice (this, wxID_ANY));
  
-       add_label_to_sizer (table, this, "Certificate", true);
-       _certificate_load = new wxButton (this, wxID_ANY, wxT ("Load from file..."));
-       table->Add (_certificate_load, 1, wxEXPAND);
+       add (_("Certificate"), true);
+       wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
+       _load_certificate = new wxButton (this, wxID_ANY, _("Load from file..."));
+       _download_certificate = new wxButton (this, wxID_ANY, _("Download"));
+       s->Add (_load_certificate, 1, wxEXPAND);
+       s->Add (_download_certificate, 1, wxEXPAND);
+       add (s);
  
-       table->AddSpacer (0);
+       add_spacer ();
        _certificate_text = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, wxSize (320, 256), wxTE_MULTILINE | wxTE_READONLY);
        if (certificate) {
                _certificate_text->SetValue (certificate->certificate ());
        wxFont font = wxSystemSettings::GetFont (wxSYS_ANSI_FIXED_FONT);
        font.SetPointSize (font.GetPointSize() / 2);
        _certificate_text->SetFont (font);
-       table->Add (_certificate_text, 1, wxEXPAND);
-       wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL);
-       overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6);
-       
-       wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL);
-       if (buttons) {
-               overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
-       }
+       add (_certificate_text);
  
-       SetSizer (overall_sizer);
-       overall_sizer->Layout ();
-       overall_sizer->SetSizeHints (this);
+       _manufacturer->Append (_("Unknown"));
+       _manufacturer->Append (_("Doremi"));
+       _manufacturer->Append (_("Dolby"));
+       _manufacturer->Append (_("Other"));
+       _manufacturer->SetSelection (0);
  
-       _certificate_load->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ScreenDialog::load_certificate, this));
+       _load_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ScreenDialog::select_certificate, this));
+       _download_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ScreenDialog::download_certificate, this));
+       _manufacturer->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ScreenDialog::setup_sensitivity, this));
  
        setup_sensitivity ();
+       layout ();
  }
  
  string
@@@ -76,34 -79,59 +79,59 @@@ ScreenDialog::name () cons
        return wx_to_std (_name->GetValue());
  }
  
 -shared_ptr<libdcp::Certificate>
 +shared_ptr<dcp::Certificate>
  ScreenDialog::certificate () const
  {
        return _certificate;
  }
  
  void
- ScreenDialog::load_certificate ()
+ ScreenDialog::load_certificate (boost::filesystem::path file)
  {
-       wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
+       try {
 -              _certificate.reset (new libdcp::Certificate (file));
++              _certificate.reset (new dcp::Certificate (file));
+               _certificate_text->SetValue (_certificate->certificate ());
 -      } catch (libdcp::MiscError& e) {
++      } catch (dcp::MiscError& e) {
+               error_dialog (this, String::compose ("Could not read certificate file (%1)", e.what()));
+       }
+ }
  
+ void
+ ScreenDialog::select_certificate ()
+ {
+       wxFileDialog* d = new wxFileDialog (this, _("Select Certificate File"));
        if (d->ShowModal () == wxID_OK) {
-               try {
-                       _certificate.reset (new dcp::Certificate (boost::filesystem::path (wx_to_std (d->GetPath ()))));
-                       _certificate_text->SetValue (_certificate->certificate ());
-               } catch (dcp::MiscError& e) {
-                       error_dialog (this, String::compose ("Could not read certificate file (%1)", e.what()));
-               }
+               load_certificate (boost::filesystem::path (wx_to_std (d->GetPath ())));
        }
-       
        d->Destroy ();
  
        setup_sensitivity ();
  }
  
+ void
+ ScreenDialog::download_certificate ()
+ {
+       if (_manufacturer->GetStringSelection() == _("Doremi")) {
+               DownloadCertificateDialog* d = new DoremiCertificateDialog (this, boost::bind (&ScreenDialog::load_certificate, this, _1));
+               d->ShowModal ();
+               d->Destroy ();
+       } else if (_manufacturer->GetStringSelection() == _("Dolby")) {
+               DownloadCertificateDialog* d = new DolbyCertificateDialog (this, boost::bind (&ScreenDialog::load_certificate, this, _1));
+               d->ShowModal ();
+               d->Destroy ();
+       }
+       setup_sensitivity ();
+ }
  void
  ScreenDialog::setup_sensitivity ()
  {
        wxButton* ok = dynamic_cast<wxButton*> (FindWindowById (wxID_OK, this));
 -      ok->Enable (_certificate);
 +      ok->Enable (_certificate.get ());
+       _download_certificate->Enable (
+               _manufacturer->GetStringSelection() == _("Doremi") ||
+               _manufacturer->GetStringSelection() == _("Dolby")
+               );
  }
diff --combined src/wx/screen_dialog.h
index 0cd7d3c4963f77da9ab7eb0f0bcb7b1cc073d1f2,3601a8f6c133e57a556ce22db289fa5bd732629a..5c6d964b8297681405d501543c14843f463d5253
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
  
  #include <wx/wx.h>
  #include <boost/shared_ptr.hpp>
 -#include <libdcp/certificates.h>
 +#include <dcp/certificates.h>
+ #include "table_dialog.h"
  
- class ScreenDialog : public wxDialog
+ class Progress;
+ class ScreenDialog : public TableDialog
  {
  public:
 -      ScreenDialog (wxWindow *, std::string, std::string name = "", boost::shared_ptr<libdcp::Certificate> c = boost::shared_ptr<libdcp::Certificate> ());
 +      ScreenDialog (wxWindow *, std::string, std::string name = "", boost::shared_ptr<dcp::Certificate> c = boost::shared_ptr<dcp::Certificate> ());
  
        std::string name () const;
 -      boost::shared_ptr<libdcp::Certificate> certificate () const;
 +      boost::shared_ptr<dcp::Certificate> certificate () const;
        
  private:
-       void load_certificate ();
+       void select_certificate ();
+       void load_certificate (boost::filesystem::path);
+       void download_certificate ();
        void setup_sensitivity ();
        
        wxTextCtrl* _name;
-       wxButton* _certificate_load;
+       wxChoice* _manufacturer;
+       wxButton* _load_certificate;
+       wxButton* _download_certificate;
        wxTextCtrl* _certificate_text;
  
 -      boost::shared_ptr<libdcp::Certificate> _certificate;
 +      boost::shared_ptr<dcp::Certificate> _certificate;
  };
diff --combined src/wx/timecode.cc
index 634a15625c7d1b7ed79f52255f33758f15b4a721,ee5b5604bef4189e95329a360525be0e0bfbaf7b..1ab4b590b2b130b4f7d0c80cd21c766c6706ff41
@@@ -83,12 -83,12 +83,12 @@@ Timecode::Timecode (wxWindow* parent
  }
  
  void
 -Timecode::set (Time t, int fps)
 +Timecode::set (DCPTime t, int fps)
  {
        /* Do this calculation with frames so that we can round
           to a frame boundary at the start rather than the end.
        */
 -      int64_t f = divide_with_round (t * fps, TIME_HZ);
 +      int64_t f = rint (t.seconds() * fps);
        
        int const h = f / (3600 * fps);
        f -= h * 3600 * fps;
        checked_set (_seconds, lexical_cast<string> (s));
        checked_set (_frames, lexical_cast<string> (f));
  
-       _fixed->SetLabel (wxString::Format ("%02d:%02d:%02d.%02ld", h, m, s, f));
+       _fixed->SetLabel (wxString::Format ("%02d:%02d:%02d.%02" wxLongLongFmtSpec "d", h, m, s, f));
  }
  
 -Time
 +DCPTime
  Timecode::get (int fps) const
  {
 -      Time t = 0;
 +      DCPTime t;
        string const h = wx_to_std (_hours->GetValue ());
 -      t += lexical_cast<int> (h.empty() ? "0" : h) * 3600 * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (h.empty() ? "0" : h) * 3600);
        string const m = wx_to_std (_minutes->GetValue());
 -      t += lexical_cast<int> (m.empty() ? "0" : m) * 60 * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (m.empty() ? "0" : m) * 60);
        string const s = wx_to_std (_seconds->GetValue());
 -      t += lexical_cast<int> (s.empty() ? "0" : s) * TIME_HZ;
 +      t += DCPTime::from_seconds (lexical_cast<int> (s.empty() ? "0" : s));
        string const f = wx_to_std (_frames->GetValue());
 -      t += lexical_cast<int> (f.empty() ? "0" : f) * TIME_HZ / fps;
 +      t += DCPTime::from_seconds (lexical_cast<double> (f.empty() ? "0" : f) / fps);
  
        return t;
  }
diff --combined src/wx/timeline.cc
index e1b507383e87523550279940d61608d9dca632b8,1858afee7cebc0a4b55de50be3b4fb7dc030dc8b..185abefeb8b5e6c1041c937d02f491c9de00ea3f
@@@ -1,5 -1,5 +1,5 @@@
  /*
-     Copyright (C) 2013 Carl Hetherington <cth@carlh.net>
+     Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net>
  
      This program is free software; you can redistribute it and/or modify
      it under the terms of the GNU General Public License as published by
@@@ -64,9 -64,9 +64,9 @@@ public
  protected:
        virtual void do_paint (wxGraphicsContext *) = 0;
        
 -      int time_x (Time t) const
 +      int time_x (DCPTime t) const
        {
 -              return _timeline.tracks_position().x + t * _timeline.pixels_per_time_unit();
 +              return _timeline.tracks_position().x + t.seconds() * _timeline.pixels_per_second ();
        }
        
        Timeline& _timeline;
@@@ -83,7 -83,6 +83,6 @@@ public
        ContentView (Timeline& tl, shared_ptr<Content> c)
                : View (tl)
                , _content (c)
-               , _track (0)
                , _selected (false)
        {
                _content_connection = c->Changed.connect (bind (&ContentView::content_changed, this, _2, _3));
@@@ -91,6 -90,8 +90,8 @@@
  
        dcpomatic::Rect<int> bbox () const
        {
+               assert (_track);
                shared_ptr<const Film> film = _timeline.film ();
                shared_ptr<const Content> content = _content.lock ();
                if (!film || !content) {
                
                return dcpomatic::Rect<int> (
                        time_x (content->position ()) - 8,
-                       y_pos (_track) - 8,
+                       y_pos (_track.get()) - 8,
 -                      content->length_after_trim () * _timeline.pixels_per_time_unit() + 16,
 +                      content->length_after_trim().seconds() * _timeline.pixels_per_second() + 16,
                        _timeline.track_height() + 16
                        );
        }
                _track = t;
        }
  
-       int track () const {
+       optional<int> track () const {
                return _track;
        }
  
@@@ -133,14 -134,16 +134,16 @@@ private
  
        void do_paint (wxGraphicsContext* gc)
        {
+               assert (_track);
                shared_ptr<const Film> film = _timeline.film ();
                shared_ptr<const Content> cont = content ();
                if (!film || !cont) {
                        return;
                }
  
 -              Time const position = cont->position ();
 -              Time const len = cont->length_after_trim ();
 +              DCPTime const position = cont->position ();
 +              DCPTime const len = cont->length_after_trim ();
  
                wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2);
  
                }
  
                wxGraphicsPath path = gc->CreatePath ();
-               path.MoveToPoint    (time_x (position),       y_pos (_track) + 4);
-               path.AddLineToPoint (time_x (position + len), y_pos (_track) + 4);
-               path.AddLineToPoint (time_x (position + len), y_pos (_track + 1) - 4);
-               path.AddLineToPoint (time_x (position),       y_pos (_track + 1) - 4);
-               path.AddLineToPoint (time_x (position),       y_pos (_track) + 4);
+               path.MoveToPoint    (time_x (position),       y_pos (_track.get()) + 4);
+               path.AddLineToPoint (time_x (position + len), y_pos (_track.get()) + 4);
+               path.AddLineToPoint (time_x (position + len), y_pos (_track.get() + 1) - 4);
+               path.AddLineToPoint (time_x (position),       y_pos (_track.get() + 1) - 4);
+               path.AddLineToPoint (time_x (position),       y_pos (_track.get()) + 4);
                gc->StrokePath (path);
                gc->FillPath (path);
  
                wxDouble name_leading;
                gc->GetTextExtent (name, &name_width, &name_height, &name_descent, &name_leading);
                
-               gc->Clip (wxRegion (time_x (position), y_pos (_track), len.seconds() * _timeline.pixels_per_second(), _timeline.track_height()));
-               gc->DrawText (name, time_x (position) + 12, y_pos (_track + 1) - name_height - 4);
 -              gc->Clip (wxRegion (time_x (position), y_pos (_track.get()), len * _timeline.pixels_per_time_unit(), _timeline.track_height()));
++              gc->Clip (wxRegion (time_x (position), y_pos (_track.get()), len.seconds() * _timeline.pixels_per_second(), _timeline.track_height()));
+               gc->DrawText (name, time_x (position) + 12, y_pos (_track.get() + 1) - name_height - 4);
                gc->ResetClip ();
        }
  
                }
  
                if (!frequent) {
 -                      _timeline.setup_pixels_per_time_unit ();
 +                      _timeline.setup_pixels_per_second ();
                        _timeline.Refresh ();
                }
        }
  
        boost::weak_ptr<Content> _content;
-       int _track;
+       optional<int> _track;
        bool _selected;
  
        boost::signals2::scoped_connection _content_connection;
@@@ -243,25 -246,6 +246,25 @@@ private
        }
  };
  
 +class SubtitleContentView : public ContentView
 +{
 +public:
 +      SubtitleContentView (Timeline& tl, shared_ptr<Content> c)
 +              : ContentView (tl, c)
 +      {}
 +
 +private:
 +      wxString type () const
 +      {
 +              return _("subtitles");
 +      }
 +
 +      wxColour colour () const
 +      {
 +              return wxColour (163, 255, 154, 255);
 +      }
 +};
 +
  class TimeAxisView : public View
  {
  public:
@@@ -287,18 -271,18 +290,18 @@@ private
        {
                gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 1, wxPENSTYLE_SOLID));
                
 -              int mark_interval = rint (128 / (TIME_HZ * _timeline.pixels_per_time_unit ()));
 +              double mark_interval = rint (128 / _timeline.pixels_per_second ());
                if (mark_interval > 5) {
 -                      mark_interval -= mark_interval % 5;
 +                      mark_interval -= int (rint (mark_interval)) % 5;
                }
                if (mark_interval > 10) {
 -                      mark_interval -= mark_interval % 10;
 +                      mark_interval -= int (rint (mark_interval)) % 10;
                }
                if (mark_interval > 60) {
 -                      mark_interval -= mark_interval % 60;
 +                      mark_interval -= int (rint (mark_interval)) % 60;
                }
                if (mark_interval > 3600) {
 -                      mark_interval -= mark_interval % 3600;
 +                      mark_interval -= int (rint (mark_interval)) % 3600;
                }
                
                if (mark_interval < 1) {
                path.AddLineToPoint (_timeline.width(), _y);
                gc->StrokePath (path);
  
 -              Time t = 0;
 -              while ((t * _timeline.pixels_per_time_unit()) < _timeline.width()) {
 +              /* Time in seconds */
 +              DCPTime t;
 +              while ((t.seconds() * _timeline.pixels_per_second()) < _timeline.width()) {
                        wxGraphicsPath path = gc->CreatePath ();
                        path.MoveToPoint (time_x (t), _y - 4);
                        path.AddLineToPoint (time_x (t), _y + 4);
                        gc->StrokePath (path);
  
 -                      int tc = t / TIME_HZ;
 +                      double tc = t.seconds ();
                        int const h = tc / 3600;
                        tc -= h * 3600;
                        int const m = tc / 60;
                        wxDouble str_leading;
                        gc->GetTextExtent (str, &str_width, &str_height, &str_descent, &str_leading);
                        
 -                      int const tx = _timeline.x_offset() + t * _timeline.pixels_per_time_unit();
 +                      int const tx = _timeline.x_offset() + t.seconds() * _timeline.pixels_per_second();
                        if ((tx + str_width) < _timeline.width()) {
                                gc->DrawText (str, time_x (t), _y + 16);
                        }
                        
 -                      t += mark_interval * TIME_HZ;
 +                      t += DCPTime::from_seconds (mark_interval);
                }
        }
  
@@@ -352,7 -335,7 +355,7 @@@ Timeline::Timeline (wxWindow* parent, F
        , _film (film)
        , _time_axis_view (new TimeAxisView (*this, 32))
        , _tracks (0)
 -      , _pixels_per_time_unit (0)
 +      , _pixels_per_second (0)
        , _left_down (false)
        , _down_view_position (0)
        , _first_move (false)
@@@ -418,68 -401,40 +421,43 @@@ Timeline::playlist_changed (
                if (dynamic_pointer_cast<AudioContent> (*i)) {
                        _views.push_back (shared_ptr<View> (new AudioContentView (*this, *i)));
                }
 +              if (dynamic_pointer_cast<SubtitleContent> (*i)) {
 +                      _views.push_back (shared_ptr<View> (new SubtitleContentView (*this, *i)));
 +              }
        }
  
        assign_tracks ();
 -      setup_pixels_per_time_unit ();
 +      setup_pixels_per_second ();
        Refresh ();
  }
  
  void
  Timeline::assign_tracks ()
  {
-       list<shared_ptr<VideoContentView> > video;
-       list<shared_ptr<AudioContentView> > audio;
-       list<shared_ptr<SubtitleContentView> > subtitle;
        for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
-               shared_ptr<VideoContentView> v = dynamic_pointer_cast<VideoContentView> (*i);
-               if (v) {
-                       video.push_back (v);
-               }
-               
-               shared_ptr<AudioContentView> a = dynamic_pointer_cast<AudioContentView> (*i);
-               if (a) {
-                       audio.push_back (a);
-               }
-               shared_ptr<SubtitleContentView> s = dynamic_pointer_cast<SubtitleContentView> (*i);
-               if (s) {
-                       subtitle.push_back (s);
-               }
-       }
-       _tracks = 0;
-       if (!video.empty ()) {
-               for (list<shared_ptr<VideoContentView> >::iterator i = video.begin(); i != video.end(); ++i) {
-                       (*i)->set_track (_tracks);
+               shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i);
+               if (!cv) {
+                       continue;
                }
-               ++_tracks;
-       }
--      
-       if (!subtitle.empty ()) {
-               for (list<shared_ptr<SubtitleContentView> >::iterator i = subtitle.begin(); i != subtitle.end(); ++i) {
-                       (*i)->set_track (_tracks);
-               }
-               ++_tracks;
-       }
 +
-       int const audio_start = _tracks;
+               shared_ptr<Content> content = cv->content();
  
-       for (list<shared_ptr<AudioContentView> >::iterator i = audio.begin(); i != audio.end(); ++i) {
-               shared_ptr<Content> acv_content = (*i)->content();
-               int t = audio_start;
-               while (1) {
-                       list<shared_ptr<AudioContentView> >::iterator j = audio.begin ();
-                       while (j != audio.end()) {
-                               if ((*j)->track() == t) {
+               int t = 0;
 -              while (1) {
++              while (true) {
+                       ViewList::iterator j = _views.begin();
+                       while (j != _views.end()) {
+                               shared_ptr<ContentView> test = dynamic_pointer_cast<ContentView> (*j);
+                               if (!test) {
+                                       ++j;
+                                       continue;
+                               }
+                               
+                               shared_ptr<Content> test_content = test->content();
+                                       
+                               if (test && test->track() && test->track().get() == t) {
                                        bool const no_overlap =
-                                               (acv_content->position() < (*j)->content()->position() && acv_content->end() < (*j)->content()->position()) ||
-                                               (acv_content->position() > (*j)->content()->end()      && acv_content->end() > (*j)->content()->end());
+                                               (content->position() < test_content->position() && content->end() < test_content->position()) ||
+                                               (content->position() > test_content->end()      && content->end() > test_content->end());
                                        
                                        if (!no_overlap) {
                                                /* we have an overlap on track `t' */
                                ++j;
                        }
  
-                       if (j == audio.end ()) {
+                       if (j == _views.end ()) {
                                /* no overlap on `t' */
                                break;
                        }
                }
  
-               (*i)->set_track (t);
+               cv->set_track (t);
                _tracks = max (_tracks, t + 1);
        }
  
@@@ -511,14 -466,14 +489,14 @@@ Timeline::tracks () cons
  }
  
  void
 -Timeline::setup_pixels_per_time_unit ()
 +Timeline::setup_pixels_per_second ()
  {
        shared_ptr<const Film> film = _film.lock ();
 -      if (!film || film->length() == 0) {
 +      if (!film || film->length() == DCPTime ()) {
                return;
        }
  
 -      _pixels_per_time_unit = static_cast<double>(width() - x_offset() * 2) / film->length ();
 +      _pixels_per_second = static_cast<double>(width() - x_offset() * 2) / film->length().seconds ();
  }
  
  shared_ptr<View>
@@@ -637,13 -592,13 +615,13 @@@ Timeline::set_position_from_event (wxMo
                return;
        }
        
 -      Time new_position = _down_view_position + (p.x - _down_point.x) / _pixels_per_time_unit;
 +      DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / _pixels_per_second);
        
        if (_snap) {
                
                bool first = true;
 -              Time nearest_distance = TIME_MAX;
 -              Time nearest_new_position = TIME_MAX;
 +              DCPTime nearest_distance = DCPTime::max ();
 +              DCPTime nearest_new_position = DCPTime::max ();
                
                /* Find the nearest content edge; this is inefficient */
                for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) {
                        
                        {
                                /* Snap starts to ends */
 -                              Time const d = abs (cv->content()->end() - new_position);
 +                              DCPTime const d = DCPTime (cv->content()->end() - new_position).abs ();
                                if (first || d < nearest_distance) {
                                        nearest_distance = d;
                                        nearest_new_position = cv->content()->end();
                        
                        {
                                /* Snap ends to starts */
 -                              Time const d = abs (cv->content()->position() - (new_position + _down_view->content()->length_after_trim()));
 +                              DCPTime const d = DCPTime (
 +                                      cv->content()->position() - (new_position + _down_view->content()->length_after_trim())
 +                                      ).abs ();
 +                              
                                if (d < nearest_distance) {
                                        nearest_distance = d;
                                        nearest_new_position = cv->content()->position() - _down_view->content()->length_after_trim ();
                
                if (!first) {
                        /* Snap if it's close; `close' means within a proportion of the time on the timeline */
 -                      if (nearest_distance < (width() / pixels_per_time_unit()) / 32) {
 +                      if (nearest_distance < DCPTime::from_seconds ((width() / pixels_per_second()) / 32)) {
                                new_position = nearest_new_position;
                        }
                }
        }
        
 -      if (new_position < 0) {
 -              new_position = 0;
 +      if (new_position < DCPTime ()) {
 +              new_position = DCPTime ();
        }
        
        _down_view->content()->set_position (new_position);
@@@ -710,7 -662,7 +688,7 @@@ Timeline::film () cons
  void
  Timeline::resized ()
  {
 -      setup_pixels_per_time_unit ();
 +      setup_pixels_per_second ();
  }
  
  void
diff --combined src/wx/timing_panel.cc
index 3fcb9b1753eeecc41d808ef2d27f7777196d1bbe,5cefe318af333d35ff68da2c02482325d57721c2..f33e052a1ad41dad2f2adea9b0b7aa39f7823ceb
@@@ -19,6 -19,7 +19,7 @@@
  
  #include "lib/content.h"
  #include "lib/image_content.h"
+ #include "lib/sndfile_content.h"
  #include "timing_panel.h"
  #include "wx_util.h"
  #include "timecode.h"
@@@ -80,48 -81,54 +81,50 @@@ TimingPanel::film_content_changed (int 
        if (cl.size() == 1) {
                content = cl.front ();
        }
+       int const film_video_frame_rate = _editor->film()->video_frame_rate ();
        
        if (property == ContentProperty::POSITION) {
                if (content) {
-                       _position->set (content->position (), _editor->film()->video_frame_rate ());
+                       _position->set (content->position (), film_video_frame_rate);
                } else {
 -                      _position->set (0, 24);
 +                      _position->set (DCPTime () , 24);
                }
        } else if (
                property == ContentProperty::LENGTH ||
                property == VideoContentProperty::VIDEO_FRAME_RATE ||
 -              property == VideoContentProperty::VIDEO_FRAME_TYPE ||
 -              property == SndfileContentProperty::VIDEO_FRAME_RATE
 +              property == VideoContentProperty::VIDEO_FRAME_TYPE
                ) {
                if (content) {
-                       _full_length->set (content->full_length (), _editor->film()->video_frame_rate ());
-                       _play_length->set (content->length_after_trim (), _editor->film()->video_frame_rate ());
+                       _full_length->set (content->full_length (), film_video_frame_rate);
+                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
                } else {
 -                      _full_length->set (0, 24);
 -                      _play_length->set (0, 24);
 +                      _full_length->set (DCPTime (), 24);
 +                      _play_length->set (DCPTime (), 24);
                }
        } else if (property == ContentProperty::TRIM_START) {
                if (content) {
-                       _trim_start->set (content->trim_start (), _editor->film()->video_frame_rate ());
-                       _play_length->set (content->length_after_trim (), _editor->film()->video_frame_rate ());
+                       _trim_start->set (content->trim_start (), film_video_frame_rate);
+                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
                } else {
 -                      _trim_start->set (0, 24);
 -                      _play_length->set (0, 24);
 +                      _trim_start->set (DCPTime (), 24);
 +                      _play_length->set (DCPTime (), 24);
                }
        } else if (property == ContentProperty::TRIM_END) {
                if (content) {
-                       _trim_end->set (content->trim_end (), _editor->film()->video_frame_rate ());
-                       _play_length->set (content->length_after_trim (), _editor->film()->video_frame_rate ());
+                       _trim_end->set (content->trim_end (), film_video_frame_rate);
+                       _play_length->set (content->length_after_trim (), film_video_frame_rate);
                } else {
 -                      _trim_end->set (0, 24);
 -                      _play_length->set (0, 24);
 +                      _trim_end->set (DCPTime (), 24);
 +                      _play_length->set (DCPTime (), 24);
                }
        }
  
        if (property == VideoContentProperty::VIDEO_FRAME_RATE) {
                if (content) {
                        shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (content);
 -                      shared_ptr<SndfileContent> sc = dynamic_pointer_cast<SndfileContent> (content);
                        if (vc) {
                                _video_frame_rate->SetValue (std_to_wx (lexical_cast<string> (vc->video_frame_rate ())));
 -                      } else if (sc) {
 -                              _video_frame_rate->SetValue (std_to_wx (lexical_cast<string> (sc->video_frame_rate ())));
                        } else {
                                _video_frame_rate->SetValue ("24");
                        }
        }
  
        shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (content);
+       shared_ptr<SndfileContent> sc = dynamic_pointer_cast<SndfileContent> (content);
        _full_length->set_editable (ic && ic->still ());
        _play_length->set_editable (!ic || !ic->still ());
-       _video_frame_rate->Enable (ic && !ic->still ());
+       _video_frame_rate->Enable ((ic && !ic->still ()) || sc);
        _set_video_frame_rate->Enable (false);
  }
  
@@@ -153,8 -161,7 +157,8 @@@ TimingPanel::full_length_changed (
        if (c.size() == 1) {
                shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (c.front ());
                if (ic && ic->still ()) {
 -                      ic->set_video_length (rint (_full_length->get (_editor->film()->video_frame_rate()) * ic->video_frame_rate() / TIME_HZ));
 +                      /* XXX: No effective FRC here... is this right? */
 +                      ic->set_video_length (ContentTime (_full_length->get (_editor->film()->video_frame_rate()), FrameRateChange (1, 1)));
                }
        }
  }
@@@ -201,8 -208,12 +205,8 @@@ TimingPanel::set_video_frame_rate (
                shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (c.front ());
                if (ic) {
                        ic->set_video_frame_rate (lexical_cast<float> (wx_to_std (_video_frame_rate->GetValue ())));
-                       _set_video_frame_rate->Enable (false);
                }
 -              shared_ptr<SndfileContent> sc = dynamic_pointer_cast<SndfileContent> (c.front ());
 -              if (sc) {
 -                      sc->set_video_frame_rate (lexical_cast<float> (wx_to_std (_video_frame_rate->GetValue ())));
 -              }
+               _set_video_frame_rate->Enable (false);
        }
  }
  
diff --combined src/wx/video_panel.cc
index fad824727074574ad0ef9f71f23c94f6237de415,ac4c29222b231e9912ca1e7306bbd91f253c8ac2..399e71aac26edba4d752847d0faa4240bb2ead69
@@@ -197,6 -197,8 +197,8 @@@ VideoPanel::VideoPanel (FilmEditor* e
        _frame_type->wrapped()->Append (_("3D left/right"));
        _frame_type->wrapped()->Append (_("3D top/bottom"));
        _frame_type->wrapped()->Append (_("3D alternate"));
+       _frame_type->wrapped()->Append (_("3D left only"));
+       _frame_type->wrapped()->Append (_("3D right only"));
  
        _filters_button->Bind           (wxEVT_COMMAND_BUTTON_CLICKED,   boost::bind (&VideoPanel::edit_filters_clicked, this));
        _colour_conversion_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED,   boost::bind (&VideoPanel::edit_colour_conversion_clicked, this));
@@@ -295,8 -297,8 +297,8 @@@ VideoPanel::setup_description (
        }
  
        Crop const crop = vcs->crop ();
 -      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != libdcp::Size (0, 0)) {
 -              libdcp::Size cropped = vcs->video_size_after_crop ();
 +      if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != dcp::Size (0, 0)) {
 +              dcp::Size cropped = vcs->video_size_after_crop ();
                d << wxString::Format (
                        _("Cropped to %dx%d (%.2f:1)\n"),
                        cropped.width, cropped.height,
                ++lines;
        }
  
 -      libdcp::Size const container_size = _editor->film()->frame_size ();
 -      libdcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size);
 +      dcp::Size const container_size = _editor->film()->frame_size ();
 +      dcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size);
  
        if (scaled != vcs->video_size_after_crop ()) {
                d << wxString::Format (
  
        d << wxString::Format (_("Content frame rate %.4f\n"), vcs->video_frame_rate ());
        ++lines;
 -      FrameRateConversion frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
 +      FrameRateChange frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ());
        d << std_to_wx (frc.description) << "\n";
        ++lines;
  
diff --combined src/wx/wscript
index 45a5bc96d5c436c455bc42bbc33fbc8dea5ad605,a04df2d41751a718f72f3f30584cf2109c94a1ee..cd78f064920081a7bbee364035af9f3f2e0c753b
@@@ -17,6 -17,9 +17,9 @@@ sources = ""
            content_menu.cc
            dci_metadata_dialog.cc
            dir_picker_ctrl.cc
+           dolby_certificate_dialog.cc
+           doremi_certificate_dialog.cc
+           download_certificate_dialog.cc
            film_editor.cc
            film_editor_panel.cc
            film_viewer.cc
@@@ -35,7 -38,7 +38,8 @@@
            server_dialog.cc
            servers_list_dialog.cc
            subtitle_panel.cc
 +          subtitle_view.cc
+           table_dialog.cc
            timecode.cc
            timeline.cc
            timeline_dialog.cc
diff --combined src/wx/wx_util.cc
index a5399e62ea4d798c2957c36eb20a6586e84c3e0f,96278b82bac0d1197a26ae68da601c9084a912a3..048f87908e8c72770e973f0450a36767c1b747f3
@@@ -123,7 -123,7 +123,7 @@@ int const ThreadedStaticText::_update_e
   *  @param initial Initial text for the wxStaticText while the computation is being run.
   *  @param fn Function which works out what the wxStaticText content should be and returns it.
   */
 -ThreadedStaticText::ThreadedStaticText (wxWindow* parent, wxString initial, function<string ()> fn)
 +ThreadedStaticText::ThreadedStaticText (wxWindow* parent, wxString initial, boost::function<string ()> fn)
        : wxStaticText (parent, wxID_ANY, initial)
  {
        Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ThreadedStaticText::thread_finished, this, _1), _update_event_id);
@@@ -139,7 -139,7 +139,7 @@@ ThreadedStaticText::~ThreadedStaticTex
  
  /** Run our thread and post the result to the GUI thread via AddPendingEvent */
  void
 -ThreadedStaticText::run (function<string ()> fn)
 +ThreadedStaticText::run (boost::function<string ()> fn)
  try
  {
        wxCommandEvent ev (wxEVT_COMMAND_TEXT_UPDATED, _update_event_id);
@@@ -296,3 -296,11 +296,11 @@@ wx_get (wxChoice* w
  {
        return w->GetSelection ();
  }
+ void
+ run_gui_loop ()
+ {
+       while (wxTheApp->Pending ()) {
+               wxTheApp->Dispatch ();
+       }
+ }
diff --combined wscript
index 67f1033c595de95ca845e21bd4cc561686201381,fe834f34378b3fe6c88dc76dc39a2a53b11ba489..88e928f12dec710dfb2a72ec54199cbcb0be439d
+++ b/wscript
@@@ -3,7 -3,7 +3,7 @@@ import o
  import sys
  
  APPNAME = 'dcpomatic'
 -VERSION = '1.66.9devel'
 +VERSION = '2.0.0devel'
  
  def options(opt):
      opt.load('compiler_cxx')
@@@ -52,9 -52,9 +52,9 @@@ def dynamic_openjpeg(conf)
      conf.check_cfg(package='libopenjpeg', args='--cflags --libs', max_version='1.5.1', mandatory=True)
  
  def static_dcp(conf, static_boost, static_xmlpp, static_xmlsec, static_ssh):
 -    conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags', uselib_store='DCP', mandatory=True)
 +    conf.check_cfg(package='libdcp-1.0', atleast_version='0.92', args='--cflags', uselib_store='DCP', mandatory=True)
      conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
 -    conf.env.STLIB_DCP = ['dcp', 'asdcp-libdcp', 'kumu-libdcp']
 +    conf.env.STLIB_DCP = ['dcp-1.0', 'asdcp-libdcp', 'kumu-libdcp']
      conf.env.LIB_DCP = ['glibmm-2.4', 'ssl', 'crypto', 'bz2', 'xslt']
  
      if static_boost:
@@@ -78,7 -78,7 +78,7 @@@
          conf.env.LIB_DCP.append('ssh')
  
  def dynamic_dcp(conf):
 -    conf.check_cfg(package='libdcp', atleast_version='0.92', args='--cflags --libs', uselib_store='DCP', mandatory=True)
 +    conf.check_cfg(package='libdcp-1.0', atleast_version='0.92', args='--cflags --libs', uselib_store='DCP', mandatory=True)
      conf.env.DEFINES_DCP = [f.replace('\\', '') for f in conf.env.DEFINES_DCP]
  
  def dynamic_ssh(conf):
@@@ -299,8 -299,6 +299,8 @@@ def configure(conf)
      conf.check_cfg(package='glib-2.0', args='--cflags --libs', uselib_store='GLIB', mandatory=True)
      conf.check_cfg(package= '', path=conf.options.magickpp_config, args='--cppflags --cxxflags --libs', uselib_store='MAGICK', mandatory=True)
      conf.check_cfg(package='libzip', args='--cflags --libs', uselib_store='ZIP', mandatory=True)
 +    conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
 +    conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
  
      conf.check_cc(fragment="""
                             #include <glib.h>
@@@ -402,3 -400,6 +402,6 @@@ def pot(bld)
  
  def pot_merge(bld):
      bld.recurse('src')
+ def tags(bld):
+     os.system('etags src/lib/*.cc src/lib/*.h src/wx/*.cc src/wx/*.h src/tools/*.cc src/tools/*.h')