Merge master.
authorCarl Hetherington <cth@carlh.net>
Tue, 4 Mar 2014 20:27:27 +0000 (20:27 +0000)
committerCarl Hetherington <cth@carlh.net>
Tue, 4 Mar 2014 20:27:27 +0000 (20:27 +0000)
14 files changed:
1  2 
src/lib/ffmpeg_content.cc
src/lib/film.cc
src/lib/image.cc
src/lib/image_content.cc
src/lib/image_content.h
src/lib/player.cc
src/lib/util.cc
src/lib/video_content.cc
src/lib/video_content.h
src/wx/video_panel.cc
test/black_fill_test.cc
test/ffmpeg_audio_test.cc
test/scaling_test.cc
wscript

index 4a6cf9e04ceb5b94f3a490eb43d59a736bff8e26,4ae5546c2c2daa38a76a07c770eba837f572047a..bd5648ecbc2a6d4166096c289d135562b87ca126
@@@ -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
@@@ -60,7 -60,7 +60,7 @@@ FFmpegContent::FFmpegContent (shared_pt
  
  FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
        : Content (f, node)
-       , VideoContent (f, node)
+       , VideoContent (f, node, version)
        , AudioContent (f, node)
        , SubtitleContent (f, node, version)
  {
@@@ -163,8 -163,9 +163,8 @@@ FFmpegContent::examine (shared_ptr<Job
  
        shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ()));
  
 -      VideoContent::Frame video_length = 0;
 -      video_length = examiner->video_length ();
 -      film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length));
 +      ContentTime video_length = examiner->video_length ();
 +      film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length.frames (video_frame_rate ())));
  
        {
                boost::mutex::scoped_lock lm (_mutex);
@@@ -227,7 -228,7 +227,7 @@@ FFmpegContent::technical_summary () con
  string
  FFmpegContent::information () const
  {
 -      if (video_length() == 0 || video_frame_rate() == 0) {
 +      if (video_length() == ContentTime (0) || video_frame_rate() == ContentTime (0)) {
                return "";
        }
        
@@@ -261,17 -262,19 +261,17 @@@ FFmpegContent::set_audio_stream (shared
        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
  }
  
 -AudioContent::Frame
 +ContentTime
  FFmpegContent::audio_length () const
  {
 -      int const cafr = content_audio_frame_rate ();
 -      int const vfr  = video_frame_rate ();
 -      VideoContent::Frame const vl = video_length ();
 -
 -      boost::mutex::scoped_lock lm (_mutex);
 -      if (!_audio_stream) {
 -              return 0;
 +      {
 +              boost::mutex::scoped_lock lm (_mutex);
 +              if (!_audio_stream) {
 +                      return ContentTime ();
 +              }
        }
 -      
 -      return video_frames_to_audio_frames (vl, cafr, vfr);
 +
 +      return video_length();
  }
  
  int
@@@ -307,15 -310,16 +307,15 @@@ FFmpegContent::output_audio_frame_rate 
        /* Resample to a DCI-approved sample rate */
        double t = dcp_audio_frame_rate (content_audio_frame_rate ());
  
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 +      FrameRateChange frc (video_frame_rate(), film->video_frame_rate());
  
        /* Compensate if the DCP is being run at a different frame rate
           to the source; that is, if the video is run such that it will
           look different in the DCP compared to the source (slower or faster).
 -         skip/repeat doesn't come into effect here.
        */
  
        if (frc.change_speed) {
 -              t *= video_frame_rate() * frc.factor() / film->video_frame_rate();
 +              t /= frc.speed_up;
        }
  
        return rint (t);
@@@ -413,12 -417,14 +413,12 @@@ FFmpegSubtitleStream::as_xml (xmlpp::No
        FFmpegStream::as_xml (root);
  }
  
 -Time
 +DCPTime
  FFmpegContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateConversion frc (video_frame_rate (), film->video_frame_rate ());
 -      return video_length() * frc.factor() * TIME_HZ / film->video_frame_rate ();
 +      return DCPTime (video_length(), FrameRateChange (video_frame_rate (), film->video_frame_rate ()));
  }
  
  AudioMapping
diff --combined src/lib/film.cc
index 11fb9e4b0a671f6eef1ab9db22da95f565083f66,00beb870f6e0ead188a1c013cc4f32b23e8594bb..aecb389fd5e67b4a7b2cb9a01a1572196e4d7cd9
  #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/kdm.h>
  #include "film.h"
  #include "job.h"
  #include "util.h"
@@@ -78,15 -78,17 +78,17 @@@ 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.
   * 6 -> 7
   * Subtitle offset changed to subtitle y offset, and subtitle x offset added.
+  * 7 -> 8
+  * Use <Scale> tag in <VideoContent> rather than <Ratio>.
   */
- int const Film::current_state_version = 7;
+ int const Film::current_state_version = 8;
  
  /** Construct a Film object in a given directory.
   *
@@@ -426,7 -428,7 +428,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"));
        _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"), _state_version);
  
        _dirty = false;
@@@ -752,7 -754,7 +754,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 (...) {
@@@ -861,7 -863,7 +863,7 @@@ Film::move_content_later (shared_ptr<Co
        _playlist->move_later (c);
  }
  
 -Time
 +DCPTime
  Film::length () const
  {
        return _playlist->length ();
@@@ -873,18 -875,12 +875,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)
  {
@@@ -903,7 -899,31 +905,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 */
@@@ -918,23 -938,23 +920,23 @@@ Film::set_sequence_video (bool s
        signal_changed (SEQUENCE_VIDEO);
  }
  
 -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 ();
  }
  
 -libdcp::KDM
 +dcp::KDM
  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
  {
        shared_ptr<const Signer> signer = make_signer ();
  
 -      libdcp::DCP dcp (dir (dcp_dir.string ()));
 +      dcp::DCP dcp (dir (dcp_dir.string ()));
        
        try {
                dcp.read ();
        
        time_t now = time (0);
        struct tm* tm = localtime (&now);
 -      string const issue_date = libdcp::tm_to_string (tm);
 +      string const issue_date = dcp::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::KDM (dcp.cpls().front(), signer, target, from, until, "DCP-o-matic", issue_date);
  }
  
 -list<libdcp::KDM>
 +list<dcp::KDM>
  Film::make_kdms (
        list<shared_ptr<Screen> > screens,
        boost::filesystem::path dcp,
        boost::posix_time::ptime until
        ) const
  {
 -      list<libdcp::KDM> kdms;
 +      list<dcp::KDM> 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/image.cc
index b706f7fdc3f68fdcf7b611835631ce315eff016a,c7dfc91cb374eb9c765b0009e22a00a407f1aff2..98645c2996a4219f02681d276090b737df66df4e
@@@ -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
@@@ -31,13 -31,15 +31,16 @@@ extern "C" 
  #include "image.h"
  #include "exceptions.h"
  #include "scaler.h"
 +#include "timer.h"
  
+ #include "i18n.h"
  using std::string;
  using std::min;
  using std::cout;
+ using std::cerr;
  using boost::shared_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
  int
  Image::line_factor (int n) const
@@@ -81,7 -83,7 +84,7 @@@ Image::components () cons
  
  /** Crop this image, scale it to `inter_size' and then place it in a black frame of `out_size' */
  shared_ptr<Image>
 -Image::crop_scale_window (Crop crop, libdcp::Size inter_size, libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
 +Image::crop_scale_window (Crop crop, dcp::Size inter_size, dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
  {
        assert (scaler);
        /* Empirical testing suggests that sws_scale() will crash if
        */
        assert (aligned ());
  
+       assert (out_size.width >= inter_size.width);
+       assert (out_size.height >= inter_size.height);
+       /* Here's an image of out_size */
        shared_ptr<Image> out (new Image (out_format, out_size, out_aligned));
        out->make_black ();
-       
-       dcp::Size cropped_size = crop.apply (size ());
  
 -      libdcp::Size const cropped_size = crop.apply (size ());
+       /* Size of the image after any crop */
++      dcp::Size const cropped_size = crop.apply (size ());
+       /* Scale context for a scale from cropped_size to inter_size */
        struct SwsContext* scale_context = sws_getContext (
 -              cropped_size.width, cropped_size.height, pixel_format(),
 -              inter_size.width, inter_size.height, out_format,
 -              scaler->ffmpeg_id (), 0, 0, 0
 +                      cropped_size.width, cropped_size.height, pixel_format(),
 +                      inter_size.width, inter_size.height, out_format,
 +                      scaler->ffmpeg_id (), 0, 0, 0
                );
  
+       if (!scale_context) {
+               throw StringError (N_("Could not allocate SwsContext"));
+       }
+       /* Prepare input data pointers with crop */
        uint8_t* scale_in_data[components()];
        for (int c = 0; c < components(); ++c) {
                scale_in_data[c] = data()[c] + int (rint (bytes_per_pixel(c) * crop.left)) + stride()[c] * (crop.top / line_factor(c));
        }
  
+       /* Corner of the image within out_size */
        Position<int> const corner ((out_size.width - inter_size.width) / 2, (out_size.height - inter_size.height) / 2);
  
-       uint8_t* scale_out_data[components()];
-       for (int c = 0; c < components(); ++c) {
+       uint8_t* scale_out_data[out->components()];
+       for (int c = 0; c < out->components(); ++c) {
                scale_out_data[c] = out->data()[c] + int (rint (out->bytes_per_pixel(c) * corner.x)) + out->stride()[c] * corner.y;
        }
  
  }
  
  shared_ptr<Image>
 -Image::scale (libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
 +Image::scale (dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const
  {
        assert (scaler);
        /* Empirical testing suggests that sws_scale() will crash if
@@@ -201,7 -215,7 +216,7 @@@ Image::post_process (string pp, bool al
  shared_ptr<Image>
  Image::crop (Crop crop, bool aligned) const
  {
 -      libdcp::Size cropped_size = crop.apply (size ());
 +      dcp::Size cropped_size = crop.apply (size ());
        shared_ptr<Image> out (new Image (pixel_format(), cropped_size, aligned));
  
        for (int c = 0; c < components(); ++c) {
@@@ -376,18 -390,8 +391,18 @@@ Image::make_black (
  void
  Image::alpha_blend (shared_ptr<const Image> other, Position<int> position)
  {
 -      /* Only implemented for RGBA onto RGB24 so far */
 -      assert (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGBA);
 +      int this_bpp = 0;
 +      int other_bpp = 0;
 +
 +      if (_pixel_format == PIX_FMT_BGRA && other->pixel_format() == PIX_FMT_RGBA) {
 +              this_bpp = 4;
 +              other_bpp = 4;
 +      } else if (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGBA) {
 +              this_bpp = 3;
 +              other_bpp = 4;
 +      } else {
 +              assert (false);
 +      }
  
        int start_tx = position.x;
        int start_ox = 0;
        }
  
        for (int ty = start_ty, oy = start_oy; ty < size().height && oy < other->size().height; ++ty, ++oy) {
 -              uint8_t* tp = data()[0] + ty * stride()[0] + position.x * 3;
 +              uint8_t* tp = data()[0] + ty * stride()[0] + position.x * this_bpp;
                uint8_t* op = other->data()[0] + oy * other->stride()[0];
                for (int tx = start_tx, ox = start_ox; tx < size().width && ox < other->size().width; ++tx, ++ox) {
                        float const alpha = float (op[3]) / 255;
                        tp[0] = (tp[0] * (1 - alpha)) + op[0] * alpha;
                        tp[1] = (tp[1] * (1 - alpha)) + op[1] * alpha;
                        tp[2] = (tp[2] * (1 - alpha)) + op[2] * alpha;
 -                      tp += 3;
 -                      op += 4;
 +                      tp += this_bpp;
 +                      op += other_bpp;
                }
        }
  }
@@@ -498,8 -502,8 +513,8 @@@ Image::bytes_per_pixel (int c) cons
   *  @param p Pixel format.
   *  @param s Size in pixels.
   */
 -Image::Image (AVPixelFormat p, libdcp::Size s, bool aligned)
 -      : libdcp::Image (s)
 +Image::Image (AVPixelFormat p, dcp::Size s, bool aligned)
 +      : dcp::Image (s)
        , _pixel_format (p)
        , _aligned (aligned)
  {
@@@ -536,7 -540,7 +551,7 @@@ Image::allocate (
  }
  
  Image::Image (Image const & other)
 -      : libdcp::Image (other)
 +      : dcp::Image (other)
        ,  _pixel_format (other._pixel_format)
        , _aligned (other._aligned)
  {
  }
  
  Image::Image (AVFrame* frame)
 -      : libdcp::Image (libdcp::Size (frame->width, frame->height))
 +      : dcp::Image (dcp::Size (frame->width, frame->height))
        , _pixel_format (static_cast<AVPixelFormat> (frame->format))
        , _aligned (true)
  {
  }
  
  Image::Image (shared_ptr<const Image> other, bool aligned)
 -      : libdcp::Image (other)
 +      : dcp::Image (other)
        , _pixel_format (other->_pixel_format)
        , _aligned (aligned)
  {
@@@ -606,7 -610,7 +621,7 @@@ Image::operator= (Image const & other
  void
  Image::swap (Image & other)
  {
 -      libdcp::Image::swap (other);
 +      dcp::Image::swap (other);
        
        std::swap (_pixel_format, other._pixel_format);
  
@@@ -649,7 -653,7 +664,7 @@@ Image::stride () cons
        return _stride;
  }
  
 -libdcp::Size
 +dcp::Size
  Image::size () const
  {
        return _size;
diff --combined src/lib/image_content.cc
index 2713280b4b7c83b1cc6d0560ab82fd89657abcd3,fd0b578943639a0871b529f2df2cd90947e110a6..db02c6059ed494f553df16c9ea658255b833569d
@@@ -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
@@@ -50,9 -50,9 +50,9 @@@ ImageContent::ImageContent (shared_ptr<
  }
  
  
- ImageContent::ImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int)
+ ImageContent::ImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
        : Content (f, node)
-       , VideoContent (f, node)
+       , VideoContent (f, node, version)
  {
        
  }
@@@ -110,7 -110,7 +110,7 @@@ ImageContent::examine (shared_ptr<Job> 
  }
  
  void
 -ImageContent::set_video_length (VideoContent::Frame len)
 +ImageContent::set_video_length (ContentTime len)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
        signal_changed (ContentProperty::LENGTH);
  }
  
 -Time
 +DCPTime
  ImageContent::full_length () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
 -      
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate ());
 -      return video_length() * frc.factor() * TIME_HZ / video_frame_rate();
 +      return DCPTime (video_length(), FrameRateChange (video_frame_rate(), film->video_frame_rate()));
  }
  
  string
diff --combined src/lib/image_content.h
index 1aff043d221b7a8066552f9c3803c1e3d204d858,e56abce4a07a8218ab0ad4a7a1b26280d131adce..6db24767d5af5850a418444f204f69b896bb6f5b
@@@ -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
@@@ -41,11 -41,11 +41,11 @@@ public
        std::string summary () const;
        std::string technical_summary () const;
        void as_xml (xmlpp::Node *) const;
 -      Time full_length () const;
 +      DCPTime full_length () const;
  
        std::string identifier () const;
        
 -      void set_video_length (VideoContent::Frame);
 +      void set_video_length (ContentTime);
        bool still () const;
        void set_video_frame_rate (float);
  };
diff --combined src/lib/player.cc
index fc2f32d40f996b2d0c186682bfbf8d37c06d149a,99aece8d660d5c67c1140410868b6bdd32d3b9f5..59d046956d570d237870308246dd4bbbb4d336bf
  */
  
  #include <stdint.h>
 +#include <algorithm>
  #include "player.h"
  #include "film.h"
  #include "ffmpeg_decoder.h"
 +#include "audio_buffers.h"
  #include "ffmpeg_content.h"
  #include "image_decoder.h"
  #include "image_content.h"
  #include "sndfile_decoder.h"
  #include "sndfile_content.h"
  #include "subtitle_content.h"
 +#include "subrip_decoder.h"
 +#include "subrip_content.h"
  #include "playlist.h"
  #include "job.h"
  #include "image.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 +#include "render_subtitles.h"
  
  using std::list;
  using std::cout;
@@@ -49,20 -45,71 +49,20 @@@ using std::map
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  class Piece
  {
  public:
 -      Piece (shared_ptr<Content> c)
 -              : content (c)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 -              , repeat_to_do (0)
 -              , repeat_done (0)
 -      {}
 -      
 -      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d)
 +      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d, FrameRateChange f)
                : content (c)
                , decoder (d)
 -              , video_position (c->position ())
 -              , audio_position (c->position ())
 -              , repeat_to_do (0)
 -              , repeat_done (0)
 +              , frc (f)
        {}
  
 -      /** Set this piece to repeat a video frame a given number of times */
 -      void set_repeat (IncomingVideo video, int num)
 -      {
 -              repeat_video = video;
 -              repeat_to_do = num;
 -              repeat_done = 0;
 -      }
 -
 -      void reset_repeat ()
 -      {
 -              repeat_video.image.reset ();
 -              repeat_to_do = 0;
 -              repeat_done = 0;
 -      }
 -
 -      bool repeating () const
 -      {
 -              return repeat_done != repeat_to_do;
 -      }
 -
 -      void repeat (Player* player)
 -      {
 -              player->process_video (
 -                      repeat_video.weak_piece,
 -                      repeat_video.image,
 -                      repeat_video.eyes,
 -                      repeat_done > 0,
 -                      repeat_video.frame,
 -                      (repeat_done + 1) * (TIME_HZ / player->_film->video_frame_rate ())
 -                      );
 -
 -              ++repeat_done;
 -      }
 -      
        shared_ptr<Content> content;
        shared_ptr<Decoder> decoder;
 -      /** Time of the last video we emitted relative to the start of the DCP */
 -      Time video_position;
 -      /** Time of the last audio we emitted relative to the start of the DCP */
 -      Time audio_position;
 -
 -      IncomingVideo repeat_video;
 -      int repeat_to_do;
 -      int repeat_done;
 +      FrameRateChange frc;
  };
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        , _have_valid_pieces (false)
        , _video_position (0)
        , _audio_position (0)
 -      , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1))
 +      , _audio_merger (f->audio_channels(), f->audio_frame_rate ())
        , _last_emit_was_black (false)
 +      , _just_did_inaccurate_seek (false)
 +      , _approximate_size (false)
  {
        _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this));
        _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3));
@@@ -103,167 -148,111 +103,167 @@@ Player::pass (
                setup_pieces ();
        }
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      /* Interrogate all our pieces to find the one with the earliest decoded data */
 +
 +      shared_ptr<Piece> earliest_piece;
 +      shared_ptr<Decoded> earliest_decoded;
 +      DCPTime earliest_time = DCPTime::max ();
 +      DCPTime earliest_audio = DCPTime::max ();
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done ()) {
 -                      continue;
 -              }
  
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              DCPTime const offset = (*i)->content->position() - (*i)->content->trim_start();
 +              
 +              bool done = false;
 +              shared_ptr<Decoded> dec;
 +              while (!done) {
 +                      dec = (*i)->decoder->peek ();
 +                      if (!dec) {
 +                              /* Decoder has nothing else to give us */
 +                              break;
 +                      }
  
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +
 +                      dec->set_dcp_times ((*i)->frc, offset);
 +                      DCPTime const t = dec->dcp_time - offset;
 +                      cout << "Peeked " << (*i)->content->paths()[0] << " for " << t << " cf " << ((*i)->content->full_length() - (*i)->content->trim_end ()) << "\n";
 +                      if (t >= ((*i)->content->full_length() - (*i)->content->trim_end ())) {
 +                              /* In the end-trimmed part; decoder has nothing else to give us */
 +                              dec.reset ();
 +                              done = true;
 +                      } else if (t >= (*i)->content->trim_start ()) {
 +                              /* Within the un-trimmed part; everything's ok */
 +                              done = true;
 +                      } else {
 +                              /* Within the start-trimmed part; get something else */
 +                              (*i)->decoder->consume ();
                        }
                }
  
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 -                      }
 +              if (!dec) {
 +                      continue;
                }
 -      }
  
 -      if (!earliest) {
 +              if (dec->dcp_time < earliest_time) {
 +                      earliest_piece = *i;
 +                      earliest_decoded = dec;
 +                      earliest_time = dec->dcp_time;
 +              }
 +
 +              if (dynamic_pointer_cast<DecodedAudio> (dec) && dec->dcp_time < earliest_audio) {
 +                      earliest_audio = dec->dcp_time;
 +              }
 +      }
 +              
 +      if (!earliest_piece) {
                flush ();
                return true;
        }
  
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 -              } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 -                      } else {
 -                              earliest->decoder->pass ();
 -                      }
 -              }
 -              break;
 -
 -      case AUDIO:
 -              if (earliest_t > _audio_position) {
 -                      emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position));
 -              } else {
 -                      earliest->decoder->pass ();
 -
 -                      if (earliest->decoder->done()) {
 -                              shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content);
 -                              assert (ac);
 -                              shared_ptr<Resampler> re = resampler (ac, false);
 -                              if (re) {
 -                                      shared_ptr<const AudioBuffers> b = re->flush ();
 -                                      if (b->frames ()) {
 -                                              process_audio (earliest, b, ac->audio_length ());
 -                                      }
 -                              }
 -                      }
 +      if (earliest_audio != DCPTime::max ()) {
 +              if (earliest_audio.get() < 0) {
 +                      earliest_audio = DCPTime ();
                }
 -              break;
 +              TimedAudioBuffers<DCPTime> tb = _audio_merger.pull (earliest_audio);
 +              Audio (tb.audio, tb.time);
 +              /* This assumes that the audio-frames-to-time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _audio_position += DCPTime::from_frames (tb.audio->frames(), _film->audio_frame_rate ());
        }
  
 -      if (_audio) {
 -              boost::optional<Time> audio_done_up_to;
 -              for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -                      if ((*i)->decoder->done ()) {
 -                              continue;
 +      /* Emit the earliest thing */
 +
 +      shared_ptr<DecodedVideo> dv = dynamic_pointer_cast<DecodedVideo> (earliest_decoded);
 +      shared_ptr<DecodedAudio> da = dynamic_pointer_cast<DecodedAudio> (earliest_decoded);
 +      shared_ptr<DecodedImageSubtitle> dis = dynamic_pointer_cast<DecodedImageSubtitle> (earliest_decoded);
 +      shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (earliest_decoded);
 +
 +      /* Will be set to false if we shouldn't consume the peeked DecodedThing */
 +      bool consume = true;
 +
 +      if (dv && _video) {
 +
 +              if (_just_did_inaccurate_seek) {
 +
 +                      /* Just emit; no subtlety */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      
 +              } else if (dv->dcp_time > _video_position) {
 +
 +                      /* Too far ahead */
 +
 +                      list<shared_ptr<Piece> >::iterator i = _pieces.begin();
 +                      while (i != _pieces.end() && ((*i)->content->position() >= _video_position || _video_position >= (*i)->content->end())) {
 +                              ++i;
                        }
  
 -                      shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -                      if (ad && ad->has_audio ()) {
 -                              audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position);
 +                      if (i == _pieces.end() || !_last_incoming_video.video || !_have_valid_pieces) {
 +                              /* We're outside all video content */
 +                              emit_black ();
 +                              _statistics.video.black++;
 +                      } else {
 +                              /* We're inside some video; repeat the frame */
 +                              _last_incoming_video.video->dcp_time = _video_position;
 +                              emit_video (_last_incoming_video.weak_piece, _last_incoming_video.video);
 +                              step_video_position (_last_incoming_video.video);
 +                              _statistics.video.repeat++;
                        }
 +
 +                      consume = false;
 +
 +              } else if (dv->dcp_time == _video_position) {
 +                      /* We're ok */
 +                      emit_video (earliest_piece, dv);
 +                      step_video_position (dv);
 +                      _statistics.video.good++;
 +              } else {
 +                      /* Too far behind: skip */
 +                      _statistics.video.skip++;
                }
  
 -              if (audio_done_up_to) {
 -                      TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ());
 -                      Audio (tb.audio, tb.time);
 -                      _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              _just_did_inaccurate_seek = false;
 +
 +      } else if (da && _audio) {
 +
 +              if (da->dcp_time > _audio_position) {
 +                      /* Too far ahead */
 +                      emit_silence (da->dcp_time - _audio_position);
 +                      consume = false;
 +                      _statistics.audio.silence += (da->dcp_time - _audio_position);
 +              } else if (da->dcp_time == _audio_position) {
 +                      /* We're ok */
 +                      emit_audio (earliest_piece, da);
 +                      _statistics.audio.good += da->data->frames();
 +              } else {
 +                      /* Too far behind: skip */
 +                      _statistics.audio.skip += da->data->frames();
                }
 -      }
                
 +      } else if (dis && _video) {
 +              _image_subtitle.piece = earliest_piece;
 +              _image_subtitle.subtitle = dis;
 +              update_subtitle_from_image ();
 +      } else if (dts && _video) {
 +              _text_subtitle.piece = earliest_piece;
 +              _text_subtitle.subtitle = dts;
 +              update_subtitle_from_text ();
 +      }
 +
 +      if (consume) {
 +              earliest_piece->decoder->consume ();
 +      }                       
 +      
        return false;
  }
  
 -/** @param extra Amount of extra time to add to the content frame's time (for repeat) */
  void
 -Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const Image> image, Eyes eyes, bool same, VideoContent::Frame frame, Time extra)
 +Player::emit_video (weak_ptr<Piece> weak_piece, shared_ptr<DecodedVideo> video)
  {
        /* Keep a note of what came in so that we can repeat it if required */
        _last_incoming_video.weak_piece = weak_piece;
 -      _last_incoming_video.image = image;
 -      _last_incoming_video.eyes = eyes;
 -      _last_incoming_video.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 +      _last_incoming_video.video = video;
        
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
        shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
        assert (content);
  
 -      FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate());
 -      if (frc.skip && (frame % 2) == 1) {
 -              return;
 -      }
 +      FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate());
  
-       float const ratio = content->ratio() ? content->ratio()->ratio() : content->video_size_after_crop().ratio();
-       dcp::Size image_size = fit_ratio_within (ratio, _video_container_size);
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 -              return;
++      dcp::Size const image_size = content->scale().size (content, _video_container_size);
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
        }
  
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
 -      libdcp::Size const image_size = content->scale().size (content, _video_container_size);
 -
        shared_ptr<PlayerImage> pi (
                new PlayerImage (
 -                      image,
 +                      video->image,
                        content->crop(),
                        image_size,
                        _video_container_size,
                        )
                );
        
 -      if (_film->with_subtitles () && _out_subtitle.image && time >= _out_subtitle.from && time <= _out_subtitle.to) {
 +      if (
 +              _film->with_subtitles () &&
 +              _out_subtitle.image &&
 +              video->dcp_time >= _out_subtitle.from && video->dcp_time <= _out_subtitle.to
 +              ) {
  
                Position<int> const container_offset (
                        (_video_container_size.width - image_size.width) / 2,
 -                      (_video_container_size.height - image_size.width) / 2
 +                      (_video_container_size.height - image_size.height) / 2
                        );
  
                pi->set_subtitle (_out_subtitle.image, _out_subtitle.position + container_offset);
        _last_video = piece->content;
  #endif
  
 -      Video (pi, eyes, content->colour_conversion(), same, time);
 -
 +      Video (pi, video->eyes, content->colour_conversion(), video->same, video->dcp_time);
 +      
        _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +}
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +void
 +Player::step_video_position (shared_ptr<DecodedVideo> video)
 +{
 +      /* This is a bit of a hack; don't update _video_position if EYES_RIGHT is on its way */
 +      if (video->eyes != EYES_LEFT) {
 +              /* This assumes that the video-frames-to-time conversion is exact
 +                 so that there are no accumulated errors caused by rounding.
 +              */
 +              _video_position += DCPTime::from_frames (1, _film->video_frame_rate ());
        }
  }
  
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame)
 +Player::emit_audio (weak_ptr<Piece> weak_piece, shared_ptr<DecodedAudio> audio)
  {
        shared_ptr<Piece> piece = weak_piece.lock ();
        if (!piece) {
  
        /* Gain */
        if (content->audio_gain() != 0) {
 -              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 +              shared_ptr<AudioBuffers> gain (new AudioBuffers (audio->data));
                gain->apply_gain (content->audio_gain ());
 -              audio = gain;
 -      }
 -
 -      /* Resample */
 -      if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) {
 -              shared_ptr<Resampler> r = resampler (content, true);
 -              pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame);
 -              audio = ro.first;
 -              frame = ro.second;
 -      }
 -      
 -      Time const relative_time = _film->audio_frames_to_time (frame);
 -
 -      if (content->trimmed (relative_time)) {
 -              return;
 +              audio->data = gain;
        }
  
 -      Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
 -      
        /* Remap channels */
 -      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames()));
 +      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->data->frames()));
        dcp_mapped->make_silent ();
 -
        AudioMapping map = content->audio_mapping ();
        for (int i = 0; i < map.content_channels(); ++i) {
                for (int j = 0; j < _film->audio_channels(); ++j) {
 -                      if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) {
 +                      if (map.get (i, static_cast<dcp::Channel> (j)) > 0) {
                                dcp_mapped->accumulate_channel (
 -                                      audio.get(),
 +                                      audio->data.get(),
                                        i,
 -                                      static_cast<libdcp::Channel> (j),
 -                                      map.get (i, static_cast<libdcp::Channel> (j))
 +                                      static_cast<dcp::Channel> (j),
 +                                      map.get (i, static_cast<dcp::Channel> (j))
                                        );
                        }
                }
        }
  
 -      audio = dcp_mapped;
 +      audio->data = dcp_mapped;
  
 -      /* We must cut off anything that comes before the start of all time */
 -      if (time < 0) {
 -              int const frames = - time * _film->audio_frame_rate() / TIME_HZ;
 -              if (frames >= audio->frames ()) {
 +      /* Delay */
 +      audio->dcp_time += DCPTime::from_seconds (content->audio_delay() / 1000.0);
 +      if (audio->dcp_time < DCPTime (0)) {
 +              int const frames = - audio->dcp_time.frames (_film->audio_frame_rate());
 +              if (frames >= audio->data->frames ()) {
                        return;
                }
  
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 +              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->data->channels(), audio->data->frames() - frames));
 +              trimmed->copy_from (audio->data.get(), audio->data->frames() - frames, frames, 0);
  
 -              audio = trimmed;
 -              time = 0;
 +              audio->data = trimmed;
 +              audio->dcp_time = DCPTime ();
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      _audio_merger.push (audio->data, audio->dcp_time);
  }
  
  void
  Player::flush ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 +      TimedAudioBuffers<DCPTime> tb = _audio_merger.flush ();
        if (_audio && tb.audio) {
                Audio (tb.audio, tb.time);
 -              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 +              _audio_position += DCPTime::from_frames (tb.audio->frames (), _film->audio_frame_rate ());
        }
  
        while (_video && _video_position < _audio_position) {
        }
  
        while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +              emit_silence (_video_position - _audio_position);
        }
 -      
  }
  
  /** Seek so that the next pass() will yield (approximately) the requested frame.
   *  @return true on error
   */
  void
 -Player::seek (Time t, bool accurate)
 +Player::seek (DCPTime t, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
  
        for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content);
 -              if (!vc) {
 -                      continue;
 -              }
 -
                /* s is the offset of t from the start position of this content */
 -              Time s = t - vc->position ();
 -              s = max (static_cast<Time> (0), s);
 -              s = min (vc->length_after_trim(), s);
 +              DCPTime s = t - (*i)->content->position ();
 +              s = max (static_cast<DCPTime> (0), s);
 +              s = min ((*i)->content->length_after_trim(), s);
  
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 +              /* Convert this to the content time */
 +              ContentTime ct (s + (*i)->content->trim_start(), (*i)->frc);
  
                /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 -
 -              (*i)->reset_repeat ();
 +              (*i)->decoder->seek (ct, accurate);
        }
  
 -      _video_position = _audio_position = t;
 +      _video_position = t.round_up (_film->video_frame_rate());
 +      _audio_position = t.round_up (_film->audio_frame_rate());
 +
 +      _audio_merger.clear (_audio_position);
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      if (!accurate) {
 +              /* We just did an inaccurate seek, so it's likely that the next thing seen
 +                 out of pass() will be a fair distance from _{video,audio}_position.  Setting
 +                 this flag stops pass() from trying to fix that: we assume that if it
 +                 was an inaccurate seek then the caller does not care too much about
 +                 inserting black/silence to keep the time tidy.
 +              */
 +              _just_did_inaccurate_seek = true;
 +      }
  }
  
  void
  Player::setup_pieces ()
  {
        list<shared_ptr<Piece> > old_pieces = _pieces;
 -
        _pieces.clear ();
  
        ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
  
        for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
                if (!(*i)->paths_valid ()) {
                        continue;
                }
 +              
 +              shared_ptr<Decoder> decoder;
 +              optional<FrameRateChange> frc;
 +
 +              /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
 +              DCPTime best_overlap_t;
 +              shared_ptr<VideoContent> best_overlap;
 +              for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
 +                      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
 +                      if (!vc) {
 +                              continue;
 +                      }
 +                      
 +                      DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
 +                      if (overlap > best_overlap_t) {
 +                              best_overlap = vc;
 +                              best_overlap_t = overlap;
 +                      }
 +              }
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 -
 -              /* XXX: into content? */
 +              optional<FrameRateChange> best_overlap_frc;
 +              if (best_overlap) {
 +                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
 +              } else {
 +                      /* No video overlap; e.g. if the DCP is just audio */
 +                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
 +              }
  
 +              /* FFmpeg */
                shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
                if (fc) {
 -                      shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio));
 -                      
 -                      fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 -                      fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4));
 -
 -                      fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true);
 -                      piece->decoder = fd;
 +                      decoder.reset (new FFmpegDecoder (_film, fc, _video, _audio));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
 -              
 +
 +              /* ImageContent */
                shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
                if (ic) {
 -                      bool reusing = false;
 -                      
                        /* See if we can re-use an old ImageDecoder */
                        for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
                                shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder);
                                if (imd && imd->content() == ic) {
 -                                      piece = *j;
 -                                      reusing = true;
 +                                      decoder = imd;
                                }
                        }
  
 -                      if (!reusing) {
 -                              shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic));
 -                              id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, 0));
 -                              piece->decoder = id;
 +                      if (!decoder) {
 +                              decoder.reset (new ImageDecoder (_film, ic));
                        }
 +
 +                      frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
  
 +              /* SndfileContent */
                shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
                if (sc) {
 -                      shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc));
 -                      sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2));
 +                      decoder.reset (new SndfileDecoder (_film, sc));
 +                      frc = best_overlap_frc;
 +              }
  
 -                      piece->decoder = sd;
 +              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
 +                      decoder.reset (new SubRipDecoder (_film, rc));
 +                      frc = best_overlap_frc;
                }
  
 -              _pieces.push_back (piece);
 +              ContentTime st ((*i)->trim_start(), frc.get ());
 +              decoder->seek (st, true);
 +              
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
  
        _have_valid_pieces = true;
 +
 +      /* The Piece for the _last_incoming_video will no longer be valid */
 +      _last_incoming_video.video.reset ();
 +
 +      _video_position = DCPTime ();
 +      _audio_position = DCPTime ();
  }
  
  void
@@@ -563,12 -532,11 +562,12 @@@ Player::content_changed (weak_ptr<Conte
                property == SubtitleContentProperty::SUBTITLE_SCALE
                ) {
  
 -              update_subtitle ();
 +              update_subtitle_from_image ();
 +              update_subtitle_from_text ();
                Changed (frequent);
  
        } else if (
-               property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_RATIO ||
+               property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_SCALE ||
                property == VideoContentProperty::VIDEO_FRAME_RATE
                ) {
                
@@@ -589,7 -557,7 +588,7 @@@ Player::playlist_changed (
  }
  
  void
 -Player::set_video_container_size (libdcp::Size s)
 +Player::set_video_container_size (dcp::Size s)
  {
        _video_container_size = s;
  
                );
  }
  
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 -{
 -      map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c);
 -      if (i != _resamplers.end ()) {
 -              return i->second;
 -      }
 -
 -      if (!create) {
 -              return shared_ptr<Resampler> ();
 -      }
 -
 -      _film->log()->log (
 -              String::compose (
 -                      "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
 -                      )
 -              );
 -      
 -      shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
 -      _resamplers[c] = r;
 -      return r;
 -}
 -
  void
  Player::emit_black ()
  {
  #endif
  
        Video (_black_frame, EYES_BOTH, ColourConversion(), _last_emit_was_black, _video_position);
 -      _video_position += _film->video_frames_to_time (1);
 +      _video_position += DCPTime::from_frames (1, _film->video_frame_rate ());
        _last_emit_was_black = true;
  }
  
  void
 -Player::emit_silence (OutputAudioFrame most)
 +Player::emit_silence (DCPTime most)
  {
        if (most == 0) {
                return;
        }
        
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 +      DCPTime t = min (most, DCPTime::from_seconds (0.5));
 +      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), t.frames (_film->audio_frame_rate())));
        silence->make_silent ();
        Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      
 +      _audio_position += t;
  }
  
  void
@@@ -648,14 -638,26 +647,14 @@@ Player::film_changed (Film::Property p
  }
  
  void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 -{
 -      _in_subtitle.piece = weak_piece;
 -      _in_subtitle.image = image;
 -      _in_subtitle.rect = rect;
 -      _in_subtitle.from = from;
 -      _in_subtitle.to = to;
 -
 -      update_subtitle ();
 -}
 -
 -void
 -Player::update_subtitle ()
 +Player::update_subtitle_from_image ()
  {
 -      shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
 +      shared_ptr<Piece> piece = _image_subtitle.piece.lock ();
        if (!piece) {
                return;
        }
  
 -      if (!_in_subtitle.image) {
 +      if (!_image_subtitle.subtitle->image) {
                _out_subtitle.image.reset ();
                return;
        }
        shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
        assert (sc);
  
 -      dcpomatic::Rect<double> in_rect = _in_subtitle.rect;
 -      libdcp::Size scaled_size;
 +      dcpomatic::Rect<double> in_rect = _image_subtitle.subtitle->rect;
 +      dcp::Size scaled_size;
  
        in_rect.x += sc->subtitle_x_offset ();
        in_rect.y += sc->subtitle_y_offset ();
        _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
        _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
        
 -      _out_subtitle.image = _in_subtitle.image->scale (
 +      _out_subtitle.image = _image_subtitle.subtitle->image->scale (
                scaled_size,
                Scaler::from_id ("bicubic"),
 -              _in_subtitle.image->pixel_format (),
 +              _image_subtitle.subtitle->image->pixel_format (),
                true
                );
 -
 -      /* XXX: hack */
 -      Time from = _in_subtitle.from;
 -      Time to = _in_subtitle.to;
 -      shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (piece->content);
 -      if (vc) {
 -              from = rint (from * vc->video_frame_rate() / _film->video_frame_rate());
 -              to = rint (to * vc->video_frame_rate() / _film->video_frame_rate());
 -      }
        
 -      _out_subtitle.from = from + piece->content->position ();
 -      _out_subtitle.to = to + piece->content->position ();
 +      _out_subtitle.from = _image_subtitle.subtitle->dcp_time + piece->content->position ();
 +      _out_subtitle.to = _image_subtitle.subtitle->dcp_time_to + piece->content->position ();
  }
  
  /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
  bool
  Player::repeat_last_video ()
  {
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 +      if (!_last_incoming_video.video || !_have_valid_pieces) {
                return false;
        }
  
 -      process_video (
 +      emit_video (
                _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 +              _last_incoming_video.video
                );
  
        return true;
  }
  
 +void
 +Player::update_subtitle_from_text ()
 +{
 +      if (_text_subtitle.subtitle->subs.empty ()) {
 +              _out_subtitle.image.reset ();
 +              return;
 +      }
 +
 +      render_subtitles (_text_subtitle.subtitle->subs, _video_container_size, _out_subtitle.image, _out_subtitle.position);
 +}
 +
 +void
 +Player::set_approximate_size ()
 +{
 +      _approximate_size = true;
 +}
 +                            
  PlayerImage::PlayerImage (
        shared_ptr<const Image> in,
        Crop crop,
 -      libdcp::Size inter_size,
 -      libdcp::Size out_size,
 +      dcp::Size inter_size,
 +      dcp::Size out_size,
        Scaler const * scaler
        )
        : _in (in)
@@@ -758,10 -756,10 +757,10 @@@ PlayerImage::set_subtitle (shared_ptr<c
  }
  
  shared_ptr<Image>
 -PlayerImage::image ()
 +PlayerImage::image (AVPixelFormat format, bool aligned)
  {
 -      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false);
 -
 +      shared_ptr<Image> out = _in->crop_scale_window (_crop, _inter_size, _out_size, _scaler, format, aligned);
 +      
        Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2);
  
        if (_subtitle_image) {
  
        return out;
  }
 +
 +void
 +PlayerStatistics::dump (shared_ptr<Log> log) const
 +{
 +      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat));
 +      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence));
 +}
 +
 +PlayerStatistics const &
 +Player::statistics () const
 +{
 +      return _statistics;
 +}
diff --combined src/lib/util.cc
index 78e7b480c9c91f3c07485e886644fc864a66d7dd,48b418d37634fe6ecdd41bb507ae6d54830485d5..e8f83d4e4d45a21fc524006b7f155addb683799f
  #include <openssl/md5.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 <pangomm/init.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,6 -70,7 +71,7 @@@
  #include "ratio.h"
  #include "job.h"
  #include "cross.h"
+ #include "video_content.h"
  #ifdef DCPOMATIC_WINDOWS
  #include "stack.hpp"
  #endif
@@@ -102,7 -102,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;
@@@ -252,7 -252,7 +253,7 @@@ dependency_version_summary (
          << 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;
 +        << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit;
  
        return s.str ();
  }
@@@ -343,10 -343,10 +344,11 @@@ dcpomatic_setup (
  
        set_terminate (terminate);
  
 -      libdcp::init ();
 +      Pango::init ();
 +      dcp::init ();
        
        Ratio::setup_ratios ();
+       VideoContentScale::setup_scales ();
        DCPContentType::setup_dcp_content_types ();
        Scaler::setup_scalers ();
        Filter::setup_filters ();
@@@ -492,6 -492,33 +494,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).
   */
@@@ -751,6 -778,17 +753,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)
  {
@@@ -816,7 -892,7 +818,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>
@@@ -915,14 -991,14 +917,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 *
diff --combined src/lib/video_content.cc
index b30aa3df4c8ae1c0e3ff5a1ee9f5d624f14d1d83,7bf2a6b621284a5c6ea3e3cc31538d2263df0f27..b33a37cb8ce385892d3b2aac663e05857e46cc1d
@@@ -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
  
  #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 "ratio.h"
  #include "compose.hpp"
+ #include "ratio.h"
  #include "config.h"
  #include "colour_conversion.h"
  #include "util.h"
@@@ -36,7 -36,7 +36,7 @@@ int const VideoContentProperty::VIDEO_S
  int const VideoContentProperty::VIDEO_FRAME_RATE  = 1;
  int const VideoContentProperty::VIDEO_FRAME_TYPE  = 2;
  int const VideoContentProperty::VIDEO_CROP      = 3;
- int const VideoContentProperty::VIDEO_RATIO     = 4;
+ int const VideoContentProperty::VIDEO_SCALE     = 4;
  int const VideoContentProperty::COLOUR_CONVERSION = 5;
  
  using std::string;
@@@ -49,22 -49,24 +49,24 @@@ using boost::lexical_cast
  using boost::optional;
  using boost::dynamic_pointer_cast;
  
+ vector<VideoContentScale> VideoContentScale::_scales;
  VideoContent::VideoContent (shared_ptr<const Film> f)
        : Content (f)
        , _video_length (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
-       , _ratio (Ratio::from_id ("185"))
+       , _scale (Ratio::from_id ("185"))
  {
        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)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
-       , _ratio (Ratio::from_id ("185"))
+       , _scale (Ratio::from_id ("185"))
  {
        setup_default_colour_conversion ();
  }
@@@ -74,16 -76,15 +76,15 @@@ VideoContent::VideoContent (shared_ptr<
        , _video_length (0)
        , _video_frame_rate (0)
        , _video_frame_type (VIDEO_FRAME_TYPE_2D)
-       , _ratio (Ratio::from_id ("185"))
+       , _scale (Ratio::from_id ("185"))
  {
        setup_default_colour_conversion ();
  }
  
- VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node)
+ VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version)
        : Content (f, node)
-       , _ratio (0)
  {
 -      _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");
        _crop.right = node->number_child<int> ("RightCrop");
        _crop.top = node->number_child<int> ("TopCrop");
        _crop.bottom = node->number_child<int> ("BottomCrop");
-       optional<string> r = node->optional_string_child ("Ratio");
-       if (r) {
-               _ratio = Ratio::from_id (r.get ());
+       if (version <= 7) {
+               optional<string> r = node->optional_string_child ("Ratio");
+               if (r) {
+                       _scale = VideoContentScale (Ratio::from_id (r.get ()));
+               }
+       } else {
+               _scale = VideoContentScale (node->node_child ("Scale"));
        }
+       
        _colour_conversion = ColourConversion (node->node_child ("ColourConversion"));
  }
  
@@@ -125,8 -132,8 +132,8 @@@ VideoContent::VideoContent (shared_ptr<
                        throw JoinError (_("Content to be joined must have the same crop."));
                }
  
-               if (vc->ratio() != ref->ratio()) {
-                       throw JoinError (_("Content to be joined must have the same ratio."));
+               if (vc->scale() != ref->scale()) {
+                       throw JoinError (_("Content to be joined must have the same scale setting."));
                }
  
                if (vc->colour_conversion() != ref->colour_conversion()) {
        _video_frame_rate = ref->video_frame_rate ();
        _video_frame_type = ref->video_frame_type ();
        _crop = ref->crop ();
-       _ratio = ref->ratio ();
+       _scale = ref->scale ();
        _colour_conversion = ref->colour_conversion ();
  }
  
@@@ -157,23 -164,21 +164,21 @@@ VideoContent::as_xml (xmlpp::Node* node
        node->add_child("RightCrop")->add_child_text (boost::lexical_cast<string> (_crop.right));
        node->add_child("TopCrop")->add_child_text (boost::lexical_cast<string> (_crop.top));
        node->add_child("BottomCrop")->add_child_text (boost::lexical_cast<string> (_crop.bottom));
-       if (_ratio) {
-               node->add_child("Ratio")->add_child_text (_ratio->id ());
-       }
+       _scale.as_xml (node->add_child("Scale"));
        _colour_conversion.as_xml (node->add_child("ColourConversion"));
  }
  
  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 ();
        
        {
@@@ -268,18 -273,18 +273,18 @@@ VideoContent::set_bottom_crop (int c
  }
  
  void
- VideoContent::set_ratio (Ratio const * r)
+ VideoContent::set_scale (VideoContentScale s)
  {
        {
                boost::mutex::scoped_lock lm (_mutex);
-               if (_ratio == r) {
+               if (_scale == s) {
                        return;
                }
  
-               _ratio = r;
+               _scale = s;
        }
  
-       signal_changed (VideoContentProperty::VIDEO_RATIO);
+       signal_changed (VideoContentProperty::VIDEO_SCALE);
  }
  
  /** @return string which includes everything about how this content looks */
@@@ -292,12 -297,9 +297,9 @@@ VideoContent::identifier () cons
          << "_" << crop().right
          << "_" << crop().top
          << "_" << crop().bottom
+         << "_" << scale().id()
          << "_" << colour_conversion().identifier ();
  
-       if (ratio()) {
-               s << "_" << ratio()->id ();
-       }
        return s.str ();
  }
  
@@@ -318,17 -320,17 +320,17 @@@ VideoContent::technical_summary () cons
        return String::compose ("video: length %1, size %2x%3, rate %4", video_length(), 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:
                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);
@@@ -346,19 -348,136 +348,129 @@@ 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)
+       : _ratio (r)
+       , _scale (true)
+ {
+ }
+ VideoContentScale::VideoContentScale ()
+       : _ratio (0)
+       , _scale (false)
+ {
+ }
+ VideoContentScale::VideoContentScale (bool scale)
+       : _ratio (0)
+       , _scale (scale)
+ {
+ }
+ VideoContentScale::VideoContentScale (shared_ptr<cxml::Node> node)
+       : _ratio (0)
+       , _scale (true)
+ {
+       optional<string> r = node->optional_string_child ("Ratio");
+       if (r) {
+               _ratio = Ratio::from_id (r.get ());
+       } else {
+               _scale = node->bool_child ("Scale");
+       }
+ }
+ void
+ VideoContentScale::as_xml (xmlpp::Node* node) const
+ {
+       if (_ratio) {
+               node->add_child("Ratio")->add_child_text (_ratio->id ());
+       } else {
+               node->add_child("Scale")->add_child_text (_scale ? "1" : "0");
+       }
+ }
+ string
+ VideoContentScale::id () const
+ {
+       stringstream s;
+       
+       if (_ratio) {
+               s << _ratio->id () << "_";
+       } else {
+               s << (_scale ? "S1" : "S0");
+       }
+       
+       return s.str ();
+ }
+ string
+ VideoContentScale::name () const
+ {
+       if (_ratio) {
+               return _ratio->nickname ();
+       }
+       if (_scale) {
+               return _("No stretch");
+       }
+       return _("No scale");
+ }
+ libdcp::Size
+ VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size container) const
+ {
+       if (_ratio) {
+               return fit_ratio_within (_ratio->ratio (), container);
+       }
+       /* Force scale if the container is smaller than the content's image */
+       if (_scale || container.width < c->video_size().width || container.height < c->video_size().height) {
+               return fit_ratio_within (c->video_size().ratio (), container);
+       }
+       return c->video_size ();
+ }
+ void
+ VideoContentScale::setup_scales ()
+ {
+       vector<Ratio const *> ratios = Ratio::all ();
+       for (vector<Ratio const *>::const_iterator i = ratios.begin(); i != ratios.end(); ++i) {
+               _scales.push_back (VideoContentScale (*i));
+       }
+       _scales.push_back (VideoContentScale (true));
+       _scales.push_back (VideoContentScale (false));
+ }
+ bool
+ operator== (VideoContentScale const & a, VideoContentScale const & b)
+ {
+       return (a.ratio() == b.ratio() && a.scale() == b.scale());
+ }
+ bool
+ operator!= (VideoContentScale const & a, VideoContentScale const & b)
+ {
+       return (a.ratio() != b.ratio() || a.scale() != b.scale());
+ }
diff --combined src/lib/video_content.h
index d673d53728b6bc1d363cddc018ba11ed3bbf7713,c98a52d3a7cc17c842ddcde6634cf7951d362541..ebd9f8c72de7b73777b84e5f8b114f4e6f5bb104
@@@ -33,19 -33,55 +33,55 @@@ public
        static int const VIDEO_FRAME_RATE;
        static int const VIDEO_FRAME_TYPE;
        static int const VIDEO_CROP;
-       static int const VIDEO_RATIO;
+       static int const VIDEO_SCALE;
        static int const COLOUR_CONVERSION;
  };
  
+ class VideoContentScale
+ {
+ public:
+       VideoContentScale ();
+       VideoContentScale (Ratio const *);
+       VideoContentScale (bool);
+       VideoContentScale (boost::shared_ptr<cxml::Node>);
+       libdcp::Size size (boost::shared_ptr<const VideoContent>, libdcp::Size) const;
+       std::string id () const;
+       std::string name () const;
+       void as_xml (xmlpp::Node *) const;
+       Ratio const * ratio () const {
+               return _ratio;
+       }
+       bool scale () const {
+               return _scale;
+       }
+       static void setup_scales ();
+       static std::vector<VideoContentScale> all () {
+               return _scales;
+       }
+ private:
+       Ratio const * _ratio;
+       bool _scale;
+       static std::vector<VideoContentScale> _scales;
+ };
+ bool operator== (VideoContentScale const & a, VideoContentScale const & b);
+ bool operator!= (VideoContentScale const & a, VideoContentScale const & b);
  class VideoContent : public virtual Content
  {
  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>);
+       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> >);
  
        void as_xml (xmlpp::Node *) const;
        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;
        }
  
 -      libdcp::Size video_size () const {
 +      dcp::Size video_size () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_size;
        }
        void set_top_crop (int);
        void set_bottom_crop (int);
  
+       void set_scale (VideoContentScale);
        void set_colour_conversion (ColourConversion);
+       
        VideoFrameType video_frame_type () const {
                boost::mutex::scoped_lock lm (_mutex);
                return _video_frame_type;
                boost::mutex::scoped_lock lm (_mutex);
                return _crop.bottom;
        }
-       
-       void set_ratio (Ratio const *);
  
-       /** @return ratio to scale to, or 0 if the content's own ratio should be preserved. */
-       Ratio const * ratio () const {
+       /** @return Description of how to scale this content (if indeed it should be scaled) */
+       VideoContentScale scale () const {
                boost::mutex::scoped_lock lm (_mutex);
-               return _ratio;
+               return _scale;
        }
  
        ColourConversion colour_conversion () const {
                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;
-       Ratio const * _ratio;
+       VideoContentScale _scale;
        ColourConversion _colour_conversion;
  };
  
diff --combined src/wx/video_panel.cc
index 6a6e694cbe0423a7431cd0636d6f1805a55420d3,492968f1bea567e35e383a6a3ff55d2b03f5ec7c..eb45d4bc81a0b82ec38c35e342866c1da0cb7432
@@@ -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 <wx/spinctrl.h>
- #include "lib/ratio.h"
  #include "lib/filter.h"
  #include "lib/ffmpeg_content.h"
  #include "lib/colour_conversion.h"
  #include "lib/config.h"
  #include "lib/util.h"
+ #include "lib/ratio.h"
  #include "filter_dialog.h"
  #include "video_panel.h"
  #include "wx_util.h"
@@@ -41,29 -41,26 +41,26 @@@ using boost::dynamic_pointer_cast
  using boost::bind;
  using boost::optional;
  
- static Ratio const *
- index_to_ratio (int n)
+ static VideoContentScale
+ index_to_scale (int n)
  {
+       vector<VideoContentScale> scales = VideoContentScale::all ();
        assert (n >= 0);
-       
-       vector<Ratio const *> ratios = Ratio::all ();
-       if (n >= int (ratios.size ())) {
-               return 0;
-       }
-       return ratios[n];
+       assert (n < int (scales.size ()));
+       return scales[n];
  }
  
  static int
ratio_to_index (Ratio const * r)
scale_to_index (VideoContentScale scale)
  {
-       vector<Ratio const *> ratios = Ratio::all ();
-       size_t i = 0;
-       while (i < ratios.size() && ratios[i] != r) {
-               ++i;
+       vector<VideoContentScale> scales = VideoContentScale::all ();
+       for (size_t i = 0; i < scales.size(); ++i) {
+               if (scales[i] == scale) {
+                       return i;
+               }
        }
  
-       return i;
+       assert (false);
  }
  
  VideoPanel::VideoPanel (FilmEditor* e)
        ++r;
  
        add_label_to_grid_bag_sizer (grid, this, _("Scale to"), true, wxGBPosition (r, 0));
-       _ratio = new ContentChoice<VideoContent, Ratio const *> (
+       _scale = new ContentChoice<VideoContent, VideoContentScale> (
                this,
                new wxChoice (this, wxID_ANY),
-               VideoContentProperty::VIDEO_RATIO,
-               boost::mem_fn (&VideoContent::ratio),
-               boost::mem_fn (&VideoContent::set_ratio),
-               &index_to_ratio,
-               &ratio_to_index
+               VideoContentProperty::VIDEO_SCALE,
+               boost::mem_fn (&VideoContent::scale),
+               boost::mem_fn (&VideoContent::set_scale),
+               &index_to_scale,
+               &scale_to_index
                );
-       _ratio->add (grid, wxGBPosition (r, 1));
+       _scale->add (grid, wxGBPosition (r, 1));
        ++r;
  
        {
        _right_crop->wrapped()->SetRange (0, 1024);
        _bottom_crop->wrapped()->SetRange (0, 1024);
  
-       vector<Ratio const *> ratios = Ratio::all ();
-       _ratio->wrapped()->Clear ();
-       for (vector<Ratio const *>::iterator i = ratios.begin(); i != ratios.end(); ++i) {
-               _ratio->wrapped()->Append (std_to_wx ((*i)->nickname ()));
+       vector<VideoContentScale> scales = VideoContentScale::all ();
+       _scale->wrapped()->Clear ();
+       for (vector<VideoContentScale>::iterator i = scales.begin(); i != scales.end(); ++i) {
+               _scale->wrapped()->Append (std_to_wx (i->name ()));
        }
-       _ratio->wrapped()->Append (_("No stretch"));
  
        _frame_type->wrapped()->Append (_("2D"));
        _frame_type->wrapped()->Append (_("3D left/right"));
@@@ -234,7 -230,7 +230,7 @@@ VideoPanel::film_content_changed (int p
                setup_description ();
        } else if (property == VideoContentProperty::VIDEO_CROP) {
                setup_description ();
-       } else if (property == VideoContentProperty::VIDEO_RATIO) {
+       } else if (property == VideoContentProperty::VIDEO_SCALE) {
                setup_description ();
        } else if (property == VideoContentProperty::VIDEO_FRAME_RATE) {
                setup_description ();
@@@ -299,8 -295,8 +295,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;
        }
  
-       Ratio const * ratio = vcs->ratio ();
-       dcp::Size container_size = fit_ratio_within (_editor->film()->container()->ratio (), _editor->film()->full_frame ());
-       float const ratio_value = ratio ? ratio->ratio() : vcs->video_size_after_crop().ratio ();
 -      libdcp::Size const container_size = fit_ratio_within (_editor->film()->container()->ratio (), _editor->film()->full_frame ());
 -      libdcp::Size const scaled = vcs->scale().size (vcs, container_size);
++      dcp::Size const container_size = fit_ratio_within (_editor->film()->container()->ratio (), _editor->film()->full_frame ());
++      dcp::Size const scaled = vcs->scale().size (vcs, container_size);
  
-       /* We have a specified ratio to scale to */
-       dcp::Size const scaled = fit_ratio_within (ratio_value, container_size);
-       
-       d << wxString::Format (
-               _("Scaled to %dx%d (%.2f:1)\n"),
-               scaled.width, scaled.height,
-               scaled.ratio ()
-               );
-       ++lines;
+       if (scaled != vcs->video_size_after_crop ()) {
+               d << wxString::Format (
+                       _("Scaled to %dx%d (%.2f:1)\n"),
+                       scaled.width, scaled.height,
+                       scaled.ratio ()
+                       );
+               ++lines;
+       }
        
        if (scaled != container_size) {
                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;
  
@@@ -374,7 -368,7 +368,7 @@@ VideoPanel::content_selection_changed (
        _top_crop->set_content (sel);
        _bottom_crop->set_content (sel);
        _frame_type->set_content (sel);
-       _ratio->set_content (sel);
+       _scale->set_content (sel);
  
        /* Things that are only allowed with single selections */
        _filters_button->Enable (single);
diff --combined test/black_fill_test.cc
index 101fe0523779d70866ef6ff58dbcedb9263aa9db,c2170d891744f3ce14848a9a372acdd0cdd5eb2a..a7e44bdfbd34f3360f5460bcc612b8562de70004
@@@ -38,18 -38,18 +38,18 @@@ BOOST_AUTO_TEST_CASE (black_fill_test
        film->set_container (Ratio::from_id ("185"));
        film->set_sequence_video (false);
        shared_ptr<ImageContent> contentA (new ImageContent (film, "test/data/simple_testcard_640x480.png"));
-       contentA->set_ratio (Ratio::from_id ("185"));
+       contentA->set_scale (VideoContentScale (Ratio::from_id ("185")));
        shared_ptr<ImageContent> contentB (new ImageContent (film, "test/data/simple_testcard_640x480.png"));
-       contentB->set_ratio (Ratio::from_id ("185"));
+       contentB->set_scale (VideoContentScale (Ratio::from_id ("185")));
  
        film->examine_and_add_content (contentA);
        film->examine_and_add_content (contentB);
        wait_for_jobs ();
  
 -      contentA->set_video_length (3);
 -      contentA->set_position (film->video_frames_to_time (2));
 -      contentB->set_video_length (1);
 -      contentB->set_position (film->video_frames_to_time (7));
 +      contentA->set_video_length (ContentTime::from_frames (3, 24));
 +      contentA->set_position (DCPTime::from_frames (2, film->video_frame_rate ()));
 +      contentB->set_video_length (ContentTime::from_frames (1, 24));
 +      contentB->set_position (DCPTime::from_frames (7, film->video_frame_rate ()));
  
        film->make_dcp ();
  
index 6699d1ef7ca6247a645d9eb902d40098ae882038,2e83d45c9101dbef0c064e3c0e26ef5446bcab85..8942558791a8e10b07eee8a64d783651a8f00960
  
  */
  
 +/** @file  test/ffmpeg_audio_test.cc
 + *  @brief A simple test of reading audio from an FFmpeg file.
 + */
 +
  #include <boost/test/unit_test.hpp>
 -#include <libdcp/cpl.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/sound_frame.h>
 -#include <libdcp/reel.h>
 +#include <dcp/cpl.h>
 +#include <dcp/dcp.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_frame.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/reel.h>
  #include "lib/sndfile_content.h"
  #include "lib/film.h"
  #include "lib/dcp_content_type.h"
@@@ -44,7 -39,7 +44,7 @@@ BOOST_AUTO_TEST_CASE (ffmpeg_audio_test
        shared_ptr<Film> film = new_test_film ("ffmpeg_audio_test");
        film->set_name ("ffmpeg_audio_test");
        shared_ptr<FFmpegContent> c (new FFmpegContent (film, "test/data/staircase.mov"));
-       c->set_ratio (Ratio::from_id ("185"));
+       c->set_scale (VideoContentScale (Ratio::from_id ("185")));
        film->examine_and_add_content (c);
  
        wait_for_jobs ();
        boost::filesystem::path path = "build/test";
        path /= "ffmpeg_audio_test";
        path /= film->dcp_name ();
 -      libdcp::DCP check (path.string ());
 +      dcp::DCP check (path.string ());
        check.read ();
  
 -      shared_ptr<const libdcp::SoundAsset> sound_asset = check.cpls().front()->reels().front()->main_sound ();
 +      shared_ptr<const dcp::ReelSoundAsset> sound_asset = check.cpls().front()->reels().front()->main_sound ();
        BOOST_CHECK (sound_asset);
 -      BOOST_CHECK (sound_asset->channels () == 6);
 +      BOOST_CHECK (sound_asset->mxf()->channels () == 6);
  
        /* Sample index in the DCP */
        int n = 0;
        /* DCP sound asset frame */
        int frame = 0;
  
 -      while (n < sound_asset->intrinsic_duration()) {
 -              shared_ptr<const libdcp::SoundFrame> sound_frame = sound_asset->get_frame (frame++);
 +      while (n < sound_asset->mxf()->intrinsic_duration()) {
 +              shared_ptr<const dcp::SoundFrame> sound_frame = sound_asset->mxf()->get_frame (frame++);
                uint8_t const * d = sound_frame->data ();
                
 -              for (int i = 0; i < sound_frame->size(); i += (3 * sound_asset->channels())) {
 +              for (int i = 0; i < sound_frame->size(); i += (3 * sound_asset->mxf()->channels())) {
  
 -                      if (sound_asset->channels() > 0) {
 +                      if (sound_asset->mxf()->channels() > 0) {
                                /* L should be silent */
                                int const sample = d[i + 0] | (d[i + 1] << 8);
                                BOOST_CHECK_EQUAL (sample, 0);
                        }
  
 -                      if (sound_asset->channels() > 1) {
 +                      if (sound_asset->mxf()->channels() > 1) {
                                /* R should be silent */
                                int const sample = d[i + 2] | (d[i + 3] << 8);
                                BOOST_CHECK_EQUAL (sample, 0);
                        }
                        
 -                      if (sound_asset->channels() > 2) {
 +                      if (sound_asset->mxf()->channels() > 2) {
                                /* Mono input so it will appear on centre */
                                int const sample = d[i + 7] | (d[i + 8] << 8);
                                BOOST_CHECK_EQUAL (sample, n);
                        }
  
 -                      if (sound_asset->channels() > 3) {
 +                      if (sound_asset->mxf()->channels() > 3) {
                                /* Lfe should be silent */
                                int const sample = d[i + 9] | (d[i + 10] << 8);
                                BOOST_CHECK_EQUAL (sample, 0);
                        }
  
 -                      if (sound_asset->channels() > 4) {
 +                      if (sound_asset->mxf()->channels() > 4) {
                                /* Ls should be silent */
                                int const sample = d[i + 11] | (d[i + 12] << 8);
                                BOOST_CHECK_EQUAL (sample, 0);
                        }
  
  
 -                      if (sound_asset->channels() > 5) {
 +                      if (sound_asset->mxf()->channels() > 5) {
                                /* Rs should be silent */
                                int const sample = d[i + 13] | (d[i + 14] << 8);
                                BOOST_CHECK_EQUAL (sample, 0);
diff --combined test/scaling_test.cc
index 7002019ddfee42ea495a2f931ff3c2323db1dc2f,f0cf3fe4a5bf89a3b473abac7af75d9071b89076..bf8da8aac59ce4288e5ec1c7cb43b3e11779eab0
@@@ -33,7 -33,7 +33,7 @@@ using boost::shared_ptr
  
  static void scaling_test_for (shared_ptr<Film> film, shared_ptr<VideoContent> content, string image, string container)
  {
-       content->set_ratio (Ratio::from_id (image));
+       content->set_scale (VideoContentScale (Ratio::from_id (image)));
        film->set_container (Ratio::from_id (container));
        film->make_dcp ();
  
@@@ -64,7 -64,7 +64,7 @@@ BOOST_AUTO_TEST_CASE (scaling_test
  
        wait_for_jobs ();
        
 -      imc->set_video_length (1);
 +      imc->set_video_length (ContentTime::from_frames (1, 24));
  
        scaling_test_for (film, imc, "133", "185");
        scaling_test_for (film, imc, "185", "185");
diff --combined wscript
index 708e48910977ceb7c68017281e735b5293bb4eb9,e5d0fc9a0f95215251a5fbeffcea6243b490f097..8027ea88c086f8d93a6501737a8ce8fddedb3934
+++ b/wscript
@@@ -3,7 -3,7 +3,11 @@@ import o
  import sys
  
  APPNAME = 'dcpomatic'
++<<<<<<< HEAD
 +VERSION = '2.0.0devel'
++=======
+ VERSION = '1.64.18devel'
++>>>>>>> master
  
  def options(opt):
      opt.load('compiler_cxx')
@@@ -55,9 -55,9 +59,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:
@@@ -81,7 -81,7 +85,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):
@@@ -296,8 -296,6 +300,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>