Merge master and multifarious hackery.
authorCarl Hetherington <cth@carlh.net>
Sat, 25 May 2013 00:07:35 +0000 (01:07 +0100)
committerCarl Hetherington <cth@carlh.net>
Sat, 25 May 2013 00:07:35 +0000 (01:07 +0100)
33 files changed:
1  2 
cscript
src/lib/ab_transcode_job.cc
src/lib/audio_decoder.cc
src/lib/dcp_video_frame.cc
src/lib/dcp_video_frame.h
src/lib/encoder.cc
src/lib/ffmpeg_content.cc
src/lib/ffmpeg_content.h
src/lib/ffmpeg_decoder.cc
src/lib/ffmpeg_decoder.h
src/lib/film.cc
src/lib/film.h
src/lib/filter_graph.cc
src/lib/filter_graph.h
src/lib/image.h
src/lib/player.cc
src/lib/po/fr_FR.po
src/lib/server.cc
src/lib/sndfile_decoder.cc
src/lib/wscript
src/tools/dcpomatic_cli.cc
src/wx/film_editor.cc
src/wx/film_viewer.cc
test/client_server_test.cc
test/dcp_test.cc
test/film_metadata_test.cc
test/format_test.cc
test/frame_rate_test.cc
test/job_test.cc
test/pixel_formats_test.cc
test/stream_test.cc
test/test.cc
wscript

diff --cc cscript
index 17f267628c3e1550698d6b174400028e77dda8d4,521bc54f9c20691ab214c0ba5fed3741a6a1eead..ffbca416880d87703525b4aa4c759b1f5d672c6d
+++ b/cscript
@@@ -7,9 -7,8 +7,9 @@@ def dependencies(target)
          return ()
      else:
          return (('openjpeg-cdist', None),
-                 ('ffmpeg-cdist', '488d5d4496af5e3a3b9d31d6b221e8eeada6b77e'),
 +                ('libcxml', None),
 -                ('libdcp', 'v0.49'))
+                 ('ffmpeg-cdist', '7a23ec9c771184ab563cfe24ad9b427f38368961'),
 +                ('libdcp', None))
  
  def build(env, target):
      cmd = './waf configure --prefix=%s' % env.work_dir_cscript()
index 2bdff47de684827ab0e0ec15664737cb624609ed,4ffdd9af6b815cbe6c572d1e72a336e82ce0445b..9a883fdd93773a87516e47b95c4448b8943f9602
@@@ -38,7 -39,8 +38,8 @@@ ABTranscodeJob::ABTranscodeJob (shared_
  {
        _film_b.reset (new Film (*_film));
        _film_b->set_scaler (Config::instance()->reference_scaler ());
--      _film_b->set_filters (Config::instance()->reference_filters ());
++      /* XXX */
++//    _film_b->set_filters (Config::instance()->reference_filters ());
  }
  
  string
index 8950e15465d3602673ff75bcae43f5f5e129a49c,a54c14843927449b4a62f00665ea83020a53bf70..9b8d15bf163927c69cbe201e5c7427825013db76
  */
  
  #include "audio_decoder.h"
 -#include "stream.h"
 +#include "audio_buffers.h"
 +#include "exceptions.h"
 +#include "log.h"
  
 +#include "i18n.h"
 +
 +using std::stringstream;
++using std::list;
++using std::pair;
  using boost::optional;
  using boost::shared_ptr;
  
 -AudioDecoder::AudioDecoder (shared_ptr<Film> f, DecodeOptions o)
 -      : Decoder (f, o)
 +AudioDecoder::AudioDecoder (shared_ptr<const Film> f, shared_ptr<const AudioContent> c)
 +      : Decoder (f)
 +      , _next_audio (0)
 +      , _audio_content (c)
  {
 +      if (_audio_content->content_audio_frame_rate() != _audio_content->output_audio_frame_rate()) {
 +
 +              shared_ptr<const Film> film = _film.lock ();
 +              assert (film);
 +
 +              stringstream s;
 +              s << String::compose (
 +                      "Will resample audio from %1 to %2",
 +                      _audio_content->content_audio_frame_rate(), _audio_content->output_audio_frame_rate()
 +                      );
 +              
 +              film->log()->log (s.str ());
 +
 +              /* We will be using planar float data when we call the
 +                 resampler.  As far as I can see, the audio channel
 +                 layout is not necessary for our purposes; it seems
 +                 only to be used get the number of channels and
 +                 decide if rematrixing is needed.  It won't be, since
 +                 input and output layouts are the same.
 +              */
 +
 +              _swr_context = swr_alloc_set_opts (
 +                      0,
 +                      av_get_default_channel_layout (MAX_AUDIO_CHANNELS),
 +                      AV_SAMPLE_FMT_FLTP,
 +                      _audio_content->output_audio_frame_rate(),
 +                      av_get_default_channel_layout (MAX_AUDIO_CHANNELS),
 +                      AV_SAMPLE_FMT_FLTP,
 +                      _audio_content->content_audio_frame_rate(),
 +                      0, 0
 +                      );
 +              
 +              swr_init (_swr_context);
 +      } else {
 +              _swr_context = 0;
 +      }
 +}
  
 +AudioDecoder::~AudioDecoder ()
 +{
 +      if (_swr_context) {
 +              swr_free (&_swr_context);
 +      }
  }
 +      
  
 +#if 0
  void
 -AudioDecoder::set_audio_stream (shared_ptr<AudioStream> s)
 +AudioDecoder::process_end ()
  {
 -      _audio_stream = s;
 +      if (_swr_context) {
 +
 +              shared_ptr<const Film> film = _film.lock ();
 +              assert (film);
 +              
 +              shared_ptr<AudioBuffers> out (new AudioBuffers (film->audio_mapping().dcp_channels(), 256));
 +                      
 +              while (1) {
 +                      int const frames = swr_convert (_swr_context, (uint8_t **) out->data(), 256, 0, 0);
 +
 +                      if (frames < 0) {
 +                              throw EncodeError (_("could not run sample-rate converter"));
 +                      }
 +
 +                      if (frames == 0) {
 +                              break;
 +                      }
 +
 +                      out->set_frames (frames);
 +                      _writer->write (out);
 +              }
 +
 +      }
  }
-       shared_ptr<AudioBuffers> dcp_mapped (film->dcp_audio_channels(), data->frames());
 +#endif
 +
 +void
 +AudioDecoder::audio (shared_ptr<const AudioBuffers> data, Time time)
 +{
 +      /* Maybe resample */
 +      if (_swr_context) {
 +
 +              /* Compute the resampled frames count and add 32 for luck */
 +              int const max_resampled_frames = ceil (
 +                      (int64_t) data->frames() * _audio_content->output_audio_frame_rate() / _audio_content->content_audio_frame_rate()
 +                      ) + 32;
 +
 +              shared_ptr<AudioBuffers> resampled (new AudioBuffers (data->channels(), max_resampled_frames));
 +
 +              /* Resample audio */
 +              int const resampled_frames = swr_convert (
 +                      _swr_context, (uint8_t **) resampled->data(), max_resampled_frames, (uint8_t const **) data->data(), data->frames()
 +                      );
 +              
 +              if (resampled_frames < 0) {
 +                      throw EncodeError (_("could not run sample-rate converter"));
 +              }
 +
 +              resampled->set_frames (resampled_frames);
 +              
 +              /* And point our variables at the resampled audio */
 +              data = resampled;
 +      }
 +
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +      
 +      /* Remap channels */
-               dcp_mapped->accumulate (data, i->first, i->second);
++      shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (film->dcp_audio_channels(), data->frames()));
 +      dcp_mapped->make_silent ();
 +      list<pair<int, libdcp::Channel> > map = _audio_content->audio_mapping().content_to_dcp ();
 +      for (list<pair<int, libdcp::Channel> >::iterator i = map.begin(); i != map.end(); ++i) {
++              dcp_mapped->accumulate_channel (data.get(), i->first, i->second);
 +      }
 +
 +      Audio (dcp_mapped, time);
 +      _next_audio = time + film->audio_frames_to_time (data->frames());
 +}
 +
 +              
index da51665d170809972547d227fa2735f51f18bd38,d674393a98023e2e02459c1bc6b8ec6c966d2a42..1c1838df79403f046f62511ec84bbe04273e4897
@@@ -79,7 -80,7 +79,7 @@@ using libdcp::Size
  DCPVideoFrame::DCPVideoFrame (
        shared_ptr<const Image> yuv, shared_ptr<Subtitle> sub,
        Size out, int p, int subtitle_offset, float subtitle_scale,
--      Scaler const * s, int f, int dcp_fps, string pp, int clut, int bw, shared_ptr<Log> l
++      Scaler const * s, int f, int dcp_fps, int clut, int bw, shared_ptr<Log> l
        )
        : _input (yuv)
        , _subtitle (sub)
@@@ -90,7 -91,7 +90,6 @@@
        , _scaler (s)
        , _frame (f)
        , _frames_per_second (dcp_fps)
--      , _post_process (pp)
        , _colour_lut (clut)
        , _j2k_bandwidth (bw)
        , _log (l)
@@@ -156,10 -157,10 +155,6 @@@ DCPVideoFrame::~DCPVideoFrame (
  shared_ptr<EncodedData>
  DCPVideoFrame::encode_locally ()
  {
--      if (!_post_process.empty ()) {
--              _input = _input->post_process (_post_process, true);
--      }
--      
        shared_ptr<Image> prepared = _input->scale_and_convert_to_rgb (_out_size, _padding, _scaler, true);
  
        if (_subtitle) {
@@@ -333,10 -334,10 +328,6 @@@ DCPVideoFrame::encode_remotely (ServerD
          << N_("frame ") << _frame << N_("\n")
          << N_("frames_per_second ") << _frames_per_second << N_("\n");
  
--      if (!_post_process.empty()) {
--              s << N_("post_process ") << _post_process << N_("\n");
--      }
--      
        s << N_("colour_lut ") << _colour_lut << N_("\n")
          << N_("j2k_bandwidth ") << _j2k_bandwidth << N_("\n");
  
index 4ceb07d2649905a87a80ebf9daa6712e97fc8d96,4ceb07d2649905a87a80ebf9daa6712e97fc8d96..ba49c95a43364a125452c8e515aa33c7d8737dda
@@@ -107,7 -107,7 +107,7 @@@ class DCPVideoFram
  public:
        DCPVideoFrame (
                boost::shared_ptr<const Image>, boost::shared_ptr<Subtitle>, libdcp::Size,
--              int, int, float, Scaler const *, int, int, std::string, int, int, boost::shared_ptr<Log>
++              int, int, float, Scaler const *, int, int, int, int, boost::shared_ptr<Log>
                );
        
        virtual ~DCPVideoFrame ();
@@@ -131,7 -131,7 +131,6 @@@ private
        Scaler const * _scaler;          ///< scaler to use
        int _frame;                      ///< frame index within the DCP's intrinsic duration
        int _frames_per_second;          ///< Frames per second that we will use for the DCP
--      std::string _post_process;       ///< FFmpeg post-processing string to use
        int _colour_lut;                 ///< Colour look-up table to use
        int _j2k_bandwidth;              ///< J2K bandwidth to use
  
index 52927c5d324f054250e939f96d9e4b841153e2a1,8549962ff01807505683266e778d16c4c8eaef43..270bf3d43b736fe888f23a537cdf1c1c68d0dfa5
@@@ -207,14 -267,13 +207,13 @@@ Encoder::process_video (shared_ptr<cons
                frame_done ();
        } else {
                /* Queue this new frame for encoding */
--              pair<string, string> const s = Filter::ffmpeg_strings (_film->filters());
                TIMING ("adding to queue of %1", _queue.size ());
 -              _queue.push_back (boost::shared_ptr<DCPVideoFrame> (
 +              /* XXX: padding */
 +              _queue.push_back (shared_ptr<DCPVideoFrame> (
                                          new DCPVideoFrame (
 -                                                image, sub, _film->format()->dcp_size(), _film->format()->dcp_padding (_film),
 +                                                image, sub, _film->container()->dcp_size(), 0,
                                                  _film->subtitle_offset(), _film->subtitle_scale(),
-                                                 _film->scaler(), _video_frames_out, _film->dcp_video_frame_rate(), s.second,
 -                                                _film->scaler(), _video_frames_out, _film->dcp_frame_rate(), s.second,
++                                                _film->scaler(), _video_frames_out, _film->dcp_video_frame_rate(),
                                                  _film->colour_lut(), _film->j2k_bandwidth(),
                                                  _film->log()
                                                  )
index ad7af07d864479123513384ecf93241e49176af7,0000000000000000000000000000000000000000..55139ca56cd8b380265a957cb45402088d6bbabb
mode 100644,000000..100644
--- /dev/null
@@@ -1,337 -1,0 +1,359 @@@
 +/* -*- c-basic-offset: 8; default-tab-width: 8; -*- */
 +
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <libcxml/cxml.h>
 +#include "ffmpeg_content.h"
 +#include "ffmpeg_decoder.h"
 +#include "compose.hpp"
 +#include "job.h"
 +#include "util.h"
++#include "filter.h"
 +#include "log.h"
 +
 +#include "i18n.h"
 +
 +using std::string;
 +using std::stringstream;
 +using std::vector;
 +using std::list;
 +using std::cout;
 +using boost::shared_ptr;
 +using boost::lexical_cast;
 +
 +int const FFmpegContentProperty::SUBTITLE_STREAMS = 100;
 +int const FFmpegContentProperty::SUBTITLE_STREAM = 101;
 +int const FFmpegContentProperty::AUDIO_STREAMS = 102;
 +int const FFmpegContentProperty::AUDIO_STREAM = 103;
++int const FFmpegContentProperty::FILTERS = 104;
 +
 +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, boost::filesystem::path p)
 +      : Content (f, p)
 +      , VideoContent (f, p)
 +      , AudioContent (f, p)
 +{
 +
 +}
 +
 +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node)
 +      : Content (f, node)
 +      , VideoContent (f, node)
 +      , AudioContent (f, node)
 +{
 +      list<shared_ptr<cxml::Node> > c = node->node_children ("SubtitleStream");
 +      for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) {
 +              _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (*i)));
 +              if ((*i)->optional_number_child<int> ("Selected")) {
 +                      _subtitle_stream = _subtitle_streams.back ();
 +              }
 +      }
 +
 +      c = node->node_children ("AudioStream");
 +      for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) {
 +              _audio_streams.push_back (shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (*i)));
 +              if ((*i)->optional_number_child<int> ("Selected")) {
 +                      _audio_stream = _audio_streams.back ();
 +              }
 +      }
++
++      c = node->node_children ("Filter");
++      for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) {
++              _filters.push_back (Filter::from_id ((*i)->content ()));
++      }
 +}
 +
 +FFmpegContent::FFmpegContent (FFmpegContent const & o)
 +      : Content (o)
 +      , VideoContent (o)
 +      , AudioContent (o)
 +      , _subtitle_streams (o._subtitle_streams)
 +      , _subtitle_stream (o._subtitle_stream)
 +      , _audio_streams (o._audio_streams)
 +      , _audio_stream (o._audio_stream)
 +{
 +
 +}
 +
 +void
 +FFmpegContent::as_xml (xmlpp::Node* node) const
 +{
 +      node->add_child("Type")->add_child_text ("FFmpeg");
 +      Content::as_xml (node);
 +      VideoContent::as_xml (node);
 +      AudioContent::as_xml (node);
 +
 +      boost::mutex::scoped_lock lm (_mutex);
 +
 +      for (vector<shared_ptr<FFmpegSubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) {
 +              xmlpp::Node* t = node->add_child("SubtitleStream");
 +              if (_subtitle_stream && *i == _subtitle_stream) {
 +                      t->add_child("Selected")->add_child_text("1");
 +              }
 +              (*i)->as_xml (t);
 +      }
 +
 +      for (vector<shared_ptr<FFmpegAudioStream> >::const_iterator i = _audio_streams.begin(); i != _audio_streams.end(); ++i) {
 +              xmlpp::Node* t = node->add_child("AudioStream");
 +              if (_audio_stream && *i == _audio_stream) {
 +                      t->add_child("Selected")->add_child_text("1");
 +              }
 +              (*i)->as_xml (t);
 +      }
++
++      for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
++              node->add_child("Filter")->add_child_text ((*i)->id ());
++      }
 +}
 +
 +void
 +FFmpegContent::examine (shared_ptr<Job> job)
 +{
 +      job->set_progress_unknown ();
 +
 +      Content::examine (job);
 +
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +
 +      shared_ptr<FFmpegDecoder> decoder (new FFmpegDecoder (film, shared_from_this (), true, false, false));
 +
 +      ContentVideoFrame video_length = 0;
 +      video_length = decoder->video_length ();
 +      film->log()->log (String::compose ("Video length obtained from header as %1 frames", decoder->video_length ()));
 +
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +
 +                _video_length = video_length;
 +
 +                _subtitle_streams = decoder->subtitle_streams ();
 +                if (!_subtitle_streams.empty ()) {
 +                        _subtitle_stream = _subtitle_streams.front ();
 +                }
 +                
 +                _audio_streams = decoder->audio_streams ();
 +                if (!_audio_streams.empty ()) {
 +                        _audio_stream = _audio_streams.front ();
 +                }
 +        }
 +
 +        take_from_video_decoder (decoder);
 +
 +        signal_changed (VideoContentProperty::VIDEO_LENGTH);
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS);
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAMS);
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
 +        signal_changed (AudioContentProperty::AUDIO_CHANNELS);
 +}
 +
 +string
 +FFmpegContent::summary () const
 +{
 +      return String::compose (_("Movie: %1"), file().filename().string());
 +}
 +
 +string
 +FFmpegContent::information () const
 +{
 +      if (video_length() == 0 || video_frame_rate() == 0) {
 +              return "";
 +      }
 +      
 +      stringstream s;
 +      
 +      s << String::compose (_("%1 frames; %2 frames per second"), video_length(), video_frame_rate()) << "\n";
 +      s << VideoContent::information ();
 +
 +      return s.str ();
 +}
 +
 +void
 +FFmpegContent::set_subtitle_stream (shared_ptr<FFmpegSubtitleStream> s)
 +{
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                _subtitle_stream = s;
 +        }
 +
 +        signal_changed (FFmpegContentProperty::SUBTITLE_STREAM);
 +}
 +
 +void
 +FFmpegContent::set_audio_stream (shared_ptr<FFmpegAudioStream> s)
 +{
 +        {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                _audio_stream = s;
 +        }
 +
 +        signal_changed (FFmpegContentProperty::AUDIO_STREAM);
 +}
 +
 +ContentAudioFrame
 +FFmpegContent::audio_length () const
 +{
 +      int const cafr = content_audio_frame_rate ();
 +      int const vfr  = video_frame_rate ();
 +      ContentVideoFrame const vl = video_length ();
 +
 +      boost::mutex::scoped_lock lm (_mutex);
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +        
 +        return video_frames_to_audio_frames (vl, cafr, vfr);
 +}
 +
 +int
 +FFmpegContent::audio_channels () const
 +{
 +      boost::mutex::scoped_lock lm (_mutex);
 +      
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +
 +        return _audio_stream->channels;
 +}
 +
 +int
 +FFmpegContent::content_audio_frame_rate () const
 +{
 +      boost::mutex::scoped_lock lm (_mutex);
 +
 +        if (!_audio_stream) {
 +                return 0;
 +        }
 +
 +        return _audio_stream->frame_rate;
 +}
 +
 +int
 +FFmpegContent::output_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 ());
 +
 +      FrameRateConversion frc (video_frame_rate(), film->dcp_video_frame_rate());
 +
 +      /* Compensate if the DCP is being run at a different frame rate
 +         to the source; that is, if the video is run such that it will
 +         look different in the DCP compared to the source (slower or faster).
 +         skip/repeat doesn't come into effect here.
 +      */
 +
 +      if (frc.change_speed) {
 +              t *= video_frame_rate() * frc.factor() / film->dcp_video_frame_rate();
 +      }
 +
 +      return rint (t);
 +}
 +
 +bool
 +operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b)
 +{
 +        return a.id == b.id;
 +}
 +
 +bool
 +operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b)
 +{
 +        return a.id == b.id;
 +}
 +
 +FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node)
 +{
 +      name = node->string_child ("Name");
 +      id = node->number_child<int> ("Id");
 +      frame_rate = node->number_child<int> ("FrameRate");
 +      channels = node->number_child<int64_t> ("Channels");
 +      mapping = AudioMapping (node->node_child ("Mapping"));
 +}
 +
 +void
 +FFmpegAudioStream::as_xml (xmlpp::Node* root) const
 +{
 +      root->add_child("Name")->add_child_text (name);
 +      root->add_child("Id")->add_child_text (lexical_cast<string> (id));
 +      root->add_child("FrameRate")->add_child_text (lexical_cast<string> (frame_rate));
 +      root->add_child("Channels")->add_child_text (lexical_cast<string> (channels));
 +      mapping.as_xml (root->add_child("Mapping"));
 +}
 +
 +/** Construct a SubtitleStream from a value returned from to_string().
 + *  @param t String returned from to_string().
 + *  @param v State file version.
 + */
 +FFmpegSubtitleStream::FFmpegSubtitleStream (shared_ptr<const cxml::Node> node)
 +{
 +      name = node->string_child ("Name");
 +      id = node->number_child<int> ("Id");
 +}
 +
 +void
 +FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const
 +{
 +      root->add_child("Name")->add_child_text (name);
 +      root->add_child("Id")->add_child_text (lexical_cast<string> (id));
 +}
 +
 +shared_ptr<Content>
 +FFmpegContent::clone () const
 +{
 +      return shared_ptr<Content> (new FFmpegContent (*this));
 +}
 +
 +Time
 +FFmpegContent::length () const
 +{
 +      shared_ptr<const Film> film = _film.lock ();
 +      assert (film);
 +      
 +      FrameRateConversion frc (video_frame_rate (), film->dcp_video_frame_rate ());
 +      return video_length() * frc.factor() * TIME_HZ / film->dcp_video_frame_rate ();
 +}
 +
 +AudioMapping
 +FFmpegContent::audio_mapping () const
 +{
 +      boost::mutex::scoped_lock lm (_mutex);
 +
 +      if (!_audio_stream) {
 +              return AudioMapping ();
 +      }
 +
 +      return _audio_stream->mapping;
 +}
 +
++void
++FFmpegContent::set_filters (vector<Filter const *> const & filters)
++{
++      {
++              boost::mutex::scoped_lock lm (_mutex);
++              _filters = filters;
++      }
++
++      signal_changed (FFmpegContentProperty::FILTERS);
++}
++
index d5b9869967074de0347244ebaec3c6ebbab17f3e,0000000000000000000000000000000000000000..8f5c773eeb853ba7f7530268bef65d4188621969
mode 100644,000000..100644
--- /dev/null
@@@ -1,135 -1,0 +1,147 @@@
 +/* -*- c-basic-offset: 8; default-tab-width: 8; -*- */
 +
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#ifndef DCPOMATIC_FFMPEG_CONTENT_H
 +#define DCPOMATIC_FFMPEG_CONTENT_H
 +
 +#include <boost/enable_shared_from_this.hpp>
 +#include "video_content.h"
 +#include "audio_content.h"
 +
++class Filter;
++
 +class FFmpegAudioStream
 +{
 +public:
 +        FFmpegAudioStream (std::string n, int i, int f, int c)
 +                : name (n)
 +                , id (i)
 +                , frame_rate (f)
 +              , channels (c)
 +              , mapping (c)
 +        {}
 +
 +      FFmpegAudioStream (boost::shared_ptr<const cxml::Node>);
 +
 +      void as_xml (xmlpp::Node *) const;
 +      
 +        std::string name;
 +        int id;
 +        int frame_rate;
 +      int channels;
 +      AudioMapping mapping;
 +};
 +
 +extern bool operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b);
 +
 +class FFmpegSubtitleStream
 +{
 +public:
 +        FFmpegSubtitleStream (std::string n, int i)
 +                : name (n)
 +                , id (i)
 +        {}
 +        
 +      FFmpegSubtitleStream (boost::shared_ptr<const cxml::Node>);
 +
 +      void as_xml (xmlpp::Node *) const;
 +      
 +        std::string name;
 +        int id;
 +};
 +
 +extern bool operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b);
 +
 +class FFmpegContentProperty : public VideoContentProperty
 +{
 +public:
 +        static int const SUBTITLE_STREAMS;
 +        static int const SUBTITLE_STREAM;
 +        static int const AUDIO_STREAMS;
 +        static int const AUDIO_STREAM;
++        static int const FILTERS;
 +};
 +
 +class FFmpegContent : public VideoContent, public AudioContent
 +{
 +public:
 +      FFmpegContent (boost::shared_ptr<const Film>, boost::filesystem::path);
 +      FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>);
 +      FFmpegContent (FFmpegContent const &);
 +
 +      boost::shared_ptr<FFmpegContent> shared_from_this () {
 +              return boost::dynamic_pointer_cast<FFmpegContent> (Content::shared_from_this ());
 +      }
 +      
 +      void examine (boost::shared_ptr<Job>);
 +      std::string summary () const;
 +      std::string information () const;
 +      void as_xml (xmlpp::Node *) const;
 +      boost::shared_ptr<Content> clone () const;
 +      Time length () const;
 +
 +        /* AudioContent */
 +        int audio_channels () const;
 +        ContentAudioFrame audio_length () const;
 +        int content_audio_frame_rate () const;
 +        int output_audio_frame_rate () const;
 +      AudioMapping audio_mapping () const;
++
++      void set_filters (std::vector<Filter const *> const &);
 +      
 +        std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                return _subtitle_streams;
 +        }
 +
 +        boost::shared_ptr<FFmpegSubtitleStream> subtitle_stream () const {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                return _subtitle_stream;
 +        }
 +
 +        std::vector<boost::shared_ptr<FFmpegAudioStream> > audio_streams () const {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                return _audio_streams;
 +        }
 +        
 +        boost::shared_ptr<FFmpegAudioStream> audio_stream () const {
 +                boost::mutex::scoped_lock lm (_mutex);
 +                return _audio_stream;
 +        }
 +
++      std::vector<Filter const *> filters () const {
++              boost::mutex::scoped_lock lm (_mutex);
++              return _filters;
++      }
++
 +        void set_subtitle_stream (boost::shared_ptr<FFmpegSubtitleStream>);
 +        void set_audio_stream (boost::shared_ptr<FFmpegAudioStream>);
 +      
 +private:
 +      std::vector<boost::shared_ptr<FFmpegSubtitleStream> > _subtitle_streams;
 +      boost::shared_ptr<FFmpegSubtitleStream> _subtitle_stream;
 +      std::vector<boost::shared_ptr<FFmpegAudioStream> > _audio_streams;
 +      boost::shared_ptr<FFmpegAudioStream> _audio_stream;
++      /** Video filters that should be used when generating DCPs */
++      std::vector<Filter const *> _filters;
 +};
 +
 +#endif
index 047829d456395e8abd039dbe9aecf9616c256b31,982139515b0af588eeb9c8180e5accd74cef444f..119e82851ec5de9433d119e2409b1b951067104f
@@@ -503,31 -595,85 +503,15 @@@ FFmpegDecoder::do_seek (Time t, bool ba
                        av_free_packet (&_packet);
                }
        }
 -              
 -      return r < 0;
 -}
 -
 -shared_ptr<FFmpegAudioStream>
 -FFmpegAudioStream::create (string t, optional<int> v)
 -{
 -      if (!v) {
 -              /* version < 1; no type in the string, and there's only FFmpeg streams anyway */
 -              return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v));
 -      }
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type;
 -      if (type != N_("ffmpeg")) {
 -              return shared_ptr<FFmpegAudioStream> ();
 -      }
 -
 -      return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v));
 -}
 -
 -FFmpegAudioStream::FFmpegAudioStream (string t, optional<int> version)
 -{
 -      stringstream n (t);
 -      
 -      int name_index = 4;
 -      if (!version) {
 -              name_index = 2;
 -              int channels;
 -              n >> _id >> channels;
 -              _channel_layout = av_get_default_channel_layout (channels);
 -              _sample_rate = 0;
 -      } else {
 -              string type;
 -              /* Current (marked version 1) */
 -              n >> type >> _id >> _sample_rate >> _channel_layout;
 -              assert (type == N_("ffmpeg"));
 -      }
 -
 -      for (int i = 0; i < name_index; ++i) {
 -              size_t const s = t.find (' ');
 -              if (s != string::npos) {
 -                      t = t.substr (s + 1);
 -              }
 -      }
 -
 -      _name = t;
 -}
 -
 -string
 -FFmpegAudioStream::to_string () const
 -{
 -      return String::compose (N_("ffmpeg %1 %2 %3 %4"), _id, _sample_rate, _channel_layout, _name);
 -}
 -
 -void
 -FFmpegDecoder::film_changed (Film::Property p)
 -{
 -      switch (p) {
 -      case Film::CROP:
 -      case Film::FILTERS:
 -      {
 -              boost::mutex::scoped_lock lm (_filter_graphs_mutex);
 -              _filter_graphs.clear ();
 -      }
 -      OutputChanged ();
 -      break;
  
 -      default:
 -              break;
 -      }
 +      return;
  }
  
- void
- FFmpegDecoder::film_changed (Film::Property p)
- {
-       switch (p) {
-       case Film::FILTERS:
-       {
-               boost::mutex::scoped_lock lm (_filter_graphs_mutex);
-               _filter_graphs.clear ();
-       }
-       break;
-       default:
-               break;
-       }
- }
  /** @return Length (in video frames) according to our content's header */
 -SourceFrame
 -FFmpegDecoder::length () const
 +ContentVideoFrame
 +FFmpegDecoder::video_length () const
  {
 -      return (double(_format_context->duration) / AV_TIME_BASE) * frames_per_second();
 +      return (double(_format_context->duration) / AV_TIME_BASE) * video_frame_rate();
  }
  
  void
@@@ -563,66 -712,3 +547,74 @@@ FFmpegDecoder::decode_audio_packet (
                }
        }
  }
-               graph.reset (new FilterGraph (_film, this, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
-               _filter_graphs.push_back (graph);
 +
 +bool
 +FFmpegDecoder::decode_video_packet ()
 +{
 +      int frame_finished;
 +      if (avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet) < 0 || !frame_finished) {
 +              return false;
 +      }
 +              
 +      boost::mutex::scoped_lock lm (_filter_graphs_mutex);
 +
 +      shared_ptr<FilterGraph> graph;
 +      
 +      list<shared_ptr<FilterGraph> >::iterator i = _filter_graphs.begin();
 +      while (i != _filter_graphs.end() && !(*i)->can_process (libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) {
 +              ++i;
 +      }
 +
 +      if (i == _filter_graphs.end ()) {
 +              shared_ptr<const Film> film = _film.lock ();
 +              assert (film);
++
++              graph.reset (new FilterGraph (_ffmpeg_content, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format));
++              _filter_graphs.push_back (graph);
++
 +              film->log()->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format));
 +      } else {
 +              graph = *i;
 +      }
 +
-                       video (*i, false, t);
 +      list<shared_ptr<Image> > images = graph->process (_frame);
++
++      string post_process = Filter::ffmpeg_strings (_ffmpeg_content->filters()).second;
 +      
 +      for (list<shared_ptr<Image> >::iterator i = images.begin(); i != images.end(); ++i) {
++
++              shared_ptr<Image> image = *i;
++              if (!post_process.empty ()) {
++                      image = image->post_process (post_process, true);
++              }
++              
 +              int64_t const bet = av_frame_get_best_effort_timestamp (_frame);
 +              if (bet != AV_NOPTS_VALUE) {
 +                      /* XXX: may need to insert extra frames / remove frames here ...
 +                         (as per old Matcher)
 +                      */
 +                      Time const t = bet * av_q2d (_format_context->streams[_video_stream]->time_base) * TIME_HZ;
++                      video (image, false, t);
 +              } else {
 +                      shared_ptr<const Film> film = _film.lock ();
 +                      assert (film);
 +                      film->log()->log ("Dropping frame without PTS");
 +              }
 +      }
 +
 +      return true;
 +}
 +
 +Time
 +FFmpegDecoder::next () const
 +{
 +      if (_decode_video && _decode_audio) {
 +              return min (_next_video, _next_audio);
 +      }
 +
 +      if (_decode_audio) {
 +              return _next_audio;
 +      }
 +
 +      return _next_video;
 +}
index dbcfe3be01017816205934e2bd8c72a3a1aa2ab4,0c89b973dfbb9b47c237cd2e9ade391327758fd9..c3747961266789a8b2da5447d15dc9c578d3cc1b
@@@ -112,12 -125,10 +112,10 @@@ private
        void maybe_add_subtitle ();
        boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t** data, int size);
  
--      void film_changed (Film::Property);
--
        std::string stream_name (AVStream* s) const;
  
 +      boost::shared_ptr<const FFmpegContent> _ffmpeg_content;
 +
        AVFormatContext* _format_context;
        int _video_stream;
        
diff --cc src/lib/film.cc
index a61a0d53daaf5b140cc9c816d79a6232e2256f56,81c7de77f72bb0b3a04f1e7d906011c8777bcca8..fc1d2d8a4ee2fa7c98ee7d34ad06a639cf42b38e
@@@ -140,11 -157,24 +140,10 @@@ Film::Film (Film const & o
        , _directory         (o._directory)
        , _name              (o._name)
        , _use_dci_name      (o._use_dci_name)
 -      , _content           (o._content)
 -      , _trust_content_header (o._trust_content_header)
        , _dcp_content_type  (o._dcp_content_type)
 -      , _format            (o._format)
 -      , _crop              (o._crop)
 -      , _filters           (o._filters)
 +      , _container         (o._container)
-       , _filters           (o._filters)
        , _scaler            (o._scaler)
 -      , _trim_start        (o._trim_start)
 -      , _trim_end          (o._trim_end)
 -      , _trim_type         (o._trim_type)
 -      , _dcp_ab            (o._dcp_ab)
 -      , _content_audio_stream (o._content_audio_stream)
 -      , _external_audio    (o._external_audio)
 -      , _use_content_audio (o._use_content_audio)
 -      , _audio_gain        (o._audio_gain)
 -      , _audio_delay       (o._audio_delay)
 -      , _still_duration    (o._still_duration)
 -      , _subtitle_stream   (o._subtitle_stream)
 +      , _ab                (o._ab)
        , _with_subtitles    (o._with_subtitles)
        , _subtitle_offset   (o._subtitle_offset)
        , _subtitle_scale    (o._subtitle_scale)
  string
  Film::video_state_identifier () const
  {
 -      assert (format ());
 +      assert (container ());
        LocaleGuard lg;
  
--      pair<string, string> f = Filter::ffmpeg_strings (filters());
--
        stringstream s;
 -      s << format()->id()
 -        << "_" << content_digest()
 -        << "_" << crop().left << "_" << crop().right << "_" << crop().top << "_" << crop().bottom
 -        << "_" << _dcp_frame_rate
 -        << "_" << f.first << "_" << f.second
 +      s << container()->id()
 +        << "_" << _playlist->video_digest()
 +        << "_" << _dcp_video_frame_rate
-         << "_" << f.first << "_" << f.second
          << "_" << scaler()->id()
          << "_" << j2k_bandwidth()
 -        << "_" << boost::lexical_cast<int> (colour_lut());
 +        << "_" << lexical_cast<int> (colour_lut());
  
 -      if (dcp_ab()) {
 +      if (ab()) {
                pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters());
                s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second;
        }
@@@ -380,39 -433,81 +376,35 @@@ Film::write_metadata () cons
  
        boost::filesystem::create_directories (directory());
  
 -      string const m = file ("metadata");
 -      ofstream f (m.c_str ());
 -      if (!f.good ()) {
 -              throw CreateFileError (m);
 -      }
 +      xmlpp::Document doc;
 +      xmlpp::Element* root = doc.create_root_node ("Metadata");
  
 -      f << "version " << state_version << endl;
 +      root->add_child("Version")->add_child_text (lexical_cast<string> (state_version));
 +      root->add_child("Name")->add_child_text (_name);
 +      root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0");
  
 -      /* User stuff */
 -      f << "name " << _name << endl;
 -      f << "use_dci_name " << _use_dci_name << endl;
 -      f << "content " << _content << endl;
 -      f << "trust_content_header " << (_trust_content_header ? "1" : "0") << endl;
        if (_dcp_content_type) {
 -              f << "dcp_content_type " << _dcp_content_type->dci_name () << endl;
 -      }
 -      if (_format) {
 -              f << "format " << _format->as_metadata () << endl;
 -      }
 -      f << "left_crop " << _crop.left << endl;
 -      f << "right_crop " << _crop.right << endl;
 -      f << "top_crop " << _crop.top << endl;
 -      f << "bottom_crop " << _crop.bottom << endl;
 -      for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
 -              f << "filter " << (*i)->id () << endl;
 -      }
 -      f << "scaler " << _scaler->id () << endl;
 -      f << "trim_start " << _trim_start << endl;
 -      f << "trim_end " << _trim_end << endl;
 -      switch (_trim_type) {
 -      case CPL:
 -              f << "trim_type cpl\n";
 -              break;
 -      case ENCODE:
 -              f << "trim_type encode\n";
 -              break;
 -      }
 -      f << "dcp_ab " << (_dcp_ab ? "1" : "0") << endl;
 -      if (_content_audio_stream) {
 -              f << "selected_content_audio_stream " << _content_audio_stream->to_string() << endl;
 -      }
 -      for (vector<string>::const_iterator i = _external_audio.begin(); i != _external_audio.end(); ++i) {
 -              f << "external_audio " << *i << endl;
 -      }
 -      f << "use_content_audio " << (_use_content_audio ? "1" : "0") << endl;
 -      f << "audio_gain " << _audio_gain << endl;
 -      f << "audio_delay " << _audio_delay << endl;
 -      f << "still_duration " << _still_duration << endl;
 -      if (_subtitle_stream) {
 -              f << "selected_subtitle_stream " << _subtitle_stream->to_string() << endl;
 -      }
 -      f << "with_subtitles " << _with_subtitles << endl;
 -      f << "subtitle_offset " << _subtitle_offset << endl;
 -      f << "subtitle_scale " << _subtitle_scale << endl;
 -      f << "colour_lut " << _colour_lut << endl;
 -      f << "j2k_bandwidth " << _j2k_bandwidth << endl;
 -      _dci_metadata.write (f);
 -      f << "dci_date " << boost::gregorian::to_iso_string (_dci_date) << endl;
 -      f << "dcp_frame_rate " << _dcp_frame_rate << endl;
 -      f << "width " << _size.width << endl;
 -      f << "height " << _size.height << endl;
 -      f << "length " << _length.get_value_or(0) << endl;
 -      f << "content_digest " << _content_digest << endl;
 -
 -      for (vector<shared_ptr<AudioStream> >::const_iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
 -              f << "content_audio_stream " << (*i)->to_string () << endl;
 +              root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ());
        }
  
 -      f << "external_audio_stream " << _sndfile_stream->to_string() << endl;
 -
 -      for (vector<shared_ptr<SubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) {
 -              f << "subtitle_stream " << (*i)->to_string () << endl;
 +      if (_container) {
 +              root->add_child("Container")->add_child_text (_container->id ());
        }
  
-       for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) {
-               root->add_child("Filter")->add_child_text ((*i)->id ());
-       }
-       
 -      f << "source_frame_rate " << _source_frame_rate << endl;
 +      root->add_child("Scaler")->add_child_text (_scaler->id ());
 +      root->add_child("AB")->add_child_text (_ab ? "1" : "0");
 +      root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0");
 +      root->add_child("SubtitleOffset")->add_child_text (lexical_cast<string> (_subtitle_offset));
 +      root->add_child("SubtitleScale")->add_child_text (lexical_cast<string> (_subtitle_scale));
 +      root->add_child("ColourLUT")->add_child_text (lexical_cast<string> (_colour_lut));
 +      root->add_child("J2KBandwidth")->add_child_text (lexical_cast<string> (_j2k_bandwidth));
 +      _dci_metadata.as_xml (root->add_child ("DCIMetadata"));
 +      root->add_child("DCPVideoFrameRate")->add_child_text (lexical_cast<string> (_dcp_video_frame_rate));
 +      root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date));
 +      root->add_child("DCPAudioChannels")->add_child_text (lexical_cast<string> (_dcp_audio_channels));
 +      _playlist->as_xml (root->add_child ("Playlist"));
 +
 +      doc.write_to_file_formatted (file ("metadata.xml"));
        
        _dirty = false;
  }
@@@ -447,27 -659,25 +439,20 @@@ Film::read_metadata (
                }
        }
  
-       {
-               list<shared_ptr<cxml::Node> > c = f.node_children ("Filter");
-               for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) {
-                       _filters.push_back (Filter::from_id ((*i)->content ()));
 -      if (!version) {
 -              if (audio_sample_rate) {
 -                      /* version < 1 didn't specify sample rate in the audio streams, so fill it in here */
 -                      for (vector<shared_ptr<AudioStream> >::iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) {
 -                              (*i)->set_sample_rate (audio_sample_rate.get());
 -                      }
--              }
-       }
 +      _scaler = Scaler::from_id (f.string_child ("Scaler"));
 +      _ab = f.bool_child ("AB");
 +      _with_subtitles = f.bool_child ("WithSubtitles");
 +      _subtitle_offset = f.number_child<float> ("SubtitleOffset");
 +      _subtitle_scale = f.number_child<float> ("SubtitleScale");
 +      _colour_lut = f.number_child<int> ("ColourLUT");
 +      _j2k_bandwidth = f.number_child<int> ("J2KBandwidth");
 +      _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata"));
 +      _dcp_video_frame_rate = f.number_child<int> ("DCPVideoFrameRate");
 +      _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate"));
 +      _dcp_audio_channels = f.number_child<int> ("DCPAudioChannels");
  
 -              /* also the selected stream was specified as an index */
 -              if (audio_stream_index && audio_stream_index.get() >= 0 && audio_stream_index.get() < (int) _content_audio_streams.size()) {
 -                      _content_audio_stream = _content_audio_streams[audio_stream_index.get()];
 -              }
 +      _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist"));
  
 -              /* similarly the subtitle */
 -              if (subtitle_stream_index && subtitle_stream_index.get() >= 0 && subtitle_stream_index.get() < (int) _subtitle_streams.size()) {
 -                      _subtitle_stream = _subtitle_streams[subtitle_stream_index.get()];
 -              }
 -      }
 -              
        _dirty = false;
  }
  
@@@ -630,21 -1103,11 +615,11 @@@ Film::set_container (Container const * 
  {
        {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              _filters = f;
 +              _container = c;
        }
 -      signal_changed (FILTERS);
 +      signal_changed (CONTAINER);
  }
  
- void
- Film::set_filters (vector<Filter const *> f)
- {
-       {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               _filters = f;
-       }
-       signal_changed (FILTERS);
- }
  void
  Film::set_scaler (Scaler const * s)
  {
@@@ -818,114 -1471,20 +793,113 @@@ Film::have_dcp () cons
        return true;
  }
  
-       boost::mutex::scoped_lock lm (_state_mutex);
 +shared_ptr<Player>
 +Film::player () const
 +{
 +      return shared_ptr<Player> (new Player (shared_from_this (), _playlist));
 +}
 +
 +shared_ptr<Playlist>
 +Film::playlist () const
 +{
 +      boost::mutex::scoped_lock lm (_state_mutex);
 +      return _playlist;
 +}
 +
 +Playlist::ContentList
 +Film::content () const
 +{
 +      return _playlist->content ();
 +}
 +
 +void
 +Film::add_content (shared_ptr<Content> c)
 +{
 +      _playlist->add (c);
 +      examine_content (c);
 +}
 +
 +void
 +Film::remove_content (shared_ptr<Content> c)
 +{
 +      _playlist->remove (c);
 +}
 +
 +Time
 +Film::length () const
 +{
 +      return _playlist->length ();
 +}
 +
  bool
 -Film::has_audio () const
 +Film::has_subtitles () const
  {
 -      if (use_content_audio()) {
 -              return audio_stream();
 -      }
 +      return _playlist->has_subtitles ();
 +}
  
 -      vector<string> const e = external_audio ();
 -      for (vector<string>::const_iterator i = e.begin(); i != e.end(); ++i) {
 -              if (!i->empty ()) {
 -                      return true;
 -              }
 +OutputVideoFrame
 +Film::best_dcp_video_frame_rate () const
 +{
 +      return _playlist->best_dcp_frame_rate ();
 +}
 +
 +void
 +Film::playlist_content_changed (boost::weak_ptr<Content> c, int p)
 +{
 +      if (p == VideoContentProperty::VIDEO_FRAME_RATE) {
 +              set_dcp_video_frame_rate (_playlist->best_dcp_frame_rate ());
 +      } 
 +
 +      if (ui_signaller) {
 +              ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p));
        }
 +}
 +
 +void
 +Film::playlist_changed ()
 +{
 +      signal_changed (CONTENT);
 +}     
 +
 +int
 +Film::loop () const
 +{
 +      return _playlist->loop ();
 +}
 +
 +void
 +Film::set_loop (int c)
 +{
 +      _playlist->set_loop (c);
 +}
  
 -      return false;
 +OutputAudioFrame
 +Film::time_to_audio_frames (Time t) const
 +{
 +      return t * dcp_audio_frame_rate () / TIME_HZ;
 +}
 +
 +OutputVideoFrame
 +Film::time_to_video_frames (Time t) const
 +{
 +      return t * dcp_video_frame_rate () / TIME_HZ;
 +}
 +
 +Time
 +Film::audio_frames_to_time (OutputAudioFrame f) const
 +{
 +      return f * TIME_HZ / dcp_audio_frame_rate ();
 +}
 +
 +Time
 +Film::video_frames_to_time (OutputVideoFrame f) const
 +{
 +      return f * TIME_HZ / dcp_video_frame_rate ();
  }
  
 +OutputAudioFrame
 +Film::dcp_audio_frame_rate () const
 +{
 +      /* XXX */
 +      return 48000;
 +}
diff --cc src/lib/film.h
index f0ccd99e77b33139b95ad23bc6d5678e18c4da9a,dd0a83d94e473f5da6d5b9e98d1378e247c0c3eb..84f0b0233ac6247634a9499b94b2c0cc30f8a934
@@@ -97,29 -106,16 +97,28 @@@ public
                return _dirty;
        }
  
 -      int audio_channels () const;
 +      bool have_dcp () const;
 +
 +      boost::shared_ptr<Player> player () const;
 +      boost::shared_ptr<Playlist> playlist () const;
  
 -      void set_dci_date_today ();
 +      OutputAudioFrame dcp_audio_frame_rate () const;
-       int dcp_audio_channels () const;
  
 -      bool have_dcp () const;
 +      OutputAudioFrame time_to_audio_frames (Time) const;
 +      OutputVideoFrame time_to_video_frames (Time) const;
 +      Time video_frames_to_time (OutputVideoFrame) const;
 +      Time audio_frames_to_time (OutputAudioFrame) const;
  
 -      enum TrimType {
 -              CPL,
 -              ENCODE
 -      };
 +      /* Proxies for some Playlist methods */
 +
 +      Playlist::ContentList content () const;
 +
 +      Time length () const;
 +      bool has_subtitles () const;
 +      OutputVideoFrame best_dcp_video_frame_rate () const;
 +
 +      void set_loop (int);
 +      int loop () const;
  
        /** Identifiers for the parts of our state;
            used for signalling changes.
                NONE,
                NAME,
                USE_DCI_NAME,
 +              /** The playlist's content list has changed (i.e. content has been added, moved around or removed) */
                CONTENT,
 -              TRUST_CONTENT_HEADER,
 +              LOOP,
                DCP_CONTENT_TYPE,
 -              FORMAT,
 -              CROP,
 -              FILTERS,
 +              CONTAINER,
-               FILTERS,
                SCALER,
 -              TRIM_START,
 -              TRIM_END,
 -              TRIM_TYPE,
 -              DCP_AB,
 -              CONTENT_AUDIO_STREAM,
 -              EXTERNAL_AUDIO,
 -              USE_CONTENT_AUDIO,
 -              AUDIO_GAIN,
 -              AUDIO_DELAY,
 -              STILL_DURATION,
 -              SUBTITLE_STREAM,
 +              AB,
                WITH_SUBTITLES,
                SUBTITLE_OFFSET,
                SUBTITLE_SCALE,
                return _dcp_content_type;
        }
  
 -      Format const * format () const {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              return _format;
 -      }
 -
 -      Crop crop () const {
 -              boost::mutex::scoped_lock lm (_state_mutex);
 -              return _crop;
 -      }
 -
 -      std::vector<Filter const *> filters () const {
 +      Container const * container () const {
                boost::mutex::scoped_lock lm (_state_mutex);
 -              return _filters;
 +              return _container;
        }
  
-       std::vector<Filter const *> filters () const {
-               boost::mutex::scoped_lock lm (_state_mutex);
-               return _filters;
-       }
        Scaler const * scaler () const {
                boost::mutex::scoped_lock lm (_state_mutex);
                return _scaler;
        void set_directory (std::string);
        void set_name (std::string);
        void set_use_dci_name (bool);
 -      void set_content (std::string);
 -      void set_trust_content_header (bool);
 +      void add_content (boost::shared_ptr<Content>);
 +      void remove_content (boost::shared_ptr<Content>);
        void set_dcp_content_type (DCPContentType const *);
 -      void set_format (Format const *);
 -      void set_crop (Crop);
 -      void set_left_crop (int);
 -      void set_right_crop (int);
 -      void set_top_crop (int);
 -      void set_bottom_crop (int);
 -      void set_filters (std::vector<Filter const *>);
 +      void set_container (Container const *);
-       void set_filters (std::vector<Filter const *>);
        void set_scaler (Scaler const *);
 -      void set_trim_start (int);
 -      void set_trim_end (int);
 -      void set_trim_type (TrimType);
 -      void set_dcp_ab (bool);
 -      void set_content_audio_stream (boost::shared_ptr<AudioStream>);
 -      void set_external_audio (std::vector<std::string>);
 -      void set_use_content_audio (bool);
 -      void set_audio_gain (float);
 -      void set_audio_delay (int);
 -      void set_still_duration (int);
 -      void set_subtitle_stream (boost::shared_ptr<SubtitleStream>);
 +      void set_ab (bool);
        void set_with_subtitles (bool);
        void set_subtitle_offset (int);
        void set_subtitle_scale (float);
@@@ -287,14 -415,30 +279,12 @@@ private
        std::string _name;
        /** True if a auto-generated DCI-compliant name should be used for our DCP */
        bool _use_dci_name;
 -      /** File or directory containing content; may be relative to our directory
 -       *  or an absolute path.
 -       */
 -      std::string _content;
 -      /** If this is true, we will believe the length specified by the content
 -       *  file's header; if false, we will run through the whole content file
 -       *  the first time we see it in order to obtain the length.
 -       */
 -      bool _trust_content_header;
        /** The type of content that this Film represents (feature, trailer etc.) */
        DCPContentType const * _dcp_content_type;
 -      /** The format to present this Film in (flat, scope, etc.) */
 -      Format const * _format;
 -      /** The crop to apply to the source */
 -      Crop _crop;
 -      /** Video filters that should be used when generating DCPs */
 -      std::vector<Filter const *> _filters;
 +      /** The container to put this Film in (flat, scope, etc.) */
 +      Container const * _container;
-       /** Video filters that should be used when generating DCPs */
-       std::vector<Filter const *> _filters;
        /** Scaler algorithm to use */
        Scaler const * _scaler;
 -      /** Frames to trim off the start of the DCP */
 -      int _trim_start;
 -      /** Frames to trim off the end of the DCP */
 -      int _trim_end;
 -      TrimType _trim_type;
        /** true to create an A/B comparison DCP, where the left half of the image
            is the video without any filters or post-processing, and the right half
            has the specified filters and post-processing.
index df8f1e9ddef2c14956633dbc6dbb2e010eb77b82,b0427a23d0566166921fd6e95eceb5b86c879505..4564033d5379dd5d6770addfdac826cff447237f
@@@ -39,8 -33,7 +33,6 @@@ extern "C" 
  #include "filter.h"
  #include "exceptions.h"
  #include "image.h"
--#include "film.h"
  #include "ffmpeg_decoder.h"
  
  #include "i18n.h"
@@@ -49,34 -42,28 +41,32 @@@ using std::stringstream
  using std::string;
  using std::list;
  using boost::shared_ptr;
 +using boost::weak_ptr;
  using libdcp::Size;
  
- /** Construct a FilterGraph for the settings in a film.
 -/** Construct a FFmpegFilterGraph for the settings in a film.
-- *  @param film Film.
-- *  @param decoder Decoder that we are using.
++/** Construct a FilterGraph for the settings in a piece of content.
++ *  @param content Content.
   *  @param s Size of the images to process.
   *  @param p Pixel format of the images to process.
   */
- FilterGraph::FilterGraph (weak_ptr<const Film> weak_film, FFmpegDecoder* decoder, libdcp::Size s, AVPixelFormat p)
 -FFmpegFilterGraph::FFmpegFilterGraph (shared_ptr<Film> film, FFmpegDecoder* decoder, libdcp::Size s, AVPixelFormat p)
++FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p)
        : _buffer_src_context (0)
        , _buffer_sink_context (0)
        , _size (s)
        , _pixel_format (p)
  {
-       shared_ptr<const Film> film = weak_film.lock ();
-       assert (film);
+       _frame = av_frame_alloc ();
        
--      string filters = Filter::ffmpeg_strings (film->filters()).first;
++      string filters = Filter::ffmpeg_strings (content->filters()).first;
        if (!filters.empty ()) {
--              filters += N_(",");
++              filters += ",";
        }
  
-       Crop crop = decoder->ffmpeg_content()->crop ();
-       libdcp::Size cropped_size = decoder->video_size ();
 -      filters += crop_string (Position (film->crop().left, film->crop().top), film->cropped_size (decoder->native_size()));
++      Crop crop = content->crop ();
++      libdcp::Size cropped_size = _size;
 +      cropped_size.width -= crop.left + crop.right;
 +      cropped_size.height -= crop.top + crop.bottom;
 +      filters += crop_string (Position (crop.left, crop.top), cropped_size);
  
        AVFilterGraph* graph = avfilter_graph_alloc();
        if (graph == 0) {
                throw DecodeError (N_("could not find buffer src filter"));
        }
  
-       AVFilter* buffer_sink = get_sink ();
+       AVFilter* buffer_sink = avfilter_get_by_name(N_("buffersink"));
+       if (buffer_sink == 0) {
+               throw DecodeError (N_("Could not create buffer sink filter"));
+       }
  
        stringstream a;
-       a << _size.width << N_(":")
-         << _size.height << N_(":")
-         << _pixel_format << N_(":")
-         << "0:1:0:1";
+       a << "video_size=" << _size.width << "x" << _size.height << ":"
+         << "pix_fmt=" << _pixel_format << ":"
 -        << "time_base=" << decoder->time_base_numerator() << "/" << decoder->time_base_denominator() << ":"
 -        << "pixel_aspect=" << decoder->sample_aspect_ratio_numerator() << "/" << decoder->sample_aspect_ratio_denominator();
++        << "time_base=0/1:"
++        << "pixel_aspect=0/1";
  
        int r;
  
        /* XXX: leaking `inputs' / `outputs' ? */
  }
  
 -FFmpegFilterGraph::~FFmpegFilterGraph ()
++FilterGraph::~FilterGraph ()
+ {
+       av_frame_free (&_frame);
+ }
  /** Take an AVFrame and process it using our configured filters, returning a
-  *  set of Images.
+  *  set of Images.  Caller handles memory management of the input frame.
   */
  list<shared_ptr<Image> >
- FilterGraph::process (AVFrame const * frame)
 -FFmpegFilterGraph::process (AVFrame* frame)
++FilterGraph::process (AVFrame* frame)
  {
        list<shared_ptr<Image> > images;
-       
- #if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 53 && LIBAVFILTER_VERSION_MINOR <= 61
-       if (av_vsrc_buffer_add_frame (_buffer_src_context, frame, 0) < 0) {
-               throw DecodeError (N_("could not push buffer into filter chain."));
-       }
- #elif LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15
-       AVRational par;
-       par.num = sample_aspect_ratio_numerator ();
-       par.den = sample_aspect_ratio_denominator ();
-       if (av_vsrc_buffer_add_frame (_buffer_src_context, frame, 0, par) < 0) {
-               throw DecodeError (N_("could not push buffer into filter chain."));
-       }
- #else
  
        if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) {
                throw DecodeError (N_("could not push buffer into filter chain."));
index c7a01f58e1f66c446c3c1ce21390d200bb182790,2138943e42b6e3e6ef11ba4967f0c85f0755d618..e294812c2baa9836f5aeff99711057bae611148e
   *  @brief A graph of FFmpeg filters.
   */
  
 -#ifndef DVDOMATIC_FILTER_GRAPH_H
 -#define DVDOMATIC_FILTER_GRAPH_H
 +#ifndef DCPOMATIC_FILTER_GRAPH_H
 +#define DCPOMATIC_FILTER_GRAPH_H
  
  #include "util.h"
- #include "ffmpeg_compatibility.h"
  
  class Image;
  class VideoFilter;
--class FFmpegDecoder;
  
 -class FilterGraph
 -{
 -public:
 -      virtual bool can_process (libdcp::Size, AVPixelFormat) const = 0;
 -      virtual std::list<boost::shared_ptr<Image> > process (AVFrame *) = 0;
 -};
 -
 -class EmptyFilterGraph : public FilterGraph
 -{
 -public:
 -      bool can_process (libdcp::Size, AVPixelFormat) const {
 -              return true;
 -      }
 -
 -      std::list<boost::shared_ptr<Image> > process (AVFrame *);
 -};
 -
 -/** @class FFmpegFilterGraph
 +/** @class FilterGraph
   *  @brief A graph of FFmpeg filters.
   */
 -class FFmpegFilterGraph : public FilterGraph
 +class FilterGraph
  {
  public:
-       FilterGraph (boost::weak_ptr<const Film>, FFmpegDecoder* decoder, libdcp::Size s, AVPixelFormat p);
 -      FFmpegFilterGraph (boost::shared_ptr<Film> film, FFmpegDecoder* decoder, libdcp::Size s, AVPixelFormat p);
 -      ~FFmpegFilterGraph ();
++      FilterGraph (boost::shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p);
++      ~FilterGraph ();
  
        bool can_process (libdcp::Size s, AVPixelFormat p) const;
-       std::list<boost::shared_ptr<Image> > process (AVFrame const * frame);
+       std::list<boost::shared_ptr<Image> > process (AVFrame * frame);
  
  private:
        AVFilterContext* _buffer_src_context;
        AVFilterContext* _buffer_sink_context;
        libdcp::Size _size; ///< size of the images that this chain can process
        AVPixelFormat _pixel_format; ///< pixel format of the images that this chain can process
+       AVFrame* _frame;
  };
  
 -boost::shared_ptr<FilterGraph> filter_graph_factory (boost::shared_ptr<Film>, FFmpegDecoder *, libdcp::Size, AVPixelFormat);
 -
  #endif
diff --cc src/lib/image.h
Simple merge
index 032b3d49bcc897129d99d5ddd7b6c53448908880,0000000000000000000000000000000000000000..ff13f95dbd2197be7be681ff520b29ab1ef4422b
mode 100644,000000..100644
--- /dev/null
@@@ -1,351 -1,0 +1,355 @@@
-         _audio_buffers.accumulate_frames (audio, 0, 0, audio->frames ());
 +/* -*- c-basic-offset: 8; default-tab-width: 8; -*- */
 +
 +/*
 +    Copyright (C) 2013 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include "player.h"
 +#include "film.h"
 +#include "ffmpeg_decoder.h"
 +#include "ffmpeg_content.h"
 +#include "imagemagick_decoder.h"
 +#include "imagemagick_content.h"
 +#include "sndfile_decoder.h"
 +#include "sndfile_content.h"
 +#include "playlist.h"
 +#include "job.h"
 +#include "image.h"
 +#include "null_content.h"
 +#include "black_decoder.h"
 +#include "silence_decoder.h"
 +
 +using std::list;
 +using std::cout;
 +using std::min;
 +using std::max;
 +using std::vector;
 +using boost::shared_ptr;
 +using boost::weak_ptr;
 +using boost::dynamic_pointer_cast;
 +
 +struct Piece
 +{
 +      Piece (shared_ptr<Content> c, shared_ptr<Decoder> d)
 +              : content (c)
 +              , decoder (d)
 +      {}
 +      
 +      shared_ptr<Content> content;
 +      shared_ptr<Decoder> decoder;
 +};
 +
 +Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p)
 +      : _film (f)
 +      , _playlist (p)
 +      , _video (true)
 +      , _audio (true)
 +      , _subtitles (true)
 +      , _have_valid_pieces (false)
 +      , _position (0)
 +      , _audio_buffers (f->dcp_audio_channels(), 0)
 +      , _next_audio (0)
 +{
 +      _playlist->Changed.connect (bind (&Player::playlist_changed, this));
 +      _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2));
 +}
 +
 +void
 +Player::disable_video ()
 +{
 +      _video = false;
 +}
 +
 +void
 +Player::disable_audio ()
 +{
 +      _audio = false;
 +}
 +
 +void
 +Player::disable_subtitles ()
 +{
 +      _subtitles = false;
 +}
 +
 +bool
 +Player::pass ()
 +{
 +      if (!_have_valid_pieces) {
 +              setup_pieces ();
 +              _have_valid_pieces = true;
 +      }
 +
 +        /* Here we are just finding the active decoder with the earliest last emission time, then
 +           calling pass on it.
 +        */
 +
 +        Time earliest_t = TIME_MAX;
 +        shared_ptr<Piece> earliest;
 +
 +      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 +              cout << "check " << (*i)->content->file() << " start=" << (*i)->content->start() << ", next=" << (*i)->decoder->next() << ", end=" << (*i)->content->end() << "\n";
 +              if (((*i)->decoder->next() + (*i)->content->start()) >= (*i)->content->end()) {
 +                      continue;
 +              }
++
++              if (!_audio && dynamic_pointer_cast<SndfileContent> ((*i)->content)) {
++                      continue;
++              }
 +              
 +              Time const t = (*i)->content->start() + (*i)->decoder->next();
 +              if (t < earliest_t) {
 +                      cout << "\t candidate; " << t << " " << (t / TIME_HZ) << ".\n";
 +                      earliest_t = t;
 +                      earliest = *i;
 +              }
 +      }
 +
 +      if (!earliest) {
 +              return true;
 +      }
 +
 +      cout << "PASS:\n";
 +      cout << "\tpass " << earliest->content->file() << " ";
 +      if (dynamic_pointer_cast<FFmpegContent> (earliest->content)) {
 +              cout << " FFmpeg.\n";
 +      } else if (dynamic_pointer_cast<ImageMagickContent> (earliest->content)) {
 +              cout << " ImageMagickContent.\n";
 +      } else if (dynamic_pointer_cast<SndfileContent> (earliest->content)) {
 +              cout << " SndfileContent.\n";
 +      } else if (dynamic_pointer_cast<BlackDecoder> (earliest->decoder)) {
 +              cout << " Black.\n";
 +      } else if (dynamic_pointer_cast<SilenceDecoder> (earliest->decoder)) {
 +              cout << " Silence.\n";
 +      }
 +      
 +      earliest->decoder->pass ();
 +      _position = earliest->content->start() + earliest->decoder->next ();
 +      cout << "\tpassed to " << _position << " " << (_position / TIME_HZ) << "\n";
 +
 +        return false;
 +}
 +
 +void
 +Player::process_video (weak_ptr<Content> weak_content, shared_ptr<const Image> image, bool same, shared_ptr<Subtitle> sub, Time time)
 +{
 +      cout << "[V]\n";
 +      
 +      shared_ptr<Content> content = weak_content.lock ();
 +      if (!content) {
 +              return;
 +      }
 +      
 +      time += content->start ();
 +      
 +        Video (image, same, sub, time);
 +}
 +
 +void
 +Player::process_audio (weak_ptr<Content> weak_content, shared_ptr<const AudioBuffers> audio, Time time)
 +{
 +      shared_ptr<Content> content = weak_content.lock ();
 +      if (!content) {
 +              return;
 +      }
 +      
 +        /* The time of this audio may indicate that some of our buffered audio is not going to
 +           be added to any more, so it can be emitted.
 +        */
 +
 +      time += content->start ();
 +
 +        if (time > _next_audio) {
 +                /* We can emit some audio from our buffers */
 +              assert (_film->time_to_audio_frames (time - _next_audio) <= _audio_buffers.frames());
 +                OutputAudioFrame const N = _film->time_to_audio_frames (time - _next_audio);
 +                shared_ptr<AudioBuffers> emit (new AudioBuffers (_audio_buffers.channels(), N));
 +                emit->copy_from (&_audio_buffers, N, 0, 0);
 +                Audio (emit, _next_audio);
 +                _next_audio += _film->audio_frames_to_time (N);
 +
 +                /* And remove it from our buffers */
 +                if (_audio_buffers.frames() > N) {
 +                        _audio_buffers.move (N, 0, _audio_buffers.frames() - N);
 +                }
 +                _audio_buffers.set_frames (_audio_buffers.frames() - N);
 +        }
 +
 +        /* Now accumulate the new audio into our buffers */
 +        _audio_buffers.ensure_size (_audio_buffers.frames() + audio->frames());
++        _audio_buffers.accumulate_frames (audio.get(), 0, 0, audio->frames ());
 +}
 +
 +/** @return true on error */
 +void
 +Player::seek (Time t)
 +{
 +      if (!_have_valid_pieces) {
 +              setup_pieces ();
 +              _have_valid_pieces = true;
 +      }
 +
 +      if (_pieces.empty ()) {
 +              return;
 +      }
 +
 +      cout << "seek to " << t << " " << (t / TIME_HZ) << "\n";
 +
 +      for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) {
 +              Time s = t - (*i)->content->start ();
 +              s = max (static_cast<Time> (0), s);
 +              s = min ((*i)->content->length(), s);
 +              cout << "seek [" << (*i)->content->file() << "," << (*i)->content->start() << "," << (*i)->content->end() << "] to " << s << "\n";
 +              (*i)->decoder->seek (s);
 +      }
 +
 +      /* XXX: don't seek audio because we don't need to... */
 +}
 +
 +
 +void
 +Player::seek_back ()
 +{
 +
 +}
 +
 +void
 +Player::seek_forward ()
 +{
 +
 +}
 +
 +struct ContentSorter
 +{
 +      bool operator() (shared_ptr<Content> a, shared_ptr<Content> b)
 +      {
 +              return a->start() < b->start();
 +      }
 +};
 +
 +void
 +Player::setup_pieces ()
 +{
 +//    cout << "----- Player SETUP PIECES.\n";
 +
 +      list<shared_ptr<Piece> > old_pieces = _pieces;
 +
 +      _pieces.clear ();
 +
 +      Playlist::ContentList content = _playlist->content ();
 +      sort (content.begin(), content.end(), ContentSorter ());
 +      
 +      for (Playlist::ContentList::iterator i = content.begin(); i != content.end(); ++i) {
 +
 +              shared_ptr<Decoder> decoder;
 +              
 +                /* 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, _subtitles));
 +                      
 +                      fd->Video.connect (bind (&Player::process_video, this, *i, _1, _2, _3, _4));
 +                      fd->Audio.connect (bind (&Player::process_audio, this, *i, _1, _2));
 +
 +                      decoder = fd;
 +//                    cout << "\tFFmpeg @ " << fc->start() << " -- " << fc->end() << "\n";
 +              }
 +              
 +              shared_ptr<const ImageMagickContent> ic = dynamic_pointer_cast<const ImageMagickContent> (*i);
 +              if (ic) {
 +                      shared_ptr<ImageMagickDecoder> id;
 +                      
 +                      /* See if we can re-use an old ImageMagickDecoder */
 +                      for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) {
 +                              shared_ptr<ImageMagickDecoder> imd = dynamic_pointer_cast<ImageMagickDecoder> ((*j)->decoder);
 +                              if (imd && imd->content() == ic) {
 +                                      id = imd;
 +                              }
 +                      }
 +
 +                      if (!id) {
 +                              id.reset (new ImageMagickDecoder (_film, ic));
 +                              id->Video.connect (bind (&Player::process_video, this, *i, _1, _2, _3, _4));
 +                      }
 +
 +                      decoder = id;
 +//                    cout << "\tImageMagick @ " << ic->start() << " -- " << ic->end() << "\n";
 +              }
 +
 +              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, *i, _1, _2));
 +
 +                      decoder = sd;
 +//                    cout << "\tSndfile @ " << sc->start() << " -- " << sc->end() << "\n";
 +              }
 +
 +              _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder)));
 +      }
 +
 +      /* Fill in visual gaps with black and audio gaps with silence */
 +
 +      Time video_pos = 0;
 +      Time audio_pos = 0;
 +      list<shared_ptr<Piece> > pieces_copy = _pieces;
 +      for (list<shared_ptr<Piece> >::iterator i = pieces_copy.begin(); i != pieces_copy.end(); ++i) {
 +              if (dynamic_pointer_cast<VideoContent> ((*i)->content)) {
 +                      Time const diff = (*i)->content->start() - video_pos;
 +                      if (diff > 0) {
 +                              shared_ptr<NullContent> nc (new NullContent (_film, video_pos, diff));
 +                              shared_ptr<BlackDecoder> bd (new BlackDecoder (_film, nc));
 +                              bd->Video.connect (bind (&Player::process_video, this, nc, _1, _2, _3, _4));
 +                              _pieces.push_back (shared_ptr<Piece> (new Piece (nc, bd)));
 +//                            cout << "\tblack @ " << video_pos << " -- " << (video_pos + diff) << "\n";
 +                      }
 +                                              
 +                      video_pos = (*i)->content->end();
 +              } else {
 +                      Time const diff = (*i)->content->start() - audio_pos;
 +                      if (diff > 0) {
 +                              shared_ptr<NullContent> nc (new NullContent (_film, audio_pos, diff));
 +                              shared_ptr<SilenceDecoder> sd (new SilenceDecoder (_film, nc));
 +                              sd->Audio.connect (bind (&Player::process_audio, this, nc, _1, _2));
 +                              _pieces.push_back (shared_ptr<Piece> (new Piece (nc, sd)));
 +//                            cout << "\tsilence @ " << audio_pos << " -- " << (audio_pos + diff) << "\n";
 +                      }
 +                      audio_pos = (*i)->content->end();
 +              }
 +      }
 +}
 +
 +void
 +Player::content_changed (weak_ptr<Content> w, int p)
 +{
 +      shared_ptr<Content> c = w.lock ();
 +      if (!c) {
 +              return;
 +      }
 +
 +      if (p == ContentProperty::START || p == VideoContentProperty::VIDEO_LENGTH) {
 +              _have_valid_pieces = false;
 +      }
 +}
 +
 +void
 +Player::playlist_changed ()
 +{
 +      _have_valid_pieces = false;
 +}
index d69e2f7d5c31d3587e2b2c747426b25f38e5b029,81f61d8b8ba128f19f3628f30fef2a130815ad7f..f7e362edafbcd72ce55a19956c25f7a1689680ca
@@@ -5,10 -5,10 +5,10 @@@
  #
  msgid ""
  msgstr ""
 -"Project-Id-Version: DVD-o-matic FRENCH\n"
 +"Project-Id-Version: DCP-o-matic FRENCH\n"
  "Report-Msgid-Bugs-To: \n"
  "POT-Creation-Date: 2013-05-09 09:51+0100\n"
- "PO-Revision-Date: 2013-05-10 14:33+0100\n"
+ "PO-Revision-Date: 2013-05-21 10:30+0100\n"
  "Last-Translator: \n"
  "Language-Team: \n"
  "Language: \n"
index ca0bec580b62a4412c77e33677814153c9b8a378,9c5a77f681b2903390fec271f5610c62cd2d8b16..07b82694666c3146be87fbae45d13cf4722f19d4
@@@ -111,7 -106,7 +111,6 @@@ Server::process (shared_ptr<Socket> soc
        string scaler_id = get_required_string (kv, N_("scaler"));
        int frame = get_required_int (kv, N_("frame"));
        int frames_per_second = get_required_int (kv, N_("frames_per_second"));
--      string post_process = get_optional_string (kv, N_("post_process"));
        int colour_lut_index = get_required_int (kv, N_("colour_lut"));
        int j2k_bandwidth = get_required_int (kv, N_("j2k_bandwidth"));
        Position subtitle_position (get_optional_int (kv, N_("subtitle_x")), get_optional_int (kv, N_("subtitle_y")));
  
        DCPVideoFrame dcp_video_frame (
                image, sub, out_size, padding, subtitle_offset, subtitle_scale,
--              scaler, frame, frames_per_second, post_process, colour_lut_index, j2k_bandwidth, _log
++              scaler, frame, frames_per_second, colour_lut_index, j2k_bandwidth, _log
                );
        
        shared_ptr<EncodedData> encoded = dcp_video_frame.encode_locally ();
index b6dac2e765bb89ec0fd17bd3c1ff0c6e91e7e3c1,7e9e67d0fa2e24fd0ede23561ac47bbe682187e8..c4c6e5f4ee065f6dca6145defbb293bedcb5b7d1
@@@ -60,60 -99,77 +60,60 @@@ SndfileDecoder::pass (
        /* Do things in half second blocks as I think there may be limits
           to what FFmpeg (and in particular the resampler) can cope with.
        */
 -      sf_count_t const block = _audio_stream->sample_rate() / 2;
 -      shared_ptr<AudioBuffers> audio (new AudioBuffers (_audio_stream->channels(), block));
 -      sf_count_t const this_time = min (block, _frames - _done);
 -      for (size_t i = 0; i < _sndfiles.size(); ++i) {
 -              if (!_sndfiles[i]) {
 -                      audio->make_silent (i);
 -              } else {
 -                      sf_read_float (_sndfiles[i], audio->data(i), this_time);
 -              }
 -      }
 +      sf_count_t const block = _sndfile_content->content_audio_frame_rate() / 2;
 +      sf_count_t const this_time = min (block, _remaining);
  
 -      audio->set_frames (this_time);
 -      Audio (audio, double(_done) / _audio_stream->sample_rate());
 -      _done += this_time;
 -
 -      return (_done == _frames);
 -}
 -
 -SndfileDecoder::~SndfileDecoder ()
 -{
 -      for (size_t i = 0; i < _sndfiles.size(); ++i) {
 -              if (_sndfiles[i]) {
 -                      sf_close (_sndfiles[i]);
 +      int const channels = _sndfile_content->audio_channels ();
 +      
-       shared_ptr<AudioBuffers> audio (new AudioBuffers (channels, this_time));
++      shared_ptr<AudioBuffers> data (new AudioBuffers (channels, this_time));
 +
 +      if (_sndfile_content->audio_channels() == 1) {
 +              /* No de-interleaving required */
-               sf_read_float (_sndfile, audio->data(0), this_time);
++              sf_read_float (_sndfile, data->data(0), this_time);
 +      } else {
 +              /* Deinterleave */
 +              if (!_deinterleave_buffer) {
 +                      _deinterleave_buffer = new float[block * channels];
 +              }
 +              sf_readf_float (_sndfile, _deinterleave_buffer, this_time);
 +              vector<float*> out_ptr (channels);
 +              for (int i = 0; i < channels; ++i) {
-                       out_ptr[i] = audio->data(i);
++                      out_ptr[i] = data->data(i);
 +              }
 +              float* in_ptr = _deinterleave_buffer;
 +              for (int i = 0; i < this_time; ++i) {
 +                      for (int j = 0; j < channels; ++j) {
 +                              *out_ptr[j]++ = *in_ptr++;
 +                      }
                }
        }
-       audio->set_frames (this_time);
-       Audio (audio, double(_done) / audio_frame_rate());
 +              
++      data->set_frames (this_time);
++      audio (data, double(_done) / audio_frame_rate());
 +      _done += this_time;
 +      _remaining -= this_time;
  }
  
 -shared_ptr<SndfileStream>
 -SndfileStream::create ()
 -{
 -      return shared_ptr<SndfileStream> (new SndfileStream);
 -}
 -
 -shared_ptr<SndfileStream>
 -SndfileStream::create (string t, optional<int> v)
 +int
 +SndfileDecoder::audio_channels () const
  {
 -      if (!v) {
 -              /* version < 1; no type in the string, and there's only FFmpeg streams anyway */
 -              return shared_ptr<SndfileStream> ();
 -      }
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type;
 -      if (type != N_("external")) {
 -              return shared_ptr<SndfileStream> ();
 -      }
 -
 -      return shared_ptr<SndfileStream> (new SndfileStream (t, v));
 +      return _info.channels;
  }
  
 -SndfileStream::SndfileStream (string t, optional<int> v)
 +ContentAudioFrame
 +SndfileDecoder::audio_length () const
  {
 -      assert (v);
 -
 -      stringstream s (t);
 -      string type;
 -      s >> type >> _sample_rate >> _channel_layout;
 +      return _info.frames;
  }
  
 -SndfileStream::SndfileStream ()
 +int
 +SndfileDecoder::audio_frame_rate () const
  {
 -
 +      return _info.samplerate;
  }
  
 -string
 -SndfileStream::to_string () const
 +Time
 +SndfileDecoder::next () const
  {
 -      return String::compose (N_("external %1 %2"), _sample_rate, _channel_layout);
 +      return _next_audio;
  }
diff --cc src/lib/wscript
index e7349a9c4d71149588ab867b4b303955ca2214f5,66207b1e4468f700729e69b412fecb28650483a9..54941df420aac8bc445f4805372711acf94a8dfe
@@@ -26,8 -22,6 +26,7 @@@ sources = ""
            examine_content_job.cc
            exceptions.cc
            filter_graph.cc
-           ffmpeg_compatibility.cc
 +          ffmpeg_content.cc
            ffmpeg_decoder.cc
            film.cc
            filter.cc
index bc95f622da36f6cbfd7fb286ec03ff6d4e5efb4e,0000000000000000000000000000000000000000..c295a011d76110f6396989e112220d53e7d54f40
mode 100644,000000..100644
--- /dev/null
@@@ -1,205 -1,0 +1,205 @@@
-       pair<string, string> const f = Filter::ffmpeg_strings (film->filters ());
-       cout << "Filters: " << f.first << " " << f.second << "\n";
 +/*
 +    Copyright (C) 2012 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
 +    the Free Software Foundation; either version 2 of the License, or
 +    (at your option) any later version.
 +
 +    This program is distributed in the hope that it will be useful,
 +    but WITHOUT ANY WARRANTY; without even the implied warranty of
 +    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 +    GNU General Public License for more details.
 +
 +    You should have received a copy of the GNU General Public License
 +    along with this program; if not, write to the Free Software
 +    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 +
 +*/
 +
 +#include <iostream>
 +#include <iomanip>
 +#include <getopt.h>
 +#include <libdcp/version.h>
 +#include "format.h"
 +#include "film.h"
 +#include "filter.h"
 +#include "transcode_job.h"
 +#include "job_manager.h"
 +#include "ab_transcode_job.h"
 +#include "util.h"
 +#include "scaler.h"
 +#include "version.h"
 +#include "cross.h"
 +#include "config.h"
 +#include "log.h"
 +
 +using std::string;
 +using std::cerr;
 +using std::cout;
 +using std::vector;
 +using std::pair;
 +using std::list;
 +using boost::shared_ptr;
 +
 +static void
 +help (string n)
 +{
 +      cerr << "Syntax: " << n << " [OPTION] <FILM>\n"
 +           << "  -v, --version      show DCP-o-matic version\n"
 +           << "  -h, --help         show this help\n"
 +           << "  -d, --deps         list DCP-o-matic dependency details and quit\n"
 +           << "  -n, --no-progress  do not print progress to stdout\n"
 +           << "  -r, --no-remote    do not use any remote servers\n"
 +           << "\n"
 +           << "<FILM> is the film directory.\n";
 +}
 +
 +int
 +main (int argc, char* argv[])
 +{
 +      string film_dir;
 +      bool progress = true;
 +      bool no_remote = false;
 +      int log_level = 0;
 +
 +      int option_index = 0;
 +      while (1) {
 +              static struct option long_options[] = {
 +                      { "version", no_argument, 0, 'v'},
 +                      { "help", no_argument, 0, 'h'},
 +                      { "deps", no_argument, 0, 'd'},
 +                      { "no-progress", no_argument, 0, 'n'},
 +                      { "no-remote", no_argument, 0, 'r'},
 +                      { "log-level", required_argument, 0, 'l' },
 +                      { 0, 0, 0, 0 }
 +              };
 +
 +              int c = getopt_long (argc, argv, "vhdnrl:", long_options, &option_index);
 +
 +              if (c == -1) {
 +                      break;
 +              }
 +
 +              switch (c) {
 +              case 'v':
 +                      cout << "dcpomatic version " << dcpomatic_version << " " << dcpomatic_git_commit << "\n";
 +                      exit (EXIT_SUCCESS);
 +              case 'h':
 +                      help (argv[0]);
 +                      exit (EXIT_SUCCESS);
 +              case 'd':
 +                      cout << dependency_version_summary () << "\n";
 +                      exit (EXIT_SUCCESS);
 +              case 'n':
 +                      progress = false;
 +                      break;
 +              case 'r':
 +                      no_remote = true;
 +                      break;
 +              case 'l':
 +                      log_level = atoi (optarg);
 +                      break;
 +              }
 +      }
 +
 +      if (optind >= argc) {
 +              help (argv[0]);
 +              exit (EXIT_FAILURE);
 +      }
 +
 +      film_dir = argv[optind];
 +                      
 +      dcpomatic_setup ();
 +
 +      if (no_remote) {
 +              Config::instance()->set_servers (vector<ServerDescription*> ());
 +      }
 +
 +      cout << "DCP-o-matic " << dcpomatic_version << " git " << dcpomatic_git_commit;
 +      char buf[256];
 +      if (gethostname (buf, 256) == 0) {
 +              cout << " on " << buf;
 +      }
 +      cout << "\n";
 +
 +      shared_ptr<Film> film;
 +      try {
 +              film.reset (new Film (film_dir));
 +              film->read_metadata ();
 +      } catch (std::exception& e) {
 +              cerr << argv[0] << ": error reading film `" << film_dir << "' (" << e.what() << ")\n";
 +              exit (EXIT_FAILURE);
 +      }
 +
 +      film->log()->set_level ((Log::Level) log_level);
 +
 +      cout << "\nMaking ";
 +      if (film->ab()) {
 +              cout << "A/B ";
 +      }
 +      cout << "DCP for " << film->name() << "\n";
 +//    cout << "Content: " << film->content() << "\n";
++//    pair<string, string> const f = Filter::ffmpeg_strings (film->filters ());
++//    cout << "Filters: " << f.first << " " << f.second << "\n";
 +
 +      film->make_dcp ();
 +
 +      bool should_stop = false;
 +      bool first = true;
 +      bool error = false;
 +      while (!should_stop) {
 +
 +              dcpomatic_sleep (5);
 +
 +              list<shared_ptr<Job> > jobs = JobManager::instance()->get ();
 +
 +              if (!first && progress) {
 +                      cout << "\033[" << jobs.size() << "A";
 +                      cout.flush ();
 +              }
 +
 +              first = false;
 +
 +              int unfinished = 0;
 +              int finished_in_error = 0;
 +
 +              for (list<shared_ptr<Job> >::iterator i = jobs.begin(); i != jobs.end(); ++i) {
 +                      if (progress) {
 +                              cout << (*i)->name() << ": ";
 +                              
 +                              float const p = (*i)->overall_progress ();
 +                              
 +                              if (p >= 0) {
 +                                      cout << (*i)->status() << "                         \n";
 +                              } else {
 +                                      cout << ": Running           \n";
 +                              }
 +                      }
 +
 +                      if (!(*i)->finished ()) {
 +                              ++unfinished;
 +                      }
 +
 +                      if ((*i)->finished_in_error ()) {
 +                              ++finished_in_error;
 +                              error = true;
 +                      }
 +
 +                      if (!progress && (*i)->finished_in_error ()) {
 +                              /* We won't see this error if we haven't been showing progress,
 +                                 so show it now.
 +                              */
 +                              cout << (*i)->status() << "\n";
 +                      }
 +              }
 +
 +              if (unfinished == 0 || finished_in_error != 0) {
 +                      should_stop = true;
 +              }
 +      }
 +
 +      return error ? EXIT_FAILURE : EXIT_SUCCESS;
 +}
 +
 +        
index 5cf0b789713a363da28d2dc494b416c93c5dff46,a21782a6f9da188530d9e4b472461e8cf363f19e..36d63b8055a4b7de144c8facdc1ac40f5d37cc8a
@@@ -576,32 -620,62 +576,20 @@@ FilmEditor::film_changed (Film::Propert
        case Film::NONE:
                break;
        case Film::CONTENT:
 -              checked_set (_content, _film->content ());
 -              setup_visibility ();
 +              setup_content ();
                setup_formats ();
 +//            setup_format ();
                setup_subtitle_control_sensitivity ();
 -              setup_streams ();
                setup_show_audio_sensitivity ();
 -              setup_frame_rate_description ();
                break;
 -      case Film::TRUST_CONTENT_HEADER:
 -              checked_set (_trust_content_header, _film->trust_content_header ());
 +      case Film::LOOP:
 +              checked_set (_loop_content, _film->loop() > 1);
 +              checked_set (_loop_count, _film->loop());
 +              setup_loop_sensitivity ();
                break;
 -      case Film::SUBTITLE_STREAMS:
 -              setup_subtitle_control_sensitivity ();
 -              setup_streams ();
 -              break;
 -      case Film::CONTENT_AUDIO_STREAMS:
 -              setup_streams ();
 -              setup_show_audio_sensitivity ();
 -              setup_frame_rate_description ();
 +      case Film::CONTAINER:
 +              setup_container ();
                break;
 -      case Film::FORMAT:
 -      {
 -              int n = 0;
 -              vector<Format const *>::iterator i = _formats.begin ();
 -              while (i != _formats.end() && *i != _film->format ()) {
 -                      ++i;
 -                      ++n;
 -              }
 -              if (i == _formats.end()) {
 -                      checked_set (_format, -1);
 -              } else {
 -                      checked_set (_format, n);
 -              }
 -              setup_dcp_name ();
 -              setup_scaling_description ();
 -              break;
 -      }
 -      case Film::CROP:
 -              checked_set (_left_crop, _film->crop().left);
 -              checked_set (_right_crop, _film->crop().right);
 -              checked_set (_top_crop, _film->crop().top);
 -              checked_set (_bottom_crop, _film->crop().bottom);
 -              setup_scaling_description ();
 -              break;
--      case Film::FILTERS:
--      {
--              pair<string, string> p = Filter::ffmpeg_strings (_film->filters ());
--              if (p.first.empty () && p.second.empty ()) {
--                      _filters->SetLabel (_("None"));
--              } else {
--                      string const b = p.first + " " + p.second;
--                      _filters->SetLabel (std_to_wx (b));
--              }
-               _dcp_sizer->Layout ();
 -              _film_sizer->Layout ();
--              break;
--      }
        case Film::NAME:
                checked_set (_name, _film->name());
                setup_dcp_name ();
  }
  
  void
 -FilmEditor::setup_frame_rate_description ()
 +FilmEditor::film_content_changed (weak_ptr<Content> weak_content, int property)
  {
 -      wxString d;
 -      int lines = 0;
 -      
 -      if (_film->source_frame_rate()) {
 -              d << std_to_wx (FrameRateConversion (_film->source_frame_rate(), _film->dcp_frame_rate()).description);
 -              ++lines;
 -#ifdef HAVE_SWRESAMPLE
 -              if (_film->audio_stream() && _film->audio_stream()->sample_rate() != _film->target_audio_sample_rate ()) {
 -                      d << wxString::Format (
 -                              _("Audio will be resampled from %dHz to %dHz\n"),
 -                              _film->audio_stream()->sample_rate(),
 -                              _film->target_audio_sample_rate()
 -                              );
 -                      ++lines;
 -              }
 -#endif                
 +      if (!_film) {
 +              /* We call this method ourselves (as well as using it as a signal handler)
 +                 so _film can be 0.
 +              */
 +              return;
        }
  
 -      for (int i = lines; i < 2; ++i) {
 -              d << wxT ("\n ");
 +      shared_ptr<Content> content = weak_content.lock ();
 +      shared_ptr<VideoContent> video_content;
 +      shared_ptr<AudioContent> audio_content;
 +      shared_ptr<FFmpegContent> ffmpeg_content;
 +      if (content) {
 +              video_content = dynamic_pointer_cast<VideoContent> (content);
 +              audio_content = dynamic_pointer_cast<AudioContent> (content);
 +              ffmpeg_content = dynamic_pointer_cast<FFmpegContent> (content);
        }
  
 -      _frame_rate_description->SetLabel (d);
 +      if (property == VideoContentProperty::VIDEO_CROP) {
 +              checked_set (_left_crop,   video_content ? video_content->crop().left :   0);
 +              checked_set (_right_crop,  video_content ? video_content->crop().right :  0);
 +              checked_set (_top_crop,    video_content ? video_content->crop().top :    0);
 +              checked_set (_bottom_crop, video_content ? video_content->crop().bottom : 0);
 +              setup_scaling_description ();
 +      } else if (property == AudioContentProperty::AUDIO_GAIN) {
 +              checked_set (_audio_gain, audio_content ? audio_content->audio_gain() : 0);
 +      } else if (property == AudioContentProperty::AUDIO_DELAY) {
 +              checked_set (_audio_delay, audio_content ? audio_content->audio_delay() : 0);
 +      } else if (property == FFmpegContentProperty::SUBTITLE_STREAMS) {
 +              _subtitle_stream->Clear ();
 +              if (ffmpeg_content) {
 +                      vector<shared_ptr<FFmpegSubtitleStream> > s = ffmpeg_content->subtitle_streams ();
 +                      if (s.empty ()) {
 +                              _subtitle_stream->Enable (false);
 +                      }
 +                      for (vector<shared_ptr<FFmpegSubtitleStream> >::iterator i = s.begin(); i != s.end(); ++i) {
 +                              _subtitle_stream->Append (std_to_wx ((*i)->name), new wxStringClientData (std_to_wx (lexical_cast<string> ((*i)->id))));
 +                      }
 +                      
 +                      if (ffmpeg_content->subtitle_stream()) {
 +                              checked_set (_subtitle_stream, lexical_cast<string> (ffmpeg_content->subtitle_stream()->id));
 +                      } else {
 +                              _subtitle_stream->SetSelection (wxNOT_FOUND);
 +                      }
 +              }
 +              setup_subtitle_control_sensitivity ();
 +      } else if (property == FFmpegContentProperty::AUDIO_STREAMS) {
 +              _audio_stream->Clear ();
 +              if (ffmpeg_content) {
 +                      vector<shared_ptr<FFmpegAudioStream> > a = ffmpeg_content->audio_streams ();
 +                      for (vector<shared_ptr<FFmpegAudioStream> >::iterator i = a.begin(); i != a.end(); ++i) {
 +                              _audio_stream->Append (std_to_wx ((*i)->name), new wxStringClientData (std_to_wx (lexical_cast<string> ((*i)->id))));
 +                      }
 +                      
 +                      if (ffmpeg_content->audio_stream()) {
 +                              checked_set (_audio_stream, lexical_cast<string> (ffmpeg_content->audio_stream()->id));
 +                      }
 +              }
 +              setup_show_audio_sensitivity ();
 +      } else if (property == FFmpegContentProperty::AUDIO_STREAM) {
 +              setup_dcp_name ();
 +              setup_show_audio_sensitivity ();
++      } else if (property == FFmpegContentProperty::FILTERS) {
++              if (ffmpeg_content) {
++                      pair<string, string> p = Filter::ffmpeg_strings (ffmpeg_content->filters ());
++                      if (p.first.empty () && p.second.empty ()) {
++                              _filters->SetLabel (_("None"));
++                      } else {
++                              string const b = p.first + " " + p.second;
++                              _filters->SetLabel (std_to_wx (b));
++                      }
++                      _dcp_sizer->Layout ();
++              }
 +      }
  }
  
 -/** Called when the format widget has been changed */
  void
 -FilmEditor::format_changed (wxCommandEvent &)
 +FilmEditor::setup_container ()
 +{
 +      int n = 0;
 +      vector<Container const *> containers = Container::all ();
 +      vector<Container const *>::iterator i = containers.begin ();
 +      while (i != containers.end() && *i != _film->container ()) {
 +              ++i;
 +              ++n;
 +      }
 +      
 +      if (i == containers.end()) {
 +              checked_set (_container, -1);
 +      } else {
 +              checked_set (_container, n);
 +      }
 +      
 +      setup_dcp_name ();
 +      setup_scaling_description ();
 +}     
 +
 +/** Called when the container widget has been changed */
 +void
 +FilmEditor::container_changed (wxCommandEvent &)
  {
        if (!_film) {
                return;
@@@ -806,26 -891,34 +805,26 @@@ FilmEditor::set_film (shared_ptr<Film> 
        film_changed (Film::NAME);
        film_changed (Film::USE_DCI_NAME);
        film_changed (Film::CONTENT);
 -      film_changed (Film::TRUST_CONTENT_HEADER);
 +      film_changed (Film::LOOP);
        film_changed (Film::DCP_CONTENT_TYPE);
 -      film_changed (Film::FORMAT);
 -      film_changed (Film::CROP);
 -      film_changed (Film::FILTERS);
 +      film_changed (Film::CONTAINER);
-       film_changed (Film::FILTERS);
        film_changed (Film::SCALER);
 -      film_changed (Film::TRIM_START);
 -      film_changed (Film::TRIM_END);
 -      film_changed (Film::TRIM_TYPE);
 -      film_changed (Film::DCP_AB);
 -      film_changed (Film::CONTENT_AUDIO_STREAM);
 -      film_changed (Film::EXTERNAL_AUDIO);
 -      film_changed (Film::USE_CONTENT_AUDIO);
 -      film_changed (Film::AUDIO_GAIN);
 -      film_changed (Film::AUDIO_DELAY);
 -      film_changed (Film::STILL_DURATION);
        film_changed (Film::WITH_SUBTITLES);
        film_changed (Film::SUBTITLE_OFFSET);
        film_changed (Film::SUBTITLE_SCALE);
        film_changed (Film::COLOUR_LUT);
        film_changed (Film::J2K_BANDWIDTH);
        film_changed (Film::DCI_METADATA);
 -      film_changed (Film::SIZE);
 -      film_changed (Film::LENGTH);
 -      film_changed (Film::CONTENT_AUDIO_STREAMS);
 -      film_changed (Film::SUBTITLE_STREAMS);
 -      film_changed (Film::SOURCE_FRAME_RATE);
 -      film_changed (Film::DCP_FRAME_RATE);
 +      film_changed (Film::DCP_VIDEO_FRAME_RATE);
 +
 +      film_content_changed (boost::shared_ptr<Content> (), VideoContentProperty::VIDEO_CROP);
 +      film_content_changed (boost::shared_ptr<Content> (), AudioContentProperty::AUDIO_GAIN);
 +      film_content_changed (boost::shared_ptr<Content> (), AudioContentProperty::AUDIO_DELAY);
 +      film_content_changed (boost::shared_ptr<Content> (), FFmpegContentProperty::SUBTITLE_STREAMS);
 +      film_content_changed (boost::shared_ptr<Content> (), FFmpegContentProperty::SUBTITLE_STREAM);
 +      film_content_changed (boost::shared_ptr<Content> (), FFmpegContentProperty::AUDIO_STREAMS);
 +      film_content_changed (boost::shared_ptr<Content> (), FFmpegContentProperty::AUDIO_STREAM);
++      film_content_changed (boost::shared_ptr<Content> (), FFmpegContentProperty::FILTERS);
  }
  
  /** Updates the sensitivity of lots of widgets to a given value.
@@@ -867,8 -965,8 +866,18 @@@ FilmEditor::set_things_sensitive (bool 
  void
  FilmEditor::edit_filters_clicked (wxCommandEvent &)
  {
--      FilterDialog* d = new FilterDialog (this, _film->filters());
--      d->ActiveChanged.connect (bind (&Film::set_filters, _film, _1));
++      shared_ptr<Content> c = selected_content ();
++      if (!c) {
++              return;
++      }
++
++      shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c);
++      if (!fc) {
++              return;
++      }
++      
++      FilterDialog* d = new FilterDialog (this, fc->filters());
++      d->ActiveChanged.connect (bind (&FFmpegContent::set_filters, fc, _1));
        d->ShowModal ();
        d->Destroy ();
  }
index d08afe7a472912f0c7e86420996ec85a51c9d25e,4f2985a061db3677ebfb6d0ed5c89bf2fd25c9f8..4b1fb442e12ba802e42eb32a70affcd81dfd68c6
@@@ -130,13 -143,14 +130,12 @@@ FilmViewer::film_changed (Film::Propert
        case Film::WITH_SUBTITLES:
        case Film::SUBTITLE_OFFSET:
        case Film::SUBTITLE_SCALE:
 -      case Film::SCALER:
 -      case Film::FILTERS:
 -              update_from_raw ();
 +              raw_to_display ();
 +              _panel->Refresh ();
 +              _panel->Update ();
                break;
 -      case Film::SUBTITLE_STREAM:
 -              if (_decoders.video) {
 -                      _decoders.video->set_subtitle_stream (_film->subtitle_stream ());
 -              }
 +      case Film::SCALER:
-       case Film::FILTERS:
 +              update_from_decoder ();
                break;
        default:
                break;
@@@ -301,15 -308,15 +300,8 @@@ FilmViewer::raw_to_display (
                return;
        }
  
-       shared_ptr<const Image> input = _raw_frame;
 -      boost::shared_ptr<const Image> input = _raw_frame;
--
--      pair<string, string> const s = Filter::ffmpeg_strings (_film->filters());
--      if (!s.second.empty ()) {
--              input = input->post_process (s.second, true);
--      }
--      
        /* Get a compacted image as we have to feed it to wxWidgets */
--      _display_frame = input->scale_and_convert_to_rgb (_film_size, 0, _film->scaler(), false);
++      _display_frame = _raw_frame->scale_and_convert_to_rgb (_film_size, 0, _film->scaler(), false);
  
        if (_raw_sub) {
  
index 0000000000000000000000000000000000000000,e5229b5ffa1794485d21d7b221507846c54c5d5f..9c4482d3c0957fcdb001ec6327cccf6d1eb964a7
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,106 +1,105 @@@
 -                      "",
+ /*
+     Copyright (C) 2012 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ void
+ do_remote_encode (shared_ptr<DCPVideoFrame> frame, ServerDescription* description, shared_ptr<EncodedData> locally_encoded)
+ {
+       shared_ptr<EncodedData> remotely_encoded;
+       BOOST_CHECK_NO_THROW (remotely_encoded = frame->encode_remotely (description));
+       BOOST_CHECK (remotely_encoded);
+       
+       BOOST_CHECK_EQUAL (locally_encoded->size(), remotely_encoded->size());
+       BOOST_CHECK (memcmp (locally_encoded->data(), remotely_encoded->data(), locally_encoded->size()) == 0);
+ }
+ BOOST_AUTO_TEST_CASE (client_server_test)
+ {
+       shared_ptr<Image> image (new SimpleImage (PIX_FMT_RGB24, libdcp::Size (1998, 1080), true));
+       uint8_t* p = image->data()[0];
+       
+       for (int y = 0; y < 1080; ++y) {
+               uint8_t* q = p;
+               for (int x = 0; x < 1998; ++x) {
+                       *q++ = x % 256;
+                       *q++ = y % 256;
+                       *q++ = (x + y) % 256;
+               }
+               p += image->stride()[0];
+       }
+       shared_ptr<Image> sub_image (new SimpleImage (PIX_FMT_RGBA, libdcp::Size (100, 200), true));
+       p = sub_image->data()[0];
+       for (int y = 0; y < 200; ++y) {
+               uint8_t* q = p;
+               for (int x = 0; x < 100; ++x) {
+                       *q++ = y % 256;
+                       *q++ = x % 256;
+                       *q++ = (x + y) % 256;
+                       *q++ = 1;
+               }
+               p += sub_image->stride()[0];
+       }
+       shared_ptr<Subtitle> subtitle (new Subtitle (Position (50, 60), sub_image));
+       shared_ptr<FileLog> log (new FileLog ("build/test/client_server_test.log"));
+       shared_ptr<DCPVideoFrame> frame (
+               new DCPVideoFrame (
+                       image,
+                       subtitle,
+                       libdcp::Size (1998, 1080),
+                       0,
+                       0,
+                       1,
+                       Scaler::from_id ("bicubic"),
+                       0,
+                       24,
 -      dvdomatic_sleep (1);
+                       0,
+                       200000000,
+                       log
+                       )
+               );
+       shared_ptr<EncodedData> locally_encoded = frame->encode_locally ();
+       BOOST_ASSERT (locally_encoded);
+       
+       Server* server = new Server (log);
+       new thread (boost::bind (&Server::run, server, 2));
+       /* Let the server get itself ready */
++      dcpomatic_sleep (1);
+       ServerDescription description ("localhost", 2);
+       list<thread*> threads;
+       for (int i = 0; i < 8; ++i) {
+               threads.push_back (new thread (boost::bind (do_remote_encode, frame, &description, locally_encoded)));
+       }
+       for (list<thread*>::iterator i = threads.begin(); i != threads.end(); ++i) {
+               (*i)->join ();
+       }
+       for (list<thread*>::iterator i = threads.begin(); i != threads.end(); ++i) {
+               delete *i;
+       }
+ }
index 0000000000000000000000000000000000000000,9312a2067dd7ec115767c6f92f4c5356c0b25001..5a698684beb2f6824deaf6dcb6eaca65741029d1
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,67 +1,66 @@@
 -      film->set_content ("../../../test/test.mp4");
 -      film->set_format (Format::from_nickname ("Flat"));
+ /*
+     Copyright (C) 2012 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ BOOST_AUTO_TEST_CASE (make_dcp_test)
+ {
+       shared_ptr<Film> film = new_test_film ("make_dcp_test");
+       film->set_name ("test_film2");
 -              dvdomatic_sleep (1);
++      film->add_content (shared_ptr<FFmpegContent> (new FFmpegContent (film, "../../../test/test.mp4")));
++      film->set_container (Container::from_id ("185"));
+       film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
+       film->make_dcp ();
+       film->write_metadata ();
+       while (JobManager::instance()->work_to_do ()) {
 -      film->set_content ("../../../test/test.mp4");
 -      film->examine_content ();
 -      film->set_format (Format::from_nickname ("Flat"));
++              dcpomatic_sleep (1);
+       }
+       
+       BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
+ }
+ /** Test Film::have_dcp().  Requires the output from make_dcp_test above */
+ BOOST_AUTO_TEST_CASE (have_dcp_test)
+ {
+       boost::filesystem::path p = test_film_dir ("make_dcp_test");
+       Film f (p.string ());
+       BOOST_CHECK (f.have_dcp());
+       p /= f.dcp_name();
+       p /= f.dcp_video_mxf_filename();
+       boost::filesystem::remove (p);
+       BOOST_CHECK (!f.have_dcp ());
+ }
+ BOOST_AUTO_TEST_CASE (make_dcp_with_range_test)
+ {
+       shared_ptr<Film> film = new_test_film ("make_dcp_with_range_test");
+       film->set_name ("test_film3");
 -      film->set_trim_end (42);
++      film->add_content (shared_ptr<Content> (new FFmpegContent (film, "../../../test/test.mp4")));
++//    film->examine_content ();
++      film->set_container (Container::from_id ("185"));
+       film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
 -              dvdomatic_sleep (1);
+       film->make_dcp ();
+       while (JobManager::instance()->work_to_do() && !JobManager::instance()->errors()) {
++              dcpomatic_sleep (1);
+       }
+       BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
+ }
index 0000000000000000000000000000000000000000,0b4495b486989f3dc8b0adde516a7d936b9bf151..8309de7f1d350037f650539bdd16a87b848624d9
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,76 +1,54 @@@
 -      BOOST_CHECK_THROW (new Film (test_film, true), OpenFileError);
 -      
 -      shared_ptr<Film> f (new Film (test_film, false));
+ /*
+     Copyright (C) 2013 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ BOOST_AUTO_TEST_CASE (film_metadata_test)
+ {
+       string const test_film = "build/test/film_metadata_test";
+       
+       if (boost::filesystem::exists (test_film)) {
+               boost::filesystem::remove_all (test_film);
+       }
 -      BOOST_CHECK (f->format() == 0);
++      shared_ptr<Film> f (new Film (test_film));
+       f->_dci_date = boost::gregorian::from_undelimited_string ("20130211");
 -      BOOST_CHECK (f->filters ().empty());
++      BOOST_CHECK (f->container() == 0);
+       BOOST_CHECK (f->dcp_content_type() == 0);
 -      BOOST_CHECK_THROW (f->set_content ("jim"), OpenFileError);
+       f->set_name ("fred");
 -      f->set_format (Format::from_nickname ("Flat"));
 -      f->set_left_crop (1);
 -      f->set_right_crop (2);
 -      f->set_top_crop (3);
 -      f->set_bottom_crop (4);
 -      vector<Filter const *> f_filters;
 -      f_filters.push_back (Filter::from_id ("pphb"));
 -      f_filters.push_back (Filter::from_id ("unsharp"));
 -      f->set_filters (f_filters);
 -      f->set_trim_start (42);
 -      f->set_trim_end (99);
 -      f->set_dcp_ab (true);
++//    BOOST_CHECK_THROW (f->add_content ("jim"), OpenFileError);
+       f->set_dcp_content_type (DCPContentType::from_pretty_name ("Short"));
 -      shared_ptr<Film> g (new Film (test_film, true));
++      f->set_container (Container::from_id ("185"));
++      f->set_ab (true);
+       f->write_metadata ();
+       stringstream s;
+       s << "diff -u test/metadata.ref " << test_film << "/metadata";
+       BOOST_CHECK_EQUAL (::system (s.str().c_str ()), 0);
 -      BOOST_CHECK_EQUAL (g->format(), Format::from_nickname ("Flat"));
 -      BOOST_CHECK_EQUAL (g->crop().left, 1);
 -      BOOST_CHECK_EQUAL (g->crop().right, 2);
 -      BOOST_CHECK_EQUAL (g->crop().top, 3);
 -      BOOST_CHECK_EQUAL (g->crop().bottom, 4);
 -      vector<Filter const *> g_filters = g->filters ();
 -      BOOST_CHECK_EQUAL (g_filters.size(), 2);
 -      BOOST_CHECK_EQUAL (g_filters.front(), Filter::from_id ("pphb"));
 -      BOOST_CHECK_EQUAL (g_filters.back(), Filter::from_id ("unsharp"));
 -      BOOST_CHECK_EQUAL (g->trim_start(), 42);
 -      BOOST_CHECK_EQUAL (g->trim_end(), 99);
 -      BOOST_CHECK_EQUAL (g->dcp_ab(), true);
++      shared_ptr<Film> g (new Film (test_film));
++      g->read_metadata ();
+       BOOST_CHECK_EQUAL (g->name(), "fred");
+       BOOST_CHECK_EQUAL (g->dcp_content_type(), DCPContentType::from_pretty_name ("Short"));
++      BOOST_CHECK_EQUAL (g->container(), Container::from_id ("185"));
++      BOOST_CHECK_EQUAL (g->ab(), true);
+       
+       g->write_metadata ();
+       BOOST_CHECK_EQUAL (::system (s.str().c_str ()), 0);
+ }
index 0000000000000000000000000000000000000000,b150738a4d7a57d41f906d082f0786afb62cdeab..71bc00359d830e3f50fa3ed5f7af8373bd42d1c6
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,61 +1,33 @@@
 -
 -
 -/* Test VariableFormat-based scaling of content */
 -BOOST_AUTO_TEST_CASE (scaling_test)
 -{
 -      shared_ptr<Film> film (new Film (test_film_dir ("scaling_test").string(), false));
 -
 -      /* 4:3 ratio */
 -      film->set_size (libdcp::Size (320, 240));
 -
 -      /* This format should preserve aspect ratio of the source */
 -      Format const * format = Format::from_id ("var-185");
 -
 -      /* We should have enough padding that the result is 4:3,
 -         which would be 1440 pixels.
 -      */
 -      BOOST_CHECK_EQUAL (format->dcp_padding (film), (1998 - 1440) / 2);
 -      
 -      /* This crops it to 1.291666667 */
 -      film->set_left_crop (5);
 -      film->set_right_crop (5);
 -
 -      /* We should now have enough padding that the result is 1.29166667,
 -         which would be 1395 pixels.
 -      */
 -      BOOST_CHECK_EQUAL (format->dcp_padding (film), rint ((1998 - 1395) / 2.0));
 -}
 -
+ /*
+     Copyright (C) 2012 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ BOOST_AUTO_TEST_CASE (format_test)
+ {
+       Format::setup_formats ();
+       
+       Format const * f = Format::from_nickname ("Flat");
+       BOOST_CHECK (f);
+       BOOST_CHECK_EQUAL (f->dcp_size().width, 1998);
+       BOOST_CHECK_EQUAL (f->dcp_size().height, 1080);
+       
+       f = Format::from_nickname ("Scope");
+       BOOST_CHECK (f);
+       BOOST_CHECK_EQUAL (f->dcp_size().width, 2048);
+       BOOST_CHECK_EQUAL (f->dcp_size().height, 858);
+ }
index 0000000000000000000000000000000000000000,00700656eeba77fc5b9c2833eef61fa83893217e..8b04d3763bb83dab152be8a97da7eeb70410db8b
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,211 +1,213 @@@
+ /*
+     Copyright (C) 2012 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ /* Test best_dcp_frame_rate and FrameRateConversion */
+ BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test)
+ {
++#if 0 
+       /* Run some tests with a limited range of allowed rates */
+       
+       std::list<int> afr;
+       afr.push_back (24);
+       afr.push_back (25);
+       afr.push_back (30);
+       Config::instance()->set_allowed_dcp_frame_rates (afr);
+       int best = best_dcp_frame_rate (60);
+       FrameRateConversion frc = FrameRateConversion (60, best);
+       BOOST_CHECK_EQUAL (best, 30);
+       BOOST_CHECK_EQUAL (frc.skip, true);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       
+       best = best_dcp_frame_rate (50);
+       frc = FrameRateConversion (50, best);
+       BOOST_CHECK_EQUAL (best, 25);
+       BOOST_CHECK_EQUAL (frc.skip, true);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       best = best_dcp_frame_rate (48);
+       frc = FrameRateConversion (48, best);
+       BOOST_CHECK_EQUAL (best, 24);
+       BOOST_CHECK_EQUAL (frc.skip, true);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       
+       best = best_dcp_frame_rate (30);
+       frc = FrameRateConversion (30, best);
+       BOOST_CHECK_EQUAL (best, 30);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       best = best_dcp_frame_rate (29.97);
+       frc = FrameRateConversion (29.97, best);
+       BOOST_CHECK_EQUAL (best, 30);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+       
+       best = best_dcp_frame_rate (25);
+       frc = FrameRateConversion (25, best);
+       BOOST_CHECK_EQUAL (best, 25);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       best = best_dcp_frame_rate (24);
+       frc = FrameRateConversion (24, best);
+       BOOST_CHECK_EQUAL (best, 24);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       best = best_dcp_frame_rate (14.5);
+       frc = FrameRateConversion (14.5, best);
+       BOOST_CHECK_EQUAL (best, 30);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, true);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+       best = best_dcp_frame_rate (12.6);
+       frc = FrameRateConversion (12.6, best);
+       BOOST_CHECK_EQUAL (best, 25);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, true);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+       best = best_dcp_frame_rate (12.4);
+       frc = FrameRateConversion (12.4, best);
+       BOOST_CHECK_EQUAL (best, 25);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, true);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+       best = best_dcp_frame_rate (12);
+       frc = FrameRateConversion (12, best);
+       BOOST_CHECK_EQUAL (best, 24);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, true);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       /* Now add some more rates and see if it will use them
+          in preference to skip/repeat.
+       */
+       afr.push_back (48);
+       afr.push_back (50);
+       afr.push_back (60);
+       Config::instance()->set_allowed_dcp_frame_rates (afr);
+       best = best_dcp_frame_rate (60);
+       frc = FrameRateConversion (60, best);
+       BOOST_CHECK_EQUAL (best, 60);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       
+       best = best_dcp_frame_rate (50);
+       frc = FrameRateConversion (50, best);
+       BOOST_CHECK_EQUAL (best, 50);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       best = best_dcp_frame_rate (48);
+       frc = FrameRateConversion (48, best);
+       BOOST_CHECK_EQUAL (best, 48);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, false);
+       /* Check some out-there conversions (not the best) */
+       
+       frc = FrameRateConversion (14.99, 24);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, true);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+       /* Check some conversions with limited DCP targets */
+       afr.clear ();
+       afr.push_back (24);
+       Config::instance()->set_allowed_dcp_frame_rates (afr);
+       best = best_dcp_frame_rate (25);
+       frc = FrameRateConversion (25, best);
+       BOOST_CHECK_EQUAL (best, 24);
+       BOOST_CHECK_EQUAL (frc.skip, false);
+       BOOST_CHECK_EQUAL (frc.repeat, false);
+       BOOST_CHECK_EQUAL (frc.change_speed, true);
+ }
+ BOOST_AUTO_TEST_CASE (audio_sampling_rate_test)
+ {
+       std::list<int> afr;
+       afr.push_back (24);
+       afr.push_back (25);
+       afr.push_back (30);
+       Config::instance()->set_allowed_dcp_frame_rates (afr);
+       shared_ptr<Film> f = new_test_film ("audio_sampling_rate_test");
+       f->set_source_frame_rate (24);
+       f->set_dcp_frame_rate (24);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 80000, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 96000);
+       f->set_source_frame_rate (23.976);
+       f->set_dcp_frame_rate (best_dcp_frame_rate (23.976));
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
+       f->set_source_frame_rate (29.97);
+       f->set_dcp_frame_rate (best_dcp_frame_rate (29.97));
+       BOOST_CHECK_EQUAL (f->dcp_frame_rate (), 30);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
+       f->set_source_frame_rate (25);
+       f->set_dcp_frame_rate (24);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
+       f->set_source_frame_rate (25);
+       f->set_dcp_frame_rate (24);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
+       /* Check some out-there conversions (not the best) */
+       
+       f->set_source_frame_rate (14.99);
+       f->set_dcp_frame_rate (25);
+       f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 16000, 0)));
+       /* The FrameRateConversion within target_audio_sample_rate should choose to double-up
+          the 14.99 fps video to 30 and then run it slow at 25.
+       */
+       BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), rint (48000 * 2 * 14.99 / 25));
++#endif        
+ }
index 0000000000000000000000000000000000000000,247d4f75665895d09f243cd7c6c76a73d6ed848b..86c6dc9e342d45648ae92793be10f029d9d07eea
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,64 +1,64 @@@
 -      dvdomatic_sleep (1);
+ /*
+     Copyright (C) 2012 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ class TestJob : public Job
+ {
+ public:
+       TestJob (shared_ptr<Film> f)
+               : Job (f)
+       {
+       }
+       void set_finished_ok () {
+               set_state (FINISHED_OK);
+       }
+       void set_finished_error () {
+               set_state (FINISHED_ERROR);
+       }
+       void run ()
+       {
+               while (1) {
+                       if (finished ()) {
+                               return;
+                       }
+               }
+       }
+       string name () const {
+               return "";
+       }
+ };
+ BOOST_AUTO_TEST_CASE (job_manager_test)
+ {
+       shared_ptr<Film> f;
+       /* Single job */
+       shared_ptr<TestJob> a (new TestJob (f));
+       JobManager::instance()->add (a);
 -      dvdomatic_sleep (2);
++      dcpomatic_sleep (1);
+       BOOST_CHECK_EQUAL (a->running (), true);
+       a->set_finished_ok ();
++      dcpomatic_sleep (2);
+       BOOST_CHECK_EQUAL (a->finished_ok(), true);
+ }
index 0000000000000000000000000000000000000000,84f2a33ce325209614ae519a36a5fc4113104dab..08c9f2d2def524e250692d67be7bcdc498ba5d35
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,77 +1,74 @@@
 -      /* This needs to happen in the first test */
 -      dvdomatic_setup ();
 -
+ /*
+     Copyright (C) 2013 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ using std::list;
+ using std::cout;
+ struct Case
+ {
+       Case (AVPixelFormat f, int c, int l0, int l1, int l2, float b0, float b1, float b2)
+               : format(f)
+               , components(c)
+       {
+               lines[0] = l0;
+               lines[1] = l1;
+               lines[2] = l2;
+               bpp[0] = b0;
+               bpp[1] = b1;
+               bpp[2] = b2;
+       }
+       
+       AVPixelFormat format;
+       int components;
+       int lines[3];
+       float bpp[3];
+ };
+ BOOST_AUTO_TEST_CASE (pixel_formats_test)
+ {
+       list<Case> cases;
+       cases.push_back(Case(AV_PIX_FMT_RGB24,       1, 480, 480, 480, 3, 0,   0  ));
+       cases.push_back(Case(AV_PIX_FMT_RGBA,        1, 480, 480, 480, 4, 0,   0  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV420P,     3, 480, 240, 240, 1, 0.5, 0.5));
+       cases.push_back(Case(AV_PIX_FMT_YUV422P,     3, 480, 480, 480, 1, 0.5, 0.5));
+       cases.push_back(Case(AV_PIX_FMT_YUV422P10LE, 3, 480, 480, 480, 2, 1,   1  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV422P16LE, 3, 480, 480, 480, 2, 1,   1  ));
+       cases.push_back(Case(AV_PIX_FMT_UYVY422,     1, 480, 480, 480, 2, 0,   0  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV444P,     3, 480, 480, 480, 1, 1,   1  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV444P9BE,  3, 480, 480, 480, 2, 2,   2  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV444P9LE,  3, 480, 480, 480, 2, 2,   2  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV444P10BE, 3, 480, 480, 480, 2, 2,   2  ));
+       cases.push_back(Case(AV_PIX_FMT_YUV444P10LE, 3, 480, 480, 480, 2, 2,   2  ));
+       for (list<Case>::iterator i = cases.begin(); i != cases.end(); ++i) {
+               AVFrame* f = av_frame_alloc ();
+               f->width = 640;
+               f->height = 480;
+               f->format = static_cast<int> (i->format);
+               SimpleImage t (f);
+               BOOST_CHECK_EQUAL(t.components(), i->components);
+               BOOST_CHECK_EQUAL(t.lines(0), i->lines[0]);
+               BOOST_CHECK_EQUAL(t.lines(1), i->lines[1]);
+               BOOST_CHECK_EQUAL(t.lines(2), i->lines[2]);
+               BOOST_CHECK_EQUAL(t.bytes_per_pixel(0), i->bpp[0]);
+               BOOST_CHECK_EQUAL(t.bytes_per_pixel(1), i->bpp[1]);
+               BOOST_CHECK_EQUAL(t.bytes_per_pixel(2), i->bpp[2]);
+       }
+ }
index 0000000000000000000000000000000000000000,b467dc9a2efa144e94b51e9b7c5f02391e03665b..4d97e82afbebda8e7de70b050053df2175d5476c
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,52 +1,54 @@@
+ /*
+     Copyright (C) 2013 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
+     the Free Software Foundation; either version 2 of the License, or
+     (at your option) any later version.
+     This program is distributed in the hope that it will be useful,
+     but WITHOUT ANY WARRANTY; without even the implied warranty of
+     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+     GNU General Public License for more details.
+     You should have received a copy of the GNU General Public License
+     along with this program; if not, write to the Free Software
+     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ */
+ BOOST_AUTO_TEST_CASE (stream_test)
+ {
++#if 0 
+       FFmpegAudioStream a ("ffmpeg 4 44100 1 hello there world", boost::optional<int> (1));
+       BOOST_CHECK_EQUAL (a.id(), 4);
+       BOOST_CHECK_EQUAL (a.sample_rate(), 44100);
+       BOOST_CHECK_EQUAL (a.channel_layout(), 1);
+       BOOST_CHECK_EQUAL (a.name(), "hello there world");
+       BOOST_CHECK_EQUAL (a.to_string(), "ffmpeg 4 44100 1 hello there world");
+       SndfileStream e ("external 44100 1", boost::optional<int> (1));
+       BOOST_CHECK_EQUAL (e.sample_rate(), 44100);
+       BOOST_CHECK_EQUAL (e.channel_layout(), 1);
+       BOOST_CHECK_EQUAL (e.to_string(), "external 44100 1");
+       SubtitleStream s ("5 a b c", boost::optional<int> (1));
+       BOOST_CHECK_EQUAL (s.id(), 5);
+       BOOST_CHECK_EQUAL (s.name(), "a b c");
+       shared_ptr<AudioStream> ff = audio_stream_factory ("ffmpeg 4 44100 1 hello there world", boost::optional<int> (1));
+       shared_ptr<FFmpegAudioStream> cff = dynamic_pointer_cast<FFmpegAudioStream> (ff);
+       BOOST_CHECK (cff);
+       BOOST_CHECK_EQUAL (cff->id(), 4);
+       BOOST_CHECK_EQUAL (cff->sample_rate(), 44100);
+       BOOST_CHECK_EQUAL (cff->channel_layout(), 1);
+       BOOST_CHECK_EQUAL (cff->name(), "hello there world");
+       BOOST_CHECK_EQUAL (cff->to_string(), "ffmpeg 4 44100 1 hello there world");
+       shared_ptr<AudioStream> fe = audio_stream_factory ("external 44100 1", boost::optional<int> (1));
+       BOOST_CHECK_EQUAL (fe->sample_rate(), 44100);
+       BOOST_CHECK_EQUAL (fe->channel_layout(), 1);
+       BOOST_CHECK_EQUAL (fe->to_string(), "external 44100 1");
++#endif        
+ }
diff --cc test/test.cc
index 269bb374b4c370ba2f5877257cde549d21e66d60,91c876412f74b15af3f6f37eea5fc932cb7d1293..cbbd0443dc8eae7df4a64b4194bf843a1f7fdc5e
@@@ -53,14 -52,20 +53,20 @@@ using boost::shared_ptr
  using boost::thread;
  using boost::dynamic_pointer_cast;
  
- void
- setup_test_config ()
+ struct TestConfig
  {
-       Config::instance()->set_num_local_encoding_threads (1);
-       Config::instance()->set_servers (vector<ServerDescription*> ());
-       Config::instance()->set_server_port (61920);
-       Config::instance()->set_default_dci_metadata (DCIMetadata ());
- }
+       TestConfig()
+       {
 -              dvdomatic_setup();
++              dcpomatic_setup();
+               Config::instance()->set_num_local_encoding_threads (1);
+               Config::instance()->set_servers (vector<ServerDescription*> ());
+               Config::instance()->set_server_port (61920);
+               Config::instance()->set_default_dci_metadata (DCIMetadata ());
+       }
+ };
+ BOOST_GLOBAL_FIXTURE (TestConfig);
  
  boost::filesystem::path
  test_film_dir (string name)
@@@ -80,616 -85,19 +86,19 @@@ new_test_film (string name
                boost::filesystem::remove_all (p);
        }
        
 -      return shared_ptr<Film> (new Film (p.string(), false));
 +      shared_ptr<Film> f = shared_ptr<Film> (new Film (p.string()));
 +      f->write_metadata ();
 +      return f;
  }
  
- /* Check that Image::make_black works, and doesn't use values which crash
-    sws_scale().
- */
- BOOST_AUTO_TEST_CASE (make_black_test)
- {
-       /* This needs to happen in the first test */
-       dcpomatic_setup ();
-       libdcp::Size in_size (512, 512);
-       libdcp::Size out_size (1024, 1024);
-       list<AVPixelFormat> pix_fmts;
-       pix_fmts.push_back (AV_PIX_FMT_RGB24);
-       pix_fmts.push_back (AV_PIX_FMT_YUV420P);
-       pix_fmts.push_back (AV_PIX_FMT_YUV422P10LE);
-       pix_fmts.push_back (AV_PIX_FMT_YUV444P9LE);
-       pix_fmts.push_back (AV_PIX_FMT_YUV444P9BE);
-       pix_fmts.push_back (AV_PIX_FMT_YUV444P10LE);
-       pix_fmts.push_back (AV_PIX_FMT_YUV444P10BE);
-       pix_fmts.push_back (AV_PIX_FMT_UYVY422);
-       int N = 0;
-       for (list<AVPixelFormat>::const_iterator i = pix_fmts.begin(); i != pix_fmts.end(); ++i) {
-               boost::shared_ptr<Image> foo (new SimpleImage (*i, in_size, true));
-               foo->make_black ();
-               boost::shared_ptr<Image> bar = foo->scale_and_convert_to_rgb (out_size, 0, Scaler::from_id ("bicubic"), true);
-               
-               uint8_t* p = bar->data()[0];
-               for (int y = 0; y < bar->size().height; ++y) {
-                       uint8_t* q = p;
-                       for (int x = 0; x < bar->line_size()[0]; ++x) {
-                               BOOST_CHECK_EQUAL (*q++, 0);
-                       }
-                       p += bar->stride()[0];
-               }
-               ++N;
-       }
- }
- BOOST_AUTO_TEST_CASE (film_metadata_test)
- {
-       setup_test_config ();
-       string const test_film = "build/test/film_metadata_test";
-       
-       if (boost::filesystem::exists (test_film)) {
-               boost::filesystem::remove_all (test_film);
-       }
-       shared_ptr<Film> f (new Film (test_film));
-       f->write_metadata ();
-       f->_dci_date = boost::gregorian::from_undelimited_string ("20130211");
-       BOOST_CHECK (f->container() == 0);
-       BOOST_CHECK (f->dcp_content_type() == 0);
-       BOOST_CHECK (f->filters ().empty());
-       f->set_name ("fred");
- //    BOOST_CHECK_THROW (f->set_content ("jim"), OpenFileError);
-       f->set_dcp_content_type (DCPContentType::from_pretty_name ("Short"));
-       f->set_container (Container::from_id ("185"));
-       vector<Filter const *> f_filters;
-       f_filters.push_back (Filter::from_id ("pphb"));
-       f_filters.push_back (Filter::from_id ("unsharp"));
-       f->set_filters (f_filters);
-       f->set_ab (true);
-       f->write_metadata ();
-       stringstream s;
-       s << "diff -u test/metadata.ref " << test_film << "/metadata";
-       BOOST_CHECK_EQUAL (::system (s.str().c_str ()), 0);
-       shared_ptr<Film> g (new Film (test_film));
-       g->read_metadata ();
-       BOOST_CHECK_EQUAL (g->name(), "fred");
-       BOOST_CHECK_EQUAL (g->dcp_content_type(), DCPContentType::from_pretty_name ("Short"));
-       BOOST_CHECK_EQUAL (g->container(), Container::from_id ("185"));
-       vector<Filter const *> g_filters = g->filters ();
-       BOOST_CHECK_EQUAL (g_filters.size(), 2);
-       BOOST_CHECK_EQUAL (g_filters.front(), Filter::from_id ("pphb"));
-       BOOST_CHECK_EQUAL (g_filters.back(), Filter::from_id ("unsharp"));
-       BOOST_CHECK_EQUAL (g->ab(), true);
-       
-       g->write_metadata ();
-       BOOST_CHECK_EQUAL (::system (s.str().c_str ()), 0);
- }
- BOOST_AUTO_TEST_CASE (format_test)
- {
-       Format::setup_formats ();
-       
-       Format const * f = Format::from_nickname ("Flat");
-       BOOST_CHECK (f);
-       BOOST_CHECK_EQUAL (f->dcp_size().width, 1998);
-       BOOST_CHECK_EQUAL (f->dcp_size().height, 1080);
-       
-       f = Format::from_nickname ("Scope");
-       BOOST_CHECK (f);
-       BOOST_CHECK_EQUAL (f->dcp_size().width, 2048);
-       BOOST_CHECK_EQUAL (f->dcp_size().height, 858);
- }
- BOOST_AUTO_TEST_CASE (util_test)
- {
-       string t = "Hello this is a string \"with quotes\" and indeed without them";
-       vector<string> b = split_at_spaces_considering_quotes (t);
-       vector<string>::iterator i = b.begin ();
-       BOOST_CHECK_EQUAL (*i++, "Hello");
-       BOOST_CHECK_EQUAL (*i++, "this");
-       BOOST_CHECK_EQUAL (*i++, "is");
-       BOOST_CHECK_EQUAL (*i++, "a");
-       BOOST_CHECK_EQUAL (*i++, "string");
-       BOOST_CHECK_EQUAL (*i++, "with quotes");
-       BOOST_CHECK_EQUAL (*i++, "and");
-       BOOST_CHECK_EQUAL (*i++, "indeed");
-       BOOST_CHECK_EQUAL (*i++, "without");
-       BOOST_CHECK_EQUAL (*i++, "them");
- }
- class NullLog : public Log
- {
- public:
-       void do_log (string) {}
- };
- BOOST_AUTO_TEST_CASE (md5_digest_test)
- {
-       string const t = md5_digest ("test/md5.test");
-       BOOST_CHECK_EQUAL (t, "15058685ba99decdc4398c7634796eb0");
-       BOOST_CHECK_THROW (md5_digest ("foobar"), OpenFileError);
- }
- void
- do_remote_encode (shared_ptr<DCPVideoFrame> frame, ServerDescription* description, shared_ptr<EncodedData> locally_encoded)
- {
-       shared_ptr<EncodedData> remotely_encoded;
-       BOOST_CHECK_NO_THROW (remotely_encoded = frame->encode_remotely (description));
-       BOOST_CHECK (remotely_encoded);
-       
-       BOOST_CHECK_EQUAL (locally_encoded->size(), remotely_encoded->size());
-       BOOST_CHECK (memcmp (locally_encoded->data(), remotely_encoded->data(), locally_encoded->size()) == 0);
- }
- BOOST_AUTO_TEST_CASE (client_server_test)
- {
-       shared_ptr<Image> image (new SimpleImage (PIX_FMT_RGB24, libdcp::Size (1998, 1080), true));
-       uint8_t* p = image->data()[0];
-       
-       for (int y = 0; y < 1080; ++y) {
-               uint8_t* q = p;
-               for (int x = 0; x < 1998; ++x) {
-                       *q++ = x % 256;
-                       *q++ = y % 256;
-                       *q++ = (x + y) % 256;
-               }
-               p += image->stride()[0];
-       }
-       shared_ptr<Image> sub_image (new SimpleImage (PIX_FMT_RGBA, libdcp::Size (100, 200), true));
-       p = sub_image->data()[0];
-       for (int y = 0; y < 200; ++y) {
-               uint8_t* q = p;
-               for (int x = 0; x < 100; ++x) {
-                       *q++ = y % 256;
-                       *q++ = x % 256;
-                       *q++ = (x + y) % 256;
-                       *q++ = 1;
-               }
-               p += sub_image->stride()[0];
-       }
-       shared_ptr<Subtitle> subtitle (new Subtitle (Position (50, 60), sub_image));
-       shared_ptr<FileLog> log (new FileLog ("build/test/client_server_test.log"));
-       shared_ptr<DCPVideoFrame> frame (
-               new DCPVideoFrame (
-                       image,
-                       subtitle,
-                       libdcp::Size (1998, 1080),
-                       0,
-                       0,
-                       1,
-                       Scaler::from_id ("bicubic"),
-                       0,
-                       24,
-                       "",
-                       0,
-                       200000000,
-                       log
-                       )
-               );
-       shared_ptr<EncodedData> locally_encoded = frame->encode_locally ();
-       BOOST_ASSERT (locally_encoded);
-       
-       Server* server = new Server (log);
-       new thread (boost::bind (&Server::run, server, 2));
-       /* Let the server get itself ready */
-       dcpomatic_sleep (1);
-       ServerDescription description ("localhost", 2);
-       list<thread*> threads;
-       for (int i = 0; i < 8; ++i) {
-               threads.push_back (new thread (boost::bind (do_remote_encode, frame, &description, locally_encoded)));
-       }
-       for (list<thread*>::iterator i = threads.begin(); i != threads.end(); ++i) {
-               (*i)->join ();
-       }
-       for (list<thread*>::iterator i = threads.begin(); i != threads.end(); ++i) {
-               delete *i;
-       }
- }
- BOOST_AUTO_TEST_CASE (make_dcp_test)
- {
-       shared_ptr<Film> film = new_test_film ("make_dcp_test");
-       film->set_name ("test_film2");
- //    film->set_content ("../../../test/test.mp4");
-       film->set_container (Container::from_id ("185"));
-       film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
-       film->make_dcp ();
-       film->write_metadata ();
-       while (JobManager::instance()->work_to_do ()) {
-               dcpomatic_sleep (1);
-       }
-       
-       BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
- }
- /** Test Film::have_dcp().  Requires the output from make_dcp_test above */
- BOOST_AUTO_TEST_CASE (have_dcp_test)
- {
-       boost::filesystem::path p = test_film_dir ("make_dcp_test");
-       Film f (p.string ());
-       BOOST_CHECK (f.have_dcp());
-       p /= f.dcp_name();
-       p /= f.dcp_video_mxf_filename();
-       boost::filesystem::remove (p);
-       BOOST_CHECK (!f.have_dcp ());
- }
- BOOST_AUTO_TEST_CASE (make_dcp_with_range_test)
- {
-       shared_ptr<Film> film = new_test_film ("make_dcp_with_range_test");
-       film->set_name ("test_film3");
- //    film->set_content ("../../../test/test.mp4");
- //    film->examine_content ();
-       film->set_container (Container::from_id ("185"));
-       film->set_dcp_content_type (DCPContentType::from_pretty_name ("Test"));
-       film->make_dcp ();
-       while (JobManager::instance()->work_to_do() && !JobManager::instance()->errors()) {
-               dcpomatic_sleep (1);
-       }
-       BOOST_CHECK_EQUAL (JobManager::instance()->errors(), false);
- }
- #if 0
- /* Test best_dcp_frame_rate and FrameRateConversion */
- BOOST_AUTO_TEST_CASE (best_dcp_frame_rate_test)
- {
-       /* Run some tests with a limited range of allowed rates */
-       
-       std::list<int> afr;
-       afr.push_back (24);
-       afr.push_back (25);
-       afr.push_back (30);
-       Config::instance()->set_allowed_dcp_frame_rates (afr);
-       int best = best_dcp_frame_rate (60);
-       FrameRateConversion frc = FrameRateConversion (60, best);
-       BOOST_CHECK_EQUAL (best, 30);
-       BOOST_CHECK_EQUAL (frc.skip, true);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       
-       best = best_dcp_frame_rate (50);
-       frc = FrameRateConversion (50, best);
-       BOOST_CHECK_EQUAL (best, 25);
-       BOOST_CHECK_EQUAL (frc.skip, true);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       best = best_dcp_frame_rate (48);
-       frc = FrameRateConversion (48, best);
-       BOOST_CHECK_EQUAL (best, 24);
-       BOOST_CHECK_EQUAL (frc.skip, true);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       
-       best = best_dcp_frame_rate (30);
-       frc = FrameRateConversion (30, best);
-       BOOST_CHECK_EQUAL (best, 30);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       best = best_dcp_frame_rate (29.97);
-       frc = FrameRateConversion (29.97, best);
-       BOOST_CHECK_EQUAL (best, 30);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
-       
-       best = best_dcp_frame_rate (25);
-       frc = FrameRateConversion (25, best);
-       BOOST_CHECK_EQUAL (best, 25);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       best = best_dcp_frame_rate (24);
-       frc = FrameRateConversion (24, best);
-       BOOST_CHECK_EQUAL (best, 24);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       best = best_dcp_frame_rate (14.5);
-       frc = FrameRateConversion (14.5, best);
-       BOOST_CHECK_EQUAL (best, 30);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, true);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
-       best = best_dcp_frame_rate (12.6);
-       frc = FrameRateConversion (12.6, best);
-       BOOST_CHECK_EQUAL (best, 25);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, true);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
-       best = best_dcp_frame_rate (12.4);
-       frc = FrameRateConversion (12.4, best);
-       BOOST_CHECK_EQUAL (best, 25);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, true);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
-       best = best_dcp_frame_rate (12);
-       frc = FrameRateConversion (12, best);
-       BOOST_CHECK_EQUAL (best, 24);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, true);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       /* Now add some more rates and see if it will use them
-          in preference to skip/repeat.
-       */
-       afr.push_back (48);
-       afr.push_back (50);
-       afr.push_back (60);
-       Config::instance()->set_allowed_dcp_frame_rates (afr);
-       best = best_dcp_frame_rate (60);
-       frc = FrameRateConversion (60, best);
-       BOOST_CHECK_EQUAL (best, 60);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       
-       best = best_dcp_frame_rate (50);
-       frc = FrameRateConversion (50, best);
-       BOOST_CHECK_EQUAL (best, 50);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       best = best_dcp_frame_rate (48);
-       frc = FrameRateConversion (48, best);
-       BOOST_CHECK_EQUAL (best, 48);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, false);
-       /* Check some out-there conversions (not the best) */
-       
-       frc = FrameRateConversion (14.99, 24);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, true);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
-       /* Check some conversions with limited DCP targets */
-       afr.clear ();
-       afr.push_back (24);
-       Config::instance()->set_allowed_dcp_frame_rates (afr);
-       best = best_dcp_frame_rate (25);
-       frc = FrameRateConversion (25, best);
-       BOOST_CHECK_EQUAL (best, 24);
-       BOOST_CHECK_EQUAL (frc.skip, false);
-       BOOST_CHECK_EQUAL (frc.repeat, false);
-       BOOST_CHECK_EQUAL (frc.change_speed, true);
- }
- #endif
- BOOST_AUTO_TEST_CASE (audio_sampling_rate_test)
- {
-       std::list<int> afr;
-       afr.push_back (24);
-       afr.push_back (25);
-       afr.push_back (30);
-       Config::instance()->set_allowed_dcp_frame_rates (afr);
-       shared_ptr<Film> f = new_test_film ("audio_sampling_rate_test");
- //    f->set_source_frame_rate (24);
- //    f->set_dcp_frame_rate (24);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 48000);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 80000, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 96000);
- //    f->set_source_frame_rate (23.976);
- //    f->set_dcp_frame_rate (best_dcp_frame_rate (23.976));
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
- //    f->set_source_frame_rate (29.97);
- //    f->set_dcp_frame_rate (best_dcp_frame_rate (29.97));
- //    BOOST_CHECK_EQUAL (f->dcp_frame_rate (), 30);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 47952);
- //    f->set_source_frame_rate (25);
- //    f->set_dcp_frame_rate (24);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 48000, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
- //    f->set_source_frame_rate (25);
- //    f->set_dcp_frame_rate (24);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 44100, 0)));
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), 50000);
-       /* Check some out-there conversions (not the best) */
-       
- //    f->set_source_frame_rate (14.99);
- //    f->set_dcp_frame_rate (25);
- //    f->set_content_audio_stream (shared_ptr<AudioStream> (new FFmpegAudioStream ("a", 42, 16000, 0)));
-       /* The FrameRateConversion within target_audio_sample_rate should choose to double-up
-          the 14.99 fps video to 30 and then run it slow at 25.
-       */
- //    BOOST_CHECK_EQUAL (f->target_audio_sample_rate(), rint (48000 * 2 * 14.99 / 25));
- }
- class TestJob : public Job
- {
- public:
-       TestJob (shared_ptr<Film> f)
-               : Job (f)
-       {
-       }
-       void set_finished_ok () {
-               set_state (FINISHED_OK);
-       }
-       void set_finished_error () {
-               set_state (FINISHED_ERROR);
-       }
-       void run ()
-       {
-               while (1) {
-                       if (finished ()) {
-                               return;
-                       }
-               }
-       }
-       string name () const {
-               return "";
-       }
- };
- BOOST_AUTO_TEST_CASE (job_manager_test)
- {
-       shared_ptr<Film> f;
-       /* Single job */
-       shared_ptr<TestJob> a (new TestJob (f));
-       JobManager::instance()->add (a);
-       dcpomatic_sleep (1);
-       BOOST_CHECK_EQUAL (a->running (), true);
-       a->set_finished_ok ();
-       dcpomatic_sleep (2);
-       BOOST_CHECK_EQUAL (a->finished_ok(), true);
- }
- BOOST_AUTO_TEST_CASE (compact_image_test)
- {
-       SimpleImage* s = new SimpleImage (PIX_FMT_RGB24, libdcp::Size (50, 50), false);
-       BOOST_CHECK_EQUAL (s->components(), 1);
-       BOOST_CHECK_EQUAL (s->stride()[0], 50 * 3);
-       BOOST_CHECK_EQUAL (s->line_size()[0], 50 * 3);
-       BOOST_CHECK (s->data()[0]);
-       BOOST_CHECK (!s->data()[1]);
-       BOOST_CHECK (!s->data()[2]);
-       BOOST_CHECK (!s->data()[3]);
-       /* copy constructor */
-       SimpleImage* t = new SimpleImage (*s);
-       BOOST_CHECK_EQUAL (t->components(), 1);
-       BOOST_CHECK_EQUAL (t->stride()[0], 50 * 3);
-       BOOST_CHECK_EQUAL (t->line_size()[0], 50 * 3);
-       BOOST_CHECK (t->data()[0]);
-       BOOST_CHECK (!t->data()[1]);
-       BOOST_CHECK (!t->data()[2]);
-       BOOST_CHECK (!t->data()[3]);
-       BOOST_CHECK (t->data() != s->data());
-       BOOST_CHECK (t->data()[0] != s->data()[0]);
-       BOOST_CHECK (t->line_size() != s->line_size());
-       BOOST_CHECK (t->line_size()[0] == s->line_size()[0]);
-       BOOST_CHECK (t->stride() != s->stride());
-       BOOST_CHECK (t->stride()[0] == s->stride()[0]);
-       /* assignment operator */
-       SimpleImage* u = new SimpleImage (PIX_FMT_YUV422P, libdcp::Size (150, 150), true);
-       *u = *s;
-       BOOST_CHECK_EQUAL (u->components(), 1);
-       BOOST_CHECK_EQUAL (u->stride()[0], 50 * 3);
-       BOOST_CHECK_EQUAL (u->line_size()[0], 50 * 3);
-       BOOST_CHECK (u->data()[0]);
-       BOOST_CHECK (!u->data()[1]);
-       BOOST_CHECK (!u->data()[2]);
-       BOOST_CHECK (!u->data()[3]);
-       BOOST_CHECK (u->data() != s->data());
-       BOOST_CHECK (u->data()[0] != s->data()[0]);
-       BOOST_CHECK (u->line_size() != s->line_size());
-       BOOST_CHECK (u->line_size()[0] == s->line_size()[0]);
-       BOOST_CHECK (u->stride() != s->stride());
-       BOOST_CHECK (u->stride()[0] == s->stride()[0]);
-       delete s;
-       delete t;
-       delete u;
- }
- BOOST_AUTO_TEST_CASE (aligned_image_test)
- {
-       SimpleImage* s = new SimpleImage (PIX_FMT_RGB24, libdcp::Size (50, 50), true);
-       BOOST_CHECK_EQUAL (s->components(), 1);
-       /* 160 is 150 aligned to the nearest 32 bytes */
-       BOOST_CHECK_EQUAL (s->stride()[0], 160);
-       BOOST_CHECK_EQUAL (s->line_size()[0], 150);
-       BOOST_CHECK (s->data()[0]);
-       BOOST_CHECK (!s->data()[1]);
-       BOOST_CHECK (!s->data()[2]);
-       BOOST_CHECK (!s->data()[3]);
-       /* copy constructor */
-       SimpleImage* t = new SimpleImage (*s);
-       BOOST_CHECK_EQUAL (t->components(), 1);
-       BOOST_CHECK_EQUAL (t->stride()[0], 160);
-       BOOST_CHECK_EQUAL (t->line_size()[0], 150);
-       BOOST_CHECK (t->data()[0]);
-       BOOST_CHECK (!t->data()[1]);
-       BOOST_CHECK (!t->data()[2]);
-       BOOST_CHECK (!t->data()[3]);
-       BOOST_CHECK (t->data() != s->data());
-       BOOST_CHECK (t->data()[0] != s->data()[0]);
-       BOOST_CHECK (t->line_size() != s->line_size());
-       BOOST_CHECK (t->line_size()[0] == s->line_size()[0]);
-       BOOST_CHECK (t->stride() != s->stride());
-       BOOST_CHECK (t->stride()[0] == s->stride()[0]);
-       /* assignment operator */
-       SimpleImage* u = new SimpleImage (PIX_FMT_YUV422P, libdcp::Size (150, 150), false);
-       *u = *s;
-       BOOST_CHECK_EQUAL (u->components(), 1);
-       BOOST_CHECK_EQUAL (u->stride()[0], 160);
-       BOOST_CHECK_EQUAL (u->line_size()[0], 150);
-       BOOST_CHECK (u->data()[0]);
-       BOOST_CHECK (!u->data()[1]);
-       BOOST_CHECK (!u->data()[2]);
-       BOOST_CHECK (!u->data()[3]);
-       BOOST_CHECK (u->data() != s->data());
-       BOOST_CHECK (u->data()[0] != s->data()[0]);
-       BOOST_CHECK (u->line_size() != s->line_size());
-       BOOST_CHECK (u->line_size()[0] == s->line_size()[0]);
-       BOOST_CHECK (u->stride() != s->stride());
-       BOOST_CHECK (u->stride()[0] == s->stride()[0]);
-       delete s;
-       delete t;
-       delete u;
- }
+ #include "pixel_formats_test.cc"
+ #include "make_black_test.cc"
 -#include "trimmer_test.cc"
+ #include "film_metadata_test.cc"
+ #include "stream_test.cc"
+ #include "format_test.cc"
+ #include "util_test.cc"
 -#include "film_test.cc"
+ #include "dcp_test.cc"
+ #include "frame_rate_test.cc"
+ #include "job_test.cc"
+ #include "client_server_test.cc"
+ #include "image_test.cc"
diff --cc wscript
Simple merge