Merge master.
authorCarl Hetherington <cth@carlh.net>
Wed, 4 Jun 2014 11:33:41 +0000 (12:33 +0100)
committerCarl Hetherington <cth@carlh.net>
Wed, 4 Jun 2014 11:33:41 +0000 (12:33 +0100)
15 files changed:
1  2 
ChangeLog
src/lib/audio_content.cc
src/lib/audio_mapping.cc
src/lib/audio_mapping.h
src/lib/colour_conversion.cc
src/lib/dcp_video_frame.cc
src/lib/image.cc
src/lib/player.cc
src/lib/playlist.cc
src/lib/util.cc
src/lib/util.h
src/lib/writer.cc
src/lib/wscript
src/wx/audio_mapping_view.cc
src/wx/audio_mapping_view.h

diff --combined ChangeLog
index e31fc719ebcf398dddbe0f0ee605bc0620b00612,a0e776b0e0bcf66a52516f815f9ec976c8fda873..78954c82da4b1fe42918bf5e7af6ddcc255d87ee
+++ b/ChangeLog
@@@ -1,7 -1,25 +1,29 @@@
 +2014-03-07  Carl Hetherington  <cth@carlh.net>
 +
 +      * Add subtitle view.
 +
+ 2014-06-03  Carl Hetherington  <cth@carlh.net>
+       * Fix bad resampling of separate sound file sources that
+       have specified video frame rates.
+       * Version 1.69.20 released.
+ 2014-06-03  Carl Hetherington  <cth@carlh.net>
+       * Re-calculate and update audio plots when the mapping is changed.
+       * Change the -3dB preset to -6dB since we are talking about
+       amplitude, not power.
+       * Version 1.69.19 released.
+ 2014-06-02  Carl Hetherington  <cth@carlh.net>
+       * Empirical hack to prevent over-read of array
+       by libswscale; may fix crashes at the start of
+       DCP encodes.
  2014-05-29  Carl Hetherington  <cth@carlh.net>
  
        * Version 1.69.18 released.
diff --combined src/lib/audio_content.cc
index c78dd3e695bbed9d11a1031d93587289345ba5ac,79912f1aee7ec4eb1f86a2b208fd33eb0bba6c75..03bfe9630a9cb304de9d23c54456c7cf432c1fcc
@@@ -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
@@@ -18,7 -18,7 +18,7 @@@
  */
  
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "audio_content.h"
  #include "analyse_audio_job.h"
  #include "job_manager.h"
  #include "i18n.h"
  
  using std::string;
+ using std::cout;
  using std::vector;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  int const AudioContentProperty::AUDIO_CHANNELS = 200;
  int const AudioContentProperty::AUDIO_LENGTH = 201;
@@@ -41,7 -42,7 +42,7 @@@ int const AudioContentProperty::AUDIO_G
  int const AudioContentProperty::AUDIO_DELAY = 204;
  int const AudioContentProperty::AUDIO_MAPPING = 205;
  
 -AudioContent::AudioContent (shared_ptr<const Film> f, Time s)
 +AudioContent::AudioContent (shared_ptr<const Film> f, DCPTime s)
        : Content (f, s)
        , _audio_gain (0)
        , _audio_delay (Config::instance()->default_audio_delay ())
@@@ -57,7 -58,7 +58,7 @@@ AudioContent::AudioContent (shared_ptr<
  
  }
  
 -AudioContent::AudioContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node)
 +AudioContent::AudioContent (shared_ptr<const Film> f, cxml::ConstNodePtr node)
        : Content (f, node)
  {
        _audio_gain = node->number_child<float> ("AudioGain");
@@@ -139,50 -140,37 +140,50 @@@ AudioContent::audio_analysis_path () co
        }
  
        boost::filesystem::path p = film->audio_analysis_dir ();
-       p /= digest ();
+       p /= digest() + "_" + audio_mapping().digest();
        return p;
  }
  
  string
  AudioContent::technical_summary () const
  {
 -      return String::compose ("audio: channels %1, length %2, raw rate %3, out rate %4", audio_channels(), audio_length(), content_audio_frame_rate(), output_audio_frame_rate());
 +      return String::compose (
 +              "audio: channels %1, length %2, content rate %3, resampled rate %4",
 +              audio_channels(),
 +              audio_length().seconds(),
 +              audio_frame_rate(),
 +              resampled_audio_frame_rate()
 +              );
  }
  
 +void
 +AudioContent::set_audio_mapping (AudioMapping)
 +{
 +      signal_changed (AudioContentProperty::AUDIO_MAPPING);
 +}
 +
 +/** @return the frame rate that this content should be resampled to in order
 + *  that it is in sync with the active video content at its start time.
 + */
  int
 -AudioContent::output_audio_frame_rate () const
 +AudioContent::resampled_audio_frame_rate () const
  {
        shared_ptr<const Film> film = _film.lock ();
        assert (film);
        
        /* Resample to a DCI-approved sample rate */
 -      double t = dcp_audio_frame_rate (content_audio_frame_rate ());
 +      double t = dcp_audio_frame_rate (audio_frame_rate ());
  
 -      FrameRateConversion frc (video_frame_rate(), film->video_frame_rate());
 +      FrameRateChange frc = film->active_frame_rate_change (position ());
  
        /* 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);
  }
 -
diff --combined src/lib/audio_mapping.cc
index 7d7e9f82863794b6495f368a677402793d8b5160,e35c1ae9464cf67061426ddb065cfb8c9d162b7a..b3757c5f1be9bd2fefc2a38199215201cde2cf11
@@@ -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 <libxml++/libxml++.h>
  #include <libcxml/cxml.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/raw_convert.h>
  #include "audio_mapping.h"
  #include "util.h"
+ #include "md5_digester.h"
  
  using std::list;
  using std::cout;
@@@ -31,7 -32,7 +32,7 @@@ using std::string
  using std::min;
  using boost::shared_ptr;
  using boost::dynamic_pointer_cast;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  AudioMapping::AudioMapping ()
        : _content_channels (0)
  }
  
  /** Create a default AudioMapping for a given channel count.
 - *  @param c Number of channels.
 + *  @param channels Number of channels.
   */
 -AudioMapping::AudioMapping (int c)
 +AudioMapping::AudioMapping (int channels)
  {
 -      setup (c);
 +      setup (channels);
  }
  
  void
@@@ -69,16 -70,16 +70,16 @@@ AudioMapping::make_default (
  
        if (_content_channels == 1) {
                /* Mono -> Centre */
 -              set (0, libdcp::CENTRE, 1);
 +              set (0, dcp::CENTRE, 1);
        } else {
                /* 1:1 mapping */
                for (int i = 0; i < min (_content_channels, MAX_DCP_AUDIO_CHANNELS); ++i) {
 -                      set (i, static_cast<libdcp::Channel> (i), 1);
 +                      set (i, static_cast<dcp::Channel> (i), 1);
                }
        }
  }
  
 -AudioMapping::AudioMapping (shared_ptr<const cxml::Node> node, int state_version)
 +AudioMapping::AudioMapping (cxml::ConstNodePtr node, int state_version)
  {
        setup (node->number_child<int> ("ContentChannels"));
  
                /* Old-style: on/off mapping */
                list<cxml::NodePtr> const c = node->node_children ("Map");
                for (list<cxml::NodePtr>::const_iterator i = c.begin(); i != c.end(); ++i) {
 -                      set ((*i)->number_child<int> ("ContentIndex"), static_cast<libdcp::Channel> ((*i)->number_child<int> ("DCP")), 1);
 +                      set ((*i)->number_child<int> ("ContentIndex"), static_cast<dcp::Channel> ((*i)->number_child<int> ("DCP")), 1);
                }
        } else {
                list<cxml::NodePtr> const c = node->node_children ("Gain");
                for (list<cxml::NodePtr>::const_iterator i = c.begin(); i != c.end(); ++i) {
                        set (
                                (*i)->number_attribute<int> ("Content"),
 -                              static_cast<libdcp::Channel> ((*i)->number_attribute<int> ("DCP")),
 +                              static_cast<dcp::Channel> ((*i)->number_attribute<int> ("DCP")),
                                raw_convert<float> ((*i)->content ())
                                );
                }
  }
  
  void
 -AudioMapping::set (int c, libdcp::Channel d, float g)
 +AudioMapping::set (int c, dcp::Channel d, float g)
  {
        _gain[c][d] = g;
  }
  
  float
 -AudioMapping::get (int c, libdcp::Channel d) const
 +AudioMapping::get (int c, dcp::Channel d) const
  {
        return _gain[c][d];
  }
@@@ -122,7 -123,24 +123,24 @@@ AudioMapping::as_xml (xmlpp::Node* node
                        xmlpp::Element* t = node->add_child ("Gain");
                        t->set_attribute ("Content", raw_convert<string> (c));
                        t->set_attribute ("DCP", raw_convert<string> (d));
 -                      t->add_child_text (raw_convert<string> (get (c, static_cast<libdcp::Channel> (d))));
 +                      t->add_child_text (raw_convert<string> (get (c, static_cast<dcp::Channel> (d))));
                }
        }
  }
+ /** @return a string which is unique for a given AudioMapping configuration, for
+  *  differentiation between different AudioMappings.
+  */
+ string
+ AudioMapping::digest () const
+ {
+       MD5Digester digester;
+       digester.add (_content_channels);
+       for (int i = 0; i < _content_channels; ++i) {
+               for (int j = 0; j < MAX_DCP_AUDIO_CHANNELS; ++j) {
+                       digester.add (_gain[i][j]);
+               }
+       }
+       return digester.get ();
+ }
diff --combined src/lib/audio_mapping.h
index 7bf974c2292a4a03ecc726df51ba1d0bfe5c4f56,b0b75ac063372c8f258dc235a6675f44ad2b8e81..8be8eeb6fa030cfbb27099abdf4c18a4d9b3e519
@@@ -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
@@@ -21,9 -21,8 +21,9 @@@
  #define DCPOMATIC_AUDIO_MAPPING_H
  
  #include <vector>
 -#include <libdcp/types.h>
  #include <boost/shared_ptr.hpp>
 +#include <dcp/types.h>
 +#include <libcxml/cxml.h>
  
  namespace xmlpp {
        class Node;
@@@ -42,8 -41,8 +42,8 @@@ class AudioMappin
  {
  public:
        AudioMapping ();
 -      AudioMapping (int);
 -      AudioMapping (boost::shared_ptr<const cxml::Node>, int);
 +      AudioMapping (int channels);
 +      AudioMapping (cxml::ConstNodePtr, int);
  
        /* Default copy constructor is fine */
        
  
        void make_default ();
  
 -      void set (int, libdcp::Channel, float);
 -      float get (int, libdcp::Channel) const;
 +      void set (int, dcp::Channel, float);
 +      float get (int, dcp::Channel) const;
  
        int content_channels () const {
                return _content_channels;
        }
+       std::string digest () const;
        
  private:
        void setup (int);
index 48fd6ed9c148d2fb4b3e1174e09287517c8e4363,f8675900ef2d59019b6e79ad4f862a5ef145b00f..aacefaa05502c9202178b2637dfa7dd5dbd50df6
  */
  
  #include <libxml++/libxml++.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
  #include <libcxml/cxml.h>
  #include "config.h"
  #include "colour_conversion.h"
  #include "util.h"
+ #include "md5_digester.h"
  
  #include "i18n.h"
  
@@@ -34,7 -35,7 +35,7 @@@ using std::cout
  using std::vector;
  using boost::shared_ptr;
  using boost::optional;
 -using libdcp::raw_convert;
 +using dcp::raw_convert;
  
  ColourConversion::ColourConversion ()
        : input_gamma (2.4)
@@@ -44,7 -45,7 +45,7 @@@
  {
        for (int i = 0; i < 3; ++i) {
                for (int j = 0; j < 3; ++j) {
 -                      matrix (i, j) = libdcp::colour_matrix::srgb_to_xyz[i][j];
 +                      matrix (i, j) = dcp::colour_matrix::srgb_to_xyz[i][j];
                }
        }
  }
@@@ -121,21 -122,18 +122,18 @@@ ColourConversion::preset () cons
  string
  ColourConversion::identifier () const
  {
-       double numbers[12];
-       int n = 0;
-       numbers[n++] = input_gamma;
-       numbers[n++] = input_gamma_linearised;
+       MD5Digester digester;
+       
+       digester.add (input_gamma);
+       digester.add (input_gamma_linearised);
        for (int i = 0; i < 3; ++i) {
                for (int j = 0; j < 3; ++j) {
-                       numbers[n++] = matrix (i, j);
+                       digester.add (matrix (i, j));
                }
        }
-       numbers[n++] = output_gamma;
-       assert (n == 12);
-       return md5_digest (numbers, 12 * sizeof (double));
+       digester.add (output_gamma);
+       
+       return digester.get ();
  }
  
  PresetColourConversion::PresetColourConversion ()
index 1aae64ac7aafd98fa40e59316dcb73872fc421b0,09b909696f2fcbb89a028d295b66d843d37cff9e..4054f05cd7fbadbc4f18ced4dfefd7bdbded4a33
  #include <boost/array.hpp>
  #include <boost/asio.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/rec709_linearised_gamma_lut.h>
 -#include <libdcp/srgb_linearised_gamma_lut.h>
 -#include <libdcp/gamma_lut.h>
 -#include <libdcp/xyz_frame.h>
 -#include <libdcp/rgb_xyz.h>
 -#include <libdcp/colour_matrix.h>
 -#include <libdcp/raw_convert.h>
 +#include <boost/lexical_cast.hpp>
- #include <openssl/md5.h>
 +#include <dcp/gamma_lut.h>
 +#include <dcp/xyz_frame.h>
 +#include <dcp/rgb_xyz.h>
 +#include <dcp/colour_matrix.h>
 +#include <dcp/raw_convert.h>
  #include <libcxml/cxml.h>
  #include "film.h"
  #include "dcp_video_frame.h"
@@@ -70,9 -70,8 +69,9 @@@ using std::string
  using std::stringstream;
  using std::cout;
  using boost::shared_ptr;
 -using libdcp::Size;
 -using libdcp::raw_convert;
 +using boost::lexical_cast;
 +using dcp::Size;
 +using dcp::raw_convert;
  
  #define DCI_COEFFICENT (48.0 / 52.37)
  
@@@ -111,10 -110,13 +110,10 @@@ DCPVideoFrame::DCPVideoFrame (shared_pt
  shared_ptr<EncodedData>
  DCPVideoFrame::encode_locally ()
  {
 -      shared_ptr<libdcp::LUT> in_lut;
 -      if (_frame->colour_conversion().input_gamma_linearised) {
 -              in_lut = libdcp::SRGBLinearisedGammaLUT::cache.get (12, _frame->colour_conversion().input_gamma);
 -      } else {
 -              in_lut = libdcp::GammaLUT::cache.get (12, _frame->colour_conversion().input_gamma);
 -      }
 -
 +      shared_ptr<dcp::GammaLUT> in_lut = dcp::GammaLUT::cache.get (
 +              12, _frame->colour_conversion().input_gamma, _frame->colour_conversion().input_gamma_linearised
 +              );
 +      
        /* XXX: libdcp should probably use boost */
        
        double matrix[3][3];
                }
        }
  
 -      shared_ptr<libdcp::XYZFrame> xyz = libdcp::rgb_to_xyz (
 +      shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz (
                _frame->image(),
                in_lut,
 -              libdcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma),
 +              dcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma, false),
                matrix
                );
  
-       {
-               MD5_CTX md5_context;
-               MD5_Init (&md5_context);
-               MD5_Update (&md5_context, xyz->data(0), 1998 * 1080 * 4);
-               MD5_Update (&md5_context, xyz->data(1), 1998 * 1080 * 4);
-               MD5_Update (&md5_context, xyz->data(2), 1998 * 1080 * 4);
-               unsigned char digest[MD5_DIGEST_LENGTH];
-               MD5_Final (digest, &md5_context);
-               
-               stringstream s;
-               for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-                       s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
-               }
-       }
        /* Set the max image and component sizes based on frame_rate */
        int max_cs_len = ((float) _j2k_bandwidth) / 8 / _frames_per_second;
        if (_frame->eyes() == EYES_LEFT || _frame->eyes() == EYES_RIGHT) {
@@@ -387,7 -374,7 +371,7 @@@ EncodedData::write (shared_ptr<const Fi
  }
  
  void
 -EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, libdcp::FrameInfo fin) const
 +EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, dcp::FrameInfo fin) const
  {
        boost::filesystem::path const info = film->info_path (frame, eyes);
        FILE* h = fopen_boost (info, "w");
diff --combined src/lib/image.cc
index d4ec6f99a6381070835b2fd54a875f414e5d7f87,8a8fb1c7b2d5cfe94f73e8e739d0cd738fd9abdb..8e7a51fd8d39371c278b65c125e710396565be21
@@@ -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
@@@ -22,7 -22,6 +22,6 @@@
   */
  
  #include <iostream>
- #include <openssl/md5.h>
  extern "C" {
  #include <libswscale/swscale.h>
  #include <libavutil/pixfmt.h>
@@@ -31,8 -30,7 +30,9 @@@
  #include "image.h"
  #include "exceptions.h"
  #include "scaler.h"
 +#include "timer.h"
 +#include "rect.h"
+ #include "md5_digester.h"
  
  #include "i18n.h"
  
@@@ -40,10 -38,9 +40,10 @@@ using std::string
  using std::min;
  using std::cout;
  using std::cerr;
 +using std::list;
  using std::stringstream;
  using boost::shared_ptr;
 -using libdcp::Size;
 +using dcp::Size;
  
  int
  Image::line_factor (int n) const
@@@ -87,7 -84,7 +87,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
        out->make_black ();
  
        /* Size of the image after any crop */
 -      libdcp::Size const cropped_size = crop.apply (size ());
 +      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) {
  }
  
  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
  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) {
@@@ -346,31 -343,11 +346,31 @@@ Image::make_black (
        }
  }
  
 +void
 +Image::make_transparent ()
 +{
 +      if (_pixel_format != PIX_FMT_RGBA) {
 +              throw PixelFormatError ("make_transparent()", _pixel_format);
 +      }
 +
 +      memset (data()[0], 0, lines(0) * stride()[0]);
 +}
 +
  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;
                }
        }
  }
@@@ -481,8 -458,8 +481,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)
  {
@@@ -513,13 -490,18 +513,18 @@@ Image::allocate (
                   OS X crashes on this illegal read, though other operating systems don't
                   seem to mind.  The nasty + 1 in this malloc makes sure there is always a byte
                   for that instruction to read safely.
+                  Further to the above, valgrind is now telling me that ff_rgb24ToY_ssse3
+                  over-reads by more then _avx.  I can't follow the code to work out how much,
+                  so I'll just over-allocate by 32 bytes and have done with it.  Empirical
+                  testing suggests that it works.
                */
-               _data[i] = (uint8_t *) wrapped_av_malloc (_stride[i] * lines (i) + 1);
+               _data[i] = (uint8_t *) wrapped_av_malloc (_stride[i] * lines (i) + 32);
        }
  }
  
  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)
  {
@@@ -589,7 -571,7 +594,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);
  
@@@ -632,7 -614,7 +637,7 @@@ Image::stride () cons
        return _stride;
  }
  
 -libdcp::Size
 +dcp::Size
  Image::size () const
  {
        return _size;
@@@ -644,44 -626,15 +649,35 @@@ Image::aligned () cons
        return _aligned;
  }
  
 +PositionImage
 +merge (list<PositionImage> images)
 +{
 +      if (images.empty ()) {
 +              return PositionImage ();
 +      }
 +
 +      dcpomatic::Rect<int> all (images.front().position, images.front().image->size().width, images.front().image->size().height);
 +      for (list<PositionImage>::const_iterator i = images.begin(); i != images.end(); ++i) {
 +              all.extend (dcpomatic::Rect<int> (i->position, i->image->size().width, i->image->size().height));
 +      }
 +
 +      shared_ptr<Image> merged (new Image (images.front().image->pixel_format (), dcp::Size (all.width, all.height), true));
 +      merged->make_transparent ();
 +      for (list<PositionImage>::const_iterator i = images.begin(); i != images.end(); ++i) {
 +              merged->alpha_blend (i->image, i->position);
 +      }
 +
 +      return PositionImage (merged, all.position ());
 +}
 +
  string
  Image::digest () const
  {
-       MD5_CTX md5_context;
-       MD5_Init (&md5_context);
+       MD5Digester digester;
  
        for (int i = 0; i < components(); ++i) {
-               MD5_Update (&md5_context, data()[i], line_size()[i]);
-       }
-       
-       unsigned char digest[MD5_DIGEST_LENGTH];
-       MD5_Final (digest, &md5_context);
-       
-       stringstream s;
-       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
+               digester.add (data()[i], line_size()[i]);
        }
  
-       return s.str ();
+       return digester.get ();
  }
 -      
diff --combined src/lib/player.cc
index 77def1e60d7eec92d83bbe66168fcb562107b827,68df8ea709afd905955be97dd410366092f5a612..c3489b7e1f227d3c6645cce9ab65fcf06348f51c
  */
  
  #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 "image_proxy.h"
  #include "ratio.h"
 -#include "resampler.h"
  #include "log.h"
  #include "scaler.h"
 +#include "render_subtitles.h"
 +#include "config.h"
 +#include "content_video.h"
  #include "player_video_frame.h"
  
  #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL);
@@@ -49,22 -43,23 +49,22 @@@ using std::list
  using std::cout;
  using std::min;
  using std::max;
 +using std::min;
  using std::vector;
  using std::pair;
  using std::map;
 +using std::make_pair;
  using boost::shared_ptr;
  using boost::weak_ptr;
  using boost::dynamic_pointer_cast;
 +using boost::optional;
  
  Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
        : _film (f)
        , _playlist (p)
 -      , _video (true)
 -      , _audio (true)
        , _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))
 -      , _last_emit_was_black (false)
 +      , _approximate_size (false)
 +      , _burn_subtitles (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));
  }
  
  void
 -Player::disable_video ()
 -{
 -      _video = false;
 -}
 -
 -void
 -Player::disable_audio ()
 +Player::setup_pieces ()
  {
 -      _audio = false;
 -}
 +      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      _pieces.clear ();
  
 -bool
 -Player::pass ()
 -{
 -      if (!_have_valid_pieces) {
 -              setup_pieces ();
 -      }
 +      ContentList content = _playlist->content ();
  
 -      Time earliest_t = TIME_MAX;
 -      shared_ptr<Piece> earliest;
 -      enum {
 -              VIDEO,
 -              AUDIO
 -      } type = VIDEO;
 +      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
  
 -      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 -              if ((*i)->decoder->done ()) {
 +              if (!(*i)->paths_valid ()) {
                        continue;
                }
 -
 -              shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder);
 -              shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 -
 -              if (_video && vd) {
 -                      if ((*i)->video_position < earliest_t) {
 -                              earliest_t = (*i)->video_position;
 -                              earliest = *i;
 -                              type = VIDEO;
 +              
 +              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;
                        }
 -              }
 -
 -              if (_audio && ad && ad->has_audio ()) {
 -                      if ((*i)->audio_position < earliest_t) {
 -                              earliest_t = (*i)->audio_position;
 -                              earliest = *i;
 -                              type = AUDIO;
 +                      
 +                      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;
                        }
                }
 -      }
 -
 -      if (!earliest) {
 -              flush ();
 -              return true;
 -      }
  
 -      switch (type) {
 -      case VIDEO:
 -              if (earliest_t > _video_position) {
 -                      emit_black ();
 +              optional<FrameRateChange> best_overlap_frc;
 +              if (best_overlap) {
 +                      best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
                } else {
 -                      if (earliest->repeating ()) {
 -                              earliest->repeat (this);
 -                      } else {
 -                              earliest->decoder->pass ();
 -                      }
 +                      /* No video overlap; e.g. if the DCP is just audio */
 +                      best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
                }
 -              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 (), true);
 -                                      }
 -                              }
 -                      }
 +              /* FFmpeg */
 +              shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
 +              if (fc) {
 +                      decoder.reset (new FFmpegDecoder (fc, _film->log()));
 +                      frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
 -              break;
 -      }
  
 -      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;
 +              /* ImageContent */
 +              shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
 +              if (ic) {
 +                      /* 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) {
 +                                      decoder = imd;
 +                              }
                        }
  
 -                      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 (!decoder) {
 +                              decoder.reset (new ImageDecoder (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) {
 +                      decoder.reset (new SndfileDecoder (sc));
 +                      frc = best_overlap_frc;
                }
  
 -              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 ());
 +              /* SubRipContent */
 +              shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
 +              if (rc) {
 +                      decoder.reset (new SubRipDecoder (rc));
 +                      frc = best_overlap_frc;
                }
 +
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ())));
        }
 -              
 -      return false;
 +
 +      _have_valid_pieces = true;
  }
  
 -/** @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 ImageProxy> image, Eyes eyes, Part part, bool same, VideoContent::Frame frame, Time extra)
 +Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
  {
 -      /* 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.part = part;
 -      _last_incoming_video.same = same;
 -      _last_incoming_video.frame = frame;
 -      _last_incoming_video.extra = extra;
 -      
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
 -
 -      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;
 -      }
 -
 -      Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate());
 -      if (content->trimmed (relative_time)) {
 +      shared_ptr<Content> c = w.lock ();
 +      if (!c) {
                return;
        }
  
 -      Time const time = content->position() + relative_time + extra - content->trim_start ();
 -      libdcp::Size const image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 -
 -      shared_ptr<PlayerVideoFrame> pi (
 -              new PlayerVideoFrame (
 -                      image,
 -                      content->crop(),
 -                      image_size,
 -                      _video_container_size,
 -                      _film->scaler(),
 -                      eyes,
 -                      part,
 -                      content->colour_conversion()
 -                      )
 -              );
 -      
 -      if (_film->with_subtitles ()) {
 -              for (list<Subtitle>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      if (i->covers (time)) {
 -                              /* This may be true for more than one of _subtitles, but the last (latest-starting)
 -                                 one is the one we want to use, so that's ok.
 -                              */
 -                              Position<int> const container_offset (
 -                                      (_video_container_size.width - image_size.width) / 2,
 -                                      (_video_container_size.height - image_size.width) / 2
 -                                      );
 -                              
 -                              pi->set_subtitle (i->out_image(), i->out_position() + container_offset);
 -                      }
 -              }
 -      }
 -
 -      /* Clear out old subtitles */
 -      for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ) {
 -              list<Subtitle>::iterator j = i;
 -              ++j;
 +      if (
 +              property == ContentProperty::POSITION ||
 +              property == ContentProperty::LENGTH ||
 +              property == ContentProperty::TRIM_START ||
 +              property == ContentProperty::TRIM_END ||
 +              property == ContentProperty::PATH ||
 +              property == VideoContentProperty::VIDEO_FRAME_TYPE
 +              ) {
                
 -              if (i->ends_before (time)) {
 -                      _subtitles.erase (i);
 -              }
 -
 -              i = j;
 -      }
 -
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video = piece->content;
 -#endif
 -
 -      Video (pi, same, time);
 -
 -      _last_emit_was_black = false;
 -      _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate());
 +              _have_valid_pieces = false;
 +              Changed (frequent);
  
 -      if (frc.repeat > 1 && !piece->repeating ()) {
 -              piece->set_repeat (_last_incoming_video, frc.repeat - 1);
 +      } else if (
 +              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 +              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
 +              property == SubtitleContentProperty::SUBTITLE_SCALE ||
 +              property == VideoContentProperty::VIDEO_CROP ||
 +              property == VideoContentProperty::VIDEO_SCALE ||
 +              property == VideoContentProperty::VIDEO_FRAME_RATE
 +              ) {
 +              
 +              Changed (frequent);
        }
  }
  
+ /** @param already_resampled true if this data has already been through the chain up to the resampler */
  void
 -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame, bool already_resampled)
 +Player::playlist_changed ()
  {
 -      shared_ptr<Piece> piece = weak_piece.lock ();
 -      if (!piece) {
 -              return;
 -      }
 +      _have_valid_pieces = false;
 +      Changed (false);
 +}
  
 -      shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content);
 -      assert (content);
 +void
 +Player::set_video_container_size (dcp::Size s)
 +{
 +      _video_container_size = s;
  
 -      if (!already_resampled) {
 -              /* Gain */
 -              if (content->audio_gain() != 0) {
 -                      shared_ptr<AudioBuffers> gain (new AudioBuffers (audio));
 -                      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);
 +      _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true));
 +      _black_image->make_black ();
 +}
  
 -      if (content->trimmed (relative_time)) {
 -              return;
 +void
 +Player::film_changed (Film::Property p)
 +{
 +      /* Here we should notice Film properties that affect our output, and
 +         alert listeners that our output now would be different to how it was
 +         last time we were run.
 +      */
 +
 +      if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 +              Changed (false);
        }
 +}
  
 -      Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start ();
 +list<PositionImage>
 +Player::process_content_image_subtitles (shared_ptr<SubtitleContent> content, list<shared_ptr<ContentImageSubtitle> > subs) const
 +{
 +      list<PositionImage> all;
        
 -      /* Remap channels */
 -      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->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) {
 -                              dcp_mapped->accumulate_channel (
 -                                      audio.get(),
 -                                      i,
 -                                      static_cast<libdcp::Channel> (j),
 -                                      map.get (i, static_cast<libdcp::Channel> (j))
 -                                      );
 -                      }
 +      for (list<shared_ptr<ContentImageSubtitle> >::const_iterator i = subs.begin(); i != subs.end(); ++i) {
 +              if (!(*i)->image) {
 +                      continue;
                }
 +
 +              dcpomatic::Rect<double> in_rect = (*i)->rectangle;
 +              dcp::Size scaled_size;
 +              
 +              in_rect.x += content->subtitle_x_offset ();
 +              in_rect.y += content->subtitle_y_offset ();
 +              
 +              /* We will scale the subtitle up to fit _video_container_size, and also by the additional subtitle_scale */
 +              scaled_size.width = in_rect.width * _video_container_size.width * content->subtitle_scale ();
 +              scaled_size.height = in_rect.height * _video_container_size.height * content->subtitle_scale ();
 +              
 +              /* Then we need a corrective translation, consisting of two parts:
 +               *
 +               * 1.  that which is the result of the scaling of the subtitle by _video_container_size; this will be
 +               *     rect.x * _video_container_size.width and rect.y * _video_container_size.height.
 +               *
 +               * 2.  that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be
 +               *     (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and
 +               *     (height_before_subtitle_scale * (1 - subtitle_scale) / 2).
 +               *
 +               * Combining these two translations gives these expressions.
 +               */
 +
 +              all.push_back (
 +                      PositionImage (
 +                              (*i)->image->scale (
 +                                      scaled_size,
 +                                      Scaler::from_id ("bicubic"),
 +                                      (*i)->image->pixel_format (),
 +                                      true
 +                                      ),
 +                              Position<int> (
 +                                      rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - content->subtitle_scale ()) / 2))),
 +                                      rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - content->subtitle_scale ()) / 2)))
 +                                      )
 +                              )
 +                      );
        }
  
 -      audio = dcp_mapped;
 +      return all;
 +}
  
 -      /* 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 ()) {
 -                      return;
 +list<PositionImage>
 +Player::process_content_text_subtitles (list<shared_ptr<ContentTextSubtitle> > sub) const
 +{
 +      list<PositionImage> all;
 +      for (list<shared_ptr<ContentTextSubtitle> >::const_iterator i = sub.begin(); i != sub.end(); ++i) {
 +              if (!(*i)->subs.empty ()) {
 +                      all.push_back (render_subtitles ((*i)->subs, _video_container_size));
                }
 -
 -              shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames));
 -              trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0);
 -
 -              audio = trimmed;
 -              time = 0;
        }
  
 -      _audio_merger.push (audio, time);
 -      piece->audio_position += _film->audio_frames_to_time (audio->frames ());
 +      return all;
  }
  
  void
 -Player::flush ()
 +Player::set_approximate_size ()
  {
 -      TimedAudioBuffers<Time> tb = _audio_merger.flush ();
 -      if (_audio && tb.audio) {
 -              Audio (tb.audio, tb.time);
 -              _audio_position += _film->audio_frames_to_time (tb.audio->frames ());
 -      }
 +      _approximate_size = true;
 +}
  
 -      while (_video && _video_position < _audio_position) {
 -              emit_black ();
 -      }
 +shared_ptr<PlayerVideoFrame>
 +Player::black_player_video_frame () const
 +{
 +      return shared_ptr<PlayerVideoFrame> (
 +              new PlayerVideoFrame (
 +                      shared_ptr<const ImageProxy> (new RawImageProxy (_black_image, _film->log ())),
 +                      Crop (),
 +                      _video_container_size,
 +                      _video_container_size,
 +                      Scaler::from_id ("bicubic"),
 +                      EYES_BOTH,
 +                      PART_WHOLE,
 +                      Config::instance()->colour_conversions().front().conversion
 +              )
 +      );
 +}
  
 -      while (_audio && _audio_position < _video_position) {
 -              emit_silence (_film->time_to_audio_frames (_video_position - _audio_position));
 +shared_ptr<PlayerVideoFrame>
 +Player::content_to_player_video_frame (
 +      shared_ptr<VideoContent> content,
 +      ContentVideo content_video,
 +      list<shared_ptr<Piece> > subs,
 +      DCPTime time,
 +      dcp::Size image_size) const
 +{
 +      shared_ptr<PlayerVideoFrame> pvf (
 +              new PlayerVideoFrame (
 +                      content_video.image,
 +                      content->crop (),
 +                      image_size,
 +                      _video_container_size,
 +                      _film->scaler(),
 +                      content_video.eyes,
 +                      content_video.part,
 +                      content->colour_conversion ()
 +                      )
 +              );
 +      
 +      
 +      /* Add subtitles */
 +      
 +      list<PositionImage> sub_images;
 +      
 +      for (list<shared_ptr<Piece> >::const_iterator i = subs.begin(); i != subs.end(); ++i) {
 +              shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*i)->decoder);
 +              shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*i)->content);
 +              ContentTime const from = dcp_to_content_subtitle (*i, time);
 +              ContentTime const to = from + ContentTime::from_frames (1, content->video_frame_rate ());
 +              
 +              list<shared_ptr<ContentImageSubtitle> > image_subtitles = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to));
 +              if (!image_subtitles.empty ()) {
 +                      list<PositionImage> im = process_content_image_subtitles (
 +                              subtitle_content,
 +                              image_subtitles
 +                              );
 +                      
 +                      copy (im.begin(), im.end(), back_inserter (sub_images));
 +              }
 +              
 +              if (_burn_subtitles) {
 +                      list<shared_ptr<ContentTextSubtitle> > text_subtitles = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to));
 +                      if (!text_subtitles.empty ()) {
 +                              list<PositionImage> im = process_content_text_subtitles (text_subtitles);
 +                              copy (im.begin(), im.end(), back_inserter (sub_images));
 +                      }
 +              }
        }
        
 +      if (!sub_images.empty ()) {
 +              pvf->set_subtitle (merge (sub_images));
 +      }
 +
 +      return pvf;
  }
  
 -/** Seek so that the next pass() will yield (approximately) the requested frame.
 - *  Pass accurate = true to try harder to get close to the request.
 - *  @return true on error
 - */
 -void
 -Player::seek (Time t, bool accurate)
 +/** @return All PlayerVideoFrames at the given time (there may be two frames for 3D) */
 +list<shared_ptr<PlayerVideoFrame> >
 +Player::get_video (DCPTime time, bool accurate)
  {
        if (!_have_valid_pieces) {
                setup_pieces ();
        }
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<VideoContent> (
 +              time,
 +              time + DCPTime::from_frames (1, _film->video_frame_rate ())
 +              );
  
 -      if (_pieces.empty ()) {
 -              return;
 +      list<shared_ptr<PlayerVideoFrame> > pvf;
 +              
 +      if (ov.empty ()) {
 +              /* No video content at this time */
 +              pvf.push_back (black_player_video_frame ());
 +              return pvf;
        }
  
 -      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);
 -
 -              /* Hence set the piece positions to the `global' time */
 -              (*i)->video_position = (*i)->audio_position = vc->position() + s;
 +      /* Create a PlayerVideoFrame from the content's video at this time */
  
 -              /* And seek the decoder */
 -              dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (
 -                      vc->time_to_content_video_frames (s + vc->trim_start ()), accurate
 -                      );
 +      shared_ptr<Piece> piece = ov.back ();
 +      shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder);
 +      assert (decoder);
 +      shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content);
 +      assert (content);
  
 -              (*i)->reset_repeat ();
 +      list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate);
 +      if (content_video.empty ()) {
 +              pvf.push_back (black_player_video_frame ());
 +              return pvf;
        }
  
 -      _video_position = _audio_position = t;
 +      dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size ());
 +      if (_approximate_size) {
 +              image_size.width &= ~3;
 +              image_size.height &= ~3;
 +      }
  
 -      /* XXX: don't seek audio because we don't need to... */
 +      for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) {
 +              list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (
 +                      time,
 +                      time + DCPTime::from_frames (1, _film->video_frame_rate ())
 +                      );
 +              
 +              pvf.push_back (content_to_player_video_frame (content, *i, subs, time, image_size));
 +      }
 +              
 +      return pvf;
  }
  
 -void
 -Player::setup_pieces ()
 +shared_ptr<AudioBuffers>
 +Player::get_audio (DCPTime time, DCPTime length, bool accurate)
  {
 -      list<shared_ptr<Piece> > old_pieces = _pieces;
 +      if (!_have_valid_pieces) {
 +              setup_pieces ();
 +      }
  
 -      _pieces.clear ();
 +      AudioFrame const length_frames = length.frames (_film->audio_frame_rate ());
  
 -      ContentList content = _playlist->content ();
 -      sort (content.begin(), content.end(), ContentSorter ());
 +      shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames));
 +      audio->make_silent ();
 +      
 +      list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length);
 +      if (ov.empty ()) {
 +              return audio;
 +      }
  
 -      for (ContentList::iterator i = content.begin(); i != content.end(); ++i) {
 +      for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) {
  
 -              if (!(*i)->paths_valid ()) {
 +              shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content);
 +              assert (content);
 +              shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder);
 +              assert (decoder);
 +
 +              if (content->audio_frame_rate() == 0) {
 +                      /* This AudioContent has no audio (e.g. if it is an FFmpegContent with no
 +                       * audio stream).
 +                       */
                        continue;
                }
  
 -              shared_ptr<Piece> piece (new Piece (*i));
 -
 -              /* XXX: into content? */
 -
 -              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, _5, 0));
 -                      fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2, false));
 -                      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;
 +              /* The time that we should request from the content */
 +              DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0);
 +              DCPTime offset;
 +              if (request < DCPTime ()) {
 +                      /* We went off the start of the content, so we will need to offset
 +                         the stuff we get back.
 +                      */
 +                      offset = -request;
 +                      request = DCPTime ();
                }
 -              
 -              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;
 -                              }
 -                      }
  
 -                      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, _5, 0));
 -                              piece->decoder = id;
 -                      }
 -              }
 +              AudioFrame const content_frame = dcp_to_content_audio (*i, request);
  
 -              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, false));
 +              /* Audio from this piece's decoder (which might be more or less than what we asked for) */
 +              shared_ptr<ContentAudio> all = decoder->get_audio (content_frame, length_frames, accurate);
  
 -                      piece->decoder = sd;
 +              /* Gain */
 +              if (content->audio_gain() != 0) {
 +                      shared_ptr<AudioBuffers> gain (new AudioBuffers (all->audio));
 +                      gain->apply_gain (content->audio_gain ());
 +                      all->audio = gain;
                }
  
 -              _pieces.push_back (piece);
 -      }
 -
 -      _have_valid_pieces = true;
 -}
 -
 -void
 -Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
 -{
 -      shared_ptr<Content> c = w.lock ();
 -      if (!c) {
 -              return;
 -      }
 -
 -      if (
 -              property == ContentProperty::POSITION || property == ContentProperty::LENGTH ||
 -              property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END ||
 -              property == VideoContentProperty::VIDEO_FRAME_TYPE 
 -              ) {
 -              
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 -
 -      } else if (
 -              property == SubtitleContentProperty::SUBTITLE_X_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_Y_OFFSET ||
 -              property == SubtitleContentProperty::SUBTITLE_SCALE
 -              ) {
 -
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->update (_film, _video_container_size);
 +              /* Remap channels */
 +              shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all->audio->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<dcp::Channel> (j)) > 0) {
 +                                      dcp_mapped->accumulate_channel (
 +                                              all->audio.get(),
 +                                              i,
 +                                              j,
 +                                              map.get (i, static_cast<dcp::Channel> (j))
 +                                              );
 +                              }
 +                      }
                }
                
 -              Changed (frequent);
 -
 -      } else if (
 -              property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_SCALE ||
 -              property == VideoContentProperty::VIDEO_FRAME_RATE
 -              ) {
 -              
 -              Changed (frequent);
 +              all->audio = dcp_mapped;
  
 -      } else if (property == ContentProperty::PATH) {
 -
 -              _have_valid_pieces = false;
 -              Changed (frequent);
 +              audio->accumulate_frames (
 +                      all->audio.get(),
 +                      content_frame - all->frame,
 +                      offset.frames (_film->audio_frame_rate()),
 +                      min (AudioFrame (all->audio->frames()), length_frames) - offset.frames (_film->audio_frame_rate ())
 +                      );
        }
  }
  
 -void
 -Player::playlist_changed ()
 -{
 -      _have_valid_pieces = false;
 -      Changed (false);
 -}
 -
 -void
 -Player::set_video_container_size (libdcp::Size s)
 -{
 -      _video_container_size = s;
 -
 -      shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true));
 -      im->make_black ();
 -      
 -      _black_frame.reset (
 -              new PlayerVideoFrame (
 -                      shared_ptr<ImageProxy> (new RawImageProxy (im, _film->log ())),
 -                      Crop(),
 -                      _video_container_size,
 -                      _video_container_size,
 -                      Scaler::from_id ("bicubic"),
 -                      EYES_BOTH,
 -                      PART_WHOLE,
 -                      ColourConversion ()
 -                      )
 -              );
 -}
 -
 -shared_ptr<Resampler>
 -Player::resampler (shared_ptr<AudioContent> c, bool create)
 +VideoFrame
 +Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      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> ();
 -      }
 -
 -      LOG_GENERAL (
 -              "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()
 -              );
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels()));
 -      _resamplers[c] = r;
 -      return r;
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) * piece->frc.factor ();
  }
  
 -void
 -Player::emit_black ()
 +AudioFrame
 +Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -#ifdef DCPOMATIC_DEBUG
 -      _last_video.reset ();
 -#endif
 -
 -      Video (_black_frame, _last_emit_was_black, _video_position);
 -      _video_position += _film->video_frames_to_time (1);
 -      _last_emit_was_black = true;
 -}
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -void
 -Player::emit_silence (OutputAudioFrame most)
 -{
 -      if (most == 0) {
 -              return;
 -      }
 -      
 -      OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2);
 -      shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N));
 -      silence->make_silent ();
 -      Audio (silence, _audio_position);
 -      _audio_position += _film->audio_frames_to_time (N);
 +      /* Convert this to the content frame */
 +      return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate());
  }
  
 -void
 -Player::film_changed (Film::Property p)
 +ContentTime
 +Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const
  {
 -      /* Here we should notice Film properties that affect our output, and
 -         alert listeners that our output now would be different to how it was
 -         last time we were run.
 -      */
 +      /* s is the offset of t from the start position of this content */
 +      DCPTime s = t - piece->content->position ();
 +      s = DCPTime (max (int64_t (0), s.get ()));
 +      s = DCPTime (min (piece->content->length_after_trim().get(), s.get()));
  
 -      if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) {
 -              Changed (false);
 -      }
 +      return ContentTime (s + piece->content->trim_start(), piece->frc);
  }
  
  void
 -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to)
 +PlayerStatistics::dump (shared_ptr<Log> log) const
  {
 -      if (!image) {
 -              /* A null image means that we should stop any current subtitles at `from' */
 -              for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) {
 -                      i->set_stop (from);
 -              }
 -      } else {
 -              _subtitles.push_back (Subtitle (_film, _video_container_size, weak_piece, image, rect, from, to));
 -      }
 +      log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL);
 +      log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL);
  }
  
 -/** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
 - *  @return false if this could not be done.
 - */
 -bool
 -Player::repeat_last_video ()
 +PlayerStatistics const &
 +Player::statistics () const
  {
 -      if (!_last_incoming_video.image || !_have_valid_pieces) {
 -              return false;
 -      }
 -
 -      process_video (
 -              _last_incoming_video.weak_piece,
 -              _last_incoming_video.image,
 -              _last_incoming_video.eyes,
 -              _last_incoming_video.part,
 -              _last_incoming_video.same,
 -              _last_incoming_video.frame,
 -              _last_incoming_video.extra
 -              );
 -
 -      return true;
 +      return _statistics;
  }
diff --combined src/lib/playlist.cc
index 214badc7ee8b8e62ce7d0ed0f5bcc287e34a5996,1f00d4d6753c27272a1e985ff5553b68d572f5f7..5203160a876373258748e6c7b0f0663c3375d66c
@@@ -30,6 -30,7 +30,7 @@@
  #include "job.h"
  #include "config.h"
  #include "util.h"
+ #include "md5_digester.h"
  
  #include "i18n.h"
  
@@@ -79,20 -80,20 +80,20 @@@ Playlist::maybe_sequence_video (
        _sequencing_video = true;
        
        ContentList cl = _content;
 -      Time next_left = 0;
 -      Time next_right = 0;
 +      DCPTime next_left;
 +      DCPTime next_right;
        for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) {
                shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i);
                if (!vc) {
                        continue;
                }
 -      
 +              
                if (vc->video_frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT) {
                        vc->set_position (next_right);
 -                      next_right = vc->end() + 1;
 +                      next_right = vc->end() + DCPTime::delta ();
                } else {
                        vc->set_position (next_left);
 -                      next_left = vc->end() + 1;
 +                      next_left = vc->end() + DCPTime::delta ();
                }
        }
  
@@@ -113,12 -114,14 +114,14 @@@ Playlist::video_identifier () cons
                }
        }
  
-       return md5_digest (t.c_str(), t.length());
+       MD5Digester digester;
+       digester.add (t.c_str(), t.length());
+       return digester.get ();
  }
  
  /** @param node <Playlist> node */
  void
 -Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int version, list<string>& notes)
 +Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, list<string>& notes)
  {
        list<cxml::NodePtr> c = node->node_children ("Content");
        for (list<cxml::NodePtr>::iterator i = c.begin(); i != c.end(); ++i) {
@@@ -259,12 -262,12 +262,12 @@@ Playlist::best_dcp_frame_rate () cons
        return best->dcp;
  }
  
 -Time
 +DCPTime
  Playlist::length () const
  {
 -      Time len = 0;
 +      DCPTime len;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 -              len = max (len, (*i)->end() + 1);
 +              len = max (len, (*i)->end() + DCPTime::delta ());
        }
  
        return len;
@@@ -284,10 -287,10 +287,10 @@@ Playlist::reconnect (
        }
  }
  
 -Time
 +DCPTime
  Playlist::video_end () const
  {
 -      Time end = 0;
 +      DCPTime end;
        for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
                if (dynamic_pointer_cast<const VideoContent> (*i)) {
                        end = max (end, (*i)->end ());
        return end;
  }
  
 +FrameRateChange
 +Playlist::active_frame_rate_change (DCPTime t, int dcp_video_frame_rate) const
 +{
 +      for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) {
 +              shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i);
 +              if (!vc) {
 +                      break;
 +              }
 +
 +              if (vc->position() >= t && t < vc->end()) {
 +                      return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate);
 +              }
 +      }
 +
 +      return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate);
 +}
 +
  void
  Playlist::set_sequence_video (bool s)
  {
@@@ -336,7 -322,7 +339,7 @@@ Playlist::content () cons
  void
  Playlist::repeat (ContentList c, int n)
  {
 -      pair<Time, Time> range (TIME_MAX, 0);
 +      pair<DCPTime, DCPTime> range (DCPTime::max (), DCPTime ());
        for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                range.first = min (range.first, (*i)->position ());
                range.second = max (range.second, (*i)->position ());
                range.second = max (range.second, (*i)->end ());
        }
  
 -      Time pos = range.second;
 +      DCPTime pos = range.second;
        for (int i = 0; i < n; ++i) {
                for (ContentList::iterator i = c.begin(); i != c.end(); ++i) {
                        shared_ptr<Content> copy = (*i)->clone ();
@@@ -377,7 -363,7 +380,7 @@@ Playlist::move_earlier (shared_ptr<Cont
                return;
        }
        
 -      Time const p = (*previous)->position ();
 +      DCPTime const p = (*previous)->position ();
        (*previous)->set_position (p + c->length_after_trim ());
        c->set_position (p);
        sort (_content.begin(), _content.end(), ContentSorter ());
@@@ -404,7 -390,7 +407,7 @@@ Playlist::move_later (shared_ptr<Conten
                return;
        }
  
 -      Time const p = (*next)->position ();
 +      DCPTime const p = (*next)->position ();
        (*next)->set_position (c->position ());
        c->set_position (p + c->length_after_trim ());
        sort (_content.begin(), _content.end(), ContentSorter ());
diff --combined src/lib/util.cc
index 6e370f577d9c8799126499564a24128709c24af3,bbe6f77e1f26e2e8542031d18911632ba83d8f67..074e08cb70e819617391f5c4a6115e830d6444cc
  #endif
  #include <glib.h>
  #include <openjpeg.h>
- #include <openssl/md5.h>
 +#include <pangomm/init.h>
  #include <magick/MagickCore.h>
  #include <magick/version.h>
 -#include <libdcp/version.h>
 -#include <libdcp/util.h>
 -#include <libdcp/signer_chain.h>
 -#include <libdcp/signer.h>
 -#include <libdcp/raw_convert.h>
 +#include <dcp/version.h>
 +#include <dcp/util.h>
 +#include <dcp/signer_chain.h>
 +#include <dcp/signer.h>
 +#include <dcp/raw_convert.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavformat/avformat.h>
@@@ -71,7 -69,7 +70,8 @@@
  #include "job.h"
  #include "cross.h"
  #include "video_content.h"
 +#include "rect.h"
+ #include "md5_digester.h"
  #ifdef DCPOMATIC_WINDOWS
  #include "stack.hpp"
  #endif
@@@ -102,8 -100,8 +102,8 @@@ using std::set_terminate
  using boost::shared_ptr;
  using boost::thread;
  using boost::optional;
 -using libdcp::Size;
 -using libdcp::raw_convert;
 +using dcp::Size;
 +using dcp::raw_convert;
  
  static boost::thread::id ui_thread;
  static boost::filesystem::path backtrace_file;
@@@ -239,6 -237,24 +239,6 @@@ ffmpeg_version_to_string (int v
        return s.str ();
  }
  
 -/** Return a user-readable string summarising the versions of our dependencies */
 -string
 -dependency_version_summary ()
 -{
 -      stringstream s;
 -      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 -        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 -        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 -        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 -        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 -        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 -        << MagickVersion << N_(", ")
 -        << N_("libssh ") << ssh_version (0) << N_(", ")
 -        << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit;
 -
 -      return s.str ();
 -}
 -
  double
  seconds (struct timeval t)
  {
@@@ -250,8 -266,9 +250,9 @@@ LONG WINAPI exception_handler(struct _E
  {
        dbg::stack s;
        FILE* f = fopen_boost (backtrace_file, "w");
+       fprintf (f, "Exception thrown:");
        for (dbg::stack::const_iterator i = s.begin(); i != s.end(); ++i) {
-               fprintf (f, "%p %s %d %s", i->instruction, i->function.c_str(), i->line, i->module.c_str());
+               fprintf (f, "%p %s %d %s\n", i->instruction, i->function.c_str(), i->line, i->module.c_str());
        }
        fclose (f);
        return EXCEPTION_CONTINUE_SEARCH;
@@@ -325,8 -342,7 +326,8 @@@ dcpomatic_setup (
  
        set_terminate (terminate);
  
 -      libdcp::init ();
 +      Pango::init ();
 +      dcp::init ();
        
        Ratio::setup_ratios ();
        VideoContentScale::setup_scales ();
@@@ -409,23 -425,6 +410,6 @@@ split_at_spaces_considering_quotes (str
        return out;
  }
  
- string
- md5_digest (void const * data, int size)
- {
-       MD5_CTX md5_context;
-       MD5_Init (&md5_context);
-       MD5_Update (&md5_context, data, size);
-       unsigned char digest[MD5_DIGEST_LENGTH];
-       MD5_Final (digest, &md5_context);
-       
-       stringstream s;
-       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
-       }
-       return s.str ();
- }
  /** @param job Optional job for which to report progress */
  string
  md5_digest (vector<boost::filesystem::path> files, shared_ptr<Job> job)
        boost::uintmax_t const buffer_size = 64 * 1024;
        char buffer[buffer_size];
  
-       MD5_CTX md5_context;
-       MD5_Init (&md5_context);
+       MD5Digester digester;
  
        vector<int64_t> sizes;
        for (size_t i = 0; i < files.size(); ++i) {
                while (remaining > 0) {
                        int const t = min (remaining, buffer_size);
                        fread (buffer, 1, t, f);
-                       MD5_Update (&md5_context, buffer, t);
+                       digester.add (buffer, t);
                        remaining -= t;
  
                        if (job) {
                fclose (f);
        }
  
-       unsigned char digest[MD5_DIGEST_LENGTH];
-       MD5_Final (digest, &md5_context);
-       stringstream s;
-       for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) {
-               s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]);
-       }
-       return s.str ();
+       return digester.get ();
  }
  
 -static bool
 -about_equal (float a, float b)
 -{
 -      /* A film of F seconds at f FPS will be Ff frames;
 -         Consider some delta FPS d, so if we run the same
 -         film at (f + d) FPS it will last F(f + d) seconds.
 -
 -         Hence the difference in length over the length of the film will
 -         be F(f + d) - Ff frames
 -          = Ff + Fd - Ff frames
 -          = Fd frames
 -          = Fd/f seconds
 - 
 -         So if we accept a difference of 1 frame, ie 1/f seconds, we can
 -         say that
 -
 -         1/f = Fd/f
 -      ie 1 = Fd
 -      ie d = 1/F
 - 
 -         So for a 3hr film, ie F = 3 * 60 * 60 = 10800, the acceptable
 -         FPS error is 1/F ~= 0.0001 ~= 10-e4
 -      */
 -
 -      return (fabs (a - b) < 1e-4);
 -}
 -
  /** @param An arbitrary audio frame rate.
   *  @return The appropriate DCP-approved frame rate (48kHz or 96kHz).
   */
@@@ -734,6 -751,17 +709,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);
 -              }
 -      }
 -}
 -
  bool
  valid_image_file (boost::filesystem::path f)
  {
@@@ -784,7 -850,7 +759,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>
@@@ -883,14 -949,14 +858,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 *
@@@ -921,34 -987,12 +896,34 @@@ divide_with_round (int64_t a, int64_t b
        }
  }
  
 +/** Return a user-readable string summarising the versions of our dependencies */
 +string
 +dependency_version_summary ()
 +{
 +      stringstream s;
 +      s << N_("libopenjpeg ") << opj_version () << N_(", ")
 +        << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ")
 +        << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ")
 +        << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ")
 +        << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ")
 +        << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ")
 +        << MagickVersion << N_(", ")
 +        << N_("libssh ") << ssh_version (0) << N_(", ")
 +        << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit;
 +
 +      return s.str ();
 +}
 +
 +/** Construct a ScopedTemporary.  A temporary filename is decided but the file is not opened
 + *  until ::open() is called.
 + */
  ScopedTemporary::ScopedTemporary ()
        : _open (0)
  {
        _file = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path ();
  }
  
 +/** Close and delete the temporary file */
  ScopedTemporary::~ScopedTemporary ()
  {
        close ();       
        boost::filesystem::remove (_file, ec);
  }
  
 +/** @return temporary filename */
  char const *
  ScopedTemporary::c_str () const
  {
        return _file.string().c_str ();
  }
  
 +/** Open the temporary file.
 + *  @return File's FILE pointer.
 + */
  FILE*
  ScopedTemporary::open (char const * params)
  {
        return _open;
  }
  
 +/** Close the file */
  void
  ScopedTemporary::close ()
  {
                _open = 0;
        }
  }
 +
 +ContentTimePeriod
 +subtitle_period (AVSubtitle const & sub)
 +{
 +      ContentTime const packet_time = ContentTime::from_seconds (static_cast<double> (sub.pts) / AV_TIME_BASE);
 +
 +      ContentTimePeriod period (
 +              packet_time + ContentTime::from_seconds (sub.start_display_time / 1e3),
 +              packet_time + ContentTime::from_seconds (sub.end_display_time / 1e3)
 +              );
 +
 +      return period;
 +}
diff --combined src/lib/util.h
index 28af8ef2f88c41bcfbdb8bad30806248ff98af35,5d93456df6c043d2bac41d6b070b81e6f83ebcc6..7d3afb022856e963ee870b9b323b82c8ff235c5c
@@@ -31,8 -31,7 +31,8 @@@
  #include <boost/asio.hpp>
  #include <boost/optional.hpp>
  #include <boost/filesystem.hpp>
 -#include <libdcp/util.h>
 +#include <dcp/util.h>
 +#include <dcp/signer.h>
  extern "C" {
  #include <libavcodec/avcodec.h>
  #include <libavfilter/avfilter.h>
@@@ -53,7 -52,6 +53,7 @@@ namespace libdcp 
  }
  
  class Job;
 +struct AVSubtitle;
  
  extern std::string seconds_to_hms (int);
  extern std::string seconds_to_approximate_hms (int);
@@@ -64,7 -62,6 +64,6 @@@ extern void dcpomatic_setup ()
  extern void dcpomatic_setup_gettext_i18n (std::string);
  extern std::vector<std::string> split_at_spaces_considering_quotes (std::string);
  extern std::string md5_digest (std::vector<boost::filesystem::path>, boost::shared_ptr<Job>);
- extern std::string md5_digest (void const *, int);
  extern void ensure_ui_thread ();
  extern std::string audio_channel_name (int);
  extern bool valid_image_file (boost::filesystem::path);
  extern boost::filesystem::path mo_path ();
  #endif
  extern std::string tidy_for_filename (std::string);
 -extern boost::shared_ptr<const libdcp::Signer> make_signer ();
 -extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size);
 +extern boost::shared_ptr<const dcp::Signer> make_signer ();
 +extern dcp::Size fit_ratio_within (float ratio, dcp::Size);
  extern std::string entities_to_text (std::string e);
  extern std::map<std::string, std::string> split_get_request (std::string url);
 -
 -struct FrameRateConversion
 -{
 -      FrameRateConversion (float, int);
 -
 -      /** @return factor by which to multiply a source frame rate
 -          to get the effective rate after any skip or repeat has happened.
 -      */
 -      float factor () const {
 -              if (skip) {
 -                      return 0.5;
 -              }
 -
 -              return repeat;
 -      }
 -
 -      /** true to skip every other frame */
 -      bool skip;
 -      /** number of times to use each frame (e.g. 1 is normal, 2 means repeat each frame once, and so on) */
 -      int repeat;
 -      /** true if this DCP will run its video faster or slower than the source
 -       *  without taking into account `repeat' nor `skip'.
 -       *  (e.g. change_speed will be true if
 -       *          source is 29.97fps, DCP is 30fps
 -       *          source is 14.50fps, DCP is 30fps
 -       *  but not if
 -       *          source is 15.00fps, DCP is 30fps
 -       *          source is 12.50fps, DCP is 25fps)
 -       */
 -      bool change_speed;
 -
 -      std::string description;
 -};
 -
  extern int dcp_audio_frame_rate (int);
  extern int stride_round_up (int, int const *, int);
  extern std::multimap<std::string, std::string> read_key_value (std::istream& s);
@@@ -86,7 -117,6 +85,7 @@@ extern int get_optional_int (std::multi
  extern std::string get_optional_string (std::multimap<std::string, std::string> const & kv, std::string k);
  extern void* wrapped_av_malloc (size_t);
  extern int64_t divide_with_round (int64_t a, int64_t b);
 +extern ContentTimePeriod subtitle_period (AVSubtitle const &);
  
  /** @class Socket
   *  @brief A class to wrap a boost::asio::ip::tcp::socket with some things
@@@ -129,20 -159,16 +128,20 @@@ private
  
  extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second);
  
 +/** @class ScopedTemporary
 + *  @brief A temporary file which is deleted when the ScopedTemporary object goes out of scope.
 + */
  class ScopedTemporary
  {
  public:
        ScopedTemporary ();
        ~ScopedTemporary ();
  
 +      /** @return temporary filename */
        boost::filesystem::path file () const {
                return _file;
        }
 -      
 +
        char const * c_str () const;
        FILE* open (char const *);
        void close ();
diff --combined src/lib/writer.cc
index 9410dd565b4a9ba963cda63af72aea763c489511,2ed55a276595be9bbcd69e348b12b22ad3ac42d8..580dfe4f21d19e0bfb452a37d32e413ad247d6b5
  
  #include <fstream>
  #include <cerrno>
 -#include <libdcp/mono_picture_asset.h>
 -#include <libdcp/stereo_picture_asset.h>
 -#include <libdcp/sound_asset.h>
 -#include <libdcp/reel.h>
 -#include <libdcp/dcp.h>
 -#include <libdcp/cpl.h>
 +#include <dcp/mono_picture_mxf.h>
 +#include <dcp/stereo_picture_mxf.h>
 +#include <dcp/sound_mxf.h>
 +#include <dcp/sound_mxf_writer.h>
 +#include <dcp/reel.h>
 +#include <dcp/reel_mono_picture_asset.h>
 +#include <dcp/reel_stereo_picture_asset.h>
 +#include <dcp/reel_sound_asset.h>
 +#include <dcp/dcp.h>
 +#include <dcp/cpl.h>
  #include "writer.h"
  #include "compose.hpp"
  #include "film.h"
@@@ -41,7 -37,7 +41,8 @@@
  #include "config.h"
  #include "job.h"
  #include "cross.h"
 +#include "audio_buffers.h"
+ #include "md5_digester.h"
  
  #include "i18n.h"
  
@@@ -60,7 -56,6 +61,7 @@@ using std::cout
  using std::stringstream;
  using boost::shared_ptr;
  using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
  
  int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4;
  
@@@ -75,6 -70,7 +76,6 @@@ Writer::Writer (shared_ptr<const Film> 
        , _last_written_eyes (EYES_RIGHT)
        , _full_written (0)
        , _fake_written (0)
 -      , _repeat_written (0)
        , _pushed_to_disk (0)
  {
        /* Remove any old DCP */
        */
  
        if (_film->three_d ()) {
 -              _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        } else {
 -              _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ()));
 +              _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1)));
        }
  
 -      _picture_asset->set_edit_rate (_film->video_frame_rate ());
 -      _picture_asset->set_size (_film->frame_size ());
 -      _picture_asset->set_interop (_film->interop ());
 +      _picture_mxf->set_size (_film->frame_size ());
  
        if (_film->encrypted ()) {
 -              _picture_asset->set_key (_film->key ());
 +              _picture_mxf->set_key (_film->key ());
        }
        
 -      _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
 +      _picture_mxf_writer = _picture_mxf->start_write (
 +              _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(),
 +              _film->interop() ? dcp::INTEROP : dcp::SMPTE,
 +              _first_nonexistant_frame > 0
 +              );
  
        if (_film->audio_channels ()) {
 -              _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ()));
 -              _sound_asset->set_edit_rate (_film->video_frame_rate ());
 -              _sound_asset->set_channels (_film->audio_channels ());
 -              _sound_asset->set_sampling_rate (_film->audio_frame_rate ());
 -              _sound_asset->set_interop (_film->interop ());
 +              _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ()));
  
                if (_film->encrypted ()) {
 -                      _sound_asset->set_key (_film->key ());
 +                      _sound_mxf->set_key (_film->key ());
                }
 -              
 -              /* Write the sound asset into the film directory so that we leave the creation
 +      
 +              /* Write the sound MXF into the film directory so that we leave the creation
                   of the DCP directory until the last minute.
                */
 -              _sound_asset_writer = _sound_asset->start_write ();
 +              _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE);
        }
  
        _thread = new boost::thread (boost::bind (&Writer::thread, this));
@@@ -173,7 -171,7 +174,7 @@@ Writer::fake_write (int frame, Eyes eye
        }
        
        FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r");
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        
        QueueItem qi;
  void
  Writer::write (shared_ptr<const AudioBuffers> audio)
  {
 -      if (_sound_asset) {
 -              _sound_asset_writer->write (audio->data(), audio->frames());
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->write (audio->data(), audio->frames());
        }
  }
  
@@@ -272,7 -270,7 +273,7 @@@ tr
                                        qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false)));
                                }
  
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size());
 +                              dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size());
                                qi.encoded->write_info (_film, qi.frame, qi.eyes, fin);
                                _last_written[qi.eyes] = qi.encoded;
                                ++_full_written;
                        }
                        case QueueItem::FAKE:
                                LOG_GENERAL (N_("Writer FAKE-writes %1 to MXF"), qi.frame);
 -                              _picture_asset_writer->fake_write (qi.size);
 +                              _picture_mxf_writer->fake_write (qi.size);
                                _last_written[qi.eyes].reset ();
                                ++_fake_written;
                                break;
 -                      case QueueItem::REPEAT:
 -                      {
 -                              LOG_GENERAL (N_("Writer REPEAT-writes %1 to MXF"), qi.frame);
 -                              libdcp::FrameInfo fin = _picture_asset_writer->write (
 -                                      _last_written[qi.eyes]->data(),
 -                                      _last_written[qi.eyes]->size()
 -                                      );
 -                              
 -                              _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin);
 -                              ++_repeat_written;
 -                              break;
 -                      }
                        }
                        lock.lock ();
  
                        _last_written_frame = qi.frame;
                        _last_written_eyes = qi.eyes;
                        
 -                      if (_film->length()) {
 -                              shared_ptr<Job> job = _job.lock ();
 -                              assert (job);
 -                              int total = _film->time_to_video_frames (_film->length ());
 -                              if (_film->three_d ()) {
 -                                      /* _full_written and so on are incremented for each eye, so we need to double the total
 -                                         frames to get the correct progress.
 -                                      */
 -                                      total *= 2;
 -                              }
 -                              job->set_progress (float (_full_written + _fake_written + _repeat_written) / total);
 +                      shared_ptr<Job> job = _job.lock ();
 +                      assert (job);
 +                      int64_t total = _film->length().frames (_film->video_frame_rate ());
 +                      if (_film->three_d ()) {
 +                              /* _full_written and so on are incremented for each eye, so we need to double the total
 +                                 frames to get the correct progress.
 +                              */
 +                              total *= 2;
 +                      }
 +                      if (total) {
 +                              job->set_progress (float (_full_written + _fake_written) / total);
                        }
                }
  
@@@ -374,11 -384,15 +375,11 @@@ Writer::finish (
        
        terminate_thread (true);
  
 -      _picture_asset_writer->finalize ();
 -      if (_sound_asset_writer) {
 -              _sound_asset_writer->finalize ();
 +      _picture_mxf_writer->finalize ();
 +      if (_sound_mxf_writer) {
 +              _sound_mxf_writer->finalize ();
        }
        
 -      int const frames = _last_written_frame + 1;
 -
 -      _picture_asset->set_duration (frames);
 -
        /* Hard-link the video MXF into the DCP */
        boost::filesystem::path video_from;
        video_from /= _film->internal_video_mxf_dir();
                LOG_WARNING_NC ("Hard-link failed; fell back to copying");
        }
  
 -      /* And update the asset */
 -
 -      _picture_asset->set_directory (_film->dir (_film->dcp_name ()));
 -      _picture_asset->set_file_name (_film->video_mxf_filename ());
 +      _picture_mxf->set_file (video_to);
  
        /* Move the audio MXF into the DCP */
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                boost::filesystem::path audio_to;
                audio_to /= _film->dir (_film->dcp_name ());
                audio_to /= _film->audio_mxf_filename ();
                                String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ())
                                );
                }
 -              
 -              _sound_asset->set_directory (_film->dir (_film->dcp_name ()));
 -              _sound_asset->set_duration (frames);
 +
 +              _sound_mxf->set_file (audio_to);
        }
 -      
 -      libdcp::DCP dcp (_film->dir (_film->dcp_name()));
  
 -      shared_ptr<libdcp::CPL> cpl (
 -              new libdcp::CPL (
 -                      _film->dir (_film->dcp_name()),
 +      dcp::DCP dcp (_film->dir (_film->dcp_name()));
 +
 +      shared_ptr<dcp::CPL> cpl (
 +              new dcp::CPL (
                        _film->dcp_name(),
 -                      _film->dcp_content_type()->libdcp_kind (),
 -                      frames,
 -                      _film->video_frame_rate ()
 +                      _film->dcp_content_type()->libdcp_kind ()
                        )
                );
        
 -      dcp.add_cpl (cpl);
 +      dcp.add (cpl);
 +
 +      shared_ptr<dcp::Reel> reel (new dcp::Reel ());
  
 -      cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel (
 -                                                       _picture_asset,
 -                                                       _sound_asset,
 -                                                       shared_ptr<libdcp::SubtitleAsset> ()
 -                                                       )
 -                             ));
 +      shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf);
 +      if (mono) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0)));
 +              dcp.add (mono);
 +      }
 +
 +      shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf);
 +      if (stereo) {
 +              reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0)));
 +              dcp.add (stereo);
 +      }
 +
 +      if (_sound_mxf) {
 +              reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0)));
 +              dcp.add (_sound_mxf);
 +      }
 +      
 +      cpl->add (reel);
  
        shared_ptr<Job> job = _job.lock ();
        assert (job);
  
        job->sub (_("Computing image digest"));
 -      _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +      _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
  
 -      if (_sound_asset) {
 +      if (_sound_mxf) {
                job->sub (_("Computing audio digest"));
 -              _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false));
 +              _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false));
        }
  
 -      libdcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
 +      dcp::XMLMetadata meta = Config::instance()->dcp_metadata ();
        meta.set_issue_date_now ();
 -      dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ());
 +
 +      dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, _film->is_signed() ? make_signer () : shared_ptr<const dcp::Signer> ());
  
        LOG_GENERAL (
 -              N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk
 +              N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk
                );
  }
  
 -/** Tell the writer that frame `f' should be a repeat of the frame before it */
 -void
 -Writer::repeat (int f, Eyes e)
 -{
 -      boost::mutex::scoped_lock lock (_mutex);
 -
 -      while (_queued_full_in_memory > _maximum_frames_in_memory) {
 -              _full_condition.wait (lock);
 -      }
 -      
 -      QueueItem qi;
 -      qi.type = QueueItem::REPEAT;
 -      qi.frame = f;
 -      if (_film->three_d() && e == EYES_BOTH) {
 -              qi.eyes = EYES_LEFT;
 -              _queue.push_back (qi);
 -              qi.eyes = EYES_RIGHT;
 -              _queue.push_back (qi);
 -      } else {
 -              qi.eyes = e;
 -              _queue.push_back (qi);
 -      }
 -
 -      _empty_condition.notify_all ();
 -}
 -
  bool
  Writer::check_existing_picture_mxf_frame (FILE* mxf, int f, Eyes eyes)
  {
                return false;
        }
        
 -      libdcp::FrameInfo info (ifi);
 +      dcp::FrameInfo info (ifi);
        fclose (ifi);
        if (info.size == 0) {
                LOG_GENERAL ("Existing frame %1 has no info file", f);
                LOG_GENERAL ("Existing frame %1 is incomplete", f);
                return false;
        }
-       
-       string const existing_hash = md5_digest (data.data(), data.size());
-       if (existing_hash != info.hash) {
+       MD5Digester digester;
+       digester.add (data.data(), data.size());
+       if (digester.get() != info.hash) {
                LOG_GENERAL ("Existing frame %1 failed hash check", f);
                return false;
        }
diff --combined src/lib/wscript
index d0fe9c8d88a1821a9d6005566c699cf370f7c27c,f932a142da6b161808d456117b662ad1fec325dd..51aadb83f40718a549eac9e222bce49395bd69bb
@@@ -13,12 -13,11 +13,12 @@@ sources = ""
            config.cc
            content.cc
            content_factory.cc
 +          content_subtitle.cc
            cross.cc
            dci_metadata.cc
            dcp_content_type.cc
            dcp_video_frame.cc
 -          decoder.cc
 +          dcpomatic_time.cc
            dolby_cp750.cc
            encoder.cc
            examine_content_job.cc
            file_group.cc
            filter_graph.cc
            ffmpeg.cc
 +          ffmpeg_audio_stream.cc
            ffmpeg_content.cc
            ffmpeg_decoder.cc
            ffmpeg_examiner.cc
 +          ffmpeg_stream.cc
 +          ffmpeg_subtitle_stream.cc
            film.cc
            filter.cc
 +          frame_rate_change.cc
            internet.cc
            image.cc
            image_content.cc
            kdm.cc
            json_server.cc
            log.cc
 -          piece.cc
+           md5_digester.cc
            player.cc
            player_video_frame.cc
            playlist.cc
            ratio.cc
 +          render_subtitles.cc
            resampler.cc
            scp_dcp_job.cc
            scaler.cc
@@@ -60,9 -56,7 +61,9 @@@
            sndfile_content.cc
            sndfile_decoder.cc
            sound_processor.cc
 -          subtitle.cc
 +          subrip.cc
 +          subrip_content.cc
 +          subrip_decoder.cc
            subtitle_content.cc
            subtitle_decoder.cc
            timer.cc
@@@ -89,7 -83,7 +90,7 @@@ def build(bld)
                   AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                   BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
                   SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
 -                 CURL ZIP QUICKMAIL
 +                 CURL ZIP QUICKMAIL PANGOMM CAIROMM
                   """
  
      if bld.env.TARGET_OSX:
index ac85407a2f4a5938fb9fd2538a022189f98d87ca,d59c4ae07fdd2a37c9c5ab9d1b9bfc268c702c54..c65eadd5a0676c9122ee968b8f902fe774a399ff
@@@ -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
  
  */
  
 +/** @file  src/wx/audio_mapping_view.cc
 + *  @brief AudioMappingView class and helpers.
 + */
 +
  #include <wx/wx.h>
  #include <wx/renderer.h>
  #include <wx/grid.h>
 -#include <libdcp/types.h>
 +#include <dcp/types.h>
  #include "lib/audio_mapping.h"
  #include "lib/util.h"
  #include "audio_mapping_view.h"
  #include "wx_util.h"
  #include "audio_gain_dialog.h"
 +#include <boost/lexical_cast.hpp>
  
  using std::cout;
  using std::list;
@@@ -44,7 -39,7 +44,7 @@@ using boost::lexical_cast
  enum {
        ID_off = 1,
        ID_full = 2,
-       ID_minus3dB = 3,
+       ID_minus6dB = 3,
        ID_edit = 4
  };
  
@@@ -57,9 -52,6 +57,9 @@@ public
        }
  };
  
 +/** @class ValueRenderer
 + *  @brief wxGridCellRenderer for a gain value.
 + */
  class ValueRenderer : public wxGridCellRenderer
  {
  public:
@@@ -138,12 -130,12 +138,12 @@@ AudioMappingView::AudioMappingView (wxW
        _menu = new wxMenu;
        _menu->Append (ID_off, _("Off"));
        _menu->Append (ID_full, _("Full"));
-       _menu->Append (ID_minus3dB, _("-3dB"));
+       _menu->Append (ID_minus6dB, _("-6dB"));
        _menu->Append (ID_edit, _("Edit..."));
  
        Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&AudioMappingView::off, this), ID_off);
        Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&AudioMappingView::full, this), ID_full);
-       Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&AudioMappingView::minus3dB, this), ID_minus3dB);
+       Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&AudioMappingView::minus6dB, this), ID_minus6dB);
        Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&AudioMappingView::edit, this), ID_edit);
  }
  
@@@ -162,7 -154,7 +162,7 @@@ AudioMappingView::left_click (wxGridEve
                return;
        }
  
 -      libdcp::Channel d = static_cast<libdcp::Channel> (ev.GetCol() - 1);
 +      dcp::Channel d = static_cast<dcp::Channel> (ev.GetCol() - 1);
        
        if (_map.get (ev.GetRow(), d) > 0) {
                _map.set (ev.GetRow(), d, 0);
@@@ -188,28 -180,28 +188,28 @@@ AudioMappingView::right_click (wxGridEv
  void
  AudioMappingView::off ()
  {
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 0);
 +      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 0);
        map_changed ();
  }
  
  void
  AudioMappingView::full ()
  {
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 1);
 +      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 1);
        map_changed ();
  }
  
  void
- AudioMappingView::minus3dB ()
+ AudioMappingView::minus6dB ()
  {
-       _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 1 / sqrt (2));
 -      _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), pow (10, -6.0 / 20));
++      _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), pow (10, -6.0 / 20));
        map_changed ();
  }
  
  void
  AudioMappingView::edit ()
  {
 -      libdcp::Channel d = static_cast<libdcp::Channel> (_menu_column - 1);
 +      dcp::Channel d = static_cast<dcp::Channel> (_menu_column - 1);
        
        AudioGainDialog* dialog = new AudioGainDialog (this, _menu_row, _menu_column - 1, _map.get (_menu_row, d));
        if (dialog->ShowModal () == wxID_OK) {
@@@ -246,7 -238,7 +246,7 @@@ AudioMappingView::update_cells (
                _grid->SetCellValue (i, 0, wxString::Format (wxT("%d"), i + 1));
  
                for (int j = 1; j < _grid->GetNumberCols(); ++j) {
 -                      _grid->SetCellValue (i, j, std_to_wx (lexical_cast<string> (_map.get (i, static_cast<libdcp::Channel> (j - 1)))));
 +                      _grid->SetCellValue (i, j, std_to_wx (lexical_cast<string> (_map.get (i, static_cast<dcp::Channel> (j - 1)))));
                }
        }
  
@@@ -350,7 -342,7 +350,7 @@@ AudioMappingView::mouse_moved (wxMouseE
        if (row != _last_tooltip_row || column != _last_tooltip_column) {
  
                wxString s;
 -              float const gain = _map.get (row, static_cast<libdcp::Channel> (column - 1));
 +              float const gain = _map.get (row, static_cast<dcp::Channel> (column - 1));
                if (gain == 0) {
                        s = wxString::Format (_("No audio will be passed from content channel %d to DCP channel %d."), row + 1, column);
                } else if (gain == 1) {
index 3d8db0cffb2a7bd94b08da1fd5a82e696154c7af,98375eb9e3c071b6b83d23fe332e2a7988fb8dcf..7ed6994633c111a7813951ae92c78dcca3023410
@@@ -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
  
  */
  
 +/** @file  src/wx/audio_mapping_view.h
 + *  @brief AudioMappingView class
 + *
 + *  This class displays the mapping of one set of audio channels to another,
 + *  with gain values on each node of the map.
 + */
 +
  #include <boost/signals2.hpp>
  #include <wx/wx.h>
  #include <wx/grid.h>
@@@ -49,7 -42,7 +49,7 @@@ private
  
        void off ();
        void full ();
-       void minus3dB ();
+       void minus6dB ();
        void edit ();
  
        wxGrid* _grid;