diff options
Diffstat (limited to 'src')
222 files changed, 10483 insertions, 4885 deletions
diff --git a/src/lib/analyse_audio_job.cc b/src/lib/analyse_audio_job.cc index ab985bdf7..60b10e7b6 100644 --- a/src/lib/analyse_audio_job.cc +++ b/src/lib/analyse_audio_job.cc @@ -18,6 +18,7 @@ */ #include "audio_analysis.h" +#include "audio_buffers.h" #include "analyse_audio_job.h" #include "compose.hpp" #include "film.h" @@ -59,19 +60,18 @@ AnalyseAudioJob::run () shared_ptr<Playlist> playlist (new Playlist); playlist->add (content); shared_ptr<Player> player (new Player (_film, playlist)); - player->disable_video (); - player->Audio.connect (bind (&AnalyseAudioJob::audio, this, _1, _2)); - - _samples_per_point = max (int64_t (1), _film->time_to_audio_frames (_film->length()) / _num_points); + int64_t const len = _film->length().frames (_film->audio_frame_rate()); + _samples_per_point = max (int64_t (1), len / _num_points); _current.resize (_film->audio_channels ()); _analysis.reset (new AudioAnalysis (_film->audio_channels ())); _done = 0; - OutputAudioFrame const len = _film->time_to_audio_frames (_film->length ()); - while (!player->pass ()) { - set_progress (double (_done) / len); + DCPTime const block = DCPTime::from_seconds (1.0 / 8); + for (DCPTime t; t < _film->length(); t += block) { + analyse (player->get_audio (t, block, false)); + set_progress (t.seconds() / _film->length().seconds()); } _analysis->write (content->audio_analysis_path ()); @@ -81,7 +81,7 @@ AnalyseAudioJob::run () } void -AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, Time) +AnalyseAudioJob::analyse (shared_ptr<const AudioBuffers> b) { for (int i = 0; i < b->frames(); ++i) { for (int j = 0; j < b->channels(); ++j) { diff --git a/src/lib/analyse_audio_job.h b/src/lib/analyse_audio_job.h index 3d4881983..a218cb340 100644 --- a/src/lib/analyse_audio_job.h +++ b/src/lib/analyse_audio_job.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,13 +17,25 @@ */ +/** @file src/lib/analyse_audio_job.h + * @brief AnalyseAudioJob class. + */ + #include "job.h" #include "audio_analysis.h" #include "types.h" +#include "dcpomatic_time.h" class AudioBuffers; class AudioContent; +/** @class AnalyseAudioJob + * @brief A job to analyse the audio of a piece of AudioContent and make a note of its + * broad peak and RMS levels. + * + * After computing the peak and RMS levels over the length of the content, the job + * will write a file to Content::audio_analysis_path. + */ class AnalyseAudioJob : public Job { public: @@ -33,10 +45,10 @@ public: void run (); private: - void audio (boost::shared_ptr<const AudioBuffers>, Time); + void analyse (boost::shared_ptr<const AudioBuffers>); boost::weak_ptr<AudioContent> _content; - OutputAudioFrame _done; + int64_t _done; int64_t _samples_per_point; std::vector<AudioPoint> _current; diff --git a/src/lib/audio_analysis.h b/src/lib/audio_analysis.h index 824472dda..b91a1cf51 100644 --- a/src/lib/audio_analysis.h +++ b/src/lib/audio_analysis.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/audio_analysis.h + * @brief AudioAnalysis and AudioPoint classes. + */ + #ifndef DCPOMATIC_AUDIO_ANALYSIS_H #define DCPOMATIC_AUDIO_ANALYSIS_H @@ -24,6 +28,9 @@ #include <list> #include <boost/filesystem.hpp> +/** @class AudioPoint + * @brief A single point of an audio analysis for one portion of one channel. + */ class AudioPoint { public: @@ -48,6 +55,14 @@ private: float _data[COUNT]; }; +/** @class AudioAnalysis + * @brief An analysis of the audio data in a piece of AudioContent. + * + * This is a set of AudioPoints for each channel. The AudioPoints + * each represent some measurement of the audio over a portion of the + * content. For example each AudioPoint may give the RMS level of + * a 1-minute portion of the audio. + */ class AudioAnalysis : public boost::noncopyable { public: diff --git a/src/lib/audio_buffers.cc b/src/lib/audio_buffers.cc index a1c9b81ac..56ca7a94b 100644 --- a/src/lib/audio_buffers.cc +++ b/src/lib/audio_buffers.cc @@ -73,6 +73,9 @@ AudioBuffers::~AudioBuffers () void AudioBuffers::allocate (int channels, int frames) { + assert (frames >= 0); + assert (channels >= 0); + _channels = channels; _frames = frames; _allocated_frames = frames; @@ -172,6 +175,11 @@ AudioBuffers::make_silent (int from, int frames) void AudioBuffers::copy_from (AudioBuffers const * from, int frames_to_copy, int read_offset, int write_offset) { + if (frames_to_copy == 0) { + /* Prevent the asserts from firing if there is nothing to do */ + return; + } + assert (from->channels() == channels()); assert (from); @@ -255,6 +263,8 @@ void AudioBuffers::accumulate_frames (AudioBuffers const * from, int read_offset, int write_offset, int frames) { assert (_channels == from->channels ()); + assert (read_offset >= 0); + assert (write_offset >= 0); for (int i = 0; i < _channels; ++i) { for (int j = 0; j < frames; ++j) { @@ -275,3 +285,29 @@ AudioBuffers::apply_gain (float dB) } } } + +/** @param c Channel index. + * @return AudioBuffers object containing only channel `c' from this AudioBuffers. + */ +shared_ptr<AudioBuffers> +AudioBuffers::channel (int c) const +{ + shared_ptr<AudioBuffers> o (new AudioBuffers (1, frames ())); + o->copy_channel_from (this, c, 0); + return o; +} + +void +AudioBuffers::copy_channel_from (AudioBuffers const * from, int from_channel, int to_channel) +{ + assert (from->frames() == frames()); + memcpy (data(to_channel), from->data(from_channel), frames() * sizeof (float)); +} + +shared_ptr<AudioBuffers> +AudioBuffers::clone () const +{ + shared_ptr<AudioBuffers> b (new AudioBuffers (channels (), frames ())); + b->copy_from (this, frames (), 0, 0); + return b; +} diff --git a/src/lib/audio_buffers.h b/src/lib/audio_buffers.h index c9030dbfa..8cd67aaa7 100644 --- a/src/lib/audio_buffers.h +++ b/src/lib/audio_buffers.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,8 +17,12 @@ */ -#ifndef DVDOMATIC_AUDIO_BUFFERS_H -#define DVDOMATIC_AUDIO_BUFFERS_H +/** @file src/lib/audio_buffers.h + * @brief AudioBuffers class. + */ + +#ifndef DCPOMATIC_AUDIO_BUFFERS_H +#define DCPOMATIC_AUDIO_BUFFERS_H #include <boost/shared_ptr.hpp> @@ -35,6 +39,9 @@ public: AudioBuffers & operator= (AudioBuffers const &); + boost::shared_ptr<AudioBuffers> clone () const; + boost::shared_ptr<AudioBuffers> channel (int) const; + void ensure_size (int); float** data () const { @@ -60,8 +67,9 @@ public: void apply_gain (float); void copy_from (AudioBuffers const * from, int frames_to_copy, int read_offset, int write_offset); + void copy_channel_from (AudioBuffers const * from, int from_channel, int to_channel); void move (int from, int to, int frames); - void accumulate_channel (AudioBuffers const *, int, int, float gain = 1); + void accumulate_channel (AudioBuffers const * from, int from_channel, int to_channel, float gain = 1); void accumulate_frames (AudioBuffers const *, int read_offset, int write_offset, int frames); private: diff --git a/src/lib/audio_content.cc b/src/lib/audio_content.cc index 29d159a29..d02728b00 100644 --- a/src/lib/audio_content.cc +++ b/src/lib/audio_content.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,7 +18,7 @@ */ #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "audio_content.h" #include "analyse_audio_job.h" #include "job_manager.h" @@ -26,6 +26,7 @@ #include "exceptions.h" #include "config.h" #include "frame_rate_change.h" +#include "audio_processor.h" #include "i18n.h" @@ -34,7 +35,7 @@ using std::cout; using std::vector; using boost::shared_ptr; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; int const AudioContentProperty::AUDIO_CHANNELS = 200; int const AudioContentProperty::AUDIO_LENGTH = 201; @@ -42,11 +43,22 @@ int const AudioContentProperty::AUDIO_FRAME_RATE = 202; int const AudioContentProperty::AUDIO_GAIN = 203; int const AudioContentProperty::AUDIO_DELAY = 204; int const AudioContentProperty::AUDIO_MAPPING = 205; +int const AudioContentProperty::AUDIO_PROCESSOR = 206; -AudioContent::AudioContent (shared_ptr<const Film> f, Time s) +AudioContent::AudioContent (shared_ptr<const Film> f) + : Content (f) + , _audio_gain (0) + , _audio_delay (Config::instance()->default_audio_delay ()) + , _audio_processor (0) +{ + +} + +AudioContent::AudioContent (shared_ptr<const Film> f, DCPTime s) : Content (f, s) , _audio_gain (0) , _audio_delay (Config::instance()->default_audio_delay ()) + , _audio_processor (0) { } @@ -55,15 +67,20 @@ AudioContent::AudioContent (shared_ptr<const Film> f, boost::filesystem::path p) : Content (f, p) , _audio_gain (0) , _audio_delay (Config::instance()->default_audio_delay ()) + , _audio_processor (0) { } -AudioContent::AudioContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) +AudioContent::AudioContent (shared_ptr<const Film> f, cxml::ConstNodePtr node) : Content (f, node) + , _audio_processor (0) { _audio_gain = node->number_child<float> ("AudioGain"); _audio_delay = node->number_child<int> ("AudioDelay"); + if (node->optional_string_child ("AudioProcessor")) { + _audio_processor = AudioProcessor::from_id (node->string_child ("AudioProcessor")); + } } AudioContent::AudioContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c) @@ -86,6 +103,7 @@ AudioContent::AudioContent (shared_ptr<const Film> f, vector<shared_ptr<Content> _audio_gain = ref->audio_gain (); _audio_delay = ref->audio_delay (); + _audio_processor = ref->audio_processor (); } void @@ -94,6 +112,9 @@ AudioContent::as_xml (xmlpp::Node* node) const boost::mutex::scoped_lock lm (_mutex); node->add_child("AudioGain")->add_child_text (raw_convert<string> (_audio_gain)); node->add_child("AudioDelay")->add_child_text (raw_convert<string> (_audio_delay)); + if (_audio_processor) { + node->add_child("AudioProcessor")->add_child_text (_audio_processor->id ()); + } } @@ -119,6 +140,22 @@ AudioContent::set_audio_delay (int d) signal_changed (AudioContentProperty::AUDIO_DELAY); } +void +AudioContent::set_audio_processor (AudioProcessor const * p) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_processor = p; + } + + /* The channel count might have changed, so reset the mapping */ + AudioMapping m (processed_audio_channels ()); + m.make_default (); + set_audio_mapping (m); + + signal_changed (AudioContentProperty::AUDIO_PROCESSOR); +} + boost::signals2::connection AudioContent::analyse_audio (boost::function<void()> finished) { @@ -148,24 +185,38 @@ AudioContent::audio_analysis_path () const string AudioContent::technical_summary () const { - return String::compose ("audio: channels %1, length %2, raw rate %3, out rate %4", audio_channels(), audio_length(), content_audio_frame_rate(), output_audio_frame_rate()); + return String::compose ( + "audio: channels %1, length %2, content rate %3, resampled rate %4", + audio_channels(), + audio_length().seconds(), + audio_frame_rate(), + resampled_audio_frame_rate() + ); +} + +void +AudioContent::set_audio_mapping (AudioMapping) +{ + signal_changed (AudioContentProperty::AUDIO_MAPPING); } +/** @return the frame rate that this content should be resampled to in order + * that it is in sync with the active video content at its start time. + */ int -AudioContent::output_audio_frame_rate () const +AudioContent::resampled_audio_frame_rate () const { shared_ptr<const Film> film = _film.lock (); assert (film); /* Resample to a DCI-approved sample rate */ - double t = dcp_audio_frame_rate (content_audio_frame_rate ()); + double t = dcp_audio_frame_rate (audio_frame_rate ()); FrameRateChange frc = film->active_frame_rate_change (position ()); /* Compensate if the DCP is being run at a different frame rate to the source; that is, if the video is run such that it will look different in the DCP compared to the source (slower or faster). - skip/repeat doesn't come into effect here. */ if (frc.change_speed) { @@ -175,3 +226,13 @@ AudioContent::output_audio_frame_rate () const return rint (t); } +int +AudioContent::processed_audio_channels () const +{ + if (!audio_processor ()) { + return audio_channels (); + } + + return audio_processor()->out_channels (audio_channels ()); +} + diff --git a/src/lib/audio_content.h b/src/lib/audio_content.h index 2c324a3a4..57085a765 100644 --- a/src/lib/audio_content.h +++ b/src/lib/audio_content.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/audio_content.h + * @brief AudioContent and AudioContentProperty classes. + */ + #ifndef DCPOMATIC_AUDIO_CONTENT_H #define DCPOMATIC_AUDIO_CONTENT_H @@ -27,6 +31,11 @@ namespace cxml { class Node; } +class AudioProcessor; + +/** @class AudioContentProperty + * @brief Names for properties of AudioContent. + */ class AudioContentProperty { public: @@ -36,34 +45,44 @@ public: static int const AUDIO_GAIN; static int const AUDIO_DELAY; static int const AUDIO_MAPPING; + static int const AUDIO_PROCESSOR; }; +/** @class AudioContent + * @brief Parent class for content which may contain audio data. + */ class AudioContent : public virtual Content { public: typedef int64_t Frame; - AudioContent (boost::shared_ptr<const Film>, Time); + AudioContent (boost::shared_ptr<const Film>); + AudioContent (boost::shared_ptr<const Film>, DCPTime); AudioContent (boost::shared_ptr<const Film>, boost::filesystem::path); - AudioContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + AudioContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr); AudioContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >); void as_xml (xmlpp::Node *) const; std::string technical_summary () const; + /** @return number of audio channels in the content */ virtual int audio_channels () const = 0; - virtual AudioContent::Frame audio_length () const = 0; - virtual int content_audio_frame_rate () const = 0; + /** @return the length of the audio in the content */ + virtual ContentTime audio_length () const = 0; + /** @return the frame rate of the content */ + virtual int audio_frame_rate () const = 0; virtual AudioMapping audio_mapping () const = 0; - virtual void set_audio_mapping (AudioMapping) = 0; + virtual void set_audio_mapping (AudioMapping); virtual boost::filesystem::path audio_analysis_path () const; - int output_audio_frame_rate () const; - + int resampled_audio_frame_rate () const; + int processed_audio_channels () const; + boost::signals2::connection analyse_audio (boost::function<void()>); void set_audio_gain (double); void set_audio_delay (int); + void set_audio_processor (AudioProcessor const *); double audio_gain () const { boost::mutex::scoped_lock lm (_mutex); @@ -75,11 +94,17 @@ public: return _audio_delay; } + AudioProcessor const * audio_processor () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_processor; + } + private: /** Gain to apply to audio in dB */ double _audio_gain; /** Delay to apply to audio (positive moves audio later) in milliseconds */ int _audio_delay; + AudioProcessor const * _audio_processor; }; #endif diff --git a/src/lib/audio_decoder.cc b/src/lib/audio_decoder.cc index 30bd2540a..f3251f306 100644 --- a/src/lib/audio_decoder.cc +++ b/src/lib/audio_decoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,39 +20,217 @@ #include <iostream> #include "audio_decoder.h" #include "audio_buffers.h" -#include "exceptions.h" -#include "log.h" +#include "audio_processor.h" #include "resampler.h" +#include "util.h" #include "i18n.h" using std::list; using std::pair; using std::cout; +using std::min; +using std::max; using boost::optional; using boost::shared_ptr; -AudioDecoder::AudioDecoder (shared_ptr<const Film> film, shared_ptr<const AudioContent> content) - : Decoder (film) - , _audio_content (content) - , _audio_position (0) +AudioDecoder::AudioDecoder (shared_ptr<const AudioContent> content) + : _audio_content (content) { + if (content->resampled_audio_frame_rate() != content->audio_frame_rate() && content->audio_channels ()) { + _resampler.reset (new Resampler (content->audio_frame_rate(), content->resampled_audio_frame_rate(), content->audio_channels ())); + } + if (content->audio_processor ()) { + _processor = content->audio_processor()->clone (content->resampled_audio_frame_rate ()); + } + + reset_decoded_audio (); } void -AudioDecoder::audio (shared_ptr<const AudioBuffers> data, AudioContent::Frame frame) +AudioDecoder::reset_decoded_audio () { - Audio (data, frame); - _audio_position = frame + data->frames (); + _decoded_audio = ContentAudio (shared_ptr<AudioBuffers> (new AudioBuffers (_audio_content->processed_audio_channels(), 0)), 0); } -/** This is a bit odd, but necessary when we have (e.g.) FFmpegDecoders with no audio. - * The player needs to know that there is no audio otherwise it will keep trying to - * pass() the decoder to get it to emit audio. +shared_ptr<ContentAudio> +AudioDecoder::get_audio (AudioFrame frame, AudioFrame length, bool accurate) +{ + shared_ptr<ContentAudio> dec; + + AudioFrame const end = frame + length - 1; + + if (frame < _decoded_audio.frame || end > (_decoded_audio.frame + length * 4)) { + /* Either we have no decoded data, or what we do have is a long way from what we want: seek */ + seek (ContentTime::from_frames (frame, _audio_content->audio_frame_rate()), accurate); + } + + /* Offset of the data that we want from the start of _decoded_audio.audio + (to be set up shortly) + */ + AudioFrame decoded_offset = 0; + + /* Now enough pass() calls will either: + * (a) give us what we want, or + * (b) hit the end of the decoder. + * + * If we are being accurate, we want the right frames, + * otherwise any frames will do. + */ + if (accurate) { + /* Keep stuffing data into _decoded_audio until we have enough data, or the subclass does not want to give us any more */ + while ((_decoded_audio.frame > frame || (_decoded_audio.frame + _decoded_audio.audio->frames()) < end) && !pass ()) {} + decoded_offset = frame - _decoded_audio.frame; + } else { + while (_decoded_audio.audio->frames() < length && !pass ()) {} + /* Use decoded_offset of 0, as we don't really care what frames we return */ + } + + /* The amount of data available in _decoded_audio.audio starting from `frame'. This could be -ve + if pass() returned true before we got enough data. + */ + AudioFrame const available = _decoded_audio.audio->frames() - decoded_offset; + + /* We will return either that, or the requested amount, whichever is smaller */ + AudioFrame const to_return = max ((AudioFrame) 0, min (available, length)); + + /* Copy our data to the output */ + shared_ptr<AudioBuffers> out (new AudioBuffers (_decoded_audio.audio->channels(), to_return)); + out->copy_from (_decoded_audio.audio.get(), to_return, decoded_offset, 0); + + AudioFrame const remaining = max ((AudioFrame) 0, available - to_return); + + /* Clean up decoded; first, move the data after what we just returned to the start of the buffer */ + _decoded_audio.audio->move (decoded_offset + to_return, 0, remaining); + /* And set up the number of frames we have left */ + _decoded_audio.audio->set_frames (remaining); + /* Also bump where those frames are in terms of the content */ + _decoded_audio.frame += decoded_offset + to_return; + + return shared_ptr<ContentAudio> (new ContentAudio (out, frame)); +} + +/** Called by subclasses when audio data is ready. + * + * Audio timestamping is made hard by many factors, but perhaps the most entertaining is resampling. + * We have to assume that we are feeding continuous data into the resampler, and so we get continuous + * data out. Hence we do the timestamping here, post-resampler, just by counting samples. + * + * The time is passed in here so that after a seek we can set up our _audio_position. The + * time is ignored once this has been done. */ -bool -AudioDecoder::has_audio () const +void +AudioDecoder::audio (shared_ptr<const AudioBuffers> data, ContentTime time) +{ + if (_resampler) { + data = _resampler->run (data); + } + + if (_processor) { + data = _processor->run (data); + } + + AudioFrame const frame_rate = _audio_content->resampled_audio_frame_rate (); + + if (_seek_reference) { + /* We've had an accurate seek and now we're seeing some data */ + ContentTime const delta = time - _seek_reference.get (); + AudioFrame const delta_frames = delta.frames (frame_rate); + if (delta_frames > 0) { + /* This data comes after the seek time. Pad the data with some silence. */ + shared_ptr<AudioBuffers> padded (new AudioBuffers (data->channels(), data->frames() + delta_frames)); + padded->make_silent (); + padded->copy_from (data.get(), data->frames(), 0, delta_frames); + data = padded; + time -= delta; + } else if (delta_frames < 0) { + /* This data comes before the seek time. Throw some data away */ + AudioFrame const to_discard = min (-delta_frames, static_cast<AudioFrame> (data->frames())); + AudioFrame const to_keep = data->frames() - to_discard; + if (to_keep == 0) { + /* We have to throw all this data away, so keep _seek_reference and + try again next time some data arrives. + */ + return; + } + shared_ptr<AudioBuffers> trimmed (new AudioBuffers (data->channels(), to_keep)); + trimmed->copy_from (data.get(), to_keep, to_discard, 0); + data = trimmed; + time += ContentTime::from_frames (to_discard, frame_rate); + } + _seek_reference = optional<ContentTime> (); + } + + if (!_audio_position) { + _audio_position = time.frames (frame_rate); + } + + assert (_audio_position.get() >= (_decoded_audio.frame + _decoded_audio.audio->frames())); + + add (data); +} + +void +AudioDecoder::add (shared_ptr<const AudioBuffers> data) +{ + if (!_audio_position) { + /* This should only happen when there is a seek followed by a flush, but + we need to cope with it. + */ + return; + } + + /* Resize _decoded_audio to fit the new data */ + int new_size = 0; + if (_decoded_audio.audio->frames() == 0) { + /* There's nothing in there, so just store the new data */ + new_size = data->frames (); + _decoded_audio.frame = _audio_position.get (); + } else { + /* Otherwise we need to extend _decoded_audio to include the new stuff */ + new_size = _audio_position.get() + data->frames() - _decoded_audio.frame; + } + + _decoded_audio.audio->ensure_size (new_size); + _decoded_audio.audio->set_frames (new_size); + + /* Copy new data in */ + _decoded_audio.audio->copy_from (data.get(), data->frames(), 0, _audio_position.get() - _decoded_audio.frame); + _audio_position = _audio_position.get() + data->frames (); + + /* Limit the amount of data we keep in case nobody is asking for it */ + int const max_frames = _audio_content->resampled_audio_frame_rate () * 10; + if (_decoded_audio.audio->frames() > max_frames) { + int const to_remove = _decoded_audio.audio->frames() - max_frames; + _decoded_audio.frame += to_remove; + _decoded_audio.audio->move (to_remove, 0, max_frames); + _decoded_audio.audio->set_frames (max_frames); + } +} + +void +AudioDecoder::flush () +{ + if (!_resampler) { + return; + } + + shared_ptr<const AudioBuffers> b = _resampler->flush (); + if (b) { + add (b); + } +} + +void +AudioDecoder::seek (ContentTime t, bool accurate) { - return _audio_content->audio_channels () > 0; + _audio_position.reset (); + reset_decoded_audio (); + if (accurate) { + _seek_reference = t; + } + if (_processor) { + _processor->flush (); + } } diff --git a/src/lib/audio_decoder.h b/src/lib/audio_decoder.h index ab6c4b8a9..f8438df52 100644 --- a/src/lib/audio_decoder.h +++ b/src/lib/audio_decoder.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -27,8 +27,10 @@ #include "decoder.h" #include "content.h" #include "audio_content.h" +#include "content_audio.h" class AudioBuffers; +class Resampler; /** @class AudioDecoder. * @brief Parent class for audio decoders. @@ -36,18 +38,38 @@ class AudioBuffers; class AudioDecoder : public virtual Decoder { public: - AudioDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const AudioContent>); - - bool has_audio () const; - - /** Emitted when some audio data is ready */ - boost::signals2::signal<void (boost::shared_ptr<const AudioBuffers>, AudioContent::Frame)> Audio; + AudioDecoder (boost::shared_ptr<const AudioContent>); + + boost::shared_ptr<const AudioContent> audio_content () const { + return _audio_content; + } + /** Try to fetch some audio from a specific place in this content. + * @param frame Frame to start from (after resampling, if applicable) + * @param length Frames to get (after resampling, if applicable) + * @param accurate true to try hard to return frames from exactly `frame', false if we don't mind nearby frames. + * @return Time-stamped audio data which may or may not be from the location (and of the length) requested. + */ + boost::shared_ptr<ContentAudio> get_audio (AudioFrame time, AudioFrame length, bool accurate); + protected: - void audio (boost::shared_ptr<const AudioBuffers>, AudioContent::Frame); + void seek (ContentTime time, bool accurate); + void audio (boost::shared_ptr<const AudioBuffers>, ContentTime); + void flush (); + void reset_decoded_audio (); + void add (boost::shared_ptr<const AudioBuffers>); + boost::shared_ptr<const AudioContent> _audio_content; - AudioContent::Frame _audio_position; + boost::shared_ptr<Resampler> _resampler; + boost::shared_ptr<AudioProcessor> _processor; + boost::optional<AudioFrame> _audio_position; + /** Currently-available decoded audio data */ + ContentAudio _decoded_audio; + /** The time of an accurate seek after which we have not yet received any actual + data at the seek time. + */ + boost::optional<ContentTime> _seek_reference; }; #endif diff --git a/src/lib/audio_examiner.h b/src/lib/audio_examiner.h new file mode 100644 index 000000000..d6d4dbe97 --- /dev/null +++ b/src/lib/audio_examiner.h @@ -0,0 +1,35 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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. + +*/ + +/** @file src/lib/audio_examiner.h + * @brief AudioExaminer class. + */ + +/** @class AudioExaminer + * @brief Parent for classes which examine AudioContent for their pertinent details. + */ +class AudioExaminer +{ +public: + virtual ~AudioExaminer () {} + + virtual int audio_channels () const = 0; + virtual ContentTime audio_length () const = 0; + virtual int audio_frame_rate () const = 0; +}; diff --git a/src/lib/audio_filter.cc b/src/lib/audio_filter.cc new file mode 100644 index 000000000..59b5684ea --- /dev/null +++ b/src/lib/audio_filter.cc @@ -0,0 +1,139 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <cmath> +#include "audio_filter.h" +#include "audio_buffers.h" + +using std::vector; +using std::min; +using boost::shared_ptr; + +vector<float> +AudioFilter::sinc_blackman (float cutoff, bool invert) const +{ + vector<float> ir (_M + 1); + + /* Impulse response */ + + for (int i = 0; i <= _M; ++i) { + if (i == (_M / 2)) { + ir[i] = 2 * M_PI * cutoff; + } else { + /* sinc */ + ir[i] = sin (2 * M_PI * cutoff * (i - _M / 2)) / (i - _M / 2); + /* Blackman window */ + ir[i] *= (0.42 - 0.5 * cos (2 * M_PI * i / _M) + 0.08 * cos (4 * M_PI * i / _M)); + } + } + + /* Normalise */ + + float sum = 0; + for (int i = 0; i <= _M; ++i) { + sum += ir[i]; + } + + for (int i = 0; i <= _M; ++i) { + ir[i] /= sum; + } + + /* Frequency inversion (swapping low-pass for high-pass, or whatever) */ + + if (invert) { + for (int i = 0; i <= _M; ++i) { + ir[i] = -ir[i]; + } + ir[_M / 2] += 1; + } + + return ir; +} + +shared_ptr<AudioBuffers> +AudioFilter::run (shared_ptr<AudioBuffers> in) +{ + shared_ptr<AudioBuffers> out (new AudioBuffers (in->channels(), in->frames())); + + if (!_tail) { + _tail.reset (new AudioBuffers (in->channels(), _M + 1)); + _tail->make_silent (); + } + + for (int i = 0; i < in->channels(); ++i) { + for (int j = 0; j < in->frames(); ++j) { + float s = 0; + for (int k = 0; k <= _M; ++k) { + if ((j - k) < 0) { + s += _tail->data(i)[j - k + _M + 1] * _ir[k]; + } else { + s += in->data(i)[j - k] * _ir[k]; + } + } + + out->data(i)[j] = s; + } + } + + int const amount = min (in->frames(), _tail->frames()); + if (amount < _tail->frames ()) { + _tail->move (amount, 0, _tail->frames() - amount); + } + _tail->copy_from (in.get(), amount, in->frames() - amount, _tail->frames () - amount); + + return out; +} + +void +AudioFilter::flush () +{ + _tail.reset (); +} + +LowPassAudioFilter::LowPassAudioFilter (float transition_bandwidth, float cutoff) + : AudioFilter (transition_bandwidth) +{ + _ir = sinc_blackman (cutoff, false); +} + + +HighPassAudioFilter::HighPassAudioFilter (float transition_bandwidth, float cutoff) + : AudioFilter (transition_bandwidth) +{ + _ir = sinc_blackman (cutoff, true); +} + +BandPassAudioFilter::BandPassAudioFilter (float transition_bandwidth, float lower, float higher) + : AudioFilter (transition_bandwidth) +{ + vector<float> lpf = sinc_blackman (lower, false); + vector<float> hpf = sinc_blackman (higher, true); + + _ir.resize (_M + 1); + for (int i = 0; i <= _M; ++i) { + _ir[i] = lpf[i] + hpf[i]; + } + + /* We now have a band-stop, so invert for band-pass */ + for (int i = 0; i <= _M; ++i) { + _ir[i] = -_ir[i]; + } + + _ir[_M / 2] += 1; +} diff --git a/src/lib/audio_filter.h b/src/lib/audio_filter.h new file mode 100644 index 000000000..9fc69daad --- /dev/null +++ b/src/lib/audio_filter.h @@ -0,0 +1,82 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <vector> +#include <boost/shared_ptr.hpp> + +class AudioBuffers; +class audio_filter_impulse_kernel_test; +struct audio_filter_impulse_input_test; + +class AudioFilter +{ +public: + AudioFilter (float transition_bandwidth) + { + _M = 4 / transition_bandwidth; + if (_M % 2) { + ++_M; + } + } + + boost::shared_ptr<AudioBuffers> run (boost::shared_ptr<AudioBuffers> in); + + void flush (); + +protected: + friend struct audio_filter_impulse_kernel_test; + friend struct audio_filter_impulse_input_test; + + std::vector<float> sinc_blackman (float cutoff, bool invert) const; + + std::vector<float> _ir; + int _M; + boost::shared_ptr<AudioBuffers> _tail; +}; + +class LowPassAudioFilter : public AudioFilter +{ +public: + /** Construct a windowed-sinc low-pass filter using the Blackman window. + * @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate. + * @param cutoff Cutoff frequency as a fraction of the sampling rate. + */ + LowPassAudioFilter (float transition_bandwidth, float cutoff); +}; + +class HighPassAudioFilter : public AudioFilter +{ +public: + /** Construct a windowed-sinc high-pass filter using the Blackman window. + * @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate. + * @param cutoff Cutoff frequency as a fraction of the sampling rate. + */ + HighPassAudioFilter (float transition_bandwidth, float cutoff); +}; + +class BandPassAudioFilter : public AudioFilter +{ +public: + /** Construct a windowed-sinc band-pass filter using the Blackman window. + * @param transition_bandwidth Transition bandwidth as a fraction of the sampling rate. + * @param lower Lower cutoff frequency as a fraction of the sampling rate. + * @param higher Higher cutoff frequency as a fraction of the sampling rate. + */ + BandPassAudioFilter (float transition_bandwidth, float lower, float higher); +}; diff --git a/src/lib/audio_mapping.cc b/src/lib/audio_mapping.cc index e35c1ae94..e86e2e2ac 100644 --- a/src/lib/audio_mapping.cc +++ b/src/lib/audio_mapping.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,7 @@ #include <libxml++/libxml++.h> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "audio_mapping.h" #include "util.h" #include "md5_digester.h" @@ -32,7 +32,7 @@ using std::string; using std::min; using boost::shared_ptr; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; AudioMapping::AudioMapping () : _content_channels (0) @@ -40,12 +40,12 @@ AudioMapping::AudioMapping () } -/** Create a default AudioMapping for a given channel count. - * @param c Number of channels. +/** Create an empty AudioMapping for a given channel count. + * @param channels Number of channels. */ -AudioMapping::AudioMapping (int c) +AudioMapping::AudioMapping (int channels) { - setup (c); + setup (channels); } void @@ -70,16 +70,16 @@ AudioMapping::make_default () if (_content_channels == 1) { /* Mono -> Centre */ - set (0, libdcp::CENTRE, 1); + set (0, dcp::CENTRE, 1); } else { /* 1:1 mapping */ for (int i = 0; i < min (_content_channels, MAX_DCP_AUDIO_CHANNELS); ++i) { - set (i, static_cast<libdcp::Channel> (i), 1); + set (i, static_cast<dcp::Channel> (i), 1); } } } -AudioMapping::AudioMapping (shared_ptr<const cxml::Node> node, int state_version) +AudioMapping::AudioMapping (cxml::ConstNodePtr node, int state_version) { setup (node->number_child<int> ("ContentChannels")); @@ -87,14 +87,14 @@ AudioMapping::AudioMapping (shared_ptr<const cxml::Node> node, int state_version /* Old-style: on/off mapping */ list<cxml::NodePtr> const c = node->node_children ("Map"); for (list<cxml::NodePtr>::const_iterator i = c.begin(); i != c.end(); ++i) { - set ((*i)->number_child<int> ("ContentIndex"), static_cast<libdcp::Channel> ((*i)->number_child<int> ("DCP")), 1); + set ((*i)->number_child<int> ("ContentIndex"), static_cast<dcp::Channel> ((*i)->number_child<int> ("DCP")), 1); } } else { list<cxml::NodePtr> const c = node->node_children ("Gain"); for (list<cxml::NodePtr>::const_iterator i = c.begin(); i != c.end(); ++i) { set ( (*i)->number_attribute<int> ("Content"), - static_cast<libdcp::Channel> ((*i)->number_attribute<int> ("DCP")), + static_cast<dcp::Channel> ((*i)->number_attribute<int> ("DCP")), raw_convert<float> ((*i)->content ()) ); } @@ -102,13 +102,13 @@ AudioMapping::AudioMapping (shared_ptr<const cxml::Node> node, int state_version } void -AudioMapping::set (int c, libdcp::Channel d, float g) +AudioMapping::set (int c, dcp::Channel d, float g) { _gain[c][d] = g; } float -AudioMapping::get (int c, libdcp::Channel d) const +AudioMapping::get (int c, dcp::Channel d) const { return _gain[c][d]; } @@ -123,7 +123,7 @@ AudioMapping::as_xml (xmlpp::Node* node) const xmlpp::Element* t = node->add_child ("Gain"); t->set_attribute ("Content", raw_convert<string> (c)); t->set_attribute ("DCP", raw_convert<string> (d)); - t->add_child_text (raw_convert<string> (get (c, static_cast<libdcp::Channel> (d)))); + t->add_child_text (raw_convert<string> (get (c, static_cast<dcp::Channel> (d)))); } } } diff --git a/src/lib/audio_mapping.h b/src/lib/audio_mapping.h index b0b75ac06..a76d83a37 100644 --- a/src/lib/audio_mapping.h +++ b/src/lib/audio_mapping.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,12 +17,17 @@ */ +/** @file src/lib/audio_mapping.h + * @brief AudioMapping class. + */ + #ifndef DCPOMATIC_AUDIO_MAPPING_H #define DCPOMATIC_AUDIO_MAPPING_H #include <vector> -#include <libdcp/types.h> #include <boost/shared_ptr.hpp> +#include <dcp/types.h> +#include <libcxml/cxml.h> namespace xmlpp { class Node; @@ -32,7 +37,9 @@ namespace cxml { class Node; } -/** A many-to-many mapping from some content channels to DCP channels. +/** @class AudioMapping. + * @brief A many-to-many mapping from some content channels to DCP channels. + * * The number of content channels is set on construction and fixed, * and then each of those content channels are mapped to each DCP channel * by a linear gain. @@ -41,8 +48,8 @@ class AudioMapping { public: AudioMapping (); - AudioMapping (int); - AudioMapping (boost::shared_ptr<const cxml::Node>, int); + AudioMapping (int channels); + AudioMapping (cxml::ConstNodePtr, int); /* Default copy constructor is fine */ @@ -50,8 +57,8 @@ public: void make_default (); - void set (int, libdcp::Channel, float); - float get (int, libdcp::Channel) const; + void set (int, dcp::Channel, float); + float get (int, dcp::Channel) const; int content_channels () const { return _content_channels; diff --git a/src/lib/audio_merger.h b/src/lib/audio_merger.h deleted file mode 100644 index 226601e0e..000000000 --- a/src/lib/audio_merger.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - 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 "audio_buffers.h" -#include "util.h" - -template <class T, class F> -class AudioMerger -{ -public: - AudioMerger (int channels, boost::function<F (T)> t_to_f, boost::function<T (F)> f_to_t) - : _buffers (new AudioBuffers (channels, 0)) - , _last_pull (0) - , _t_to_f (t_to_f) - , _f_to_t (f_to_t) - {} - - /** Pull audio up to a given time; after this call, no more data can be pushed - * before the specified time. - */ - TimedAudioBuffers<T> - pull (T time) - { - TimedAudioBuffers<T> out; - - F const to_return = _t_to_f (time - _last_pull); - out.audio.reset (new AudioBuffers (_buffers->channels(), to_return)); - /* And this is how many we will get from our buffer */ - F const to_return_from_buffers = min (to_return, _buffers->frames ()); - - /* Copy the data that we have to the back end of the return buffer */ - out.audio->copy_from (_buffers.get(), to_return_from_buffers, 0, to_return - to_return_from_buffers); - /* Silence any gap at the start */ - out.audio->make_silent (0, to_return - to_return_from_buffers); - - out.time = _last_pull; - _last_pull = time; - - /* And remove the data we're returning from our buffers */ - if (_buffers->frames() > to_return_from_buffers) { - _buffers->move (to_return_from_buffers, 0, _buffers->frames() - to_return_from_buffers); - } - _buffers->set_frames (_buffers->frames() - to_return_from_buffers); - - return out; - } - - void - push (boost::shared_ptr<const AudioBuffers> audio, T time) - { - assert (time >= _last_pull); - - F frame = _t_to_f (time); - F after = max (_buffers->frames(), frame + audio->frames() - _t_to_f (_last_pull)); - _buffers->ensure_size (after); - _buffers->accumulate_frames (audio.get(), 0, frame - _t_to_f (_last_pull), audio->frames ()); - _buffers->set_frames (after); - } - - F min (F a, int b) - { - if (a < b) { - return a; - } - - return b; - } - - F max (int a, F b) - { - if (a > b) { - return a; - } - - return b; - } - - TimedAudioBuffers<T> - flush () - { - if (_buffers->frames() == 0) { - return TimedAudioBuffers<T> (); - } - - return TimedAudioBuffers<T> (_buffers, _last_pull); - } - -private: - boost::shared_ptr<AudioBuffers> _buffers; - T _last_pull; - boost::function<F (T)> _t_to_f; - boost::function<T (F)> _f_to_t; -}; diff --git a/src/lib/audio_processor.cc b/src/lib/audio_processor.cc new file mode 100644 index 000000000..f350cc2aa --- /dev/null +++ b/src/lib/audio_processor.cc @@ -0,0 +1,52 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "audio_processor.h" +#include "mid_side_decoder.h" +#include "upmixer_a.h" + +using std::string; +using std::list; + +list<AudioProcessor const *> AudioProcessor::_all; + +void +AudioProcessor::setup_audio_processors () +{ + _all.push_back (new MidSideDecoder ()); + _all.push_back (new UpmixerA (48000)); +} + +AudioProcessor const * +AudioProcessor::from_id (string id) +{ + for (list<AudioProcessor const *>::const_iterator i = _all.begin(); i != _all.end(); ++i) { + if ((*i)->id() == id) { + return *i; + } + } + + return 0; +} + +list<AudioProcessor const *> +AudioProcessor::all () +{ + return _all; +} diff --git a/src/lib/audio_processor.h b/src/lib/audio_processor.h new file mode 100644 index 000000000..9b332e7fe --- /dev/null +++ b/src/lib/audio_processor.h @@ -0,0 +1,51 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_AUDIO_PROCESSOR_H +#define DCPOMATIC_AUDIO_PROCESSOR_H + +#include <list> +#include <string> +#include <boost/shared_ptr.hpp> +#include "channel_count.h" + +class AudioBuffers; + +class AudioProcessor +{ +public: + virtual ~AudioProcessor () {} + + virtual std::string name () const = 0; + virtual std::string id () const = 0; + virtual ChannelCount in_channels () const = 0; + virtual int out_channels (int) const = 0; + virtual boost::shared_ptr<AudioProcessor> clone (int sampling_rate) const = 0; + virtual boost::shared_ptr<AudioBuffers> run (boost::shared_ptr<const AudioBuffers>) = 0; + virtual void flush () {} + + static std::list<AudioProcessor const *> all (); + static void setup_audio_processors (); + static AudioProcessor const * from_id (std::string); + +private: + static std::list<AudioProcessor const *> _all; +}; + +#endif diff --git a/src/lib/channel_count.h b/src/lib/channel_count.h new file mode 100644 index 000000000..4247fc063 --- /dev/null +++ b/src/lib/channel_count.h @@ -0,0 +1,49 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_CHANNEL_COUNT_H +#define DCPOMATIC_CHANNEL_COUNT_H + +class ChannelCount +{ +public: + ChannelCount () + : min (0) + , max (0) + {} + + ChannelCount (int n) + : min (n) + , max (n) + {} + + ChannelCount (int min_, int max_) + : min (min_) + , max (max_) + {} + + bool includes (int c) { + return min <= c && c <= max; + } + + int min; + int max; +}; + +#endif diff --git a/src/lib/cinema.cc b/src/lib/cinema.cc index fca6b6afd..620236186 100644 --- a/src/lib/cinema.cc +++ b/src/lib/cinema.cc @@ -24,7 +24,7 @@ using std::list; using boost::shared_ptr; -Cinema::Cinema (shared_ptr<const cxml::Node> node) +Cinema::Cinema (cxml::ConstNodePtr node) : name (node->string_child ("Name")) , email (node->string_child ("Email")) { @@ -35,7 +35,7 @@ Cinema::Cinema (shared_ptr<const cxml::Node> node) a constructor) */ void -Cinema::read_screens (shared_ptr<const cxml::Node> node) +Cinema::read_screens (cxml::ConstNodePtr node) { list<cxml::NodePtr> s = node->node_children ("Screen"); for (list<cxml::NodePtr>::iterator i = s.begin(); i != s.end(); ++i) { @@ -67,17 +67,21 @@ Cinema::remove_screen (shared_ptr<Screen> s) _screens.remove (s); } -Screen::Screen (shared_ptr<const cxml::Node> node) +Screen::Screen (cxml::ConstNodePtr node) { name = node->string_child ("Name"); - certificate = shared_ptr<libdcp::Certificate> (new libdcp::Certificate (node->string_child ("Certificate"))); + if (node->optional_string_child ("Certificate")) { + certificate = dcp::Certificate (node->string_child ("Certificate")); + } } void Screen::as_xml (xmlpp::Element* parent) const { parent->add_child("Name")->add_child_text (name); - parent->add_child("Certificate")->add_child_text (certificate->certificate (true)); + if (certificate) { + parent->add_child("Certificate")->add_child_text (certificate->certificate (true)); + } } diff --git a/src/lib/cinema.h b/src/lib/cinema.h index 40dc15ae0..8421f4687 100644 --- a/src/lib/cinema.h +++ b/src/lib/cinema.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,32 +17,45 @@ */ +/** @file src/lib/cinema.h + * @brief Screen and Cinema classes. + */ + #include <boost/enable_shared_from_this.hpp> -#include <libdcp/certificates.h> +#include <dcp/certificates.h> +#include <libcxml/cxml.h> class Cinema; -namespace cxml { - class Node; -} - +/** @class Screen + * @brief A representation of a Screen for KDM generation. + * + * This is the name of the screen and the certificate of its + * server. + */ class Screen { public: - Screen (std::string const & n, boost::shared_ptr<libdcp::Certificate> cert) + Screen (std::string const & n, boost::optional<dcp::Certificate> cert) : name (n) , certificate (cert) {} - Screen (boost::shared_ptr<const cxml::Node>); + Screen (cxml::ConstNodePtr); void as_xml (xmlpp::Element *) const; boost::shared_ptr<Cinema> cinema; std::string name; - boost::shared_ptr<libdcp::Certificate> certificate; + boost::optional<dcp::Certificate> certificate; }; +/** @class Cinema + * @brief A description of a Cinema for KDM generation. + * + * This is a cinema name, contact email address and a list of + * Screen objects. + */ class Cinema : public boost::enable_shared_from_this<Cinema> { public: @@ -51,9 +64,9 @@ public: , email (e) {} - Cinema (boost::shared_ptr<const cxml::Node>); + Cinema (cxml::ConstNodePtr); - void read_screens (boost::shared_ptr<const cxml::Node>); + void read_screens (cxml::ConstNodePtr); void as_xml (xmlpp::Element *) const; diff --git a/src/lib/sound_processor.cc b/src/lib/cinema_sound_processor.cc index 9be6621cc..6a7905114 100644 --- a/src/lib/sound_processor.cc +++ b/src/lib/cinema_sound_processor.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,22 +18,22 @@ */ /** @file src/sound_processor.cc - * @brief A class to describe a sound processor. + * @brief CinemaSoundProcessor class. */ #include <iostream> #include <cassert> -#include "sound_processor.h" +#include "cinema_sound_processor.h" #include "dolby_cp750.h" using namespace std; -vector<SoundProcessor const *> SoundProcessor::_sound_processors; +vector<CinemaSoundProcessor const *> CinemaSoundProcessor::_cinema_sound_processors; /** @param i Our id. * @param n User-visible name. */ -SoundProcessor::SoundProcessor (string i, string n) +CinemaSoundProcessor::CinemaSoundProcessor (string i, string n) : _id (i) , _name (n) { @@ -41,33 +41,33 @@ SoundProcessor::SoundProcessor (string i, string n) } /** @return All available sound processors */ -vector<SoundProcessor const *> -SoundProcessor::all () +vector<CinemaSoundProcessor const *> +CinemaSoundProcessor::all () { - return _sound_processors; + return _cinema_sound_processors; } /** Set up the static _sound_processors vector; must be called before from_* * methods are used. */ void -SoundProcessor::setup_sound_processors () +CinemaSoundProcessor::setup_cinema_sound_processors () { - _sound_processors.push_back (new DolbyCP750); + _cinema_sound_processors.push_back (new DolbyCP750); } /** @param id One of our ids. * @return Corresponding sound processor, or 0. */ -SoundProcessor const * -SoundProcessor::from_id (string id) +CinemaSoundProcessor const * +CinemaSoundProcessor::from_id (string id) { - vector<SoundProcessor const *>::iterator i = _sound_processors.begin (); - while (i != _sound_processors.end() && (*i)->id() != id) { + vector<CinemaSoundProcessor const *>::iterator i = _cinema_sound_processors.begin (); + while (i != _cinema_sound_processors.end() && (*i)->id() != id) { ++i; } - if (i == _sound_processors.end ()) { + if (i == _cinema_sound_processors.end ()) { return 0; } @@ -78,14 +78,14 @@ SoundProcessor::from_id (string id) * @return Index of the sound processor with the list, or -1. */ int -SoundProcessor::as_index (SoundProcessor const * s) +CinemaSoundProcessor::as_index (CinemaSoundProcessor const * s) { - vector<SoundProcessor*>::size_type i = 0; - while (i < _sound_processors.size() && _sound_processors[i] != s) { + vector<CinemaSoundProcessor*>::size_type i = 0; + while (i < _cinema_sound_processors.size() && _cinema_sound_processors[i] != s) { ++i; } - if (i == _sound_processors.size ()) { + if (i == _cinema_sound_processors.size ()) { return -1; } @@ -95,9 +95,9 @@ SoundProcessor::as_index (SoundProcessor const * s) /** @param i An index returned from as_index(). * @return Corresponding sound processor. */ -SoundProcessor const * -SoundProcessor::from_index (int i) +CinemaSoundProcessor const * +CinemaSoundProcessor::from_index (int i) { - assert (i <= int(_sound_processors.size ())); - return _sound_processors[i]; + assert (i <= int(_cinema_sound_processors.size ())); + return _cinema_sound_processors[i]; } diff --git a/src/lib/sound_processor.h b/src/lib/cinema_sound_processor.h index 8f2652243..f735b1227 100644 --- a/src/lib/sound_processor.h +++ b/src/lib/cinema_sound_processor.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,24 +17,27 @@ */ -/** @file src/sound_processor.h - * @brief A class to describe a sound processor. +/** @file src/cinema_sound_processor.h + * @brief CinemaSoundProcessor class */ -#ifndef DCPOMATIC_SOUND_PROCESSOR_H -#define DCPOMATIC_SOUND_PROCESSOR_H +#ifndef DCPOMATIC_CINEMA_SOUND_PROCESSOR_H +#define DCPOMATIC_CINEMA_SOUND_PROCESSOR_H #include <string> #include <vector> #include <boost/utility.hpp> -/** @class SoundProcessor - * @brief Class to describe a sound processor. +/** @class CinemaSoundProcessor + * @brief Class to describe a cimema's sound processor. + * + * In other words, the box in the rack that handles sound decoding and processing + * in a cinema. */ -class SoundProcessor : public boost::noncopyable +class CinemaSoundProcessor : public boost::noncopyable { public: - SoundProcessor (std::string i, std::string n); + CinemaSoundProcessor (std::string i, std::string n); virtual float db_for_fader_change (float from, float to) const = 0; @@ -48,11 +51,11 @@ public: return _name; } - static std::vector<SoundProcessor const *> all (); - static void setup_sound_processors (); - static SoundProcessor const * from_id (std::string id); - static SoundProcessor const * from_index (int); - static int as_index (SoundProcessor const *); + static std::vector<CinemaSoundProcessor const *> all (); + static void setup_cinema_sound_processors (); + static CinemaSoundProcessor const * from_id (std::string id); + static CinemaSoundProcessor const * from_index (int); + static int as_index (CinemaSoundProcessor const *); private: /** id for our use */ @@ -60,8 +63,8 @@ private: /** user-visible name for this sound processor */ std::string _name; - /** sll available sound processors */ - static std::vector<SoundProcessor const *> _sound_processors; + /** sll available cinema sound processors */ + static std::vector<CinemaSoundProcessor const *> _cinema_sound_processors; }; #endif diff --git a/src/lib/colour_conversion.cc b/src/lib/colour_conversion.cc index e5b1104ff..c836cc271 100644 --- a/src/lib/colour_conversion.cc +++ b/src/lib/colour_conversion.cc @@ -18,8 +18,8 @@ */ #include <libxml++/libxml++.h> -#include <libdcp/colour_matrix.h> -#include <libdcp/raw_convert.h> +#include <dcp/colour_matrix.h> +#include <dcp/raw_convert.h> #include <libcxml/cxml.h> #include "config.h" #include "colour_conversion.h" @@ -34,7 +34,7 @@ using std::cout; using std::vector; using boost::shared_ptr; using boost::optional; -using libdcp::raw_convert; +using dcp::raw_convert; ColourConversion::ColourConversion () : input_gamma (2.4) @@ -44,7 +44,7 @@ ColourConversion::ColourConversion () { for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { - matrix (i, j) = libdcp::colour_matrix::srgb_to_xyz[i][j]; + matrix (i, j) = dcp::colour_matrix::srgb_to_xyz[i][j]; } } } diff --git a/src/lib/config.cc b/src/lib/config.cc index 878fedaa4..2b7b81cfe 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -22,8 +22,10 @@ #include <glib.h> #include <boost/filesystem.hpp> #include <boost/algorithm/string.hpp> -#include <libdcp/colour_matrix.h> -#include <libdcp/raw_convert.h> +#include <dcp/colour_matrix.h> +#include <dcp/raw_convert.h> +#include <dcp/signer.h> +#include <dcp/certificate_chain.h> #include <libcxml/cxml.h> #include "config.h" #include "server.h" @@ -31,10 +33,11 @@ #include "filter.h" #include "ratio.h" #include "dcp_content_type.h" -#include "sound_processor.h" +#include "cinema_sound_processor.h" #include "colour_conversion.h" #include "cinema.h" #include "util.h" +#include "cross.h" #include "i18n.h" @@ -51,7 +54,7 @@ using boost::shared_ptr; using boost::optional; using boost::algorithm::is_any_of; using boost::algorithm::split; -using libdcp::raw_convert; +using dcp::raw_convert; Config* Config::_instance = 0; @@ -61,7 +64,7 @@ Config::Config () , _server_port_base (6192) , _use_any_servers (true) , _tms_path (".") - , _sound_processor (SoundProcessor::from_id (N_("dolby_cp750"))) + , _cinema_sound_processor (CinemaSoundProcessor::from_id (N_("dolby_cp750"))) , _allow_any_dcp_frame_rate (false) , _default_still_length (10) , _default_scale (VideoContentScale (Ratio::from_id ("185"))) @@ -73,6 +76,9 @@ Config::Config () , _check_for_test_updates (false) , _maximum_j2k_bandwidth (250000000) , _log_types (Log::TYPE_GENERAL | Log::TYPE_WARNING | Log::TYPE_ERROR) +#ifdef DCPOMATIC_WINDOWS + , _win32_console (false) +#endif { _allowed_dcp_frame_rates.push_back (24); _allowed_dcp_frame_rates.push_back (25); @@ -81,9 +87,9 @@ Config::Config () _allowed_dcp_frame_rates.push_back (50); _allowed_dcp_frame_rates.push_back (60); - _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6)); - _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, libdcp::colour_matrix::srgb_to_xyz, 2.6)); - _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6)); + _colour_conversions.push_back (PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6)); + _colour_conversions.push_back (PresetColourConversion (_("sRGB non-linearised"), 2.4, false, dcp::colour_matrix::srgb_to_xyz, 2.6)); + _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6)); reset_kdm_email (); } @@ -91,13 +97,16 @@ Config::Config () void Config::read () { - if (!boost::filesystem::exists (file (false))) { - read_old_metadata (); + if (!boost::filesystem::exists (file ())) { + /* Make a new set of signing certificates and key */ + _signer.reset (new dcp::Signer (openssl_path ())); + /* And decryption keys */ + make_decryption_keys (); return; } cxml::Document f ("Config"); - f.read_file (file (false)); + f.read_file (file ()); optional<string> c; optional<int> version = f.optional_number_child<int> ("Version"); @@ -130,7 +139,11 @@ Config::read () c = f.optional_string_child ("SoundProcessor"); if (c) { - _sound_processor = SoundProcessor::from_id (c.get ()); + _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ()); + } + c = f.optional_string_child ("CinemaSoundProcessor"); + if (c) { + _cinema_sound_processor = CinemaSoundProcessor::from_id (c.get ()); } _language = f.optional_string_child ("Language"); @@ -180,7 +193,7 @@ Config::read () /* Loading version 0 (before Rec. 709 was added as a preset). Add it in. */ - _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, libdcp::colour_matrix::rec709_to_xyz, 2.6)); + _colour_conversions.push_back (PresetColourConversion (_("Rec. 709"), 2.2, false, dcp::colour_matrix::rec709_to_xyz, 2.6)); } list<cxml::NodePtr> cin = f.node_children ("Cinema"); @@ -209,99 +222,64 @@ Config::read () _allow_any_dcp_frame_rate = f.optional_bool_child ("AllowAnyDCPFrameRate"); _log_types = f.optional_number_child<int> ("LogTypes").get_value_or (Log::TYPE_GENERAL | Log::TYPE_WARNING | Log::TYPE_ERROR); +#ifdef DCPOMATIC_WINDOWS + _win32_console = f.optional_bool_child ("Win32Console").get_value_or (false); +#endif list<cxml::NodePtr> his = f.node_children ("History"); for (list<cxml::NodePtr>::const_iterator i = his.begin(); i != his.end(); ++i) { _history.push_back ((*i)->content ()); } -} - -void -Config::read_old_metadata () -{ - /* XXX: this won't work with non-Latin filenames */ - ifstream f (file(true).string().c_str ()); - string line; - while (getline (f, line)) { - if (line.empty ()) { - continue; + cxml::NodePtr signer = f.optional_node_child ("Signer"); + dcp::CertificateChain signer_chain; + if (signer) { + /* Read the signing certificates and private key in from the config file */ + list<cxml::NodePtr> certificates = signer->node_children ("Certificate"); + for (list<cxml::NodePtr>::const_iterator i = certificates.begin(); i != certificates.end(); ++i) { + signer_chain.add (dcp::Certificate ((*i)->content ())); } - if (line[0] == '#') { - continue; - } + _signer.reset (new dcp::Signer (signer_chain, signer->string_child ("PrivateKey"))); + } else { + /* Make a new set of signing certificates and key */ + _signer.reset (new dcp::Signer (openssl_path ())); + } - size_t const s = line.find (' '); - if (s == string::npos) { - continue; - } - - string const k = line.substr (0, s); - string const v = line.substr (s + 1); - - if (k == N_("num_local_encoding_threads")) { - _num_local_encoding_threads = atoi (v.c_str ()); - } else if (k == N_("default_directory")) { - _default_directory = v; - } else if (k == N_("server_port")) { - _server_port_base = atoi (v.c_str ()); - } else if (k == N_("server")) { - vector<string> b; - split (b, v, is_any_of (" ")); - if (b.size() == 2) { - _servers.push_back (b[0]); - } - } else if (k == N_("tms_ip")) { - _tms_ip = v; - } else if (k == N_("tms_path")) { - _tms_path = v; - } else if (k == N_("tms_user")) { - _tms_user = v; - } else if (k == N_("tms_password")) { - _tms_password = v; - } else if (k == N_("sound_processor")) { - _sound_processor = SoundProcessor::from_id (v); - } else if (k == "language") { - _language = v; - } else if (k == "default_container") { - _default_container = Ratio::from_id (v); - } else if (k == "default_dcp_content_type") { - _default_dcp_content_type = DCPContentType::from_isdcf_name (v); - } else if (k == "dcp_metadata_issuer") { - _dcp_issuer = v; - } + if (f.optional_string_child ("DecryptionCertificate")) { + _decryption_certificate = dcp::Certificate (f.string_child ("DecryptionCertificate")); + } - _default_isdcf_metadata.read_old_metadata (k, v); + if (f.optional_string_child ("DecryptionPrivateKey")) { + _decryption_private_key = f.string_child ("DecryptionPrivateKey"); + } + + if (!f.optional_string_child ("DecryptionCertificate") || !f.optional_string_child ("DecryptionPrivateKey")) { + /* Generate our own decryption certificate and key if either is not present in config */ + make_decryption_keys (); } } -/** @return Filename to write configuration to */ -boost::filesystem::path -Config::file (bool old) const +void +Config::make_decryption_keys () { - boost::filesystem::path p; - p /= g_get_user_config_dir (); - boost::system::error_code ec; - boost::filesystem::create_directory (p, ec); - if (old) { - p /= ".dvdomatic"; - } else { - p /= "dcpomatic"; - boost::filesystem::create_directory (p, ec); - p /= "config.xml"; - } - return p; + boost::filesystem::path p = dcp::make_certificate_chain (openssl_path ()); + _decryption_certificate = dcp::Certificate (dcp::file_to_string (p / "leaf.signed.pem")); + _decryption_private_key = dcp::file_to_string (p / "leaf.key"); + boost::filesystem::remove_all (p); } +/** @return Filename to write configuration to */ boost::filesystem::path -Config::signer_chain_directory () const +Config::file () const { boost::filesystem::path p; p /= g_get_user_config_dir (); - p /= "dcpomatic"; - p /= "crypt"; - boost::filesystem::create_directories (p); + boost::system::error_code ec; + boost::filesystem::create_directory (p, ec); + p /= "dcpomatic2"; + boost::filesystem::create_directory (p, ec); + p /= "config.xml"; return p; } @@ -347,8 +325,8 @@ Config::write () const root->add_child("TMSPath")->add_child_text (_tms_path); root->add_child("TMSUser")->add_child_text (_tms_user); root->add_child("TMSPassword")->add_child_text (_tms_password); - if (_sound_processor) { - root->add_child("SoundProcessor")->add_child_text (_sound_processor->id ()); + if (_cinema_sound_processor) { + root->add_child("CinemaSoundProcessor")->add_child_text (_cinema_sound_processor->id ()); } if (_language) { root->add_child("Language")->add_child_text (_language.get()); @@ -391,12 +369,25 @@ Config::write () const root->add_child("MaximumJ2KBandwidth")->add_child_text (raw_convert<string> (_maximum_j2k_bandwidth)); root->add_child("AllowAnyDCPFrameRate")->add_child_text (_allow_any_dcp_frame_rate ? "1" : "0"); root->add_child("LogTypes")->add_child_text (raw_convert<string> (_log_types)); +#ifdef DCPOMATIC_WINDOWS + root->add_child("Win32Console")->add_child_text (_win32_console ? "1" : "0"); +#endif + + xmlpp::Element* signer = root->add_child ("Signer"); + dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf (); + for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) { + signer->add_child("Certificate")->add_child_text (i->certificate (true)); + } + signer->add_child("PrivateKey")->add_child_text (_signer->key ()); + + root->add_child("DecryptionCertificate")->add_child_text (_decryption_certificate.certificate (true)); + root->add_child("DecryptionPrivateKey")->add_child_text (_decryption_private_key); for (vector<boost::filesystem::path>::const_iterator i = _history.begin(); i != _history.end(); ++i) { root->add_child("History")->add_child_text (i->string ()); } - doc.write_to_file_formatted (file(false).string ()); + doc.write_to_file_formatted (file().string ()); } boost::filesystem::path diff --git a/src/lib/config.h b/src/lib/config.h index 0639382a0..55a172d78 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -28,15 +28,17 @@ #include <boost/shared_ptr.hpp> #include <boost/signals2.hpp> #include <boost/filesystem.hpp> +#include <dcp/metadata.h> +#include <dcp/certificates.h> +#include <dcp/signer.h> #include "isdcf_metadata.h" #include "colour_conversion.h" -#include "server.h" #include "video_content.h" class ServerDescription; class Scaler; class Filter; -class SoundProcessor; +class CinemaSoundProcessor; class DCPContentType; class Ratio; class Cinema; @@ -104,9 +106,9 @@ public: return _tms_password; } - /** @return The sound processor that we are using */ - SoundProcessor const * sound_processor () const { - return _sound_processor; + /** @return The cinema sound processor that we are using */ + CinemaSoundProcessor const * cinema_sound_processor () const { + return _cinema_sound_processor; } std::list<boost::shared_ptr<Cinema> > cinemas () const { @@ -193,6 +195,18 @@ public: return _kdm_email; } + boost::shared_ptr<const dcp::Signer> signer () const { + return _signer; + } + + dcp::Certificate decryption_certificate () const { + return _decryption_certificate; + } + + std::string decryption_private_key () const { + return _decryption_private_key; + } + bool check_for_updates () const { return _check_for_updates; } @@ -209,6 +223,12 @@ public: return _log_types; } +#ifdef DCPOMATIC_WINDOWS + bool win32_console () const { + return _win32_console; + } +#endif + std::vector<boost::filesystem::path> history () const { return _history; } @@ -371,6 +391,21 @@ public: void reset_kdm_email (); + void set_signer (boost::shared_ptr<const dcp::Signer> s) { + _signer = s; + changed (); + } + + void set_decryption_certificate (dcp::Certificate c) { + _decryption_certificate = c; + changed (); + } + + void set_decryption_private_key (std::string k) { + _decryption_private_key = k; + changed (); + } + void set_check_for_updates (bool c) { _check_for_updates = c; changed (); @@ -391,6 +426,13 @@ public: changed (); } +#ifdef DCPOMATIC_WINDOWS + void set_win32_console (bool c) { + _win32_console = c; + changed (); + } +#endif + void clear_history () { _history.clear (); changed (); @@ -398,8 +440,6 @@ public: void add_to_history (boost::filesystem::path p); - boost::filesystem::path signer_chain_directory () const; - void changed (); boost::signals2::signal<void ()> Changed; @@ -408,10 +448,10 @@ public: private: Config (); - boost::filesystem::path file (bool) const; + boost::filesystem::path file () const; void read (); - void read_old_metadata (); void write () const; + void make_decryption_keys (); /** number of threads to use for J2K encoding on the local machine */ int _num_local_encoding_threads; @@ -433,8 +473,8 @@ private: std::string _tms_user; /** Password to log into the TMS with */ std::string _tms_password; - /** Our sound processor */ - SoundProcessor const * _sound_processor; + /** Our cinema sound processor */ + CinemaSoundProcessor const * _cinema_sound_processor; std::list<int> _allowed_dcp_frame_rates; /** Allow any video frame rate for the DCP; if true, overrides _allowed_dcp_frame_rates */ bool _allow_any_dcp_frame_rate; @@ -458,12 +498,18 @@ private: std::string _kdm_cc; std::string _kdm_bcc; std::string _kdm_email; + boost::shared_ptr<const dcp::Signer> _signer; + dcp::Certificate _decryption_certificate; + std::string _decryption_private_key; /** true to check for updates on startup */ bool _check_for_updates; bool _check_for_test_updates; /** maximum allowed J2K bandwidth in bits per second */ int _maximum_j2k_bandwidth; int _log_types; +#ifdef DCPOMATIC_WINDOWS + bool _win32_console; +#endif std::vector<boost::filesystem::path> _history; /** Singleton instance, or 0 */ diff --git a/src/lib/content.cc b/src/lib/content.cc index 11a4b21cc..21e49a2c9 100644 --- a/src/lib/content.cc +++ b/src/lib/content.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,10 +17,14 @@ */ +/** @file src/lib/content.cc + * @brief Content class. + */ + #include <boost/thread/mutex.hpp> #include <libxml++/libxml++.h> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "content.h" #include "util.h" #include "content_factory.h" @@ -38,7 +42,7 @@ using std::cout; using std::vector; using std::max; using boost::shared_ptr; -using libdcp::raw_convert; +using dcp::raw_convert; int const ContentProperty::PATH = 400; int const ContentProperty::POSITION = 401; @@ -56,7 +60,7 @@ Content::Content (shared_ptr<const Film> f) } -Content::Content (shared_ptr<const Film> f, Time p) +Content::Content (shared_ptr<const Film> f, DCPTime p) : _film (f) , _position (p) , _trim_start (0) @@ -76,7 +80,7 @@ Content::Content (shared_ptr<const Film> f, boost::filesystem::path p) _paths.push_back (p); } -Content::Content (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) +Content::Content (shared_ptr<const Film> f, cxml::ConstNodePtr node) : _film (f) , _change_signals_frequent (false) { @@ -85,9 +89,9 @@ Content::Content (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) _paths.push_back ((*i)->content ()); } _digest = node->string_child ("Digest"); - _position = node->number_child<Time> ("Position"); - _trim_start = node->number_child<Time> ("TrimStart"); - _trim_end = node->number_child<Time> ("TrimEnd"); + _position = DCPTime (node->number_child<double> ("Position")); + _trim_start = DCPTime (node->number_child<double> ("TrimStart")); + _trim_end = DCPTime (node->number_child<double> ("TrimEnd")); } Content::Content (shared_ptr<const Film> f, vector<shared_ptr<Content> > c) @@ -98,11 +102,11 @@ Content::Content (shared_ptr<const Film> f, vector<shared_ptr<Content> > c) , _change_signals_frequent (false) { for (size_t i = 0; i < c.size(); ++i) { - if (i > 0 && c[i]->trim_start ()) { + if (i > 0 && c[i]->trim_start() > DCPTime()) { throw JoinError (_("Only the first piece of content to be joined can have a start trim.")); } - if (i < (c.size() - 1) && c[i]->trim_end ()) { + if (i < (c.size() - 1) && c[i]->trim_end () > DCPTime()) { throw JoinError (_("Only the last piece of content to be joined can have an end trim.")); } @@ -121,9 +125,9 @@ Content::as_xml (xmlpp::Node* node) const node->add_child("Path")->add_child_text (i->string ()); } node->add_child("Digest")->add_child_text (_digest); - node->add_child("Position")->add_child_text (raw_convert<string> (_position)); - node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start)); - node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end)); + node->add_child("Position")->add_child_text (raw_convert<string> (_position.get ())); + node->add_child("TrimStart")->add_child_text (raw_convert<string> (_trim_start.get ())); + node->add_child("TrimEnd")->add_child_text (raw_convert<string> (_trim_end.get ())); } void @@ -148,7 +152,7 @@ Content::signal_changed (int p) } void -Content::set_position (Time p) +Content::set_position (DCPTime p) { { boost::mutex::scoped_lock lm (_mutex); @@ -163,7 +167,7 @@ Content::set_position (Time p) } void -Content::set_trim_start (Time t) +Content::set_trim_start (DCPTime t) { { boost::mutex::scoped_lock lm (_mutex); @@ -174,7 +178,7 @@ Content::set_trim_start (Time t) } void -Content::set_trim_end (Time t) +Content::set_trim_end (DCPTime t) { { boost::mutex::scoped_lock lm (_mutex); @@ -206,22 +210,13 @@ Content::clone () const string Content::technical_summary () const { - return String::compose ("%1 %2 %3", path_summary(), digest(), position()); + return String::compose ("%1 %2 %3", path_summary(), digest(), position().seconds()); } -Time +DCPTime Content::length_after_trim () const { - return max (int64_t (0), full_length() - trim_start() - trim_end()); -} - -/** @param t A time relative to the start of this content (not the position). - * @return true if this time is trimmed by our trim settings. - */ -bool -Content::trimmed (Time t) const -{ - return (t < trim_start() || t > (full_length() - trim_end ())); + return max (DCPTime (), full_length() - trim_start() - trim_end()); } /** @return string which includes everything about how this content affects @@ -233,9 +228,9 @@ Content::identifier () const SafeStringStream s; s << Content::digest() - << "_" << position() - << "_" << trim_start() - << "_" << trim_end(); + << "_" << position().get() + << "_" << trim_start().get() + << "_" << trim_end().get(); return s.str (); } diff --git a/src/lib/content.h b/src/lib/content.h index 596a0a905..f7e97feac 100644 --- a/src/lib/content.h +++ b/src/lib/content.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/content.h + * @brief Content class. + */ + #ifndef DCPOMATIC_CONTENT_H #define DCPOMATIC_CONTENT_H @@ -26,7 +30,9 @@ #include <boost/thread/mutex.hpp> #include <boost/enable_shared_from_this.hpp> #include <libxml++/libxml++.h> +#include <libcxml/cxml.h> #include "types.h" +#include "dcpomatic_time.h" namespace cxml { class Node; @@ -45,25 +51,38 @@ public: static int const TRIM_END; }; +/** @class Content + * @brief A piece of content represented by one or more files on disk. + */ class Content : public boost::enable_shared_from_this<Content>, public boost::noncopyable { public: Content (boost::shared_ptr<const Film>); - Content (boost::shared_ptr<const Film>, Time); + Content (boost::shared_ptr<const Film>, DCPTime); Content (boost::shared_ptr<const Film>, boost::filesystem::path); - Content (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + Content (boost::shared_ptr<const Film>, cxml::ConstNodePtr); Content (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >); virtual ~Content () {} + + /** Examine the content to establish digest, frame rates and any other + * useful metadata. + * @param job Job to use to report progress, or 0. + */ + virtual void examine (boost::shared_ptr<Job> job); - virtual void examine (boost::shared_ptr<Job>); + /** @return Quick one-line summary of the content, as will be presented in the + * film editor. + */ virtual std::string summary () const = 0; + /** @return Technical details of this content; these are written to logs to * help with debugging. */ virtual std::string technical_summary () const; + virtual std::string information () const = 0; virtual void as_xml (xmlpp::Node *) const; - virtual Time full_length () const = 0; + virtual DCPTime full_length () const = 0; virtual std::string identifier () const; boost::shared_ptr<Content> clone () const; @@ -95,41 +114,43 @@ public: return _digest; } - void set_position (Time); + void set_position (DCPTime); - /** Time that this content starts; i.e. the time that the first + /** DCPTime that this content starts; i.e. the time that the first * bit of the content (trimmed or not) will happen. */ - Time position () const { + DCPTime position () const { boost::mutex::scoped_lock lm (_mutex); return _position; } - void set_trim_start (Time); + void set_trim_start (DCPTime); - Time trim_start () const { + DCPTime trim_start () const { boost::mutex::scoped_lock lm (_mutex); return _trim_start; } - void set_trim_end (Time); + void set_trim_end (DCPTime); - Time trim_end () const { + DCPTime trim_end () const { boost::mutex::scoped_lock lm (_mutex); return _trim_end; } - Time end () const { - return position() + length_after_trim() - 1; + DCPTime end () const { + return position() + length_after_trim(); } - Time length_after_trim () const; + DCPTime length_after_trim () const; void set_change_signals_frequent (bool f) { _change_signals_frequent = f; } - bool trimmed (Time) const; + boost::shared_ptr<const Film> film () const { + return _film.lock (); + } boost::signals2::signal<void (boost::weak_ptr<Content>, int, bool)> Changed; @@ -139,8 +160,8 @@ protected: boost::weak_ptr<const Film> _film; /** _mutex which should be used to protect accesses, as examine - jobs can update content state in threads other than the main one. - */ + * jobs can update content state in threads other than the main one. + */ mutable boost::mutex _mutex; /** Paths of our data files */ @@ -148,9 +169,9 @@ protected: private: std::string _digest; - Time _position; - Time _trim_start; - Time _trim_end; + DCPTime _position; + DCPTime _trim_start; + DCPTime _trim_end; bool _change_signals_frequent; }; diff --git a/src/lib/content_audio.h b/src/lib/content_audio.h new file mode 100644 index 000000000..535c0b491 --- /dev/null +++ b/src/lib/content_audio.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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. + +*/ + +/** @file src/lib/content_audio.h + * @brief ContentAudio class. + */ + +#include "audio_buffers.h" + +/** @class ContentAudio + * @brief A block of audio from a piece of content, with a timestamp as a frame within that content. + */ +class ContentAudio +{ +public: + ContentAudio () + : audio (new AudioBuffers (0, 0)) + , frame (0) + {} + + ContentAudio (boost::shared_ptr<AudioBuffers> a, AudioFrame f) + : audio (a) + , frame (f) + {} + + boost::shared_ptr<AudioBuffers> audio; + AudioFrame frame; +}; diff --git a/src/lib/content_factory.cc b/src/lib/content_factory.cc index 98b1dd859..16340adb4 100644 --- a/src/lib/content_factory.cc +++ b/src/lib/content_factory.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,16 +17,30 @@ */ +/** @file src/lib/content_factory.cc + * @brief Methods to create content objects. + */ + #include <libcxml/cxml.h> #include "ffmpeg_content.h" #include "image_content.h" #include "sndfile_content.h" +#include "subrip_content.h" +#include "dcp_content.h" +#include "dcp_subtitle_content.h" #include "util.h" using std::string; using std::list; using boost::shared_ptr; +/** Create a Content object from an XML node. + * @param film Film that the content will be in. + * @param node XML description. + * @param version XML state version. + * @param notes A list to which is added descriptions of any non-critial warnings / messages. + * @return Content object, or 0 if no content was recognised in the XML. + */ shared_ptr<Content> content_factory (shared_ptr<const Film> film, cxml::NodePtr node, int version, list<string>& notes) { @@ -40,20 +54,38 @@ content_factory (shared_ptr<const Film> film, cxml::NodePtr node, int version, l content.reset (new ImageContent (film, node, version)); } else if (type == "Sndfile") { content.reset (new SndfileContent (film, node, version)); + } else if (type == "SubRip") { + content.reset (new SubRipContent (film, node, version)); + } else if (type == "DCP") { + content.reset (new DCPContent (film, node, version)); + } else if (type == "DCPSubtitle") { + content.reset (new DCPSubtitleContent (film, node, version)); } return content; } +/** Create a Content object from a file, depending on its extension. + * @param film Film that the content will be in. + * @param path File's path. + * @return Content object. + */ shared_ptr<Content> content_factory (shared_ptr<const Film> film, boost::filesystem::path path) { shared_ptr<Content> content; + + string ext = path.extension().string (); + transform (ext.begin(), ext.end(), ext.begin(), ::tolower); if (valid_image_file (path)) { content.reset (new ImageContent (film, path)); } else if (SndfileContent::valid_file (path)) { content.reset (new SndfileContent (film, path)); + } else if (ext == ".srt") { + content.reset (new SubRipContent (film, path)); + } else if (ext == ".xml") { + content.reset (new DCPSubtitleContent (film, path)); } else { content.reset (new FFmpegContent (film, path)); } diff --git a/src/lib/content_factory.h b/src/lib/content_factory.h index 2eeebbc9f..fae7648ea 100644 --- a/src/lib/content_factory.h +++ b/src/lib/content_factory.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/content_factory.h + * @brief Methods to create content objects. + */ + class Film; extern boost::shared_ptr<Content> content_factory (boost::shared_ptr<const Film>, cxml::NodePtr, int, std::list<std::string> &); diff --git a/src/lib/content_subtitle.cc b/src/lib/content_subtitle.cc new file mode 100644 index 000000000..93e0677bb --- /dev/null +++ b/src/lib/content_subtitle.cc @@ -0,0 +1,31 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "content_subtitle.h" + +ContentTimePeriod +ContentTextSubtitle::period () const +{ + /* XXX: assuming we have some subs and they are all at the same time */ + assert (!subs.empty ()); + return ContentTimePeriod ( + ContentTime::from_seconds (double (subs.front().in().to_ticks()) / 250), + ContentTime::from_seconds (double (subs.front().out().to_ticks()) / 250) + ); +} diff --git a/src/lib/content_subtitle.h b/src/lib/content_subtitle.h new file mode 100644 index 000000000..8868618ad --- /dev/null +++ b/src/lib/content_subtitle.h @@ -0,0 +1,68 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_CONTENT_SUBTITLE_H +#define DCPOMATIC_CONTENT_SUBTITLE_H + +#include <list> +#include <dcp/subtitle_string.h> +#include "dcpomatic_time.h" +#include "rect.h" +#include "image_subtitle.h" + +class Image; + +class ContentSubtitle +{ +public: + virtual ContentTimePeriod period () const = 0; +}; + +class ContentImageSubtitle : public ContentSubtitle +{ +public: + ContentImageSubtitle (ContentTimePeriod p, boost::shared_ptr<Image> im, dcpomatic::Rect<double> r) + : sub (im, r) + , _period (p) + {} + + ContentTimePeriod period () const { + return _period; + } + + /* Our subtitle, with its rectangle unmodified by any offsets or scales that the content specifies */ + ImageSubtitle sub; + +private: + ContentTimePeriod _period; +}; + +class ContentTextSubtitle : public ContentSubtitle +{ +public: + ContentTextSubtitle (std::list<dcp::SubtitleString> s) + : subs (s) + {} + + ContentTimePeriod period () const; + + std::list<dcp::SubtitleString> subs; +}; + +#endif diff --git a/src/lib/content_video.h b/src/lib/content_video.h new file mode 100644 index 000000000..a7f73597c --- /dev/null +++ b/src/lib/content_video.h @@ -0,0 +1,48 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_CONTENT_VIDEO_H +#define DCPOMATIC_CONTENT_VIDEO_H + +class ImageProxy; + +/** @class ContentVideo + * @brief A frame of video straight out of some content. + */ +class ContentVideo +{ +public: + ContentVideo () + : eyes (EYES_BOTH) + {} + + ContentVideo (boost::shared_ptr<const ImageProxy> i, Eyes e, Part p, VideoFrame f) + : image (i) + , eyes (e) + , part (p) + , frame (f) + {} + + boost::shared_ptr<const ImageProxy> image; + Eyes eyes; + Part part; + VideoFrame frame; +}; + +#endif diff --git a/src/lib/cross.cc b/src/lib/cross.cc index 9b7d5594f..d84c17c55 100644 --- a/src/lib/cross.cc +++ b/src/lib/cross.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/lib/cross.h b/src/lib/cross.h index 1c7754503..c206fa55d 100644 --- a/src/lib/cross.h +++ b/src/lib/cross.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/cross.h + * @brief Cross-platform compatibility code. + */ + #ifndef DCPOMATIC_CROSS_H #define DCPOMATIC_CROSS_H @@ -42,7 +46,9 @@ extern boost::filesystem::path app_contents (); extern FILE * fopen_boost (boost::filesystem::path, std::string); extern int dcpomatic_fseek (FILE *, int64_t, int); -/** A class which tries to keep the computer awake on various operating systems. +/** @class Waker + * @brief A class which tries to keep the computer awake on various operating systems. + * * Create a Waker to prevent sleep, and call ::nudge every so often (every minute or so). * Destroy the Waker to allow sleep again. */ diff --git a/src/lib/dcp_content.cc b/src/lib/dcp_content.cc new file mode 100644 index 000000000..a5b5f37e1 --- /dev/null +++ b/src/lib/dcp_content.cc @@ -0,0 +1,162 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/dcp.h> +#include <dcp/exceptions.h> +#include "dcp_content.h" +#include "dcp_examiner.h" +#include "job.h" +#include "film.h" +#include "config.h" +#include "compose.hpp" + +#include "i18n.h" + +using std::string; +using std::cout; +using boost::shared_ptr; +using boost::optional; + +int const DCPContentProperty::CAN_BE_PLAYED = 600; + +DCPContent::DCPContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f) + , VideoContent (f) + , SingleStreamAudioContent (f) + , SubtitleContent (f) + , _has_subtitles (false) + , _encrypted (false) + , _directory (p) + , _kdm_valid (false) +{ + read_directory (p); +} + +DCPContent::DCPContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) + : Content (f, node) + , VideoContent (f, node, version) + , SingleStreamAudioContent (f, node, version) + , SubtitleContent (f, node, version) +{ + _name = node->string_child ("Name"); + _has_subtitles = node->bool_child ("HasSubtitles"); + _directory = node->string_child ("Directory"); + _encrypted = node->bool_child ("Encrypted"); + if (node->optional_node_child ("KDM")) { + _kdm = dcp::EncryptedKDM (node->string_child ("KDM")); + } + _kdm_valid = node->bool_child ("KDMValid"); +} + +void +DCPContent::read_directory (boost::filesystem::path p) +{ + for (boost::filesystem::directory_iterator i(p); i != boost::filesystem::directory_iterator(); ++i) { + if (boost::filesystem::is_regular_file (i->path ())) { + _paths.push_back (i->path ()); + } else if (boost::filesystem::is_directory (i->path ())) { + read_directory (i->path ()); + } + } +} + +void +DCPContent::examine (shared_ptr<Job> job) +{ + bool const could_be_played = can_be_played (); + + job->set_progress_unknown (); + Content::examine (job); + + shared_ptr<DCPExaminer> examiner (new DCPExaminer (shared_from_this ())); + take_from_video_examiner (examiner); + take_from_audio_examiner (examiner); + + boost::mutex::scoped_lock lm (_mutex); + _name = examiner->name (); + _has_subtitles = examiner->has_subtitles (); + _encrypted = examiner->encrypted (); + _kdm_valid = examiner->kdm_valid (); + + if (could_be_played != can_be_played ()) { + signal_changed (DCPContentProperty::CAN_BE_PLAYED); + } +} + +string +DCPContent::summary () const +{ + boost::mutex::scoped_lock lm (_mutex); + return String::compose (_("%1 [DCP]"), _name); +} + +string +DCPContent::technical_summary () const +{ + return Content::technical_summary() + " - " + + VideoContent::technical_summary() + " - " + + AudioContent::technical_summary() + " - "; +} + +void +DCPContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("DCP"); + + Content::as_xml (node); + VideoContent::as_xml (node); + SingleStreamAudioContent::as_xml (node); + SubtitleContent::as_xml (node); + + boost::mutex::scoped_lock lm (_mutex); + node->add_child("Name")->add_child_text (_name); + node->add_child("HasSubtitles")->add_child_text (_has_subtitles ? "1" : "0"); + node->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); + node->add_child("Directory")->add_child_text (_directory.string ()); + if (_kdm) { + node->add_child("KDM")->add_child_text (_kdm->as_xml ()); + } + node->add_child("KDMValid")->add_child_text (_kdm_valid ? "1" : "0"); +} + +DCPTime +DCPContent::full_length () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + return DCPTime (video_length (), FrameRateChange (video_frame_rate (), film->video_frame_rate ())); +} + +string +DCPContent::identifier () const +{ + return SubtitleContent::identifier (); +} + +void +DCPContent::add_kdm (dcp::EncryptedKDM k) +{ + _kdm = k; +} + +bool +DCPContent::can_be_played () const +{ + return !_encrypted || _kdm_valid; +} diff --git a/src/lib/dcp_content.h b/src/lib/dcp_content.h new file mode 100644 index 000000000..da78e6d72 --- /dev/null +++ b/src/lib/dcp_content.h @@ -0,0 +1,98 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_DCP_CONTENT_H +#define DCPOMATIC_DCP_CONTENT_H + +/** @file src/lib/dcp_content.h + * @brief DCPContent class. + */ + +#include <libcxml/cxml.h> +#include <dcp/encrypted_kdm.h> +#include <dcp/decrypted_kdm.h> +#include "video_content.h" +#include "single_stream_audio_content.h" +#include "subtitle_content.h" + +class DCPContentProperty +{ +public: + static int const CAN_BE_PLAYED; +}; + +/** @class DCPContent + * @brief An existing DCP used as input. + */ +class DCPContent : public VideoContent, public SingleStreamAudioContent, public SubtitleContent +{ +public: + DCPContent (boost::shared_ptr<const Film> f, boost::filesystem::path p); + DCPContent (boost::shared_ptr<const Film> f, cxml::ConstNodePtr, int version); + + boost::shared_ptr<DCPContent> shared_from_this () { + return boost::dynamic_pointer_cast<DCPContent> (Content::shared_from_this ()); + } + + DCPTime full_length () const; + + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + void as_xml (xmlpp::Node *) const; + std::string identifier () const; + + /* SubtitleContent */ + bool has_subtitles () const { + boost::mutex::scoped_lock lm (_mutex); + return _has_subtitles; + } + + boost::filesystem::path directory () const { + boost::mutex::scoped_lock lm (_mutex); + return _directory; + } + + bool encrypted () const { + boost::mutex::scoped_lock lm (_mutex); + return _encrypted; + } + + void add_kdm (dcp::EncryptedKDM); + + boost::optional<dcp::EncryptedKDM> kdm () const { + return _kdm; + } + + bool can_be_played () const; + +private: + void read_directory (boost::filesystem::path); + + std::string _name; + bool _has_subtitles; + /** true if our DCP is encrypted */ + bool _encrypted; + boost::filesystem::path _directory; + boost::optional<dcp::EncryptedKDM> _kdm; + /** true if _kdm successfully decrypts the first frame of our DCP */ + bool _kdm_valid; +}; + +#endif diff --git a/src/lib/dcp_content_type.cc b/src/lib/dcp_content_type.cc index f24ed95ea..e5466e139 100644 --- a/src/lib/dcp_content_type.cc +++ b/src/lib/dcp_content_type.cc @@ -30,7 +30,7 @@ using namespace std; vector<DCPContentType const *> DCPContentType::_dcp_content_types; -DCPContentType::DCPContentType (string p, libdcp::ContentKind k, string d) +DCPContentType::DCPContentType (string p, dcp::ContentKind k, string d) : _pretty_name (p) , _libdcp_kind (k) , _isdcf_name (d) @@ -41,16 +41,16 @@ DCPContentType::DCPContentType (string p, libdcp::ContentKind k, string d) void DCPContentType::setup_dcp_content_types () { - _dcp_content_types.push_back (new DCPContentType (_("Feature"), libdcp::FEATURE, N_("FTR"))); - _dcp_content_types.push_back (new DCPContentType (_("Short"), libdcp::SHORT, N_("SHR"))); - _dcp_content_types.push_back (new DCPContentType (_("Trailer"), libdcp::TRAILER, N_("TLR"))); - _dcp_content_types.push_back (new DCPContentType (_("Test"), libdcp::TEST, N_("TST"))); - _dcp_content_types.push_back (new DCPContentType (_("Transitional"), libdcp::TRANSITIONAL, N_("XSN"))); - _dcp_content_types.push_back (new DCPContentType (_("Rating"), libdcp::RATING, N_("RTG"))); - _dcp_content_types.push_back (new DCPContentType (_("Teaser"), libdcp::TEASER, N_("TSR"))); - _dcp_content_types.push_back (new DCPContentType (_("Policy"), libdcp::POLICY, N_("POL"))); - _dcp_content_types.push_back (new DCPContentType (_("Public Service Announcement"), libdcp::PUBLIC_SERVICE_ANNOUNCEMENT, N_("PSA"))); - _dcp_content_types.push_back (new DCPContentType (_("Advertisement"), libdcp::ADVERTISEMENT, N_("ADV"))); + _dcp_content_types.push_back (new DCPContentType (_("Feature"), dcp::FEATURE, N_("FTR"))); + _dcp_content_types.push_back (new DCPContentType (_("Short"), dcp::SHORT, N_("SHR"))); + _dcp_content_types.push_back (new DCPContentType (_("Trailer"), dcp::TRAILER, N_("TLR"))); + _dcp_content_types.push_back (new DCPContentType (_("Test"), dcp::TEST, N_("TST"))); + _dcp_content_types.push_back (new DCPContentType (_("Transitional"), dcp::TRANSITIONAL, N_("XSN"))); + _dcp_content_types.push_back (new DCPContentType (_("Rating"), dcp::RATING, N_("RTG"))); + _dcp_content_types.push_back (new DCPContentType (_("Teaser"), dcp::TEASER, N_("TSR"))); + _dcp_content_types.push_back (new DCPContentType (_("Policy"), dcp::POLICY, N_("POL"))); + _dcp_content_types.push_back (new DCPContentType (_("Public Service Announcement"), dcp::PUBLIC_SERVICE_ANNOUNCEMENT, N_("PSA"))); + _dcp_content_types.push_back (new DCPContentType (_("Advertisement"), dcp::ADVERTISEMENT, N_("ADV"))); } DCPContentType const * diff --git a/src/lib/dcp_content_type.h b/src/lib/dcp_content_type.h index 88f3c4a85..ebfe09518 100644 --- a/src/lib/dcp_content_type.h +++ b/src/lib/dcp_content_type.h @@ -26,7 +26,7 @@ #include <string> #include <vector> -#include <libdcp/dcp.h> +#include <dcp/dcp.h> /** @class DCPContentType * @brief A description of the type of content for a DCP (e.g. feature, trailer etc.) @@ -34,14 +34,14 @@ class DCPContentType : public boost::noncopyable { public: - DCPContentType (std::string, libdcp::ContentKind, std::string); + DCPContentType (std::string, dcp::ContentKind, std::string); /** @return user-visible `pretty' name */ std::string pretty_name () const { return _pretty_name; } - libdcp::ContentKind libdcp_kind () const { + dcp::ContentKind libdcp_kind () const { return _libdcp_kind; } @@ -58,7 +58,7 @@ public: private: std::string _pretty_name; - libdcp::ContentKind _libdcp_kind; + dcp::ContentKind _libdcp_kind; std::string _isdcf_name; /** All available DCP content types */ diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc new file mode 100644 index 000000000..bf016ef87 --- /dev/null +++ b/src/lib/dcp_decoder.cc @@ -0,0 +1,140 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/dcp.h> +#include <dcp/cpl.h> +#include <dcp/reel.h> +#include <dcp/mono_picture_mxf.h> +#include <dcp/stereo_picture_mxf.h> +#include <dcp/reel_picture_asset.h> +#include <dcp/reel_sound_asset.h> +#include <dcp/mono_picture_frame.h> +#include <dcp/stereo_picture_frame.h> +#include <dcp/sound_frame.h> +#include "dcp_decoder.h" +#include "dcp_content.h" +#include "j2k_image_proxy.h" +#include "image.h" +#include "config.h" + +using std::list; +using std::cout; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +DCPDecoder::DCPDecoder (shared_ptr<const DCPContent> c, shared_ptr<Log> log) + : VideoDecoder (c) + , AudioDecoder (c) + , SubtitleDecoder (c) + , _log (log) + , _dcp_content (c) +{ + dcp::DCP dcp (c->directory ()); + dcp.read (); + if (c->kdm ()) { + dcp.add (dcp::DecryptedKDM (c->kdm().get (), Config::instance()->decryption_private_key ())); + } + assert (dcp.cpls().size() == 1); + _reels = dcp.cpls().front()->reels (); + _reel = _reels.begin (); +} + +bool +DCPDecoder::pass () +{ + if (_reel == _reels.end () || !_dcp_content->can_be_played ()) { + return true; + } + + float const vfr = _dcp_content->video_frame_rate (); + int64_t const frame = _next.frames (vfr); + + if ((*_reel)->main_picture ()) { + shared_ptr<dcp::PictureMXF> mxf = (*_reel)->main_picture()->mxf (); + shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (mxf); + shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (mxf); + int64_t const entry_point = (*_reel)->main_picture()->entry_point (); + if (mono) { + video (shared_ptr<ImageProxy> (new J2KImageProxy (mono->get_frame (entry_point + frame), mxf->size(), _log)), frame); + } else { + video ( + shared_ptr<ImageProxy> (new J2KImageProxy (stereo->get_frame (entry_point + frame), mxf->size(), dcp::EYE_LEFT, _log)), + frame + ); + + video ( + shared_ptr<ImageProxy> (new J2KImageProxy (stereo->get_frame (entry_point + frame), mxf->size(), dcp::EYE_RIGHT, _log)), + frame + ); + } + } + + if ((*_reel)->main_sound ()) { + int64_t const entry_point = (*_reel)->main_sound()->entry_point (); + shared_ptr<const dcp::SoundFrame> sf = (*_reel)->main_sound()->mxf()->get_frame (entry_point + frame); + uint8_t const * from = sf->data (); + + int const channels = _dcp_content->audio_channels (); + int const frames = sf->size() / (3 * channels); + shared_ptr<AudioBuffers> data (new AudioBuffers (channels, frames)); + for (int i = 0; i < frames; ++i) { + for (int j = 0; j < channels; ++j) { + data->data()[j][i] = float (from[0] | (from[1] << 8) | (from[2] << 16)) / (1 << 23); + from += 3; + } + } + + audio (data, _next); + } + + /* XXX: subtitle */ + + _next += ContentTime::from_frames (1, vfr); + + if ((*_reel)->main_picture ()) { + if (_next.frames (vfr) >= (*_reel)->main_picture()->duration()) { + ++_reel; + } + } + + return false; +} + +void +DCPDecoder::seek (ContentTime t, bool accurate) +{ + VideoDecoder::seek (t, accurate); + AudioDecoder::seek (t, accurate); + SubtitleDecoder::seek (t, accurate); + + _reel = _reels.begin (); + while (_reel != _reels.end() && t >= ContentTime::from_frames ((*_reel)->main_picture()->duration(), _dcp_content->video_frame_rate ())) { + t -= ContentTime::from_frames ((*_reel)->main_picture()->duration(), _dcp_content->video_frame_rate ()); + ++_reel; + } + + _next = t; +} + + +list<ContentTimePeriod> +DCPDecoder::subtitles_during (ContentTimePeriod, bool starting) const +{ + return list<ContentTimePeriod> (); +} diff --git a/src/lib/dcp_decoder.h b/src/lib/dcp_decoder.h new file mode 100644 index 000000000..d81b20b5c --- /dev/null +++ b/src/lib/dcp_decoder.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "video_decoder.h" +#include "audio_decoder.h" +#include "subtitle_decoder.h" + +namespace dcp { + class Reel; +} + +class DCPContent; +class Log; + +class DCPDecoder : public VideoDecoder, public AudioDecoder, public SubtitleDecoder +{ +public: + DCPDecoder (boost::shared_ptr<const DCPContent>, boost::shared_ptr<Log>); + +private: + void seek (ContentTime t, bool accurate); + bool pass (); + std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const; + + ContentTime _next; + std::list<boost::shared_ptr<dcp::Reel> > _reels; + std::list<boost::shared_ptr<dcp::Reel> >::iterator _reel; + boost::shared_ptr<Log> _log; + boost::shared_ptr<const DCPContent> _dcp_content; +}; diff --git a/src/lib/dcp_examiner.cc b/src/lib/dcp_examiner.cc new file mode 100644 index 000000000..1e4cc899d --- /dev/null +++ b/src/lib/dcp_examiner.cc @@ -0,0 +1,136 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/dcp.h> +#include <dcp/cpl.h> +#include <dcp/reel.h> +#include <dcp/reel_picture_asset.h> +#include <dcp/reel_sound_asset.h> +#include <dcp/mono_picture_mxf.h> +#include <dcp/mono_picture_frame.h> +#include <dcp/stereo_picture_mxf.h> +#include <dcp/stereo_picture_frame.h> +#include <dcp/sound_mxf.h> +#include "dcp_examiner.h" +#include "dcp_content.h" +#include "exceptions.h" +#include "image.h" +#include "config.h" + +#include "i18n.h" + +using std::list; +using std::cout; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +DCPExaminer::DCPExaminer (shared_ptr<const DCPContent> content) + : _video_length (0) + , _audio_length (0) + , _has_subtitles (false) + , _encrypted (false) + , _kdm_valid (false) +{ + dcp::DCP dcp (content->directory ()); + dcp.read (); + + if (content->kdm ()) { + dcp.add (dcp::DecryptedKDM (content->kdm().get(), Config::instance()->decryption_private_key ())); + } + + if (dcp.cpls().size() == 0) { + throw DCPError ("No CPLs found in DCP"); + } else if (dcp.cpls().size() > 1) { + throw DCPError ("Multiple CPLs found in DCP"); + } + + _name = dcp.cpls().front()->content_title_text (); + + list<shared_ptr<dcp::Reel> > reels = dcp.cpls().front()->reels (); + for (list<shared_ptr<dcp::Reel> >::const_iterator i = reels.begin(); i != reels.end(); ++i) { + + if ((*i)->main_picture ()) { + dcp::Fraction const frac = (*i)->main_picture()->frame_rate (); + float const fr = float(frac.numerator) / frac.denominator; + if (!_video_frame_rate) { + _video_frame_rate = fr; + } else if (_video_frame_rate.get() != fr) { + throw DCPError (_("Mismatched frame rates in DCP")); + } + + shared_ptr<dcp::PictureMXF> mxf = (*i)->main_picture()->mxf (); + if (!_video_size) { + _video_size = mxf->size (); + } else if (_video_size.get() != mxf->size ()) { + throw DCPError (_("Mismatched video sizes in DCP")); + } + + _video_length += ContentTime::from_frames ((*i)->main_picture()->duration(), _video_frame_rate.get ()); + } + + if ((*i)->main_sound ()) { + shared_ptr<dcp::SoundMXF> mxf = (*i)->main_sound()->mxf (); + + if (!_audio_channels) { + _audio_channels = mxf->channels (); + } else if (_audio_channels.get() != mxf->channels ()) { + throw DCPError (_("Mismatched audio channel counts in DCP")); + } + + if (!_audio_frame_rate) { + _audio_frame_rate = mxf->sampling_rate (); + } else if (_audio_frame_rate.get() != mxf->sampling_rate ()) { + throw DCPError (_("Mismatched audio frame rates in DCP")); + } + + _audio_length += ContentTime::from_frames ((*i)->main_sound()->duration(), _video_frame_rate.get ()); + } + + if ((*i)->main_subtitle ()) { + _has_subtitles = true; + } + } + + _encrypted = dcp.encrypted (); + _kdm_valid = true; + + /* Check that we can read the first picture frame */ + try { + if (!dcp.cpls().empty () && !dcp.cpls().front()->reels().empty ()) { + shared_ptr<dcp::PictureMXF> mxf = dcp.cpls().front()->reels().front()->main_picture()->mxf (); + shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (mxf); + shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (mxf); + + shared_ptr<Image> image (new Image (PIX_FMT_RGB24, _video_size.get(), false)); + + if (mono) { + mono->get_frame(0)->rgb_frame (image->data()[0]); + } else { + stereo->get_frame(0)->rgb_frame (dcp::EYE_LEFT, image->data()[0]); + } + + } + } catch (dcp::DCPReadError& e) { + _kdm_valid = false; + if (_encrypted && content->kdm ()) { + /* XXX: maybe don't use an exception for this */ + throw StringError (_("The KDM does not decrypt the DCP. Perhaps it is targeted at the wrong CPL")); + } + } +} diff --git a/src/lib/dcp_examiner.h b/src/lib/dcp_examiner.h new file mode 100644 index 000000000..03d43d0f6 --- /dev/null +++ b/src/lib/dcp_examiner.h @@ -0,0 +1,81 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "video_examiner.h" +#include "audio_examiner.h" + +class DCPContent; + +class DCPExaminer : public VideoExaminer, public AudioExaminer +{ +public: + DCPExaminer (boost::shared_ptr<const DCPContent>); + + float video_frame_rate () const { + return _video_frame_rate.get_value_or (24); + } + + dcp::Size video_size () const { + return _video_size.get_value_or (dcp::Size (1998, 1080)); + } + + ContentTime video_length () const { + return _video_length; + } + + std::string name () const { + return _name; + } + + bool has_subtitles () const { + return _has_subtitles; + } + + bool encrypted () const { + return _encrypted; + } + + int audio_channels () const { + return _audio_channels.get_value_or (0); + } + + ContentTime audio_length () const { + return _audio_length; + } + + int audio_frame_rate () const { + return _audio_frame_rate.get_value_or (48000); + } + + bool kdm_valid () const { + return _kdm_valid; + } + +private: + boost::optional<float> _video_frame_rate; + boost::optional<dcp::Size> _video_size; + ContentTime _video_length; + boost::optional<int> _audio_channels; + boost::optional<int> _audio_frame_rate; + ContentTime _audio_length; + std::string _name; + bool _has_subtitles; + bool _encrypted; + bool _kdm_valid; +}; diff --git a/src/lib/dcp_subtitle_content.cc b/src/lib/dcp_subtitle_content.cc new file mode 100644 index 000000000..83b0d200c --- /dev/null +++ b/src/lib/dcp_subtitle_content.cc @@ -0,0 +1,88 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/subtitle_content.h> +#include <dcp/raw_convert.h> +#include "dcp_subtitle_content.h" + +#include "i18n.h" + +using std::string; +using std::list; +using boost::shared_ptr; +using dcp::raw_convert; + +DCPSubtitleContent::DCPSubtitleContent (shared_ptr<const Film> film, boost::filesystem::path path) + : Content (film, path) + , SubtitleContent (film, path) +{ + +} + +DCPSubtitleContent::DCPSubtitleContent (shared_ptr<const Film> film, cxml::ConstNodePtr node, int version) + : Content (film, node) + , SubtitleContent (film, node, version) + , _length (node->number_child<DCPTime::Type> ("Length")) +{ + +} + +void +DCPSubtitleContent::examine (shared_ptr<Job> job) +{ + Content::examine (job); + dcp::SubtitleContent sc (path (0), false); + _length = DCPTime::from_seconds (sc.latest_subtitle_out().to_seconds ()); +} + +DCPTime +DCPSubtitleContent::full_length () const +{ + /* XXX: this assumes that the timing of the subtitle file is appropriate + for the DCP's frame rate. + */ + return _length; +} + +string +DCPSubtitleContent::summary () const +{ + return path_summary() + " " + _("[subtitles]"); +} + +string +DCPSubtitleContent::technical_summary () const +{ + return Content::technical_summary() + " - " + _("DCP XML subtitles"); +} + +string +DCPSubtitleContent::information () const +{ + +} + +void +DCPSubtitleContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("DCPSubtitle"); + Content::as_xml (node); + SubtitleContent::as_xml (node); + node->add_child("Length")->add_child_text (raw_convert<string> (_length.get ())); +} diff --git a/src/lib/dcp_subtitle_content.h b/src/lib/dcp_subtitle_content.h new file mode 100644 index 000000000..5794b5951 --- /dev/null +++ b/src/lib/dcp_subtitle_content.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "subtitle_content.h" + +class DCPSubtitleContent : public SubtitleContent +{ +public: + DCPSubtitleContent (boost::shared_ptr<const Film>, boost::filesystem::path); + DCPSubtitleContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int); + + /* Content */ + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + std::string information () const; + void as_xml (xmlpp::Node *) const; + DCPTime full_length () const; + + /* SubtitleContent */ + bool has_subtitles () const { + return true; + } + +private: + DCPTime _length; +}; diff --git a/src/lib/dcp_subtitle_decoder.cc b/src/lib/dcp_subtitle_decoder.cc new file mode 100644 index 000000000..20a9f32fe --- /dev/null +++ b/src/lib/dcp_subtitle_decoder.cc @@ -0,0 +1,83 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/subtitle_content.h> +#include "dcp_subtitle_decoder.h" +#include "dcp_subtitle_content.h" + +using std::list; +using std::cout; +using boost::shared_ptr; + +DCPSubtitleDecoder::DCPSubtitleDecoder (shared_ptr<const DCPSubtitleContent> content) + : SubtitleDecoder (content) +{ + dcp::SubtitleContent c (content->path (0), false); + _subtitles = c.subtitles (); + _next = _subtitles.begin (); +} + +void +DCPSubtitleDecoder::seek (ContentTime time, bool accurate) +{ + SubtitleDecoder::seek (time, accurate); + + _next = _subtitles.begin (); + list<dcp::SubtitleString>::const_iterator i = _subtitles.begin (); + while (i != _subtitles.end() && ContentTime::from_seconds (_next->in().to_seconds()) < time) { + ++i; + } +} + +bool +DCPSubtitleDecoder::pass () +{ + if (_next == _subtitles.end ()) { + return true; + } + + list<dcp::SubtitleString> s; + s.push_back (*_next); + text_subtitle (s); + ++_next; + + return false; +} + +list<ContentTimePeriod> +DCPSubtitleDecoder::subtitles_during (ContentTimePeriod p, bool starting) const +{ + /* XXX: inefficient */ + + list<ContentTimePeriod> d; + + for (list<dcp::SubtitleString>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { + ContentTimePeriod period ( + ContentTime::from_seconds (i->in().to_seconds ()), + ContentTime::from_seconds (i->out().to_seconds ()) + ); + + if ((starting && p.contains (period.from)) || (!starting && p.overlaps (period))) { + d.push_back (period); + } + } + + return d; +} + diff --git a/src/lib/dcp_subtitle_decoder.h b/src/lib/dcp_subtitle_decoder.h new file mode 100644 index 000000000..070562458 --- /dev/null +++ b/src/lib/dcp_subtitle_decoder.h @@ -0,0 +1,38 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "subtitle_decoder.h" + +class DCPSubtitleContent; + +class DCPSubtitleDecoder : public SubtitleDecoder +{ +public: + DCPSubtitleDecoder (boost::shared_ptr<const DCPSubtitleContent>); + +protected: + void seek (ContentTime time, bool accurate); + bool pass (); + +private: + std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const; + + std::list<dcp::SubtitleString> _subtitles; + std::list<dcp::SubtitleString>::const_iterator _next; +}; diff --git a/src/lib/dcp_video_frame.cc b/src/lib/dcp_video.cc index bb7eaa064..ccfc800c8 100644 --- a/src/lib/dcp_video_frame.cc +++ b/src/lib/dcp_video.cc @@ -41,16 +41,15 @@ #include <boost/array.hpp> #include <boost/asio.hpp> #include <boost/filesystem.hpp> -#include <libdcp/rec709_linearised_gamma_lut.h> -#include <libdcp/srgb_linearised_gamma_lut.h> -#include <libdcp/gamma_lut.h> -#include <libdcp/xyz_frame.h> -#include <libdcp/rgb_xyz.h> -#include <libdcp/colour_matrix.h> -#include <libdcp/raw_convert.h> +#include <boost/lexical_cast.hpp> +#include <dcp/gamma_lut.h> +#include <dcp/xyz_frame.h> +#include <dcp/rgb_xyz.h> +#include <dcp/colour_matrix.h> +#include <dcp/raw_convert.h> #include <libcxml/cxml.h> #include "film.h" -#include "dcp_video_frame.h" +#include "dcp_video.h" #include "config.h" #include "exceptions.h" #include "server.h" @@ -59,7 +58,8 @@ #include "image.h" #include "log.h" #include "cross.h" -#include "player_video_frame.h" +#include "player_video.h" +#include "encoded_data.h" #define LOG_GENERAL(...) _log->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); @@ -68,8 +68,9 @@ using std::string; using std::cout; using boost::shared_ptr; -using libdcp::Size; -using libdcp::raw_convert; +using boost::lexical_cast; +using dcp::Size; +using dcp::raw_convert; #define DCI_COEFFICENT (48.0 / 52.37) @@ -79,20 +80,21 @@ using libdcp::raw_convert; * @param bw J2K bandwidth to use (see Config::j2k_bandwidth ()) * @param l Log to write to. */ -DCPVideoFrame::DCPVideoFrame ( - shared_ptr<const PlayerVideoFrame> frame, int index, int dcp_fps, int bw, Resolution r, shared_ptr<Log> l +DCPVideo::DCPVideo ( + shared_ptr<const PlayerVideo> frame, int index, int dcp_fps, int bw, Resolution r, bool b, shared_ptr<Log> l ) : _frame (frame) , _index (index) , _frames_per_second (dcp_fps) , _j2k_bandwidth (bw) , _resolution (r) + , _burn_subtitles (b) , _log (l) { } -DCPVideoFrame::DCPVideoFrame (shared_ptr<const PlayerVideoFrame> frame, shared_ptr<const cxml::Node> node, shared_ptr<Log> log) +DCPVideo::DCPVideo (shared_ptr<const PlayerVideo> frame, shared_ptr<const cxml::Node> node, shared_ptr<Log> log) : _frame (frame) , _log (log) { @@ -100,21 +102,19 @@ DCPVideoFrame::DCPVideoFrame (shared_ptr<const PlayerVideoFrame> frame, shared_p _frames_per_second = node->number_child<int> ("FramesPerSecond"); _j2k_bandwidth = node->number_child<int> ("J2KBandwidth"); _resolution = Resolution (node->optional_number_child<int>("Resolution").get_value_or (RESOLUTION_2K)); + _burn_subtitles = node->bool_child ("BurnSubtitles"); } /** J2K-encode this frame on the local host. * @return Encoded data. */ shared_ptr<EncodedData> -DCPVideoFrame::encode_locally () +DCPVideo::encode_locally () { - shared_ptr<libdcp::LUT> in_lut; - if (_frame->colour_conversion().input_gamma_linearised) { - in_lut = libdcp::SRGBLinearisedGammaLUT::cache.get (12, _frame->colour_conversion().input_gamma); - } else { - in_lut = libdcp::GammaLUT::cache.get (12, _frame->colour_conversion().input_gamma); - } - + shared_ptr<dcp::GammaLUT> in_lut = dcp::GammaLUT::cache.get ( + 12, _frame->colour_conversion().input_gamma, _frame->colour_conversion().input_gamma_linearised + ); + /* XXX: libdcp should probably use boost */ double matrix[3][3]; @@ -124,10 +124,10 @@ DCPVideoFrame::encode_locally () } } - shared_ptr<libdcp::XYZFrame> xyz = libdcp::rgb_to_xyz ( - _frame->image(), + shared_ptr<dcp::XYZFrame> xyz = dcp::rgb_to_xyz ( + _frame->image (_burn_subtitles), in_lut, - libdcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma), + dcp::GammaLUT::cache.get (16, 1 / _frame->colour_conversion().output_gamma, false), matrix ); @@ -260,7 +260,7 @@ DCPVideoFrame::encode_locally () * @return Encoded data. */ shared_ptr<EncodedData> -DCPVideoFrame::encode_remotely (ServerDescription serv) +DCPVideo::encode_remotely (ServerDescription serv) { boost::asio::io_service io_service; boost::asio::ip::tcp::resolver resolver (io_service); @@ -285,7 +285,7 @@ DCPVideoFrame::encode_remotely (ServerDescription serv) socket->write ((uint8_t *) xml.c_str(), xml.length() + 1); /* Send binary data */ - _frame->send_binary (socket); + _frame->send_binary (socket, _burn_subtitles); /* Read the response (JPEG2000-encoded data); this blocks until the data is ready and sent back. @@ -299,105 +299,34 @@ DCPVideoFrame::encode_remotely (ServerDescription serv) } void -DCPVideoFrame::add_metadata (xmlpp::Element* el) const +DCPVideo::add_metadata (xmlpp::Element* el) const { el->add_child("Index")->add_child_text (raw_convert<string> (_index)); el->add_child("FramesPerSecond")->add_child_text (raw_convert<string> (_frames_per_second)); el->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth)); el->add_child("Resolution")->add_child_text (raw_convert<string> (int (_resolution))); - _frame->add_metadata (el); + el->add_child("BurnSubtitles")->add_child_text (_burn_subtitles ? "1" : "0"); + _frame->add_metadata (el, _burn_subtitles); } Eyes -DCPVideoFrame::eyes () const +DCPVideo::eyes () const { return _frame->eyes (); } -EncodedData::EncodedData (int s) - : _data (new uint8_t[s]) - , _size (s) -{ - -} - -EncodedData::EncodedData (boost::filesystem::path file) -{ - _size = boost::filesystem::file_size (file); - _data = new uint8_t[_size]; - - FILE* f = fopen_boost (file, "rb"); - if (!f) { - throw FileError (_("could not open file for reading"), file); - } - - size_t const r = fread (_data, 1, _size, f); - if (r != size_t (_size)) { - fclose (f); - throw FileError (_("could not read encoded data"), file); - } - - fclose (f); -} - - -EncodedData::~EncodedData () -{ - delete[] _data; -} - -/** Write this data to a J2K file. - * @param Film Film. - * @param frame DCP frame index. +/** @return true if this DCPVideo is definitely the same as another; + * (apart from the frame index), false if it is probably not. */ -void -EncodedData::write (shared_ptr<const Film> film, int frame, Eyes eyes) const +bool +DCPVideo::same (shared_ptr<const DCPVideo> other) const { - boost::filesystem::path const tmp_j2c = film->j2c_path (frame, eyes, true); - - FILE* f = fopen_boost (tmp_j2c, "wb"); - - if (!f) { - throw WriteFileError (tmp_j2c, errno); + if (_frames_per_second != other->_frames_per_second || + _j2k_bandwidth != other->_j2k_bandwidth || + _resolution != other->_resolution || + _burn_subtitles != other->_burn_subtitles) { + return false; } - fwrite (_data, 1, _size, f); - fclose (f); - - boost::filesystem::path const real_j2c = film->j2c_path (frame, eyes, false); - - /* Rename the file from foo.j2c.tmp to foo.j2c now that it is complete */ - boost::filesystem::rename (tmp_j2c, real_j2c); -} - -void -EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, libdcp::FrameInfo fin) const -{ - boost::filesystem::path const info = film->info_path (frame, eyes); - FILE* h = fopen_boost (info, "w"); - fin.write (h); - fclose (h); -} - -/** Send this data to a socket. - * @param socket Socket - */ -void -EncodedData::send (shared_ptr<Socket> socket) -{ - socket->write (_size); - socket->write (_data, _size); -} - -LocallyEncodedData::LocallyEncodedData (uint8_t* d, int s) - : EncodedData (s) -{ - memcpy (_data, d, s); -} - -/** @param s Size of data in bytes */ -RemotelyEncodedData::RemotelyEncodedData (int s) - : EncodedData (s) -{ - + return _frame->same (other->_frame); } diff --git a/src/lib/dcp_video.h b/src/lib/dcp_video.h new file mode 100644 index 000000000..d517a8f02 --- /dev/null +++ b/src/lib/dcp_video.h @@ -0,0 +1,75 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + Taken from code Copyright (C) 2010-2011 Terrence Meiczinger + + This program is free software; you can redistribute it and/or modify + 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 <dcp/picture_mxf_writer.h> +#include "util.h" + +/** @file src/dcp_video_frame.h + * @brief A single frame of video destined for a DCP. + */ + +class Film; +class ServerDescription; +class Scaler; +class Image; +class Log; +class Subtitle; +class PlayerVideo; +class EncodedData; + +/** @class DCPVideo + * @brief A single frame of video destined for a DCP. + * + * Given an Image and some settings, this class knows how to encode + * the image to J2K either on the local host or on a remote server. + * + * Objects of this class are used for the queue that we keep + * of images that require encoding. + */ +class DCPVideo : public boost::noncopyable +{ +public: + DCPVideo (boost::shared_ptr<const PlayerVideo>, int, int, int, Resolution, bool b, boost::shared_ptr<Log>); + DCPVideo (boost::shared_ptr<const PlayerVideo>, cxml::ConstNodePtr, boost::shared_ptr<Log>); + + boost::shared_ptr<EncodedData> encode_locally (); + boost::shared_ptr<EncodedData> encode_remotely (ServerDescription); + + int index () const { + return _index; + } + + Eyes eyes () const; + + bool same (boost::shared_ptr<const DCPVideo> other) const; + +private: + + void add_metadata (xmlpp::Element *) const; + + boost::shared_ptr<const PlayerVideo> _frame; + int _index; ///< frame index within the DCP's intrinsic duration + int _frames_per_second; ///< Frames per second that we will use for the DCP + int _j2k_bandwidth; ///< J2K bandwidth to use + Resolution _resolution; ///< Resolution (2K or 4K) + bool _burn_subtitles; ///< true to burn subtitles into the image + + boost::shared_ptr<Log> _log; ///< log +}; diff --git a/src/lib/dcpomatic_time.cc b/src/lib/dcpomatic_time.cc new file mode 100644 index 000000000..812c756ec --- /dev/null +++ b/src/lib/dcpomatic_time.cc @@ -0,0 +1,63 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "dcpomatic_time.h" + +using std::ostream; + +ContentTime::ContentTime (DCPTime d, FrameRateChange f) + : Time (rint (d.get() * f.speed_up)) +{ + +} + +DCPTime min (DCPTime a, DCPTime b) +{ + if (a < b) { + return a; + } + + return b; +} + +ostream & +operator<< (ostream& s, ContentTime t) +{ + s << "[CONT " << t.get() << " " << t.seconds() << "s]"; + return s; +} + +ostream & +operator<< (ostream& s, DCPTime t) +{ + s << "[DCP " << t.get() << " " << t.seconds() << "s]"; + return s; +} + +bool +ContentTimePeriod::overlaps (ContentTimePeriod const & other) const +{ + return (from < other.to && to >= other.from); +} + +bool +ContentTimePeriod::contains (ContentTime const & other) const +{ + return (from <= other && other < to); +} diff --git a/src/lib/dcpomatic_time.h b/src/lib/dcpomatic_time.h new file mode 100644 index 000000000..55476d5b5 --- /dev/null +++ b/src/lib/dcpomatic_time.h @@ -0,0 +1,299 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_TIME_H +#define DCPOMATIC_TIME_H + +#include <cmath> +#include <ostream> +#include <sstream> +#include <iomanip> +#include <stdint.h> +#include "frame_rate_change.h" +#include "safe_stringstream.h" + +class dcpomatic_round_up_test; + +class Time; + +/** A time in seconds, expressed as a number scaled up by Time::HZ. */ +class Time +{ +public: + Time () + : _t (0) + {} + + typedef int64_t Type; + + explicit Time (Type t) + : _t (t) + {} + + virtual ~Time () {} + + Type get () const { + return _t; + } + + double seconds () const { + return double (_t) / HZ; + } + + template <typename T> + int64_t frames (T r) const { + return rint (_t * r / HZ); + } + + template <typename T> + void split (T r, int& h, int& m, int& s, int& f) const + { + /* Do this calculation with frames so that we can round + to a frame boundary at the start rather than the end. + */ + int64_t ff = frames (r); + + h = ff / (3600 * r); + ff -= h * 3600 * r; + m = ff / (60 * r); + ff -= m * 60 * r; + s = ff / r; + ff -= s * r; + + f = static_cast<int> (ff); + } + + template <typename T> + std::string timecode (T r) const { + int h; + int m; + int s; + int f; + split (r, h, m, s, f); + + SafeStringStream o; + o.width (2); + o.fill ('0'); + o << std::setw(2) << std::setfill('0') << h << ":" + << std::setw(2) << std::setfill('0') << m << ":" + << std::setw(2) << std::setfill('0') << s << ":" + << std::setw(2) << std::setfill('0') << f; + return o.str (); + } + +protected: + friend struct dcptime_round_up_test; + + Type _t; + static const int HZ = 96000; +}; + +class DCPTime; + +class ContentTime : public Time +{ +public: + ContentTime () : Time () {} + explicit ContentTime (Type t) : Time (t) {} + ContentTime (Type n, Type d) : Time (n * HZ / d) {} + ContentTime (DCPTime d, FrameRateChange f); + + bool operator< (ContentTime const & o) const { + return _t < o._t; + } + + bool operator<= (ContentTime const & o) const { + return _t <= o._t; + } + + bool operator== (ContentTime const & o) const { + return _t == o._t; + } + + bool operator!= (ContentTime const & o) const { + return _t != o._t; + } + + bool operator>= (ContentTime const & o) const { + return _t >= o._t; + } + + bool operator> (ContentTime const & o) const { + return _t > o._t; + } + + ContentTime operator+ (ContentTime const & o) const { + return ContentTime (_t + o._t); + } + + ContentTime & operator+= (ContentTime const & o) { + _t += o._t; + return *this; + } + + ContentTime operator- () const { + return ContentTime (-_t); + } + + ContentTime operator- (ContentTime const & o) const { + return ContentTime (_t - o._t); + } + + ContentTime & operator-= (ContentTime const & o) { + _t -= o._t; + return *this; + } + + /** Round up to the nearest sampling interval + * at some sampling rate. + * @param r Sampling rate. + */ + ContentTime round_up (float r) { + Type const n = rint (HZ / r); + Type const a = _t + n - 1; + return ContentTime (a - (a % n)); + } + + static ContentTime from_seconds (double s) { + return ContentTime (s * HZ); + } + + template <class T> + static ContentTime from_frames (int64_t f, T r) { + assert (r > 0); + return ContentTime (f * HZ / r); + } + + static ContentTime max () { + return ContentTime (INT64_MAX); + } +}; + +std::ostream& operator<< (std::ostream& s, ContentTime t); + +class ContentTimePeriod +{ +public: + ContentTimePeriod () {} + ContentTimePeriod (ContentTime f, ContentTime t) + : from (f) + , to (t) + {} + + ContentTime from; + ContentTime to; + + ContentTimePeriod operator+ (ContentTime const & o) const { + return ContentTimePeriod (from + o, to + o); + } + + bool overlaps (ContentTimePeriod const & o) const; + bool contains (ContentTime const & o) const; +}; + +class DCPTime : public Time +{ +public: + DCPTime () : Time () {} + explicit DCPTime (Type t) : Time (t) {} + DCPTime (ContentTime t, FrameRateChange c) : Time (rint (t.get() / c.speed_up)) {} + + bool operator< (DCPTime const & o) const { + return _t < o._t; + } + + bool operator<= (DCPTime const & o) const { + return _t <= o._t; + } + + bool operator== (DCPTime const & o) const { + return _t == o._t; + } + + bool operator!= (DCPTime const & o) const { + return _t != o._t; + } + + bool operator>= (DCPTime const & o) const { + return _t >= o._t; + } + + bool operator> (DCPTime const & o) const { + return _t > o._t; + } + + DCPTime operator+ (DCPTime const & o) const { + return DCPTime (_t + o._t); + } + + DCPTime & operator+= (DCPTime const & o) { + _t += o._t; + return *this; + } + + DCPTime operator- () const { + return DCPTime (-_t); + } + + DCPTime operator- (DCPTime const & o) const { + return DCPTime (_t - o._t); + } + + DCPTime & operator-= (DCPTime const & o) { + _t -= o._t; + return *this; + } + + /** Round up to the nearest sampling interval + * at some sampling rate. + * @param r Sampling rate. + */ + DCPTime round_up (float r) { + Type const n = rint (HZ / r); + Type const a = _t + n - 1; + return DCPTime (a - (a % n)); + } + + DCPTime abs () const { + return DCPTime (std::abs (_t)); + } + + static DCPTime from_seconds (double s) { + return DCPTime (s * HZ); + } + + template <class T> + static DCPTime from_frames (int64_t f, T r) { + assert (r > 0); + return DCPTime (f * HZ / r); + } + + static DCPTime delta () { + return DCPTime (1); + } + + static DCPTime max () { + return DCPTime (INT64_MAX); + } +}; + +DCPTime min (DCPTime a, DCPTime b); +std::ostream& operator<< (std::ostream& s, DCPTime t); + +#endif diff --git a/src/lib/decoder.h b/src/lib/decoder.h index d67592ed8..583a92636 100644 --- a/src/lib/decoder.h +++ b/src/lib/decoder.h @@ -18,7 +18,7 @@ */ /** @file src/decoder.h - * @brief Parent class for decoders of content. + * @brief Decoder class. */ #ifndef DCPOMATIC_DECODER_H @@ -27,8 +27,10 @@ #include <boost/shared_ptr.hpp> #include <boost/weak_ptr.hpp> #include <boost/utility.hpp> +#include "types.h" +#include "dcpomatic_time.h" -class Film; +class Decoded; /** @class Decoder. * @brief Parent class for decoders of content. @@ -36,21 +38,19 @@ class Film; class Decoder : public boost::noncopyable { public: - Decoder (boost::shared_ptr<const Film>); virtual ~Decoder () {} - /** Perform one decode pass of the content, which may or may not - * cause the object to emit some data. +protected: + /** Seek so that the next pass() will yield the next thing + * (video/sound frame, subtitle etc.) at or after the requested + * time. Pass accurate = true to try harder to ensure that, at worst, + * the next thing we yield comes before `time'. This may entail + * seeking some way before `time' to be on the safe side. + * Alternatively, if seeking is 100% accurate for this decoder, + * it may seek to just the right spot. */ - virtual void pass () = 0; - virtual bool done () const = 0; - -protected: - - virtual void flush () {}; - - /** The Film that we are decoding in */ - boost::weak_ptr<const Film> _film; + virtual void seek (ContentTime time, bool accurate) = 0; + virtual bool pass () = 0; }; #endif diff --git a/src/lib/dolby_cp750.cc b/src/lib/dolby_cp750.cc index aeb469d29..317d129d9 100644 --- a/src/lib/dolby_cp750.cc +++ b/src/lib/dolby_cp750.cc @@ -24,7 +24,7 @@ using namespace std; DolbyCP750::DolbyCP750 () - : SoundProcessor ("dolby_cp750", _("Dolby CP650 and CP750")) + : CinemaSoundProcessor ("dolby_cp750", _("Dolby CP650 and CP750")) { } diff --git a/src/lib/dolby_cp750.h b/src/lib/dolby_cp750.h index b6c0e7df2..c545844fe 100644 --- a/src/lib/dolby_cp750.h +++ b/src/lib/dolby_cp750.h @@ -17,9 +17,9 @@ */ -#include "sound_processor.h" +#include "cinema_sound_processor.h" -class DolbyCP750 : public SoundProcessor +class DolbyCP750 : public CinemaSoundProcessor { public: DolbyCP750 (); diff --git a/src/lib/encoded_data.cc b/src/lib/encoded_data.cc new file mode 100644 index 000000000..61d2644da --- /dev/null +++ b/src/lib/encoded_data.cc @@ -0,0 +1,122 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "encoded_data.h" +#include "cross.h" +#include "exceptions.h" +#include "film.h" + +#include "i18n.h" + +using boost::shared_ptr; + +EncodedData::EncodedData (int s) + : _data (new uint8_t[s]) + , _size (s) +{ + +} + +EncodedData::EncodedData (uint8_t const * d, int s) + : _data (new uint8_t[s]) + , _size (s) +{ + memcpy (_data, d, s); +} + +EncodedData::EncodedData (boost::filesystem::path file) +{ + _size = boost::filesystem::file_size (file); + _data = new uint8_t[_size]; + + FILE* f = fopen_boost (file, "rb"); + if (!f) { + throw FileError (_("could not open file for reading"), file); + } + + size_t const r = fread (_data, 1, _size, f); + if (r != size_t (_size)) { + fclose (f); + throw FileError (_("could not read encoded data"), file); + } + + fclose (f); +} + + +EncodedData::~EncodedData () +{ + delete[] _data; +} + +/** Write this data to a J2K file. + * @param Film Film. + * @param frame DCP frame index. + */ +void +EncodedData::write (shared_ptr<const Film> film, int frame, Eyes eyes) const +{ + boost::filesystem::path const tmp_j2c = film->j2c_path (frame, eyes, true); + + FILE* f = fopen_boost (tmp_j2c, "wb"); + + if (!f) { + throw WriteFileError (tmp_j2c, errno); + } + + fwrite (_data, 1, _size, f); + fclose (f); + + boost::filesystem::path const real_j2c = film->j2c_path (frame, eyes, false); + + /* Rename the file from foo.j2c.tmp to foo.j2c now that it is complete */ + boost::filesystem::rename (tmp_j2c, real_j2c); +} + +void +EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, dcp::FrameInfo fin) const +{ + boost::filesystem::path const info = film->info_path (frame, eyes); + FILE* h = fopen_boost (info, "w"); + fin.write (h); + fclose (h); +} + +/** Send this data to a socket. + * @param socket Socket + */ +void +EncodedData::send (shared_ptr<Socket> socket) +{ + socket->write (_size); + socket->write (_data, _size); +} + +LocallyEncodedData::LocallyEncodedData (uint8_t* d, int s) + : EncodedData (s) +{ + memcpy (_data, d, s); +} + +/** @param s Size of data in bytes */ +RemotelyEncodedData::RemotelyEncodedData (int s) + : EncodedData (s) +{ + +} diff --git a/src/lib/dcp_video_frame.h b/src/lib/encoded_data.h index e4006d986..232ed6e8a 100644 --- a/src/lib/dcp_video_frame.h +++ b/src/lib/encoded_data.h @@ -1,6 +1,5 @@ /* Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> - Taken from code Copyright (C) 2010-2011 Terrence Meiczinger This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,22 +17,13 @@ */ -#include <openjpeg.h> -#include <libdcp/picture_asset.h> -#include <libdcp/picture_asset_writer.h> -#include "util.h" - -/** @file src/dcp_video_frame.h - * @brief A single frame of video destined for a DCP. - */ +#include <boost/noncopyable.hpp> +#include <boost/filesystem.hpp> +#include <dcp/picture_mxf_writer.h> +#include "types.h" +class Socket; class Film; -class ServerDescription; -class Scaler; -class Image; -class Log; -class Subtitle; -class PlayerVideoFrame; /** @class EncodedData * @brief Container for J2K-encoded data. @@ -43,6 +33,7 @@ class EncodedData : public boost::noncopyable public: /** @param s Size of data, in bytes */ EncodedData (int s); + EncodedData (uint8_t const * d, int s); EncodedData (boost::filesystem::path); @@ -50,7 +41,7 @@ public: void send (boost::shared_ptr<Socket> socket); void write (boost::shared_ptr<const Film>, int, Eyes) const; - void write_info (boost::shared_ptr<const Film>, int, Eyes, libdcp::FrameInfo) const; + void write_info (boost::shared_ptr<const Film>, int, Eyes, dcp::FrameInfo) const; /** @return data */ uint8_t* data () const { @@ -90,40 +81,3 @@ class RemotelyEncodedData : public EncodedData public: RemotelyEncodedData (int s); }; - -/** @class DCPVideoFrame - * @brief A single frame of video destined for a DCP. - * - * Given an Image and some settings, this class knows how to encode - * the image to J2K either on the local host or on a remote server. - * - * Objects of this class are used for the queue that we keep - * of images that require encoding. - */ -class DCPVideoFrame : public boost::noncopyable -{ -public: - DCPVideoFrame (boost::shared_ptr<const PlayerVideoFrame>, int, int, int, Resolution, boost::shared_ptr<Log>); - DCPVideoFrame (boost::shared_ptr<const PlayerVideoFrame>, boost::shared_ptr<const cxml::Node>, boost::shared_ptr<Log>); - - boost::shared_ptr<EncodedData> encode_locally (); - boost::shared_ptr<EncodedData> encode_remotely (ServerDescription); - - int index () const { - return _index; - } - - Eyes eyes () const; - -private: - - void add_metadata (xmlpp::Element *) const; - - boost::shared_ptr<const PlayerVideoFrame> _frame; - int _index; ///< frame index within the DCP's intrinsic duration - int _frames_per_second; ///< Frames per second that we will use for the DCP - int _j2k_bandwidth; ///< J2K bandwidth to use - Resolution _resolution; ///< Resolution (2K or 4K) - - boost::shared_ptr<Log> _log; ///< log -}; diff --git a/src/lib/encoder.cc b/src/lib/encoder.cc index 693fd587e..0c9faa70d 100644 --- a/src/lib/encoder.cc +++ b/src/lib/encoder.cc @@ -29,13 +29,13 @@ #include "film.h" #include "log.h" #include "config.h" -#include "dcp_video_frame.h" +#include "dcp_video.h" #include "server.h" #include "cross.h" #include "writer.h" #include "server_finder.h" #include "player.h" -#include "player_video_frame.h" +#include "player_video.h" #include "i18n.h" @@ -58,15 +58,14 @@ using boost::scoped_array; int const Encoder::_history_size = 25; /** @param f Film that we are encoding */ -Encoder::Encoder (shared_ptr<const Film> f, weak_ptr<Job> j) +Encoder::Encoder (shared_ptr<const Film> f, weak_ptr<Job> j, shared_ptr<Writer> writer) : _film (f) , _job (j) , _video_frames_out (0) , _terminate (false) + , _writer (writer) { - _have_a_real_frame[EYES_BOTH] = false; - _have_a_real_frame[EYES_LEFT] = false; - _have_a_real_frame[EYES_RIGHT] = false; + } Encoder::~Encoder () @@ -88,18 +87,17 @@ Encoder::add_worker_threads (ServerDescription d) } void -Encoder::process_begin () +Encoder::begin () { for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) { _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, optional<ServerDescription> ()))); } - _writer.reset (new Writer (_film, _job)); ServerFinder::instance()->connect (boost::bind (&Encoder::server_found, this, _1)); } void -Encoder::process_end () +Encoder::end () { boost::mutex::scoped_lock lock (_mutex); @@ -126,7 +124,7 @@ Encoder::process_end () So just mop up anything left in the queue here. */ - for (list<shared_ptr<DCPVideoFrame> >::iterator i = _queue.begin(); i != _queue.end(); ++i) { + for (list<shared_ptr<DCPVideo> >::iterator i = _queue.begin(); i != _queue.end(); ++i) { LOG_GENERAL (N_("Encode left-over frame %1"), (*i)->index ()); try { _writer->write ((*i)->encode_locally(), (*i)->index (), (*i)->eyes ()); @@ -135,9 +133,6 @@ Encoder::process_end () LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); } } - - _writer->finish (); - _writer.reset (); } /** @return an estimate of the current number of frames we are encoding per second, @@ -181,8 +176,11 @@ Encoder::frame_done () } } +/** Called in order, so each time this is called the supplied frame is the one + * after the previous one. + */ void -Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf, bool same) +Encoder::enqueue (shared_ptr<PlayerVideo> pv) { _waker.nudge (); @@ -209,20 +207,24 @@ Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf, bool same) rethrow (); if (_writer->can_fake_write (_video_frames_out)) { - _writer->fake_write (_video_frames_out, pvf->eyes ()); - _have_a_real_frame[pvf->eyes()] = false; - frame_done (); - } else if (same && _have_a_real_frame[pvf->eyes()]) { - /* Use the last frame that we encoded. */ - _writer->repeat (_video_frames_out, pvf->eyes()); + /* We can fake-write this frame */ + _writer->fake_write (_video_frames_out, pv->eyes ()); frame_done (); + } else if (pv->has_j2k ()) { + /* This frame already has JPEG2000 data, so just write it */ + _writer->write (pv->j2k(), _video_frames_out, pv->eyes ()); } else { /* Queue this new frame for encoding */ LOG_TIMING ("adding to queue of %1", _queue.size ()); - _queue.push_back (shared_ptr<DCPVideoFrame> ( - new DCPVideoFrame ( - pvf, _video_frames_out, _film->video_frame_rate(), - _film->j2k_bandwidth(), _film->resolution(), _film->log() + _queue.push_back (shared_ptr<DCPVideo> ( + new DCPVideo ( + pv, + _video_frames_out, + _film->video_frame_rate(), + _film->j2k_bandwidth(), + _film->resolution(), + _film->burn_subtitles(), + _film->log() ) )); @@ -230,21 +232,14 @@ Encoder::process_video (shared_ptr<PlayerVideoFrame> pvf, bool same) waiting on that. */ _empty_condition.notify_all (); - _have_a_real_frame[pvf->eyes()] = true; } - if (pvf->eyes() != EYES_LEFT) { + if (pv->eyes() != EYES_LEFT) { ++_video_frames_out; } } void -Encoder::process_audio (shared_ptr<const AudioBuffers> data) -{ - _writer->write (data); -} - -void Encoder::terminate_threads () { { @@ -273,6 +268,8 @@ try encodings. */ int remote_backoff = 0; + shared_ptr<DCPVideo> last_dcp_video; + shared_ptr<EncodedData> last_encoded; while (true) { @@ -287,7 +284,7 @@ try } LOG_TIMING ("[%1] encoder thread wakes with queue of %2", boost::this_thread::get_id(), _queue.size()); - shared_ptr<DCPVideoFrame> vf = _queue.front (); + shared_ptr<DCPVideo> vf = _queue.front (); LOG_TIMING ("[%1] encoder thread pops frame %2 (%3) from queue", boost::this_thread::get_id(), vf->index(), vf->eyes ()); _queue.pop_front (); @@ -295,38 +292,47 @@ try shared_ptr<EncodedData> encoded; - if (server) { - try { - encoded = vf->encode_remotely (server.get ()); - - if (remote_backoff > 0) { - LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ()); + if (last_dcp_video && vf->same (last_dcp_video)) { + /* We already have encoded data for the same input as this one, so take a short-cut */ + encoded = last_encoded; + } else { + /* We need to encode this input */ + if (server) { + try { + encoded = vf->encode_remotely (server.get ()); + + if (remote_backoff > 0) { + LOG_GENERAL ("%1 was lost, but now she is found; removing backoff", server->host_name ()); + } + + /* This job succeeded, so remove any backoff */ + remote_backoff = 0; + + } catch (std::exception& e) { + if (remote_backoff < 60) { + /* back off more */ + remote_backoff += 10; + } + LOG_ERROR ( + N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"), + vf->index(), server->host_name(), e.what(), remote_backoff + ); } - /* This job succeeded, so remove any backoff */ - remote_backoff = 0; - - } catch (std::exception& e) { - if (remote_backoff < 60) { - /* back off more */ - remote_backoff += 10; + } else { + try { + LOG_TIMING ("[%1] encoder thread begins local encode of %2", boost::this_thread::get_id(), vf->index()); + encoded = vf->encode_locally (); + LOG_TIMING ("[%1] encoder thread finishes local encode of %2", boost::this_thread::get_id(), vf->index()); + } catch (std::exception& e) { + LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); } - LOG_ERROR ( - N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"), - vf->index(), server->host_name(), e.what(), remote_backoff - ); - } - - } else { - try { - LOG_TIMING ("[%1] encoder thread begins local encode of %2", boost::this_thread::get_id(), vf->index()); - encoded = vf->encode_locally (); - LOG_TIMING ("[%1] encoder thread finishes local encode of %2", boost::this_thread::get_id(), vf->index()); - } catch (std::exception& e) { - LOG_ERROR (N_("Local encode failed (%1)"), e.what ()); } } + last_dcp_video = vf; + last_encoded = encoded; + if (encoded) { _writer->write (encoded, vf->index (), vf->eyes ()); frame_done (); diff --git a/src/lib/encoder.h b/src/lib/encoder.h index 8d5aa2c40..51df0176b 100644 --- a/src/lib/encoder.h +++ b/src/lib/encoder.h @@ -38,45 +38,42 @@ extern "C" { #include "util.h" #include "config.h" #include "cross.h" +#include "exceptions.h" class Image; class AudioBuffers; class Film; class ServerDescription; -class DCPVideoFrame; +class DCPVideo; class EncodedData; class Writer; class Job; class ServerFinder; -class PlayerVideoFrame; +class PlayerVideo; /** @class Encoder - * @brief Encoder to J2K and WAV for DCP. + * @brief Class to manage encoding to JPEG2000. * - * Video is supplied to process_video as RGB frames, and audio - * is supplied as uncompressed PCM in blocks of various sizes. + * This class keeps a queue of frames to be encoded and distributes + * the work around threads and encoding servers. */ class Encoder : public boost::noncopyable, public ExceptionStore { public: - Encoder (boost::shared_ptr<const Film> f, boost::weak_ptr<Job>); + Encoder (boost::shared_ptr<const Film> f, boost::weak_ptr<Job>, boost::shared_ptr<Writer>); virtual ~Encoder (); /** Called to indicate that a processing run is about to begin */ - void process_begin (); + void begin (); /** Call with a frame of video. - * @param pvf Video frame image. - * @param same true if pvf is the same as the last time we were called. + * @param f Video frame. */ - void process_video (boost::shared_ptr<PlayerVideoFrame> pvf, bool same); - - /** Call with some audio data */ - void process_audio (boost::shared_ptr<const AudioBuffers>); + void enqueue (boost::shared_ptr<PlayerVideo> f); /** Called when a processing run has finished */ - void process_end (); + void end (); float current_encoding_rate () const; int video_frames_out () const; @@ -106,9 +103,8 @@ private: /** Number of video frames written for the DCP so far */ int _video_frames_out; - bool _have_a_real_frame[EYES_COUNT]; bool _terminate; - std::list<boost::shared_ptr<DCPVideoFrame> > _queue; + std::list<boost::shared_ptr<DCPVideo> > _queue; std::list<boost::thread *> _threads; mutable boost::mutex _mutex; /** condition to manage thread wakeups when we have nothing to do */ diff --git a/src/lib/exceptions.cc b/src/lib/exceptions.cc index 8144f41b9..95d4c8cce 100644 --- a/src/lib/exceptions.cc +++ b/src/lib/exceptions.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -56,8 +56,20 @@ MissingSettingError::MissingSettingError (string s) } -PixelFormatError::PixelFormatError (std::string o, AVPixelFormat f) +PixelFormatError::PixelFormatError (string o, AVPixelFormat f) : StringError (String::compose (_("Cannot handle pixel format %1 during %2"), f, o)) { } + +SubRipError::SubRipError (string saw, string expecting, boost::filesystem::path f) + : FileError (String::compose (_("Error in SubRip file: saw %1 while expecting %2"), saw.empty() ? "[nothing]" : saw, expecting), f) +{ + +} + +InvalidSignerError::InvalidSignerError () + : StringError (_("The certificate chain for signing is invalid")) +{ + +} diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 3423a5754..52f257a8d 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,13 +17,13 @@ */ -#ifndef DCPOMATIC_EXCEPTIONS_H -#define DCPOMATIC_EXCEPTIONS_H - -/** @file src/exceptions.h +/** @file src/lib/exceptions.h * @brief Our exceptions. */ +#ifndef DCPOMATIC_EXCEPTIONS_H +#define DCPOMATIC_EXCEPTIONS_H + #include <stdexcept> #include <cstring> #include <boost/exception/all.hpp> @@ -205,7 +205,7 @@ public: {} }; -/** @class NetworkError. +/** @class NetworkError * @brief Indicates some problem with communication on the network. */ class NetworkError : public StringError @@ -216,6 +216,9 @@ public: {} }; +/** @class KDMError + * @brief A problem with a KDM. + */ class KDMError : public StringError { public: @@ -224,15 +227,44 @@ public: {} }; +/** @class PixelFormatError + * @brief A problem with an unsupported pixel format. + */ class PixelFormatError : public StringError { public: PixelFormatError (std::string o, AVPixelFormat f); }; -/** A parent class for classes which have a need to catch and - * re-throw exceptions. This is intended for classes - * which run their own thread; they should do something like +/** @class SubRipError + * @brief An error that occurs while parsing a SubRip file. + */ +class SubRipError : public FileError +{ +public: + SubRipError (std::string, std::string, boost::filesystem::path); +}; + +class DCPError : public StringError +{ +public: + DCPError (std::string s) + : StringError (s) + {} +}; + +class InvalidSignerError : public StringError +{ +public: + InvalidSignerError (); +}; + +/** @class ExceptionStore + * @brief A parent class for classes which have a need to catch and + * re-throw exceptions. + * + * This is intended for classes which run their own thread; they should do + * something like * * void my_thread () * try { @@ -269,6 +301,4 @@ private: mutable boost::mutex _mutex; }; - - #endif diff --git a/src/lib/ffmpeg.cc b/src/lib/ffmpeg.cc index ebe62b51f..fa369dda4 100644 --- a/src/lib/ffmpeg.cc +++ b/src/lib/ffmpeg.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -22,9 +22,11 @@ extern "C" { #include <libavformat/avformat.h> #include <libswscale/swscale.h> } -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "ffmpeg.h" #include "ffmpeg_content.h" +#include "ffmpeg_audio_stream.h" +#include "ffmpeg_subtitle_stream.h" #include "exceptions.h" #include "util.h" @@ -33,7 +35,7 @@ extern "C" { using std::string; using std::cout; using boost::shared_ptr; -using libdcp::raw_convert; +using dcp::raw_convert; boost::mutex FFmpeg::_mutex; @@ -47,8 +49,7 @@ FFmpeg::FFmpeg (boost::shared_ptr<const FFmpegContent> c) , _video_stream (-1) { setup_general (); - setup_video (); - setup_audio (); + setup_decoders (); } FFmpeg::~FFmpeg () @@ -56,14 +57,10 @@ FFmpeg::~FFmpeg () boost::mutex::scoped_lock lm (_mutex); for (uint32_t i = 0; i < _format_context->nb_streams; ++i) { - AVCodecContext* context = _format_context->streams[i]->codec; - if (context->codec_type == AVMEDIA_TYPE_VIDEO || context->codec_type == AVMEDIA_TYPE_AUDIO) { - avcodec_close (context); - } + avcodec_close (_format_context->streams[i]->codec); } av_frame_free (&_frame); - avformat_close_input (&_format_context); } @@ -143,46 +140,24 @@ FFmpeg::setup_general () } void -FFmpeg::setup_video () -{ - boost::mutex::scoped_lock lm (_mutex); - - assert (_video_stream >= 0); - AVCodecContext* context = _format_context->streams[_video_stream]->codec; - AVCodec* codec = avcodec_find_decoder (context->codec_id); - - if (codec == 0) { - throw DecodeError (_("could not find video decoder")); - } - - if (avcodec_open2 (context, codec, 0) < 0) { - throw DecodeError (N_("could not open video decoder")); - } -} - -void -FFmpeg::setup_audio () +FFmpeg::setup_decoders () { boost::mutex::scoped_lock lm (_mutex); for (uint32_t i = 0; i < _format_context->nb_streams; ++i) { AVCodecContext* context = _format_context->streams[i]->codec; - if (context->codec_type != AVMEDIA_TYPE_AUDIO) { - continue; - } AVCodec* codec = avcodec_find_decoder (context->codec_id); - if (codec == 0) { - throw DecodeError (_("could not find audio decoder")); - } - - if (avcodec_open2 (context, codec, 0) < 0) { - throw DecodeError (N_("could not open audio decoder")); + if (codec) { + if (avcodec_open2 (context, codec, 0) < 0) { + throw DecodeError (N_("could not open decoder")); + } } + + /* We are silently ignoring any failures to find suitable decoders here */ } } - AVCodecContext * FFmpeg::video_codec_context () const { @@ -192,9 +167,23 @@ FFmpeg::video_codec_context () const AVCodecContext * FFmpeg::audio_codec_context () const { + if (!_ffmpeg_content->audio_stream ()) { + return 0; + } + return _ffmpeg_content->audio_stream()->stream(_format_context)->codec; } +AVCodecContext * +FFmpeg::subtitle_codec_context () const +{ + if (!_ffmpeg_content->subtitle_stream ()) { + return 0; + } + + return _ffmpeg_content->subtitle_stream()->stream(_format_context)->codec; +} + int FFmpeg::avio_read (uint8_t* buffer, int const amount) { diff --git a/src/lib/ffmpeg.h b/src/lib/ffmpeg.h index 760918437..8aaa54f84 100644 --- a/src/lib/ffmpeg.h +++ b/src/lib/ffmpeg.h @@ -56,6 +56,7 @@ public: protected: AVCodecContext* video_codec_context () const; AVCodecContext* audio_codec_context () const; + AVCodecContext* subtitle_codec_context () const; boost::shared_ptr<const FFmpegContent> _ffmpeg_content; @@ -79,8 +80,7 @@ protected: private: void setup_general (); - void setup_video (); - void setup_audio (); + void setup_decoders (); }; #endif diff --git a/src/lib/ffmpeg_audio_stream.cc b/src/lib/ffmpeg_audio_stream.cc new file mode 100644 index 000000000..d8666e89e --- /dev/null +++ b/src/lib/ffmpeg_audio_stream.cc @@ -0,0 +1,47 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <libxml++/libxml++.h> +#include <libcxml/cxml.h> +#include <dcp/raw_convert.h> +#include "ffmpeg_audio_stream.h" + +using std::string; +using dcp::raw_convert; + +FFmpegAudioStream::FFmpegAudioStream (cxml::ConstNodePtr node, int version) + : FFmpegStream (node) + , _frame_rate (node->number_child<int> ("FrameRate")) + , _channels (node->number_child<int64_t> ("Channels")) + , _mapping (node->node_child ("Mapping"), version) +{ + first_audio = node->optional_number_child<double> ("FirstAudio"); +} + +void +FFmpegAudioStream::as_xml (xmlpp::Node* root) const +{ + FFmpegStream::as_xml (root); + root->add_child("FrameRate")->add_child_text (raw_convert<string> (_frame_rate)); + root->add_child("Channels")->add_child_text (raw_convert<string> (_channels)); + if (first_audio) { + root->add_child("FirstAudio")->add_child_text (raw_convert<string> (first_audio.get().get())); + } + _mapping.as_xml (root->add_child("Mapping")); +} diff --git a/src/lib/ffmpeg_audio_stream.h b/src/lib/ffmpeg_audio_stream.h new file mode 100644 index 000000000..1587afcae --- /dev/null +++ b/src/lib/ffmpeg_audio_stream.h @@ -0,0 +1,74 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "ffmpeg_stream.h" +#include "audio_mapping.h" +#include "dcpomatic_time.h" + +struct ffmpeg_pts_offset_test; + +class FFmpegAudioStream : public FFmpegStream +{ +public: + FFmpegAudioStream (std::string n, int i, int f, int c) + : FFmpegStream (n, i) + , _frame_rate (f) + , _channels (c) + , _mapping (c) + { + _mapping.make_default (); + } + + FFmpegAudioStream (cxml::ConstNodePtr, int); + + void as_xml (xmlpp::Node *) const; + + int frame_rate () const { + return _frame_rate; + } + + int channels () const { + return _channels; + } + + AudioMapping mapping () const { + return _mapping; + } + + void set_mapping (AudioMapping m) { + _mapping = m; + } + + boost::optional<ContentTime> first_audio; + +private: + friend struct ffmpeg_pts_offset_test; + + /* Constructor for tests */ + FFmpegAudioStream () + : FFmpegStream ("", 0) + , _frame_rate (0) + , _channels (0) + , _mapping (1) + {} + + int _frame_rate; + int _channels; + AudioMapping _mapping; +}; diff --git a/src/lib/ffmpeg_content.cc b/src/lib/ffmpeg_content.cc index a4209f5b6..bb4e02230 100644 --- a/src/lib/ffmpeg_content.cc +++ b/src/lib/ffmpeg_content.cc @@ -21,9 +21,11 @@ extern "C" { #include <libavformat/avformat.h> } #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "ffmpeg_content.h" #include "ffmpeg_examiner.h" +#include "ffmpeg_subtitle_stream.h" +#include "ffmpeg_audio_stream.h" #include "compose.hpp" #include "job.h" #include "util.h" @@ -45,7 +47,7 @@ using std::cout; using std::pair; using boost::shared_ptr; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; int const FFmpegContentProperty::SUBTITLE_STREAMS = 100; int const FFmpegContentProperty::SUBTITLE_STREAM = 101; @@ -62,7 +64,7 @@ FFmpegContent::FFmpegContent (shared_ptr<const Film> f, boost::filesystem::path } -FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version, list<string>& notes) +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version, list<string>& notes) : Content (f, node) , VideoContent (f, node, version) , AudioContent (f, node) @@ -108,7 +110,7 @@ FFmpegContent::FFmpegContent (shared_ptr<const Film> f, vector<boost::shared_ptr for (size_t i = 0; i < c.size(); ++i) { shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c[i]); - if (f->with_subtitles() && *(fc->_subtitle_stream.get()) != *(ref->_subtitle_stream.get())) { + if (fc->use_subtitles() && *(fc->_subtitle_stream.get()) != *(ref->_subtitle_stream.get())) { throw JoinError (_("Content to be joined must use the same subtitle stream.")); } @@ -156,7 +158,7 @@ FFmpegContent::as_xml (xmlpp::Node* node) const } if (_first_video) { - node->add_child("FirstVideo")->add_child_text (raw_convert<string> (_first_video.get ())); + node->add_child("FirstVideo")->add_child_text (raw_convert<string> (_first_video.get().get())); } } @@ -167,20 +169,15 @@ FFmpegContent::examine (shared_ptr<Job> job) Content::examine (job); - shared_ptr<const Film> film = _film.lock (); - assert (film); - shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ())); + take_from_video_examiner (examiner); - VideoContent::Frame video_length = 0; - video_length = examiner->video_length (); - LOG_GENERAL ("Video length obtained from header as %1 frames", video_length); + shared_ptr<const Film> film = _film.lock (); + assert (film); { boost::mutex::scoped_lock lm (_mutex); - _video_length = video_length; - _subtitle_streams = examiner->subtitle_streams (); if (!_subtitle_streams.empty ()) { _subtitle_stream = _subtitle_streams.front (); @@ -194,9 +191,6 @@ FFmpegContent::examine (shared_ptr<Job> job) _first_video = examiner->first_video (); } - take_from_video_examiner (examiner); - - signal_changed (ContentProperty::LENGTH); signal_changed (FFmpegContentProperty::SUBTITLE_STREAMS); signal_changed (FFmpegContentProperty::SUBTITLE_STREAM); signal_changed (FFmpegContentProperty::AUDIO_STREAMS); @@ -237,13 +231,13 @@ FFmpegContent::technical_summary () const string FFmpegContent::information () const { - if (video_length() == 0 || video_frame_rate() == 0) { + if (video_length() == ContentTime (0) || video_frame_rate() == 0) { return ""; } SafeStringStream s; - s << String::compose (_("%1 frames; %2 frames per second"), video_length_after_3d_combine(), video_frame_rate()) << "\n"; + s << String::compose (_("%1 frames; %2 frames per second"), video_length_after_3d_combine().frames (video_frame_rate()), video_frame_rate()) << "\n"; s << VideoContent::information (); return s.str (); @@ -271,19 +265,14 @@ FFmpegContent::set_audio_stream (shared_ptr<FFmpegAudioStream> s) signal_changed (FFmpegContentProperty::AUDIO_STREAM); } -AudioContent::Frame +ContentTime FFmpegContent::audio_length () const { - int const cafr = content_audio_frame_rate (); - float const vfr = video_frame_rate (); - VideoContent::Frame const vl = video_length_after_3d_combine (); - - boost::mutex::scoped_lock lm (_mutex); - if (!_audio_stream) { - return 0; + if (!audio_stream ()) { + return ContentTime (); } - - return video_frames_to_audio_frames (vl, cafr, vfr); + + return video_length (); } int @@ -295,11 +284,11 @@ FFmpegContent::audio_channels () const return 0; } - return _audio_stream->channels; + return _audio_stream->channels (); } int -FFmpegContent::content_audio_frame_rate () const +FFmpegContent::audio_frame_rate () const { boost::mutex::scoped_lock lm (_mutex); @@ -307,7 +296,7 @@ FFmpegContent::content_audio_frame_rate () const return 0; } - return _audio_stream->frame_rate; + return _audio_stream->frame_rate (); } bool @@ -322,94 +311,12 @@ operator!= (FFmpegStream const & a, FFmpegStream const & b) return a._id != b._id; } -FFmpegStream::FFmpegStream (shared_ptr<const cxml::Node> node) - : name (node->string_child ("Name")) - , _id (node->number_child<int> ("Id")) -{ - -} - -void -FFmpegStream::as_xml (xmlpp::Node* root) const -{ - root->add_child("Name")->add_child_text (name); - root->add_child("Id")->add_child_text (raw_convert<string> (_id)); -} - -FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node, int version) - : FFmpegStream (node) - , mapping (node->node_child ("Mapping"), version) -{ - frame_rate = node->number_child<int> ("FrameRate"); - channels = node->number_child<int64_t> ("Channels"); - first_audio = node->optional_number_child<double> ("FirstAudio"); -} - -void -FFmpegAudioStream::as_xml (xmlpp::Node* root) const -{ - FFmpegStream::as_xml (root); - root->add_child("FrameRate")->add_child_text (raw_convert<string> (frame_rate)); - root->add_child("Channels")->add_child_text (raw_convert<string> (channels)); - if (first_audio) { - root->add_child("FirstAudio")->add_child_text (raw_convert<string> (first_audio.get ())); - } - mapping.as_xml (root->add_child("Mapping")); -} - -bool -FFmpegStream::uses_index (AVFormatContext const * fc, int index) const -{ - size_t i = 0; - while (i < fc->nb_streams) { - if (fc->streams[i]->id == _id) { - return int (i) == index; - } - ++i; - } - - return false; -} - -AVStream * -FFmpegStream::stream (AVFormatContext const * fc) const -{ - size_t i = 0; - while (i < fc->nb_streams) { - if (fc->streams[i]->id == _id) { - return fc->streams[i]; - } - ++i; - } - - assert (false); - return 0; -} - -/** 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) - : FFmpegStream (node) -{ - -} - -void -FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const -{ - FFmpegStream::as_xml (root); -} - -Time +DCPTime FFmpegContent::full_length () const { shared_ptr<const Film> film = _film.lock (); assert (film); - - FrameRateChange frc (video_frame_rate (), film->video_frame_rate ()); - return video_length_after_3d_combine() * frc.factor() * TIME_HZ / film->video_frame_rate (); + return DCPTime (video_length_after_3d_combine(), FrameRateChange (video_frame_rate (), film->video_frame_rate ())); } AudioMapping @@ -421,7 +328,7 @@ FFmpegContent::audio_mapping () const return AudioMapping (); } - return _audio_stream->mapping; + return _audio_stream->mapping (); } void @@ -438,8 +345,8 @@ FFmpegContent::set_filters (vector<Filter const *> const & filters) void FFmpegContent::set_audio_mapping (AudioMapping m) { - audio_stream()->mapping = m; - signal_changed (AudioContentProperty::AUDIO_MAPPING); + audio_stream()->set_mapping (m); + AudioContent::set_audio_mapping (m); } string @@ -482,3 +389,29 @@ FFmpegContent::audio_analysis_path () const p /= name; return p; } + +list<ContentTimePeriod> +FFmpegContent::subtitles_during (ContentTimePeriod period, bool starting) const +{ + list<ContentTimePeriod> d; + + shared_ptr<FFmpegSubtitleStream> stream = subtitle_stream (); + if (!stream) { + return d; + } + + /* XXX: inefficient */ + for (vector<ContentTimePeriod>::const_iterator i = stream->periods.begin(); i != stream->periods.end(); ++i) { + if ((starting && period.contains (i->from)) || (!starting && period.overlaps (*i))) { + d.push_back (*i); + } + } + + return d; +} + +bool +FFmpegContent::has_subtitles () const +{ + return !subtitle_streams().empty (); +} diff --git a/src/lib/ffmpeg_content.h b/src/lib/ffmpeg_content.h index c15e5c10e..da8152c0d 100644 --- a/src/lib/ffmpeg_content.h +++ b/src/lib/ffmpeg_content.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,6 +21,7 @@ #define DCPOMATIC_FFMPEG_CONTENT_H #include <boost/enable_shared_from_this.hpp> +#include <boost/lexical_cast.hpp> #include "video_content.h" #include "audio_content.h" #include "subtitle_content.h" @@ -30,88 +31,9 @@ struct AVFormatContext; struct AVStream; class Filter; -class ffmpeg_pts_offset_test; - -class FFmpegStream -{ -public: - FFmpegStream (std::string n, int i) - : name (n) - , _id (i) - {} - - FFmpegStream (boost::shared_ptr<const cxml::Node>); - - void as_xml (xmlpp::Node *) const; - - /** @param c An AVFormatContext. - * @param index A stream index within the AVFormatContext. - * @return true if this FFmpegStream uses the given stream index. - */ - bool uses_index (AVFormatContext const * c, int index) const; - AVStream* stream (AVFormatContext const * c) const; - - std::string technical_summary () const { - return "id " + boost::lexical_cast<std::string> (_id); - } - - std::string identifier () const { - return boost::lexical_cast<std::string> (_id); - } - - std::string name; - - friend bool operator== (FFmpegStream const & a, FFmpegStream const & b); - friend bool operator!= (FFmpegStream const & a, FFmpegStream const & b); - -private: - int _id; -}; - -class FFmpegAudioStream : public FFmpegStream -{ -public: - FFmpegAudioStream (std::string n, int i, int f, int c) - : FFmpegStream (n, i) - , frame_rate (f) - , channels (c) - , mapping (c) - { - mapping.make_default (); - } - - FFmpegAudioStream (boost::shared_ptr<const cxml::Node>, int); - - void as_xml (xmlpp::Node *) const; - - int frame_rate; - int channels; - AudioMapping mapping; - boost::optional<double> first_audio; - -private: - friend class ffmpeg_pts_offset_test; - - /* Constructor for tests */ - FFmpegAudioStream () - : FFmpegStream ("", 0) - , frame_rate (0) - , channels (0) - , mapping (1) - {} -}; - -class FFmpegSubtitleStream : public FFmpegStream -{ -public: - FFmpegSubtitleStream (std::string n, int i) - : FFmpegStream (n, i) - {} - - FFmpegSubtitleStream (boost::shared_ptr<const cxml::Node>); - - void as_xml (xmlpp::Node *) const; -}; +class FFmpegSubtitleStream; +class FFmpegAudioStream; +struct ffmpeg_pts_offset_test; class FFmpegContentProperty : public VideoContentProperty { @@ -127,7 +49,7 @@ class FFmpegContent : public VideoContent, public AudioContent, public SubtitleC { public: FFmpegContent (boost::shared_ptr<const Film>, boost::filesystem::path); - FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int version, std::list<std::string> &); + FFmpegContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int version, std::list<std::string> &); FFmpegContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >); boost::shared_ptr<FFmpegContent> shared_from_this () { @@ -139,18 +61,21 @@ public: std::string technical_summary () const; std::string information () const; void as_xml (xmlpp::Node *) const; - Time full_length () const; + DCPTime full_length () const; std::string identifier () const; /* AudioContent */ int audio_channels () const; - AudioContent::Frame audio_length () const; - int content_audio_frame_rate () const; + ContentTime audio_length () const; + int audio_frame_rate () const; AudioMapping audio_mapping () const; void set_audio_mapping (AudioMapping); boost::filesystem::path audio_analysis_path () const; + /* SubtitleContent */ + bool has_subtitles () const; + void set_filters (std::vector<Filter const *> const &); std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const { @@ -181,19 +106,21 @@ public: void set_subtitle_stream (boost::shared_ptr<FFmpegSubtitleStream>); void set_audio_stream (boost::shared_ptr<FFmpegAudioStream>); - boost::optional<double> first_video () const { + boost::optional<ContentTime> first_video () const { boost::mutex::scoped_lock lm (_mutex); return _first_video; } + std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const; + private: - friend class ffmpeg_pts_offset_test; + friend struct ffmpeg_pts_offset_test; 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; - boost::optional<double> _first_video; + boost::optional<ContentTime> _first_video; /** Video filters that should be used when generating DCPs */ std::vector<Filter const *> _filters; }; diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc index d40b798ba..15443c346 100644 --- a/src/lib/ffmpeg_decoder.cc +++ b/src/lib/ffmpeg_decoder.cc @@ -31,23 +31,26 @@ extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> } -#include "film.h" #include "filter.h" #include "exceptions.h" #include "image.h" #include "util.h" #include "log.h" #include "ffmpeg_decoder.h" +#include "ffmpeg_audio_stream.h" +#include "ffmpeg_subtitle_stream.h" #include "filter_graph.h" #include "audio_buffers.h" #include "ffmpeg_content.h" -#include "image_proxy.h" +#include "raw_image_proxy.h" +#include "film.h" +#include "timer.h" #include "i18n.h" -#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); -#define LOG_ERROR(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR); -#define LOG_WARNING(...) film->log()->log (__VA_ARGS__, Log::TYPE_WARNING); +#define LOG_GENERAL(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); +#define LOG_ERROR(...) _video_content->film()->log()->log (String::compose (__VA_ARGS__), Log::TYPE_ERROR); +#define LOG_WARNING(...) _video_content->film()->log()->log (__VA_ARGS__, Log::TYPE_WARNING); using std::cout; using std::string; @@ -55,26 +58,19 @@ using std::vector; using std::list; using std::min; using std::pair; +using std::make_pair; using boost::shared_ptr; using boost::optional; using boost::dynamic_pointer_cast; -using libdcp::Size; +using dcp::Size; -FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio) - : Decoder (f) - , VideoDecoder (f, c) - , AudioDecoder (f, c) - , SubtitleDecoder (f) +FFmpegDecoder::FFmpegDecoder (shared_ptr<const FFmpegContent> c, shared_ptr<Log> log) + : VideoDecoder (c) + , AudioDecoder (c) + , SubtitleDecoder (c) , FFmpeg (c) - , _subtitle_codec_context (0) - , _subtitle_codec (0) - , _decode_video (video) - , _decode_audio (audio) - , _pts_offset (0) - , _just_sought (false) + , _log (log) { - setup_subtitle (); - /* Audio and video frame PTS values may not start with 0. We want to fiddle them so that: @@ -84,13 +80,11 @@ FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegC Then we remove big initial gaps in PTS and we allow our insertion of black frames to work. - We will do: - audio_pts_to_use = audio_pts_from_ffmpeg + pts_offset; - video_pts_to_use = video_pts_from_ffmpeg + pts_offset; + We will do pts_to_use = pts_from_ffmpeg + pts_offset; */ - bool const have_video = video && c->first_video(); - bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio; + bool const have_video = c->first_video(); + bool const have_audio = c->audio_stream () && c->audio_stream()->first_audio; /* First, make one of them start at 0 */ @@ -104,24 +98,9 @@ FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegC /* Now adjust both so that the video pts starts on a frame */ if (have_video && have_audio) { - double first_video = c->first_video().get() + _pts_offset; - double const old_first_video = first_video; - - /* Round the first video up to a frame boundary */ - if (fabs (rint (first_video * c->video_frame_rate()) - first_video * c->video_frame_rate()) > 1e-6) { - first_video = ceil (first_video * c->video_frame_rate()) / c->video_frame_rate (); - } - - _pts_offset += first_video - old_first_video; - } -} - -FFmpegDecoder::~FFmpegDecoder () -{ - boost::mutex::scoped_lock lm (_mutex); - - if (_subtitle_codec_context) { - avcodec_close (_subtitle_codec_context); + ContentTime first_video = c->first_video().get() + _pts_offset; + ContentTime const old_first_video = first_video; + _pts_offset += first_video.round_up (c->video_frame_rate ()) - old_first_video; } } @@ -135,20 +114,15 @@ FFmpegDecoder::flush () /* XXX: should we reset _packet.data and size after each *_decode_* call? */ - if (_decode_video) { - while (decode_video_packet ()) {} - } + while (decode_video_packet ()) {} - if (_ffmpeg_content->audio_stream() && _decode_audio) { + if (_ffmpeg_content->audio_stream()) { decode_audio_packet (); + AudioDecoder::flush (); } - - /* Stop us being asked for any more data */ - _video_position = _ffmpeg_content->video_length_after_3d_combine (); - _audio_position = _ffmpeg_content->audio_length (); } -void +bool FFmpegDecoder::pass () { int r = av_read_frame (_format_context, &_packet); @@ -158,29 +132,25 @@ FFmpegDecoder::pass () /* Maybe we should fail here, but for now we'll just finish off instead */ char buf[256]; av_strerror (r, buf, sizeof(buf)); - shared_ptr<const Film> film = _film.lock (); - assert (film); LOG_ERROR (N_("error on av_read_frame (%1) (%2)"), buf, r); } flush (); - return; + return true; } - shared_ptr<const Film> film = _film.lock (); - assert (film); - int const si = _packet.stream_index; - - if (si == _video_stream && _decode_video) { + + if (si == _video_stream) { decode_video_packet (); - } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si) && _decode_audio) { + } else if (_ffmpeg_content->audio_stream() && _ffmpeg_content->audio_stream()->uses_index (_format_context, si)) { decode_audio_packet (); - } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si) && film->with_subtitles ()) { + } else if (_ffmpeg_content->subtitle_stream() && _ffmpeg_content->subtitle_stream()->uses_index (_format_context, si)) { decode_subtitle_packet (); } av_free_packet (&_packet); + return false; } /** @param data pointer to array of pointers to buffers. @@ -313,82 +283,40 @@ FFmpegDecoder::bytes_per_audio_sample () const } void -FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate) +FFmpegDecoder::seek (ContentTime time, bool accurate) { - double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base); - - /* If we are doing an accurate seek, our initial shot will be 5 frames (5 being - a number plucked from the air) earlier than we want to end up. The loop below - will hopefully then step through to where we want to be. + VideoDecoder::seek (time, accurate); + AudioDecoder::seek (time, accurate); + + /* If we are doing an `accurate' seek, we need to use pre-roll, as + we don't really know what the seek will give us. */ - int initial = frame; - if (accurate) { - initial -= 5; - } + ContentTime pre_roll = accurate ? ContentTime::from_seconds (2) : ContentTime (0); + time -= pre_roll; - if (initial < 0) { - initial = 0; - } - - /* Initial seek time in the stream's timebase */ - int64_t const initial_vt = ((initial / _ffmpeg_content->original_video_frame_rate()) - _pts_offset) / time_base; - - av_seek_frame (_format_context, _video_stream, initial_vt, AVSEEK_FLAG_BACKWARD); - - avcodec_flush_buffers (video_codec_context()); - if (_subtitle_codec_context) { - avcodec_flush_buffers (_subtitle_codec_context); - } - - /* This !accurate is piling hack upon hack; setting _just_sought to true - even with accurate == true defeats our attempt to align the start - of the video and audio. Here we disable that defeat when accurate == true - i.e. when we are making a DCP rather than just previewing one. - Ewww. This should be gone in 2.0. + /* XXX: it seems debatable whether PTS should be used here... + http://www.mjbshaw.com/2012/04/seeking-in-ffmpeg-know-your-timestamp.html */ - if (!accurate) { - _just_sought = true; - } - - _video_position = frame; - if (frame == 0 || !accurate) { - /* We're already there, or we're as close as we need to be */ - return; - } + ContentTime const u = time - _pts_offset; + int64_t s = u.seconds() / av_q2d (_format_context->streams[_video_stream]->time_base); - while (true) { - int r = av_read_frame (_format_context, &_packet); - if (r < 0) { - return; - } + if (_ffmpeg_content->audio_stream ()) { + s = min ( + s, int64_t (u.seconds() / av_q2d (_ffmpeg_content->audio_stream()->stream(_format_context)->time_base)) + ); + } - if (_packet.stream_index != _video_stream) { - av_free_packet (&_packet); - continue; - } - - int finished = 0; - r = avcodec_decode_video2 (video_codec_context(), _frame, &finished, &_packet); - if (r >= 0 && finished) { - _video_position = rint ( - (av_frame_get_best_effort_timestamp (_frame) * time_base + _pts_offset) * _ffmpeg_content->original_video_frame_rate() - ); + av_seek_frame (_format_context, _video_stream, s, 0); - if (_video_position >= (frame - 1)) { - av_free_packet (&_packet); - break; - } - } - - av_free_packet (&_packet); + avcodec_flush_buffers (video_codec_context()); + if (audio_codec_context ()) { + avcodec_flush_buffers (audio_codec_context ()); + } + if (subtitle_codec_context ()) { + avcodec_flush_buffers (subtitle_codec_context ()); } - - /* _video_position should be the next thing to be emitted, which will the one after the thing - we just saw. - */ - _video_position++; } void @@ -404,42 +332,23 @@ FFmpegDecoder::decode_audio_packet () int frame_finished; int const decode_result = avcodec_decode_audio4 (audio_codec_context(), _frame, &frame_finished, ©_packet); + if (decode_result < 0) { - shared_ptr<const Film> film = _film.lock (); - assert (film); LOG_ERROR ("avcodec_decode_audio4 failed (%1)", decode_result); return; } if (frame_finished) { - - if (_audio_position == 0) { - /* Where we are in the source, in seconds */ - double const pts = av_q2d (_format_context->streams[copy_packet.stream_index]->time_base) - * av_frame_get_best_effort_timestamp(_frame) + _pts_offset; - - if (pts > 0) { - /* Emit some silence */ - int64_t frames = pts * _ffmpeg_content->content_audio_frame_rate (); - while (frames > 0) { - int64_t const this_time = min (frames, (int64_t) _ffmpeg_content->content_audio_frame_rate() / 2); - - shared_ptr<AudioBuffers> silence ( - new AudioBuffers (_ffmpeg_content->audio_channels(), this_time) - ); - - silence->make_silent (); - audio (silence, _audio_position); - frames -= this_time; - } - } - } + ContentTime const ct = ContentTime::from_seconds ( + av_frame_get_best_effort_timestamp (_frame) * + av_q2d (_ffmpeg_content->audio_stream()->stream (_format_context)->time_base)) + + _pts_offset; int const data_size = av_samples_get_buffer_size ( 0, audio_codec_context()->channels, _frame->nb_samples, audio_sample_format (), 1 ); - - audio (deinterleave_audio (_frame->data, data_size), _audio_position); + + audio (deinterleave_audio (_frame->data, data_size), ct); } copy_packet.data += decode_result; @@ -460,17 +369,13 @@ FFmpegDecoder::decode_video_packet () 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)) { + while (i != _filter_graphs.end() && !(*i)->can_process (dcp::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)); + graph.reset (new FilterGraph (_ffmpeg_content, dcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)); _filter_graphs.push_back (graph); - LOG_GENERAL (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format); } else { graph = *i; @@ -478,56 +383,16 @@ FFmpegDecoder::decode_video_packet () list<pair<shared_ptr<Image>, int64_t> > images = graph->process (_frame); - shared_ptr<const Film> film = _film.lock (); - assert (film); - for (list<pair<shared_ptr<Image>, int64_t> >::iterator i = images.begin(); i != images.end(); ++i) { shared_ptr<Image> image = i->first; if (i->second != AV_NOPTS_VALUE) { - - double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset; - - if (_just_sought) { - /* We just did a seek, so disable any attempts to correct for where we - are / should be. - */ - _video_position = rint (pts * _ffmpeg_content->original_video_frame_rate ()); - _just_sought = false; - } - - double const next = _video_position / _ffmpeg_content->original_video_frame_rate(); - double const one_frame = 1 / _ffmpeg_content->original_video_frame_rate (); - double delta = pts - next; - - while (delta > one_frame) { - /* This PTS is more than one frame forward in time of where we think we should be; emit - a black frame. - */ - - /* XXX: I think this should be a copy of the last frame... */ - boost::shared_ptr<Image> black ( - new Image ( - static_cast<AVPixelFormat> (_frame->format), - libdcp::Size (video_codec_context()->width, video_codec_context()->height), - true - ) - ); - - shared_ptr<const Film> film = _film.lock (); - assert (film); - - black->make_black (); - video (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), false, _video_position); - delta -= one_frame; - } - - if (delta > -one_frame) { - /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */ - video (shared_ptr<ImageProxy> (new RawImageProxy (image, film->log())), false, _video_position); - } - + double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _pts_offset.seconds (); + video ( + shared_ptr<ImageProxy> (new RawImageProxy (image, _video_content->film()->log())), + rint (pts * _ffmpeg_content->video_frame_rate ()) + ); } else { LOG_WARNING ("Dropping frame without PTS"); } @@ -535,47 +400,13 @@ FFmpegDecoder::decode_video_packet () return true; } - - -void -FFmpegDecoder::setup_subtitle () -{ - boost::mutex::scoped_lock lm (_mutex); - - if (!_ffmpeg_content->subtitle_stream()) { - return; - } - - _subtitle_codec_context = _ffmpeg_content->subtitle_stream()->stream(_format_context)->codec; - if (_subtitle_codec_context == 0) { - throw DecodeError (N_("could not find subtitle stream")); - } - - _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id); - - if (_subtitle_codec == 0) { - throw DecodeError (N_("could not find subtitle decoder")); - } - - if (avcodec_open2 (_subtitle_codec_context, _subtitle_codec, 0) < 0) { - throw DecodeError (N_("could not open subtitle decoder")); - } -} - -bool -FFmpegDecoder::done () const -{ - bool const vd = !_decode_video || (_video_position >= _ffmpeg_content->video_length()); - bool const ad = !_decode_audio || !_ffmpeg_content->audio_stream() || (_audio_position >= _ffmpeg_content->audio_length()); - return vd && ad; -} void FFmpegDecoder::decode_subtitle_packet () { int got_subtitle; AVSubtitle sub; - if (avcodec_decode_subtitle2 (_subtitle_codec_context, &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) { + if (avcodec_decode_subtitle2 (subtitle_codec_context(), &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) { return; } @@ -583,31 +414,29 @@ FFmpegDecoder::decode_subtitle_packet () indicate that the previous subtitle should stop. */ if (sub.num_rects <= 0) { - subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0); + image_subtitle (ContentTimePeriod (), shared_ptr<Image> (), dcpomatic::Rect<double> ()); return; } else if (sub.num_rects > 1) { throw DecodeError (_("multi-part subtitles not yet supported")); } - /* Subtitle PTS in seconds (within the source, not taking into account any of the + /* Subtitle PTS (within the source, not taking into account any of the source that we may have chopped off for the DCP) */ - double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset; - - /* hence start time for this sub */ - Time const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ; - Time const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ; + ContentTimePeriod period = subtitle_period (sub) + _pts_offset; AVSubtitleRect const * rect = sub.rects[0]; if (rect->type != SUBTITLE_BITMAP) { - throw DecodeError (_("non-bitmap subtitles not yet supported")); + /* XXX */ + // throw DecodeError (_("non-bitmap subtitles not yet supported")); + return; } /* Note RGBA is expressed little-endian, so the first byte in the word is R, second G, third B, fourth A. */ - shared_ptr<Image> image (new Image (PIX_FMT_RGBA, libdcp::Size (rect->w, rect->h), true)); + shared_ptr<Image> image (new Image (PIX_FMT_RGBA, dcp::Size (rect->w, rect->h), true)); /* Start of the first line in the subtitle */ uint8_t* sub_p = rect->pict.data[0]; @@ -629,20 +458,24 @@ FFmpegDecoder::decode_subtitle_packet () out_p += image->stride()[0] / sizeof (uint32_t); } - libdcp::Size const vs = _ffmpeg_content->video_size (); + dcp::Size const vs = _ffmpeg_content->video_size (); - subtitle ( + image_subtitle ( + period, image, dcpomatic::Rect<double> ( static_cast<double> (rect->x) / vs.width, static_cast<double> (rect->y) / vs.height, static_cast<double> (rect->w) / vs.width, static_cast<double> (rect->h) / vs.height - ), - from, - to + ) ); - avsubtitle_free (&sub); } + +list<ContentTimePeriod> +FFmpegDecoder::subtitles_during (ContentTimePeriod p, bool starting) const +{ + return _ffmpeg_content->subtitles_during (p, starting); +} diff --git a/src/lib/ffmpeg_decoder.h b/src/lib/ffmpeg_decoder.h index d4b4fa1c0..9f85c2dca 100644 --- a/src/lib/ffmpeg_decoder.h +++ b/src/lib/ffmpeg_decoder.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -37,7 +37,7 @@ extern "C" { #include "subtitle_decoder.h" #include "ffmpeg.h" -class Film; +class Log; class FilterGraph; class ffmpeg_pts_offset_test; @@ -47,22 +47,15 @@ class ffmpeg_pts_offset_test; class FFmpegDecoder : public VideoDecoder, public AudioDecoder, public SubtitleDecoder, public FFmpeg { public: - FFmpegDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const FFmpegContent>, bool video, bool audio); - ~FFmpegDecoder (); - - void pass (); - void seek (VideoContent::Frame, bool); - bool done () const; + FFmpegDecoder (boost::shared_ptr<const FFmpegContent>, boost::shared_ptr<Log>); private: - friend class ::ffmpeg_pts_offset_test; - - static double compute_pts_offset (double, double, float); + friend struct ::ffmpeg_pts_offset_test; + void seek (ContentTime time, bool); + bool pass (); void flush (); - void setup_subtitle (); - AVSampleFormat audio_sample_format () const; int bytes_per_audio_sample () const; @@ -73,15 +66,12 @@ private: void maybe_add_subtitle (); boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t** data, int size); - AVCodecContext* _subtitle_codec_context; ///< may be 0 if there is no subtitle - AVCodec* _subtitle_codec; ///< may be 0 if there is no subtitle + std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const; + + boost::shared_ptr<Log> _log; std::list<boost::shared_ptr<FilterGraph> > _filter_graphs; boost::mutex _filter_graphs_mutex; - bool _decode_video; - bool _decode_audio; - - double _pts_offset; - bool _just_sought; + ContentTime _pts_offset; }; diff --git a/src/lib/ffmpeg_examiner.cc b/src/lib/ffmpeg_examiner.cc index 5ccc8028b..48d85da6f 100644 --- a/src/lib/ffmpeg_examiner.cc +++ b/src/lib/ffmpeg_examiner.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,6 +23,9 @@ extern "C" { } #include "ffmpeg_examiner.h" #include "ffmpeg_content.h" +#include "ffmpeg_audio_stream.h" +#include "ffmpeg_subtitle_stream.h" +#include "util.h" #include "safe_stringstream.h" #include "i18n.h" @@ -61,55 +64,93 @@ FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c) } } - /* Run through until we find the first audio (for each stream) and video */ - + /* Run through until we find: + * - the first video. + * - the first audio for each stream. + * - the subtitle periods for each stream. + * + * We have to note subtitle periods as otherwise we have no way of knowing + * where we should look for subtitles (video and audio are always present, + * so they are ok). + */ while (true) { int r = av_read_frame (_format_context, &_packet); if (r < 0) { break; } - int frame_finished; - AVCodecContext* context = _format_context->streams[_packet.stream_index]->codec; - if (_packet.stream_index == _video_stream && !_first_video) { - if (avcodec_decode_video2 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { - _first_video = frame_time (_format_context->streams[_video_stream]); - } - } else { - for (size_t i = 0; i < _audio_streams.size(); ++i) { - if (_audio_streams[i]->uses_index (_format_context, _packet.stream_index) && !_audio_streams[i]->first_audio) { - if (avcodec_decode_audio4 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { - _audio_streams[i]->first_audio = frame_time (_audio_streams[i]->stream (_format_context)); - } - } + if (_packet.stream_index == _video_stream) { + video_packet (context); + } + + for (size_t i = 0; i < _audio_streams.size(); ++i) { + if (_audio_streams[i]->uses_index (_format_context, _packet.stream_index)) { + audio_packet (context, _audio_streams[i]); } } - bool have_all_audio = true; - size_t i = 0; - while (i < _audio_streams.size() && have_all_audio) { - have_all_audio = _audio_streams[i]->first_audio; - ++i; + for (size_t i = 0; i < _subtitle_streams.size(); ++i) { + if (_subtitle_streams[i]->uses_index (_format_context, _packet.stream_index)) { + subtitle_packet (context, _subtitle_streams[i]); + } } av_free_packet (&_packet); - - if (_first_video && have_all_audio) { - break; + } +} + +void +FFmpegExaminer::video_packet (AVCodecContext* context) +{ + if (_first_video) { + return; + } + + int frame_finished; + if (avcodec_decode_video2 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { + _first_video = frame_time (_format_context->streams[_video_stream]); + } +} + +void +FFmpegExaminer::audio_packet (AVCodecContext* context, shared_ptr<FFmpegAudioStream> stream) +{ + if (stream->first_audio) { + return; + } + + int frame_finished; + if (avcodec_decode_audio4 (context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { + stream->first_audio = frame_time (stream->stream (_format_context)); + } +} + +void +FFmpegExaminer::subtitle_packet (AVCodecContext* context, shared_ptr<FFmpegSubtitleStream> stream) +{ + int frame_finished; + AVSubtitle sub; + if (avcodec_decode_subtitle2 (context, &sub, &frame_finished, &_packet) >= 0 && frame_finished) { + ContentTimePeriod const period = subtitle_period (sub); + if (sub.num_rects == 0 && !stream->periods.empty () && stream->periods.back().to > period.from) { + /* Finish the last subtitle */ + stream->periods.back().to = period.from; + } else if (sub.num_rects == 1) { + stream->periods.push_back (period); } } } -optional<double> +optional<ContentTime> FFmpegExaminer::frame_time (AVStream* s) const { - optional<double> t; + optional<ContentTime> t; int64_t const bet = av_frame_get_best_effort_timestamp (_frame); if (bet != AV_NOPTS_VALUE) { - t = bet * av_q2d (s->time_base); + t = ContentTime::from_seconds (bet * av_q2d (s->time_base)); } return t; @@ -118,27 +159,25 @@ FFmpegExaminer::frame_time (AVStream* s) const float FFmpegExaminer::video_frame_rate () const { - AVStream* s = _format_context->streams[_video_stream]; - - if (s->avg_frame_rate.num && s->avg_frame_rate.den) { - return av_q2d (s->avg_frame_rate); - } - - return av_q2d (s->r_frame_rate); + /* This use of r_frame_rate is debateable; there's a few different + * frame rates in the format context, but this one seems to be the most + * reliable. + */ + return av_q2d (av_stream_get_r_frame_rate (_format_context->streams[_video_stream])); } -libdcp::Size +dcp::Size FFmpegExaminer::video_size () const { - return libdcp::Size (video_codec_context()->width, video_codec_context()->height); + return dcp::Size (video_codec_context()->width, video_codec_context()->height); } -/** @return Length (in video frames) according to our content's header */ -VideoContent::Frame +/** @return Length according to our content's header */ +ContentTime FFmpegExaminer::video_length () const { - VideoContent::Frame const length = (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate(); - return max (1, length); + ContentTime const length = ContentTime::from_seconds (double (_format_context->duration) / AV_TIME_BASE); + return ContentTime (max (ContentTime::Type (1), length.get ())); } string diff --git a/src/lib/ffmpeg_examiner.h b/src/lib/ffmpeg_examiner.h index 369dac29c..8c31eb2c4 100644 --- a/src/lib/ffmpeg_examiner.h +++ b/src/lib/ffmpeg_examiner.h @@ -30,8 +30,8 @@ public: FFmpegExaminer (boost::shared_ptr<const FFmpegContent>); float video_frame_rate () const; - libdcp::Size video_size () const; - VideoContent::Frame video_length () const; + dcp::Size video_size () const; + ContentTime video_length () const; std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const { return _subtitle_streams; @@ -41,17 +41,21 @@ public: return _audio_streams; } - boost::optional<double> first_video () const { + boost::optional<ContentTime> first_video () const { return _first_video; } private: + void video_packet (AVCodecContext *); + void audio_packet (AVCodecContext *, boost::shared_ptr<FFmpegAudioStream>); + void subtitle_packet (AVCodecContext *, boost::shared_ptr<FFmpegSubtitleStream>); + std::string stream_name (AVStream* s) const; std::string audio_stream_name (AVStream* s) const; std::string subtitle_stream_name (AVStream* s) const; - boost::optional<double> frame_time (AVStream* s) const; + boost::optional<ContentTime> frame_time (AVStream* s) const; std::vector<boost::shared_ptr<FFmpegSubtitleStream> > _subtitle_streams; std::vector<boost::shared_ptr<FFmpegAudioStream> > _audio_streams; - boost::optional<double> _first_video; + boost::optional<ContentTime> _first_video; }; diff --git a/src/lib/ffmpeg_stream.cc b/src/lib/ffmpeg_stream.cc new file mode 100644 index 000000000..3fac33327 --- /dev/null +++ b/src/lib/ffmpeg_stream.cc @@ -0,0 +1,71 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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. + +*/ + +extern "C" { +#include <libavformat/avformat.h> +} +#include <libxml++/libxml++.h> +#include <dcp/raw_convert.h> +#include "ffmpeg_stream.h" + +using std::string; +using dcp::raw_convert; + +FFmpegStream::FFmpegStream (cxml::ConstNodePtr node) + : name (node->string_child ("Name")) + , _id (node->number_child<int> ("Id")) +{ + +} + +void +FFmpegStream::as_xml (xmlpp::Node* root) const +{ + root->add_child("Name")->add_child_text (name); + root->add_child("Id")->add_child_text (raw_convert<string> (_id)); +} + +bool +FFmpegStream::uses_index (AVFormatContext const * fc, int index) const +{ + size_t i = 0; + while (i < fc->nb_streams) { + if (fc->streams[i]->id == _id) { + return int (i) == index; + } + ++i; + } + + return false; +} + +AVStream * +FFmpegStream::stream (AVFormatContext const * fc) const +{ + size_t i = 0; + while (i < fc->nb_streams) { + if (fc->streams[i]->id == _id) { + return fc->streams[i]; + } + ++i; + } + + assert (false); + return 0; +} diff --git a/src/lib/ffmpeg_stream.h b/src/lib/ffmpeg_stream.h new file mode 100644 index 000000000..6bbcd0b01 --- /dev/null +++ b/src/lib/ffmpeg_stream.h @@ -0,0 +1,65 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_STREAM_H +#define DCPOMATIC_FFMPEG_STREAM_H + +#include <boost/lexical_cast.hpp> +#include <libcxml/cxml.h> + +struct AVFormatContext; +struct AVStream; + +class FFmpegStream +{ +public: + FFmpegStream (std::string n, int i) + : name (n) + , _id (i) + {} + + FFmpegStream (cxml::ConstNodePtr); + + void as_xml (xmlpp::Node *) const; + + /** @param c An AVFormatContext. + * @param index A stream index within the AVFormatContext. + * @return true if this FFmpegStream uses the given stream index. + */ + bool uses_index (AVFormatContext const * c, int index) const; + AVStream* stream (AVFormatContext const * c) const; + + std::string technical_summary () const { + return "id " + boost::lexical_cast<std::string> (_id); + } + + std::string identifier () const { + return boost::lexical_cast<std::string> (_id); + } + + std::string name; + + friend bool operator== (FFmpegStream const & a, FFmpegStream const & b); + friend bool operator!= (FFmpegStream const & a, FFmpegStream const & b); + +private: + int _id; +}; + +#endif diff --git a/src/lib/ffmpeg_subtitle_stream.cc b/src/lib/ffmpeg_subtitle_stream.cc new file mode 100644 index 000000000..3d8fd4e83 --- /dev/null +++ b/src/lib/ffmpeg_subtitle_stream.cc @@ -0,0 +1,36 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "ffmpeg_subtitle_stream.h" + +/** Construct a SubtitleStream from a value returned from to_string(). + * @param t String returned from to_string(). + * @param v State file version. + */ +FFmpegSubtitleStream::FFmpegSubtitleStream (cxml::ConstNodePtr node) + : FFmpegStream (node) +{ + +} + +void +FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const +{ + FFmpegStream::as_xml (root); +} diff --git a/src/lib/ffmpeg_subtitle_stream.h b/src/lib/ffmpeg_subtitle_stream.h new file mode 100644 index 000000000..b16b825e7 --- /dev/null +++ b/src/lib/ffmpeg_subtitle_stream.h @@ -0,0 +1,36 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "dcpomatic_time.h" +#include "ffmpeg_stream.h" + +class FFmpegSubtitleStream : public FFmpegStream +{ +public: + FFmpegSubtitleStream (std::string n, int i) + : FFmpegStream (n, i) + {} + + FFmpegSubtitleStream (cxml::ConstNodePtr); + + void as_xml (xmlpp::Node *) const; + + std::vector<ContentTimePeriod> periods; +}; + diff --git a/src/lib/file_group.cc b/src/lib/file_group.cc index 048f69233..9c8d43204 100644 --- a/src/lib/file_group.cc +++ b/src/lib/file_group.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/file_group.cc + * @brief FileGroup class. + */ + #include <cstdio> #include <sndfile.h> #include "file_group.h" @@ -26,6 +30,7 @@ using std::vector; using std::cout; +/** Construct a FileGroup with no files */ FileGroup::FileGroup () : _current_path (0) , _current_file (0) @@ -33,14 +38,17 @@ FileGroup::FileGroup () } +/** Construct a FileGroup with a single file */ FileGroup::FileGroup (boost::filesystem::path p) : _current_path (0) , _current_file (0) { _paths.push_back (p); + ensure_open_path (0); seek (0, SEEK_SET); } +/** Construct a FileGroup with multiple files */ FileGroup::FileGroup (vector<boost::filesystem::path> const & p) : _paths (p) , _current_path (0) @@ -50,6 +58,7 @@ FileGroup::FileGroup (vector<boost::filesystem::path> const & p) seek (0, SEEK_SET); } +/** Destroy a FileGroup, closing any open file */ FileGroup::~FileGroup () { if (_current_file) { @@ -160,6 +169,7 @@ FileGroup::read (uint8_t* buffer, int amount) const return read; } +/** @return Combined length of all the files */ int64_t FileGroup::length () const { diff --git a/src/lib/file_group.h b/src/lib/file_group.h index 65091c936..5a65de96f 100644 --- a/src/lib/file_group.h +++ b/src/lib/file_group.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,12 +17,19 @@ */ +/** @file src/lib/file_group.h + * @brief FileGroup class. + */ + #ifndef DCPOMATIC_FILE_GROUP_H #define DCPOMATIC_FILE_GROUP_H #include <vector> #include <boost/filesystem.hpp> +/** @class FileGroup + * @brief A class to make a list of files behave like they were concatenated. + */ class FileGroup { public: diff --git a/src/lib/film.cc b/src/lib/film.cc index 54503ef72..268109921 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -26,15 +26,14 @@ #include <unistd.h> #include <boost/filesystem.hpp> #include <boost/algorithm/string.hpp> -#include <boost/date_time.hpp> +#include <boost/lexical_cast.hpp> #include <libxml++/libxml++.h> #include <libcxml/cxml.h> -#include <libdcp/signer_chain.h> -#include <libdcp/cpl.h> -#include <libdcp/signer.h> -#include <libdcp/util.h> -#include <libdcp/kdm.h> -#include <libdcp/raw_convert.h> +#include <dcp/cpl.h> +#include <dcp/signer.h> +#include <dcp/util.h> +#include <dcp/local_time.h> +#include <dcp/raw_convert.h> #include "film.h" #include "job.h" #include "util.h" @@ -77,9 +76,10 @@ using boost::ends_with; using boost::starts_with; using boost::optional; using boost::is_any_of; -using libdcp::Size; -using libdcp::Signer; -using libdcp::raw_convert; +using dcp::Size; +using dcp::Signer; +using dcp::raw_convert; +using dcp::raw_convert; #define LOG_GENERAL(...) log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); #define LOG_GENERAL_NC(...) log()->log (__VA_ARGS__, Log::TYPE_GENERAL); @@ -91,11 +91,14 @@ using libdcp::raw_convert; * 7 -> 8 * Use <Scale> tag in <VideoContent> rather than <Ratio>. * 8 -> 9 - * DCI -> ISDCF. + * DCI -> ISDCF * 9 -> 10 * Subtitle X and Y scale. + * + * Bumped to 32 for 2.0 branch; some times are expressed in Times rather + * than frames now. */ -int const Film::current_state_version = 10; +int const Film::current_state_version = 32; /** Construct a Film object in a given directory. * @@ -109,7 +112,6 @@ Film::Film (boost::filesystem::path dir, bool log) , _container (Config::instance()->default_container ()) , _resolution (RESOLUTION_2K) , _scaler (Scaler::from_id ("bicubic")) - , _with_subtitles (false) , _signed (true) , _encrypted (false) , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ()) @@ -119,6 +121,7 @@ Film::Film (boost::filesystem::path dir, bool log) , _three_d (false) , _sequence_video (true) , _interop (false) + , _burn_subtitles (false) , _state_version (current_state_version) , _dirty (false) { @@ -182,12 +185,12 @@ Film::video_identifier () const s << "_S"; } - if (_three_d) { - s << "_3D"; + if (_burn_subtitles) { + s << "_B"; } - if (_with_subtitles) { - s << "_WS"; + if (_three_d) { + s << "_3D"; } return s.str (); @@ -227,6 +230,12 @@ Film::audio_mxf_filename () const return filename_safe_name() + "_audio.mxf"; } +boost::filesystem::path +Film::subtitle_xml_filename () const +{ + return filename_safe_name() + "_subtitle.xml"; +} + string Film::filename_safe_name () const { @@ -369,7 +378,6 @@ Film::metadata () const root->add_child("Resolution")->add_child_text (resolution_to_string (_resolution)); root->add_child("Scaler")->add_child_text (_scaler->id ()); - root->add_child("WithSubtitles")->add_child_text (_with_subtitles ? "1" : "0"); root->add_child("J2KBandwidth")->add_child_text (raw_convert<string> (_j2k_bandwidth)); _isdcf_metadata.as_xml (root->add_child ("ISDCFMetadata")); root->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate)); @@ -378,6 +386,7 @@ Film::metadata () const root->add_child("ThreeD")->add_child_text (_three_d ? "1" : "0"); root->add_child("SequenceVideo")->add_child_text (_sequence_video ? "1" : "0"); root->add_child("Interop")->add_child_text (_interop ? "1" : "0"); + root->add_child("BurnSubtitles")->add_child_text (_burn_subtitles ? "1" : "0"); root->add_child("Signed")->add_child_text (_signed ? "1" : "0"); root->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); root->add_child("Key")->add_child_text (_key.hex ()); @@ -441,7 +450,6 @@ Film::read_metadata () _resolution = string_to_resolution (f.string_child ("Resolution")); _scaler = Scaler::from_id (f.string_child ("Scaler")); - _with_subtitles = f.bool_child ("WithSubtitles"); _j2k_bandwidth = f.number_child<int> ("J2KBandwidth"); _video_frame_rate = f.number_child<int> ("VideoFrameRate"); _signed = f.optional_bool_child("Signed").get_value_or (true); @@ -450,7 +458,10 @@ Film::read_metadata () _sequence_video = f.bool_child ("SequenceVideo"); _three_d = f.bool_child ("ThreeD"); _interop = f.bool_child ("Interop"); - _key = libdcp::Key (f.string_child ("Key")); + if (_state_version >= 32) { + _burn_subtitles = f.bool_child ("BurnSubtitles"); + } + _key = dcp::Key (f.string_child ("Key")); list<string> notes; /* This method is the only one that can return notes (so far) */ @@ -585,9 +596,9 @@ Film::isdcf_name (bool if_created_now) const /* XXX: this uses the first bit of content only */ /* The standard says we don't do this for trailers, for some strange reason */ - if (dcp_content_type() && dcp_content_type()->libdcp_kind() != libdcp::TRAILER) { - Ratio const * content_ratio = 0; + if (dcp_content_type() && dcp_content_type()->libdcp_kind() != dcp::TRAILER) { ContentList cl = content (); + Ratio const * content_ratio = 0; for (ContentList::iterator i = cl.begin(); i != cl.end(); ++i) { shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i); if (vc) { @@ -689,7 +700,6 @@ Film::dcp_name (bool if_created_now) const return name(); } - void Film::set_directory (boost::filesystem::path d) { @@ -740,13 +750,6 @@ Film::set_scaler (Scaler const * s) } void -Film::set_with_subtitles (bool w) -{ - _with_subtitles = w; - signal_changed (WITH_SUBTITLES); -} - -void Film::set_j2k_bandwidth (int b) { _j2k_bandwidth = b; @@ -789,6 +792,13 @@ Film::set_interop (bool i) } void +Film::set_burn_subtitles (bool b) +{ + _burn_subtitles = b; + signal_changed (BURN_SUBTITLES); +} + +void Film::signal_changed (Property p) { _dirty = true; @@ -869,7 +879,7 @@ Film::j2c_path (int f, Eyes e, bool t) const return file (p); } -/** Find all the DCPs in our directory that can be libdcp::DCP::read() and return details of their CPLs */ +/** Find all the DCPs in our directory that can be dcp::DCP::read() and return details of their CPLs */ vector<CPLSummary> Film::cpls () const { @@ -883,11 +893,14 @@ Film::cpls () const ) { try { - libdcp::DCP dcp (*i); + dcp::DCP dcp (*i); dcp.read (); out.push_back ( CPLSummary ( - i->path().leaf().string(), dcp.cpls().front()->id(), dcp.cpls().front()->name(), dcp.cpls().front()->filename() + i->path().leaf().string(), + dcp.cpls().front()->id(), + dcp.cpls().front()->annotation_text(), + dcp.cpls().front()->file() ) ); } catch (...) { @@ -932,6 +945,13 @@ Film::content () const } void +Film::examine_content (shared_ptr<Content> c) +{ + shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c)); + JobManager::instance()->add (j); +} + +void Film::examine_and_add_content (shared_ptr<Content> c) { if (dynamic_pointer_cast<FFmpegContent> (c)) { @@ -986,22 +1006,22 @@ Film::move_content_later (shared_ptr<Content> c) _playlist->move_later (c); } -Time +DCPTime Film::length () const { return _playlist->length (); } -bool -Film::has_subtitles () const +int +Film::best_video_frame_rate () const { - return _playlist->has_subtitles (); + return _playlist->best_dcp_frame_rate (); } -OutputVideoFrame -Film::best_video_frame_rate () const +FrameRateChange +Film::active_frame_rate_change (DCPTime t) const { - return _playlist->best_dcp_frame_rate (); + return _playlist->active_frame_rate_change (t, video_frame_rate ()); } void @@ -1022,31 +1042,7 @@ Film::playlist_changed () signal_changed (CONTENT); } -OutputAudioFrame -Film::time_to_audio_frames (Time t) const -{ - return divide_with_round (t * audio_frame_rate (), TIME_HZ); -} - -OutputVideoFrame -Film::time_to_video_frames (Time t) const -{ - return divide_with_round (t * video_frame_rate (), TIME_HZ); -} - -Time -Film::audio_frames_to_time (OutputAudioFrame f) const -{ - return divide_with_round (f * TIME_HZ, audio_frame_rate ()); -} - -Time -Film::video_frames_to_time (OutputVideoFrame f) const -{ - return divide_with_round (f * TIME_HZ, video_frame_rate ()); -} - -OutputAudioFrame +int Film::audio_frame_rate () const { /* XXX */ @@ -1062,61 +1058,62 @@ Film::set_sequence_video (bool s) } /** @return Size of the largest possible image in whatever resolution we are using */ -libdcp::Size +dcp::Size Film::full_frame () const { switch (_resolution) { case RESOLUTION_2K: - return libdcp::Size (2048, 1080); + return dcp::Size (2048, 1080); case RESOLUTION_4K: - return libdcp::Size (4096, 2160); + return dcp::Size (4096, 2160); } assert (false); - return libdcp::Size (); + return dcp::Size (); } /** @return Size of the frame */ -libdcp::Size +dcp::Size Film::frame_size () const { - return fit_ratio_within (container()->ratio(), full_frame ()); + return fit_ratio_within (container()->ratio(), full_frame (), 1); } -/** @param from KDM from time in local time. - * @param to KDM to time in local time. - */ -libdcp::KDM +dcp::EncryptedKDM Film::make_kdm ( - shared_ptr<libdcp::Certificate> target, + dcp::Certificate target, boost::filesystem::path cpl_file, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime until, + dcp::Formulation formulation ) const { - shared_ptr<const Signer> signer = make_signer (); - - time_t now = time (0); - struct tm* tm = localtime (&now); - string const issue_date = libdcp::tm_to_string (tm); + shared_ptr<const dcp::CPL> cpl (new dcp::CPL (cpl_file)); + shared_ptr<const dcp::Signer> signer = Config::instance()->signer(); + if (!signer->valid ()) { + throw InvalidSignerError (); + } - return libdcp::KDM (cpl_file, signer, target, key (), from, until, "DCP-o-matic", issue_date, formulation); + return dcp::DecryptedKDM ( + cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string() + ).encrypt (signer, target, formulation); } -list<libdcp::KDM> +list<dcp::EncryptedKDM> Film::make_kdms ( list<shared_ptr<Screen> > screens, boost::filesystem::path dcp, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime until, + dcp::Formulation formulation ) const { - list<libdcp::KDM> kdms; + list<dcp::EncryptedKDM> kdms; for (list<shared_ptr<Screen> >::iterator i = screens.begin(); i != screens.end(); ++i) { - kdms.push_back (make_kdm ((*i)->certificate, dcp, from, until, formulation)); + if ((*i)->certificate) { + kdms.push_back (make_kdm ((*i)->certificate.get(), dcp, from, until, formulation)); + } } return kdms; @@ -1128,7 +1125,7 @@ Film::make_kdms ( uint64_t Film::required_disk_space () const { - return uint64_t (j2k_bandwidth() / 8) * length() / TIME_HZ; + return uint64_t (j2k_bandwidth() / 8) * length().seconds(); } /** This method checks the disk that the Film is on and tries to decide whether or not @@ -1146,10 +1143,3 @@ Film::should_be_enough_disk_space (double& required, double& available) const available = double (s.available) / 1073741824.0f; return (available - required) > 1; } - -FrameRateChange -Film::active_frame_rate_change (Time t) const -{ - return _playlist->active_frame_rate_change (t, video_frame_rate ()); -} - diff --git a/src/lib/film.h b/src/lib/film.h index b7d105688..8a0823094 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -31,8 +31,9 @@ #include <boost/signals2.hpp> #include <boost/enable_shared_from_this.hpp> #include <boost/filesystem.hpp> -#include <libdcp/key.h> -#include <libdcp/kdm.h> +#include <dcp/key.h> +#include <dcp/decrypted_kdm.h> +#include <dcp/encrypted_kdm.h> #include "util.h" #include "types.h" #include "isdcf_metadata.h" @@ -46,7 +47,7 @@ class Playlist; class AudioContent; class Scaler; class Screen; -class isdcf_name_test; +struct isdcf_name_test; /** @class Film * @@ -69,6 +70,7 @@ public: boost::filesystem::path video_mxf_filename () const; boost::filesystem::path audio_mxf_filename () const; + boost::filesystem::path subtitle_xml_filename () const; void send_dcp_to_tms (); void make_dcp (); @@ -97,20 +99,15 @@ public: return _dirty; } - libdcp::Size full_frame () const; - libdcp::Size frame_size () const; + dcp::Size full_frame () const; + dcp::Size frame_size () const; std::vector<CPLSummary> cpls () const; boost::shared_ptr<Player> make_player () const; boost::shared_ptr<Playlist> playlist () const; - OutputAudioFrame audio_frame_rate () 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; + int audio_frame_rate () const; uint64_t required_disk_space () const; bool should_be_enough_disk_space (double &, double &) const; @@ -118,29 +115,28 @@ public: /* Proxies for some Playlist methods */ ContentList content () const; - Time length () const; - bool has_subtitles () const; - OutputVideoFrame best_video_frame_rate () const; - FrameRateChange active_frame_rate_change (Time) const; + DCPTime length () const; + int best_video_frame_rate () const; + FrameRateChange active_frame_rate_change (DCPTime) const; - libdcp::KDM + dcp::EncryptedKDM make_kdm ( - boost::shared_ptr<libdcp::Certificate> target, + dcp::Certificate target, boost::filesystem::path cpl_file, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime until, + dcp::Formulation formulation ) const; - std::list<libdcp::KDM> make_kdms ( + std::list<dcp::EncryptedKDM> make_kdms ( std::list<boost::shared_ptr<Screen> >, boost::filesystem::path cpl_file, - boost::posix_time::ptime from, - boost::posix_time::ptime until, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime until, + dcp::Formulation formulation ) const; - libdcp::Key key () const { + dcp::Key key () const { return _key; } @@ -161,17 +157,18 @@ public: CONTAINER, RESOLUTION, SCALER, - WITH_SUBTITLES, SIGNED, ENCRYPTED, J2K_BANDWIDTH, ISDCF_METADATA, VIDEO_FRAME_RATE, AUDIO_CHANNELS, - /** The setting of _three_d has been changed */ + /** The setting of _three_d has changed */ THREE_D, SEQUENCE_VIDEO, INTEROP, + /** The setting of _burn_subtitles has changed */ + BURN_SUBTITLES, }; @@ -205,10 +202,6 @@ public: return _scaler; } - bool with_subtitles () const { - return _with_subtitles; - } - /* signed is a reserved word */ bool is_signed () const { return _signed; @@ -246,6 +239,10 @@ public: bool interop () const { return _interop; } + + bool burn_subtitles () const { + return _burn_subtitles; + } /* SET */ @@ -253,6 +250,7 @@ public: void set_directory (boost::filesystem::path); void set_name (std::string); void set_use_isdcf_name (bool); + void examine_content (boost::shared_ptr<Content>); void examine_and_add_content (boost::shared_ptr<Content>); void add_content (boost::shared_ptr<Content>); void remove_content (boost::shared_ptr<Content>); @@ -262,7 +260,6 @@ public: void set_container (Ratio const *); void set_resolution (Resolution); void set_scaler (Scaler const *); - void set_with_subtitles (bool); void set_signed (bool); void set_encrypted (bool); void set_j2k_bandwidth (int); @@ -273,6 +270,7 @@ public: void set_isdcf_date_today (); void set_sequence_video (bool); void set_interop (bool); + void set_burn_subtitles (bool); /** Emitted when some property has of the Film has changed */ mutable boost::signals2::signal<void (Property)> Changed; @@ -285,7 +283,7 @@ public: private: - friend class ::isdcf_name_test; + friend struct ::isdcf_name_test; void signal_changed (Property); std::string video_identifier () const; @@ -315,8 +313,6 @@ private: Resolution _resolution; /** Scaler algorithm to use */ Scaler const * _scaler; - /** True if subtitles should be shown for this film */ - bool _with_subtitles; bool _signed; bool _encrypted; /** bandwidth for J2K files in bits per second */ @@ -335,15 +331,16 @@ private: bool _three_d; bool _sequence_video; bool _interop; - libdcp::Key _key; + bool _burn_subtitles; + dcp::Key _key; int _state_version; /** true if our state has changed since we last saved it */ mutable bool _dirty; - friend class paths_test; - friend class film_metadata_test; + friend struct paths_test; + friend struct film_metadata_test; }; #endif diff --git a/src/lib/filter_graph.cc b/src/lib/filter_graph.cc index 639992d70..2d8f83aa7 100644 --- a/src/lib/filter_graph.cc +++ b/src/lib/filter_graph.cc @@ -45,26 +45,29 @@ using std::make_pair; using std::cout; using boost::shared_ptr; using boost::weak_ptr; -using libdcp::Size; +using dcp::Size; /** 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 (shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p) - : _buffer_src_context (0) +FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, dcp::Size s, AVPixelFormat p) + : _copy (false) + , _buffer_src_context (0) , _buffer_sink_context (0) , _size (s) , _pixel_format (p) + , _frame (0) { - _frame = av_frame_alloc (); - - string filters = Filter::ffmpeg_string (content->filters()); + string const filters = Filter::ffmpeg_string (content->filters()); if (filters.empty ()) { - filters = "copy"; + _copy = true; + return; } + _frame = av_frame_alloc (); + AVFilterGraph* graph = avfilter_graph_alloc(); if (graph == 0) { throw DecodeError (N_("could not create filter graph.")); @@ -122,12 +125,15 @@ FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, libdcp::Size throw DecodeError (N_("could not configure filter graph.")); } - /* XXX: leaking `inputs' / `outputs' ? */ + avfilter_inout_free (&inputs); + avfilter_inout_free (&outputs); } FilterGraph::~FilterGraph () { - av_frame_free (&_frame); + if (_frame) { + av_frame_free (&_frame); + } } /** Take an AVFrame and process it using our configured filters, returning a @@ -138,19 +144,23 @@ FilterGraph::process (AVFrame* frame) { list<pair<shared_ptr<Image>, int64_t> > images; - if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) { - throw DecodeError (N_("could not push buffer into filter chain.")); - } - - while (true) { - if (av_buffersink_get_frame (_buffer_sink_context, _frame) < 0) { - break; + if (_copy) { + images.push_back (make_pair (shared_ptr<Image> (new Image (frame)), av_frame_get_best_effort_timestamp (frame))); + } else { + if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) { + throw DecodeError (N_("could not push buffer into filter chain.")); + } + + while (true) { + if (av_buffersink_get_frame (_buffer_sink_context, _frame) < 0) { + break; + } + + images.push_back (make_pair (shared_ptr<Image> (new Image (_frame)), av_frame_get_best_effort_timestamp (_frame))); + av_frame_unref (_frame); } - - images.push_back (make_pair (shared_ptr<Image> (new Image (_frame)), av_frame_get_best_effort_timestamp (_frame))); - av_frame_unref (_frame); } - + return images; } @@ -159,7 +169,7 @@ FilterGraph::process (AVFrame* frame) * @return true if this chain can process images with `s' and `p', otherwise false. */ bool -FilterGraph::can_process (libdcp::Size s, AVPixelFormat p) const +FilterGraph::can_process (dcp::Size s, AVPixelFormat p) const { return (_size == s && _pixel_format == p); } diff --git a/src/lib/filter_graph.h b/src/lib/filter_graph.h index 9b403c2bc..5b43c5512 100644 --- a/src/lib/filter_graph.h +++ b/src/lib/filter_graph.h @@ -36,16 +36,18 @@ class FFmpegContent; class FilterGraph : public boost::noncopyable { public: - FilterGraph (boost::shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p); + FilterGraph (boost::shared_ptr<const FFmpegContent> content, dcp::Size s, AVPixelFormat p); ~FilterGraph (); - bool can_process (libdcp::Size s, AVPixelFormat p) const; + bool can_process (dcp::Size s, AVPixelFormat p) const; std::list<std::pair<boost::shared_ptr<Image>, int64_t> > process (AVFrame * frame); private: + /** true if this graph has no filters in, so it just copies stuff straight through */ + bool _copy; AVFilterContext* _buffer_src_context; AVFilterContext* _buffer_sink_context; - libdcp::Size _size; ///< size of the images that this chain can process + dcp::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; }; diff --git a/src/lib/image.cc b/src/lib/image.cc index 066f12c07..0da2d1e48 100644 --- a/src/lib/image.cc +++ b/src/lib/image.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,6 +30,8 @@ extern "C" { #include "image.h" #include "exceptions.h" #include "scaler.h" +#include "timer.h" +#include "rect.h" #include "md5_digester.h" #include "i18n.h" @@ -38,8 +40,9 @@ using std::string; using std::min; using std::cout; using std::cerr; +using std::list; using boost::shared_ptr; -using libdcp::Size; +using dcp::Size; int Image::line_factor (int n) const @@ -83,7 +86,7 @@ Image::components () const /** Crop this image, scale it to `inter_size' and then place it in a black frame of `out_size' */ shared_ptr<Image> -Image::crop_scale_window (Crop crop, libdcp::Size inter_size, libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const +Image::crop_scale_window (Crop crop, dcp::Size inter_size, dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const { assert (scaler); /* Empirical testing suggests that sws_scale() will crash if @@ -99,13 +102,13 @@ Image::crop_scale_window (Crop crop, libdcp::Size inter_size, libdcp::Size out_s out->make_black (); /* Size of the image after any crop */ - libdcp::Size const cropped_size = crop.apply (size ()); + dcp::Size const cropped_size = crop.apply (size ()); /* Scale context for a scale from cropped_size to inter_size */ struct SwsContext* scale_context = sws_getContext ( - cropped_size.width, cropped_size.height, pixel_format(), - inter_size.width, inter_size.height, out_format, - scaler->ffmpeg_id (), 0, 0, 0 + cropped_size.width, cropped_size.height, pixel_format(), + inter_size.width, inter_size.height, out_format, + scaler->ffmpeg_id (), 0, 0, 0 ); if (!scale_context) { @@ -139,7 +142,7 @@ Image::crop_scale_window (Crop crop, libdcp::Size inter_size, libdcp::Size out_s } shared_ptr<Image> -Image::scale (libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const +Image::scale (dcp::Size out_size, Scaler const * scaler, AVPixelFormat out_format, bool out_aligned) const { assert (scaler); /* Empirical testing suggests that sws_scale() will crash if @@ -170,7 +173,7 @@ Image::scale (libdcp::Size out_size, Scaler const * scaler, AVPixelFormat out_fo shared_ptr<Image> Image::crop (Crop crop, bool aligned) const { - libdcp::Size cropped_size = crop.apply (size ()); + dcp::Size cropped_size = crop.apply (size ()); shared_ptr<Image> out (new Image (pixel_format(), cropped_size, aligned)); for (int c = 0; c < components(); ++c) { @@ -344,10 +347,33 @@ Image::make_black () } void +Image::make_transparent () +{ + if (_pixel_format != PIX_FMT_RGBA) { + throw PixelFormatError ("make_transparent()", _pixel_format); + } + + memset (data()[0], 0, lines(0) * stride()[0]); +} + +void Image::alpha_blend (shared_ptr<const Image> other, Position<int> position) { - /* Only implemented for RGBA onto RGB24 so far */ - assert (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGBA); + assert (other->pixel_format() == PIX_FMT_RGBA); + int const other_bpp = 4; + + int this_bpp = 0; + switch (_pixel_format) { + case PIX_FMT_BGRA: + case PIX_FMT_RGBA: + this_bpp = 4; + break; + case PIX_FMT_RGB24: + this_bpp = 3; + break; + default: + assert (false); + } int start_tx = position.x; int start_ox = 0; @@ -366,15 +392,17 @@ Image::alpha_blend (shared_ptr<const Image> other, Position<int> position) } for (int ty = start_ty, oy = start_oy; ty < size().height && oy < other->size().height; ++ty, ++oy) { - uint8_t* tp = data()[0] + ty * stride()[0] + position.x * 3; + uint8_t* tp = data()[0] + ty * stride()[0] + start_tx * this_bpp; uint8_t* op = other->data()[0] + oy * other->stride()[0]; for (int tx = start_tx, ox = start_ox; tx < size().width && ox < other->size().width; ++tx, ++ox) { float const alpha = float (op[3]) / 255; - tp[0] = (tp[0] * (1 - alpha)) + op[0] * alpha; - tp[1] = (tp[1] * (1 - alpha)) + op[1] * alpha; - tp[2] = (tp[2] * (1 - alpha)) + op[2] * alpha; - tp += 3; - op += 4; + tp[0] = op[0] + (tp[0] * (1 - alpha)); + tp[1] = op[1] + (tp[1] * (1 - alpha)); + tp[2] = op[2] + (tp[2] * (1 - alpha)); + tp[3] = op[3] + (tp[3] * (1 - alpha)); + + tp += this_bpp; + op += other_bpp; } } } @@ -458,8 +486,8 @@ Image::bytes_per_pixel (int c) const * @param p Pixel format. * @param s Size in pixels. */ -Image::Image (AVPixelFormat p, libdcp::Size s, bool aligned) - : libdcp::Image (s) +Image::Image (AVPixelFormat p, dcp::Size s, bool aligned) + : dcp::Image (s) , _pixel_format (p) , _aligned (aligned) { @@ -501,7 +529,7 @@ Image::allocate () } Image::Image (Image const & other) - : libdcp::Image (other) + : dcp::Image (other) , _pixel_format (other._pixel_format) , _aligned (other._aligned) { @@ -519,7 +547,7 @@ Image::Image (Image const & other) } Image::Image (AVFrame* frame) - : libdcp::Image (libdcp::Size (frame->width, frame->height)) + : dcp::Image (dcp::Size (frame->width, frame->height)) , _pixel_format (static_cast<AVPixelFormat> (frame->format)) , _aligned (true) { @@ -538,7 +566,7 @@ Image::Image (AVFrame* frame) } Image::Image (shared_ptr<const Image> other, bool aligned) - : libdcp::Image (other) + : dcp::Image (other) , _pixel_format (other->_pixel_format) , _aligned (aligned) { @@ -571,7 +599,7 @@ Image::operator= (Image const & other) void Image::swap (Image & other) { - libdcp::Image::swap (other); + dcp::Image::swap (other); std::swap (_pixel_format, other._pixel_format); @@ -614,7 +642,7 @@ Image::stride () const return _stride; } -libdcp::Size +dcp::Size Image::size () const { return _size; @@ -626,6 +654,31 @@ Image::aligned () const return _aligned; } +PositionImage +merge (list<PositionImage> images) +{ + if (images.empty ()) { + return PositionImage (); + } + + if (images.size() == 1) { + return images.front (); + } + + dcpomatic::Rect<int> all (images.front().position, images.front().image->size().width, images.front().image->size().height); + for (list<PositionImage>::const_iterator i = images.begin(); i != images.end(); ++i) { + all.extend (dcpomatic::Rect<int> (i->position, i->image->size().width, i->image->size().height)); + } + + shared_ptr<Image> merged (new Image (images.front().image->pixel_format (), dcp::Size (all.width, all.height), true)); + merged->make_transparent (); + for (list<PositionImage>::const_iterator i = images.begin(); i != images.end(); ++i) { + merged->alpha_blend (i->image, i->position - all.position()); + } + + return PositionImage (merged, all.position ()); +} + string Image::digest () const { @@ -637,4 +690,137 @@ Image::digest () const return digester.get (); } - + +bool +operator== (Image const & a, Image const & b) +{ + if (a.components() != b.components() || a.pixel_format() != b.pixel_format() || a.aligned() != b.aligned()) { + return false; + } + + for (int c = 0; c < a.components(); ++c) { + if (a.lines(c) != b.lines(c) || a.line_size()[c] != b.line_size()[c] || a.stride()[c] != b.stride()[c]) { + return false; + } + + uint8_t* p = a.data()[c]; + uint8_t* q = b.data()[c]; + for (int y = 0; y < a.lines(c); ++y) { + if (memcmp (p, q, a.line_size()[c]) != 0) { + return false; + } + + p += a.stride()[c]; + q += b.stride()[c]; + } + } + + return true; +} + +void +Image::fade (float f) +{ + switch (_pixel_format) { + case PIX_FMT_YUV420P: + case PIX_FMT_YUV422P: + case PIX_FMT_YUV444P: + case PIX_FMT_YUV411P: + case PIX_FMT_YUVJ420P: + case PIX_FMT_YUVJ422P: + case PIX_FMT_YUVJ444P: + case PIX_FMT_RGB24: + case PIX_FMT_ARGB: + case PIX_FMT_RGBA: + case PIX_FMT_ABGR: + case PIX_FMT_BGRA: + case PIX_FMT_RGB555LE: + /* 8-bit */ + for (int c = 0; c < 3; ++c) { + uint8_t* p = data()[c]; + for (int y = 0; y < lines(c); ++y) { + uint8_t* q = p; + for (int x = 0; x < line_size()[c]; ++x) { + *q = int (float (*q) * f); + ++q; + } + p += stride()[c]; + } + } + break; + + case PIX_FMT_YUV422P9LE: + case PIX_FMT_YUV444P9LE: + case PIX_FMT_YUV422P10LE: + case PIX_FMT_YUV444P10LE: + case PIX_FMT_YUV422P16LE: + case PIX_FMT_YUV444P16LE: + case AV_PIX_FMT_YUVA420P9LE: + case AV_PIX_FMT_YUVA422P9LE: + case AV_PIX_FMT_YUVA444P9LE: + case AV_PIX_FMT_YUVA420P10LE: + case AV_PIX_FMT_YUVA422P10LE: + case AV_PIX_FMT_YUVA444P10LE: + /* 16-bit little-endian */ + for (int c = 0; c < 3; ++c) { + int const stride_pixels = stride()[c] / 2; + int const line_size_pixels = line_size()[c] / 2; + uint16_t* p = reinterpret_cast<uint16_t*> (data()[c]); + for (int y = 0; y < lines(c); ++y) { + uint16_t* q = p; + for (int x = 0; x < line_size_pixels; ++x) { + *q = int (float (*q) * f); + ++q; + } + p += stride_pixels; + } + } + break; + + case PIX_FMT_YUV422P9BE: + case PIX_FMT_YUV444P9BE: + case PIX_FMT_YUV444P10BE: + case PIX_FMT_YUV422P10BE: + case AV_PIX_FMT_YUVA420P9BE: + case AV_PIX_FMT_YUVA422P9BE: + case AV_PIX_FMT_YUVA444P9BE: + case AV_PIX_FMT_YUVA420P10BE: + case AV_PIX_FMT_YUVA422P10BE: + case AV_PIX_FMT_YUVA444P10BE: + case AV_PIX_FMT_YUVA420P16BE: + case AV_PIX_FMT_YUVA422P16BE: + case AV_PIX_FMT_YUVA444P16BE: + /* 16-bit big-endian */ + for (int c = 0; c < 3; ++c) { + int const stride_pixels = stride()[c] / 2; + int const line_size_pixels = line_size()[c] / 2; + uint16_t* p = reinterpret_cast<uint16_t*> (data()[c]); + for (int y = 0; y < lines(c); ++y) { + uint16_t* q = p; + for (int x = 0; x < line_size_pixels; ++x) { + *q = swap_16 (int (float (swap_16 (*q)) * f)); + ++q; + } + p += stride_pixels; + } + } + break; + + case PIX_FMT_UYVY422: + { + int const Y = lines(0); + int const X = line_size()[0]; + uint8_t* p = data()[0]; + for (int y = 0; y < Y; ++y) { + for (int x = 0; x < X; ++x) { + *p = int (float (*p) * f); + ++p; + } + } + break; + } + + default: + throw PixelFormatError ("fade()", _pixel_format); + } +} diff --git a/src/lib/image.h b/src/lib/image.h index f83bf6998..172250eb1 100644 --- a/src/lib/image.h +++ b/src/lib/image.h @@ -31,16 +31,17 @@ extern "C" { #include <libavcodec/avcodec.h> #include <libavfilter/avfilter.h> } -#include <libdcp/image.h> +#include <dcp/image.h> #include "util.h" #include "position.h" +#include "position_image.h" class Scaler; -class Image : public libdcp::Image +class Image : public dcp::Image { public: - Image (AVPixelFormat, libdcp::Size, bool); + Image (AVPixelFormat, dcp::Size, bool); Image (AVFrame *); Image (Image const &); Image (boost::shared_ptr<const Image>, bool); @@ -50,21 +51,23 @@ public: uint8_t ** data () const; int * line_size () const; int * stride () const; - libdcp::Size size () const; + dcp::Size size () const; bool aligned () const; int components () const; int line_factor (int) const; int lines (int) const; - boost::shared_ptr<Image> scale (libdcp::Size, Scaler const *, AVPixelFormat, bool aligned) const; + boost::shared_ptr<Image> scale (dcp::Size, Scaler const *, AVPixelFormat, bool aligned) const; boost::shared_ptr<Image> crop (Crop c, bool aligned) const; - boost::shared_ptr<Image> crop_scale_window (Crop c, libdcp::Size, libdcp::Size, Scaler const *, AVPixelFormat, bool aligned) const; + boost::shared_ptr<Image> crop_scale_window (Crop c, dcp::Size, dcp::Size, Scaler const *, AVPixelFormat, bool aligned) const; void make_black (); + void make_transparent (); void alpha_blend (boost::shared_ptr<const Image> image, Position<int> pos); void copy (boost::shared_ptr<const Image> image, Position<int> pos); + void fade (float); void read_from_socket (boost::shared_ptr<Socket>); void write_to_socket (boost::shared_ptr<Socket>) const; @@ -76,7 +79,7 @@ public: std::string digest () const; private: - friend class pixel_formats_test; + friend struct pixel_formats_test; void allocate (); void swap (Image &); @@ -91,4 +94,7 @@ private: bool _aligned; }; +extern PositionImage merge (std::list<PositionImage> images); +extern bool operator== (Image const & a, Image const & b); + #endif diff --git a/src/lib/image_content.cc b/src/lib/image_content.cc index 915da7beb..84b0b75c9 100644 --- a/src/lib/image_content.cc +++ b/src/lib/image_content.cc @@ -20,11 +20,11 @@ #include <libcxml/cxml.h> #include "image_content.h" #include "image_examiner.h" -#include "config.h" #include "compose.hpp" #include "film.h" #include "job.h" #include "frame_rate_change.h" +#include "exceptions.h" #include "safe_stringstream.h" #include "i18n.h" @@ -55,7 +55,7 @@ ImageContent::ImageContent (shared_ptr<const Film> f, boost::filesystem::path p) } -ImageContent::ImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version) +ImageContent::ImageContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) : Content (f, node) , VideoContent (f, node, version) { @@ -109,13 +109,11 @@ ImageContent::examine (shared_ptr<Job> job) assert (film); shared_ptr<ImageExaminer> examiner (new ImageExaminer (film, shared_from_this(), job)); - take_from_video_examiner (examiner); - set_video_length (examiner->video_length ()); } void -ImageContent::set_video_length (VideoContent::Frame len) +ImageContent::set_video_length (ContentTime len) { { boost::mutex::scoped_lock lm (_mutex); @@ -125,14 +123,12 @@ ImageContent::set_video_length (VideoContent::Frame len) signal_changed (ContentProperty::LENGTH); } -Time +DCPTime ImageContent::full_length () const { shared_ptr<const Film> film = _film.lock (); assert (film); - - FrameRateChange frc (video_frame_rate(), film->video_frame_rate ()); - return video_length_after_3d_combine() * frc.factor() * TIME_HZ / video_frame_rate(); + return DCPTime (video_length_after_3d_combine(), FrameRateChange (video_frame_rate(), film->video_frame_rate())); } string @@ -140,7 +136,7 @@ ImageContent::identifier () const { SafeStringStream s; s << VideoContent::identifier (); - s << "_" << video_length(); + s << "_" << video_length().get(); return s.str (); } diff --git a/src/lib/image_content.h b/src/lib/image_content.h index e56abce4a..a1b1437c8 100644 --- a/src/lib/image_content.h +++ b/src/lib/image_content.h @@ -31,7 +31,7 @@ class ImageContent : public VideoContent { public: ImageContent (boost::shared_ptr<const Film>, boost::filesystem::path); - ImageContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int); + ImageContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int); boost::shared_ptr<ImageContent> shared_from_this () { return boost::dynamic_pointer_cast<ImageContent> (Content::shared_from_this ()); @@ -41,11 +41,11 @@ public: std::string summary () const; std::string technical_summary () const; void as_xml (xmlpp::Node *) const; - Time full_length () const; + DCPTime full_length () const; std::string identifier () const; - void set_video_length (VideoContent::Frame); + void set_video_length (ContentTime); bool still () const; void set_video_frame_rate (float); }; diff --git a/src/lib/image_decoder.cc b/src/lib/image_decoder.cc index 7a9acd9e4..8702c1a33 100644 --- a/src/lib/image_decoder.cc +++ b/src/lib/image_decoder.cc @@ -23,7 +23,7 @@ #include "image_content.h" #include "image_decoder.h" #include "image.h" -#include "image_proxy.h" +#include "magick_image_proxy.h" #include "film.h" #include "exceptions.h" @@ -31,43 +31,35 @@ using std::cout; using boost::shared_ptr; -using libdcp::Size; +using dcp::Size; -ImageDecoder::ImageDecoder (shared_ptr<const Film> f, shared_ptr<const ImageContent> c) - : Decoder (f) - , VideoDecoder (f, c) +ImageDecoder::ImageDecoder (shared_ptr<const ImageContent> c) + : VideoDecoder (c) , _image_content (c) { } -void +bool ImageDecoder::pass () { - if (_video_position >= _image_content->video_length ()) { - return; + if (_video_position >= _image_content->video_length().frames (_image_content->video_frame_rate ())) { + return true; } - if (_image && _image_content->still ()) { - video (_image, true, _video_position); - return; + if (!_image_content->still() || !_image) { + /* Either we need an image or we are using moving images, so load one */ + _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position), _image_content->film()->log ())); } - - shared_ptr<const Film> film = _film.lock (); - assert (film); - - _image.reset (new MagickImageProxy (_image_content->path (_image_content->still() ? 0 : _video_position), film->log ())); - video (_image, false, _video_position); + + video (_image, _video_position); + ++_video_position; + return false; } void -ImageDecoder::seek (VideoContent::Frame frame, bool) -{ - _video_position = frame; -} - -bool -ImageDecoder::done () const +ImageDecoder::seek (ContentTime time, bool accurate) { - return _video_position >= _image_content->video_length (); + VideoDecoder::seek (time, accurate); + _video_position = time.frames (_image_content->video_frame_rate ()); } diff --git a/src/lib/image_decoder.h b/src/lib/image_decoder.h index 5b82dd85c..242f69477 100644 --- a/src/lib/image_decoder.h +++ b/src/lib/image_decoder.h @@ -28,20 +28,19 @@ class ImageContent; class ImageDecoder : public VideoDecoder { public: - ImageDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const ImageContent>); + ImageDecoder (boost::shared_ptr<const ImageContent> c); boost::shared_ptr<const ImageContent> content () { return _image_content; } - /* Decoder */ - - void pass (); - void seek (VideoContent::Frame, bool); - bool done () const; + void seek (ContentTime, bool); private: + bool pass (); + boost::shared_ptr<const ImageContent> _image_content; boost::shared_ptr<ImageProxy> _image; + VideoFrame _video_position; }; diff --git a/src/lib/image_examiner.cc b/src/lib/image_examiner.cc index 4ff324f68..004b89e65 100644 --- a/src/lib/image_examiner.cc +++ b/src/lib/image_examiner.cc @@ -36,21 +36,20 @@ using boost::shared_ptr; ImageExaminer::ImageExaminer (shared_ptr<const Film> film, shared_ptr<const ImageContent> content, shared_ptr<Job>) : _film (film) , _image_content (content) - , _video_length (0) { using namespace MagickCore; Magick::Image* image = new Magick::Image (content->path(0).string()); - _video_size = libdcp::Size (image->columns(), image->rows()); + _video_size = dcp::Size (image->columns(), image->rows()); delete image; if (content->still ()) { - _video_length = Config::instance()->default_still_length() * video_frame_rate(); + _video_length = ContentTime::from_seconds (Config::instance()->default_still_length()); } else { - _video_length = _image_content->number_of_paths (); + _video_length = ContentTime::from_frames (_image_content->number_of_paths (), video_frame_rate ()); } } -libdcp::Size +dcp::Size ImageExaminer::video_size () const { return _video_size.get (); diff --git a/src/lib/image_examiner.h b/src/lib/image_examiner.h index 8887f0d3d..6ae0422cb 100644 --- a/src/lib/image_examiner.h +++ b/src/lib/image_examiner.h @@ -31,14 +31,14 @@ public: ImageExaminer (boost::shared_ptr<const Film>, boost::shared_ptr<const ImageContent>, boost::shared_ptr<Job>); float video_frame_rate () const; - libdcp::Size video_size () const; - VideoContent::Frame video_length () const { + dcp::Size video_size () const; + ContentTime video_length () const { return _video_length; } private: boost::weak_ptr<const Film> _film; boost::shared_ptr<const ImageContent> _image_content; - boost::optional<libdcp::Size> _video_size; - VideoContent::Frame _video_length; + boost::optional<dcp::Size> _video_size; + ContentTime _video_length; }; diff --git a/src/lib/image_proxy.cc b/src/lib/image_proxy.cc index 3aba6cf7c..b6b387b76 100644 --- a/src/lib/image_proxy.cc +++ b/src/lib/image_proxy.cc @@ -17,10 +17,12 @@ */ -#include <Magick++.h> -#include <libdcp/util.h> -#include <libdcp/raw_convert.h> +#include <dcp/util.h> +#include <dcp/raw_convert.h> #include "image_proxy.h" +#include "raw_image_proxy.h" +#include "magick_image_proxy.h" +#include "j2k_image_proxy.h" #include "image.h" #include "exceptions.h" #include "cross.h" @@ -40,147 +42,6 @@ ImageProxy::ImageProxy (shared_ptr<Log> log) } -RawImageProxy::RawImageProxy (shared_ptr<Image> image, shared_ptr<Log> log) - : ImageProxy (log) - , _image (image) -{ - -} - -RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log) - : ImageProxy (log) -{ - libdcp::Size size ( - xml->number_child<int> ("Width"), xml->number_child<int> ("Height") - ); - - _image.reset (new Image (static_cast<AVPixelFormat> (xml->number_child<int> ("PixelFormat")), size, true)); - _image->read_from_socket (socket); -} - -shared_ptr<Image> -RawImageProxy::image () const -{ - return _image; -} - -void -RawImageProxy::add_metadata (xmlpp::Node* node) const -{ - node->add_child("Type")->add_child_text (N_("Raw")); - node->add_child("Width")->add_child_text (libdcp::raw_convert<string> (_image->size().width)); - node->add_child("Height")->add_child_text (libdcp::raw_convert<string> (_image->size().height)); - node->add_child("PixelFormat")->add_child_text (libdcp::raw_convert<string> (_image->pixel_format ())); -} - -void -RawImageProxy::send_binary (shared_ptr<Socket> socket) const -{ - _image->write_to_socket (socket); -} - -MagickImageProxy::MagickImageProxy (boost::filesystem::path path, shared_ptr<Log> log) - : ImageProxy (log) -{ - /* Read the file into a Blob */ - - boost::uintmax_t const size = boost::filesystem::file_size (path); - FILE* f = fopen_boost (path, "rb"); - if (!f) { - throw OpenFileError (path); - } - - uint8_t* data = new uint8_t[size]; - if (fread (data, 1, size, f) != size) { - delete[] data; - throw ReadFileError (path); - } - - fclose (f); - _blob.update (data, size); - delete[] data; -} - -MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket, shared_ptr<Log> log) - : ImageProxy (log) -{ - uint32_t const size = socket->read_uint32 (); - uint8_t* data = new uint8_t[size]; - socket->read (data, size); - _blob.update (data, size); - delete[] data; -} - -shared_ptr<Image> -MagickImageProxy::image () const -{ - if (_image) { - return _image; - } - - LOG_TIMING ("[%1] MagickImageProxy begins decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length()); - - Magick::Image* magick_image = 0; - string error; - try { - magick_image = new Magick::Image (_blob); - } catch (Magick::Exception& e) { - error = e.what (); - } - - if (!magick_image) { - /* ImageMagick cannot auto-detect Targa files, it seems, so try here with an - explicit format. I can't find it documented that passing a (0, 0) geometry - is allowed, but it seems to work. - */ - try { - magick_image = new Magick::Image (_blob, Magick::Geometry (0, 0), "TGA"); - } catch (...) { - - } - } - - if (!magick_image) { - /* If we failed both an auto-detect and a forced-Targa we give the error from - the auto-detect. - */ - throw DecodeError (String::compose (_("Could not decode image file (%1)"), error)); - } - - LOG_TIMING ("[%1] MagickImageProxy decode finished", boost::this_thread::get_id ()); - - libdcp::Size size (magick_image->columns(), magick_image->rows()); - - _image.reset (new Image (PIX_FMT_RGB24, size, true)); - - /* Write line-by-line here as _image must be aligned, and write() cannot be told about strides */ - uint8_t* p = _image->data()[0]; - for (int i = 0; i < size.height; ++i) { - using namespace MagickCore; - magick_image->write (0, i, size.width, 1, "RGB", CharPixel, p); - p += _image->stride()[0]; - } - - delete magick_image; - - LOG_TIMING ("[%1] MagickImageProxy completes decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length()); - - return _image; -} - -void -MagickImageProxy::add_metadata (xmlpp::Node* node) const -{ - node->add_child("Type")->add_child_text (N_("Magick")); -} - -void -MagickImageProxy::send_binary (shared_ptr<Socket> socket) const -{ - socket->write (_blob.length ()); - socket->write ((uint8_t *) _blob.data (), _blob.length ()); -} - shared_ptr<ImageProxy> image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log) { @@ -188,6 +49,8 @@ image_proxy_factory (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shar return shared_ptr<ImageProxy> (new RawImageProxy (xml, socket, log)); } else if (xml->string_child("Type") == N_("Magick")) { return shared_ptr<MagickImageProxy> (new MagickImageProxy (xml, socket, log)); + } else if (xml->string_child("Type") == N_("J2K")) { + return shared_ptr<J2KImageProxy> (new J2KImageProxy (xml, socket, log)); } throw NetworkError (_("Unexpected image type received by server")); diff --git a/src/lib/image_proxy.h b/src/lib/image_proxy.h index c0ccd9125..7ff28e174 100644 --- a/src/lib/image_proxy.h +++ b/src/lib/image_proxy.h @@ -17,6 +17,9 @@ */ +#ifndef DCPOMATIC_IMAGE_PROXY_H +#define DCPOMATIC_IMAGE_PROXY_H + /** @file src/lib/image_proxy.h * @brief ImageProxy and subclasses. */ @@ -34,6 +37,11 @@ namespace cxml { class Node; } +namespace dcp { + class MonoPictureFrame; + class StereoPictureFrame; +} + /** @class ImageProxy * @brief A class which holds an Image, and can produce it on request. * @@ -42,7 +50,7 @@ namespace cxml { * of happening in a single-threaded decoder. * * For example, large TIFFs are slow to decode, so this class will keep - * the TIFF data TIFF until such a time that the actual image is needed. + * the TIFF data compressed until the decompressed image is needed. * At this point, the class decodes the TIFF to an Image. */ class ImageProxy : public boost::noncopyable @@ -55,38 +63,15 @@ public: virtual boost::shared_ptr<Image> image () const = 0; virtual void add_metadata (xmlpp::Node *) const = 0; virtual void send_binary (boost::shared_ptr<Socket>) const = 0; + /** @return true if our image is definitely the same as another, false if it is probably not */ + virtual bool same (boost::shared_ptr<const ImageProxy>) const { + return false; + } protected: boost::shared_ptr<Log> _log; }; -class RawImageProxy : public ImageProxy -{ -public: - RawImageProxy (boost::shared_ptr<Image>, boost::shared_ptr<Log> log); - RawImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); - - boost::shared_ptr<Image> image () const; - void add_metadata (xmlpp::Node *) const; - void send_binary (boost::shared_ptr<Socket>) const; - -private: - boost::shared_ptr<Image> _image; -}; - -class MagickImageProxy : public ImageProxy -{ -public: - MagickImageProxy (boost::filesystem::path, boost::shared_ptr<Log> log); - MagickImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); - - boost::shared_ptr<Image> image () const; - void add_metadata (xmlpp::Node *) const; - void send_binary (boost::shared_ptr<Socket>) const; - -private: - Magick::Blob _blob; - mutable boost::shared_ptr<Image> _image; -}; - boost::shared_ptr<ImageProxy> image_proxy_factory (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); + +#endif diff --git a/src/lib/image_subtitle.h b/src/lib/image_subtitle.h new file mode 100644 index 000000000..b25943ae1 --- /dev/null +++ b/src/lib/image_subtitle.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_IMAGE_SUBTITLE_H +#define DCPOMATIC_IMAGE_SUBTITLE_H + +#include "rect.h" + +class Image; + +class ImageSubtitle +{ +public: + ImageSubtitle (boost::shared_ptr<Image> i, dcpomatic::Rect<double> r) + : image (i) + , rectangle (r) + {} + + boost::shared_ptr<Image> image; + /** Area that the subtitle covers on its corresponding video, expressed in + * proportions of the image size; e.g. rectangle.x = 0.5 would mean that + * the rectangle starts half-way across the video. + * + * This rectangle may or may not have had a SubtitleContent's offsets and + * scale applied to it, depending on context. + */ + dcpomatic::Rect<double> rectangle; +}; + +#endif diff --git a/src/lib/isdcf_metadata.cc b/src/lib/isdcf_metadata.cc index dfba50a9a..7d960b6ac 100644 --- a/src/lib/isdcf_metadata.cc +++ b/src/lib/isdcf_metadata.cc @@ -19,16 +19,16 @@ #include <iostream> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "isdcf_metadata.h" #include "i18n.h" using std::string; using boost::shared_ptr; -using libdcp::raw_convert; +using dcp::raw_convert; -ISDCFMetadata::ISDCFMetadata (shared_ptr<const cxml::Node> node) +ISDCFMetadata::ISDCFMetadata (cxml::ConstNodePtr node) { content_version = node->number_child<int> ("ContentVersion"); audio_language = node->string_child ("AudioLanguage"); diff --git a/src/lib/isdcf_metadata.h b/src/lib/isdcf_metadata.h index 0fb7e7baa..e63f290e4 100644 --- a/src/lib/isdcf_metadata.h +++ b/src/lib/isdcf_metadata.h @@ -22,10 +22,7 @@ #include <string> #include <libxml++/libxml++.h> - -namespace cxml { - class Node; -} +#include <libcxml/cxml.h> class ISDCFMetadata { @@ -38,7 +35,7 @@ public: , two_d_version_of_three_d (false) {} - ISDCFMetadata (boost::shared_ptr<const cxml::Node>); + ISDCFMetadata (cxml::ConstNodePtr); void as_xml (xmlpp::Node *) const; void read_old_metadata (std::string, std::string); diff --git a/src/lib/j2k_image_proxy.cc b/src/lib/j2k_image_proxy.cc new file mode 100644 index 000000000..6924fad79 --- /dev/null +++ b/src/lib/j2k_image_proxy.cc @@ -0,0 +1,123 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/raw_convert.h> +#include <dcp/mono_picture_frame.h> +#include <dcp/stereo_picture_frame.h> +#include "j2k_image_proxy.h" +#include "util.h" +#include "image.h" +#include "encoded_data.h" + +#include "i18n.h" + +using std::string; +using boost::shared_ptr; + +J2KImageProxy::J2KImageProxy (shared_ptr<const dcp::MonoPictureFrame> frame, dcp::Size size, shared_ptr<Log> log) + : ImageProxy (log) + , _mono (frame) + , _size (size) +{ + +} + +J2KImageProxy::J2KImageProxy (shared_ptr<const dcp::StereoPictureFrame> frame, dcp::Size size, dcp::Eye eye, shared_ptr<Log> log) + : ImageProxy (log) + , _stereo (frame) + , _size (size) + , _eye (eye) +{ + +} + +J2KImageProxy::J2KImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log) + : ImageProxy (log) +{ + _size = dcp::Size (xml->number_child<int> ("Width"), xml->number_child<int> ("Height")); + if (xml->optional_number_child<int> ("Eye")) { + _eye = static_cast<dcp::Eye> (xml->number_child<int> ("Eye")); + int const left_size = xml->number_child<int> ("LeftSize"); + int const right_size = xml->number_child<int> ("RightSize"); + shared_ptr<dcp::StereoPictureFrame> f (new dcp::StereoPictureFrame ()); + socket->read (f->left_j2k_data(), left_size); + socket->read (f->right_j2k_data(), right_size); + _stereo = f; + } else { + int const size = xml->number_child<int> ("Size"); + shared_ptr<dcp::MonoPictureFrame> f (new dcp::MonoPictureFrame ()); + socket->read (f->j2k_data (), size); + _mono = f; + } +} + +shared_ptr<Image> +J2KImageProxy::image () const +{ + shared_ptr<Image> image (new Image (PIX_FMT_RGB24, _size, false)); + + if (_mono) { + _mono->rgb_frame (image->data()[0]); + } else { + _stereo->rgb_frame (_eye, image->data()[0]); + } + + return shared_ptr<Image> (new Image (image, true)); +} + +void +J2KImageProxy::add_metadata (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text (N_("J2K")); + node->add_child("Width")->add_child_text (dcp::raw_convert<string> (_size.width)); + node->add_child("Height")->add_child_text (dcp::raw_convert<string> (_size.height)); + if (_stereo) { + node->add_child("Eye")->add_child_text (dcp::raw_convert<string> (_eye)); + node->add_child("LeftSize")->add_child_text (dcp::raw_convert<string> (_stereo->left_j2k_size ())); + node->add_child("RightSize")->add_child_text (dcp::raw_convert<string> (_stereo->right_j2k_size ())); + } else { + node->add_child("Size")->add_child_text (dcp::raw_convert<string> (_mono->j2k_size ())); + } +} + +void +J2KImageProxy::send_binary (shared_ptr<Socket> socket) const +{ + if (_mono) { + socket->write (_mono->j2k_data(), _mono->j2k_size ()); + } else { + socket->write (_stereo->left_j2k_data(), _stereo->left_j2k_size ()); + socket->write (_stereo->right_j2k_data(), _stereo->right_j2k_size ()); + } +} + +shared_ptr<EncodedData> +J2KImageProxy::j2k () const +{ + if (_mono) { + return shared_ptr<EncodedData> (new EncodedData (_mono->j2k_data(), _mono->j2k_size())); + } else { + if (_eye == dcp::EYE_LEFT) { + return shared_ptr<EncodedData> (new EncodedData (_stereo->left_j2k_data(), _stereo->left_j2k_size())); + } else { + return shared_ptr<EncodedData> (new EncodedData (_stereo->right_j2k_data(), _stereo->right_j2k_size())); + } + } +} diff --git a/src/lib/j2k_image_proxy.h b/src/lib/j2k_image_proxy.h new file mode 100644 index 000000000..d7b5c83fc --- /dev/null +++ b/src/lib/j2k_image_proxy.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/util.h> +#include "image_proxy.h" + +class EncodedData; + +class J2KImageProxy : public ImageProxy +{ +public: + J2KImageProxy (boost::shared_ptr<const dcp::MonoPictureFrame> frame, dcp::Size, boost::shared_ptr<Log> log); + J2KImageProxy (boost::shared_ptr<const dcp::StereoPictureFrame> frame, dcp::Size, dcp::Eye, boost::shared_ptr<Log> log); + J2KImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); + + boost::shared_ptr<Image> image () const; + void add_metadata (xmlpp::Node *) const; + void send_binary (boost::shared_ptr<Socket>) const; + + boost::shared_ptr<EncodedData> j2k () const; + dcp::Size size () const { + return _size; + } + +private: + boost::shared_ptr<const dcp::MonoPictureFrame> _mono; + boost::shared_ptr<const dcp::StereoPictureFrame> _stereo; + dcp::Size _size; + dcp::Eye _eye; +}; diff --git a/src/lib/job.cc b/src/lib/job.cc index 52ec1426c..1d3cb73b6 100644 --- a/src/lib/job.cc +++ b/src/lib/job.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,13 +23,14 @@ #include <boost/thread.hpp> #include <boost/filesystem.hpp> -#include <libdcp/exceptions.h> +#include <dcp/exceptions.h> #include "job.h" #include "util.h" #include "cross.h" #include "ui_signaller.h" #include "exceptions.h" -#include "safe_stringstream.h" +#include "film.h" +#include "log.h" #include "i18n.h" @@ -66,8 +67,8 @@ Job::run_wrapper () run (); - } catch (libdcp::FileError& e) { - + } catch (dcp::FileError& e) { + string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf()); try { @@ -204,7 +205,7 @@ Job::set_state (State s) } } -/** @return Time (in seconds) that this sub-job has been running */ +/** @return DCPTime (in seconds) that this sub-job has been running */ int Job::elapsed_time () const { @@ -279,6 +280,7 @@ Job::error_summary () const void Job::set_error (string s, string d) { + _film->log()->log (String::compose ("Error in job: %1 (%2)", s, d), Log::TYPE_ERROR); boost::mutex::scoped_lock lm (_state_mutex); _error_summary = s; _error_details = d; diff --git a/src/lib/kdm.cc b/src/lib/kdm.cc index f5054b8ed..108860594 100644 --- a/src/lib/kdm.cc +++ b/src/lib/kdm.cc @@ -21,7 +21,8 @@ #include <boost/shared_ptr.hpp> #include <quickmail.h> #include <zip.h> -#include <libdcp/kdm.h> +#include <dcp/encrypted_kdm.h> +#include <dcp/types.h> #include "kdm.h" #include "cinema.h" #include "exceptions.h" @@ -37,13 +38,13 @@ using boost::shared_ptr; struct ScreenKDM { - ScreenKDM (shared_ptr<Screen> s, libdcp::KDM k) + ScreenKDM (shared_ptr<Screen> s, dcp::EncryptedKDM k) : screen (s) , kdm (k) {} shared_ptr<Screen> screen; - libdcp::KDM kdm; + dcp::EncryptedKDM kdm; }; static string @@ -104,17 +105,17 @@ make_screen_kdms ( shared_ptr<const Film> film, list<shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation ) { - list<libdcp::KDM> kdms = film->make_kdms (screens, cpl, from, to, formulation); + list<dcp::EncryptedKDM> kdms = film->make_kdms (screens, cpl, from, to, formulation); list<ScreenKDM> screen_kdms; list<shared_ptr<Screen> >::iterator i = screens.begin (); - list<libdcp::KDM>::iterator j = kdms.begin (); + list<dcp::EncryptedKDM>::iterator j = kdms.begin (); while (i != screens.end() && j != kdms.end ()) { screen_kdms.push_back (ScreenKDM (*i, *j)); ++i; @@ -129,9 +130,9 @@ make_cinema_kdms ( shared_ptr<const Film> film, list<shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation ) { list<ScreenKDM> screen_kdms = make_screen_kdms (film, screens, cpl, from, to, formulation); @@ -175,9 +176,9 @@ write_kdm_files ( shared_ptr<const Film> film, list<shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation, + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation, boost::filesystem::path directory ) { @@ -196,9 +197,9 @@ write_kdm_zip_files ( shared_ptr<const Film> film, list<shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation, + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation, boost::filesystem::path directory ) { @@ -216,9 +217,9 @@ email_kdms ( shared_ptr<const Film> film, list<shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation ) { list<CinemaKDMs> cinema_kdms = make_cinema_kdms (film, screens, cpl, from, to, formulation); diff --git a/src/lib/kdm.h b/src/lib/kdm.h index 8fb4ec494..b80ef454f 100644 --- a/src/lib/kdm.h +++ b/src/lib/kdm.h @@ -27,9 +27,9 @@ extern void write_kdm_files ( boost::shared_ptr<const Film> film, std::list<boost::shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation, + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation, boost::filesystem::path directory ); @@ -37,9 +37,9 @@ extern void write_kdm_zip_files ( boost::shared_ptr<const Film> film, std::list<boost::shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation, + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation, boost::filesystem::path directory ); @@ -47,8 +47,8 @@ extern void email_kdms ( boost::shared_ptr<const Film> film, std::list<boost::shared_ptr<Screen> > screens, boost::filesystem::path cpl, - boost::posix_time::ptime from, - boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation + dcp::LocalTime from, + dcp::LocalTime to, + dcp::Formulation formulation ); diff --git a/src/lib/magick_image_proxy.cc b/src/lib/magick_image_proxy.cc new file mode 100644 index 000000000..e5265187f --- /dev/null +++ b/src/lib/magick_image_proxy.cc @@ -0,0 +1,152 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <Magick++.h> +#include "magick_image_proxy.h" +#include "cross.h" +#include "exceptions.h" +#include "util.h" +#include "log.h" +#include "image.h" +#include "log.h" + +#include "i18n.h" + +#define LOG_TIMING(...) _log->microsecond_log (String::compose (__VA_ARGS__), Log::TYPE_TIMING); + +using std::string; +using std::cout; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +MagickImageProxy::MagickImageProxy (boost::filesystem::path path, shared_ptr<Log> log) + : ImageProxy (log) +{ + /* Read the file into a Blob */ + + boost::uintmax_t const size = boost::filesystem::file_size (path); + FILE* f = fopen_boost (path, "rb"); + if (!f) { + throw OpenFileError (path); + } + + uint8_t* data = new uint8_t[size]; + if (fread (data, 1, size, f) != size) { + delete[] data; + throw ReadFileError (path); + } + + fclose (f); + _blob.update (data, size); + delete[] data; +} + +MagickImageProxy::MagickImageProxy (shared_ptr<cxml::Node>, shared_ptr<Socket> socket, shared_ptr<Log> log) + : ImageProxy (log) +{ + uint32_t const size = socket->read_uint32 (); + uint8_t* data = new uint8_t[size]; + socket->read (data, size); + _blob.update (data, size); + delete[] data; +} + +shared_ptr<Image> +MagickImageProxy::image () const +{ + if (_image) { + return _image; + } + + LOG_TIMING ("[%1] MagickImageProxy begins decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length()); + + Magick::Image* magick_image = 0; + string error; + try { + magick_image = new Magick::Image (_blob); + } catch (Magick::Exception& e) { + error = e.what (); + } + + if (!magick_image) { + /* ImageMagick cannot auto-detect Targa files, it seems, so try here with an + explicit format. I can't find it documented that passing a (0, 0) geometry + is allowed, but it seems to work. + */ + try { + magick_image = new Magick::Image (_blob, Magick::Geometry (0, 0), "TGA"); + } catch (...) { + + } + } + + if (!magick_image) { + /* If we failed both an auto-detect and a forced-Targa we give the error from + the auto-detect. + */ + throw DecodeError (String::compose (_("Could not decode image file (%1)"), error)); + } + + dcp::Size size (magick_image->columns(), magick_image->rows()); + LOG_TIMING ("[%1] MagickImageProxy decode finished", boost::this_thread::get_id ()); + + _image.reset (new Image (PIX_FMT_RGB24, size, true)); + + /* Write line-by-line here as _image must be aligned, and write() cannot be told about strides */ + uint8_t* p = _image->data()[0]; + for (int i = 0; i < size.height; ++i) { + using namespace MagickCore; + magick_image->write (0, i, size.width, 1, "RGB", CharPixel, p); + p += _image->stride()[0]; + } + + delete magick_image; + + LOG_TIMING ("[%1] MagickImageProxy completes decode and convert of %2 bytes", boost::this_thread::get_id(), _blob.length()); + + return _image; +} + +void +MagickImageProxy::add_metadata (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text (N_("Magick")); +} + +void +MagickImageProxy::send_binary (shared_ptr<Socket> socket) const +{ + socket->write (_blob.length ()); + socket->write ((uint8_t *) _blob.data (), _blob.length ()); +} + +bool +MagickImageProxy::same (shared_ptr<const ImageProxy> other) const +{ + shared_ptr<const MagickImageProxy> mp = dynamic_pointer_cast<const MagickImageProxy> (other); + if (!mp) { + return false; + } + + if (_blob.length() != mp->_blob.length()) { + return false; + } + + return memcmp (_blob.data(), mp->_blob.data(), _blob.length()) == 0; +} diff --git a/src/lib/magick_image_proxy.h b/src/lib/magick_image_proxy.h new file mode 100644 index 000000000..8b43d0a00 --- /dev/null +++ b/src/lib/magick_image_proxy.h @@ -0,0 +1,36 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "image_proxy.h" + +class MagickImageProxy : public ImageProxy +{ +public: + MagickImageProxy (boost::filesystem::path, boost::shared_ptr<Log> log); + MagickImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); + + boost::shared_ptr<Image> image () const; + void add_metadata (xmlpp::Node *) const; + void send_binary (boost::shared_ptr<Socket>) const; + bool same (boost::shared_ptr<const ImageProxy> other) const; + +private: + Magick::Blob _blob; + mutable boost::shared_ptr<Image> _image; +}; diff --git a/src/lib/mid_side_decoder.cc b/src/lib/mid_side_decoder.cc new file mode 100644 index 000000000..be82f6754 --- /dev/null +++ b/src/lib/mid_side_decoder.cc @@ -0,0 +1,72 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "mid_side_decoder.h" +#include "audio_buffers.h" + +#include "i18n.h" + +using std::string; +using boost::shared_ptr; + +string +MidSideDecoder::name () const +{ + return _("Mid-side decoder"); +} + +string +MidSideDecoder::id () const +{ + return N_("mid-side-decoder"); +} + +ChannelCount +MidSideDecoder::in_channels () const +{ + return ChannelCount (2); +} + +int +MidSideDecoder::out_channels (int) const +{ + return 3; +} + +shared_ptr<AudioProcessor> +MidSideDecoder::clone (int) const +{ + return shared_ptr<AudioProcessor> (new MidSideDecoder ()); +} + +shared_ptr<AudioBuffers> +MidSideDecoder::run (shared_ptr<const AudioBuffers> in) +{ + shared_ptr<AudioBuffers> out (new AudioBuffers (3, in->frames ())); + for (int i = 0; i < in->frames(); ++i) { + float const left = in->data()[0][i]; + float const right = in->data()[1][i]; + float const mid = (left + right) / 2; + out->data()[0][i] = left - mid; + out->data()[1][i] = right - mid; + out->data()[2][i] = mid; + } + + return out; +} diff --git a/src/lib/mid_side_decoder.h b/src/lib/mid_side_decoder.h new file mode 100644 index 000000000..dac6cb7d9 --- /dev/null +++ b/src/lib/mid_side_decoder.h @@ -0,0 +1,33 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "audio_processor.h" + +class MidSideDecoder : public AudioProcessor +{ +public: + std::string name () const; + std::string id () const; + ChannelCount in_channels () const; + int out_channels (int) const; + boost::shared_ptr<AudioProcessor> clone (int) const; + boost::shared_ptr<AudioBuffers> run (boost::shared_ptr<const AudioBuffers>); +}; + + diff --git a/src/lib/piece.cc b/src/lib/piece.cc deleted file mode 100644 index 494fb17a0..000000000 --- a/src/lib/piece.cc +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - 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 "piece.h" -#include "player.h" - -using boost::shared_ptr; - -Piece::Piece (shared_ptr<Content> c) - : content (c) - , video_position (c->position ()) - , audio_position (c->position ()) - , repeat_to_do (0) - , repeat_done (0) -{ - -} - -Piece::Piece (shared_ptr<Content> c, shared_ptr<Decoder> d) - : content (c) - , decoder (d) - , video_position (c->position ()) - , audio_position (c->position ()) - , repeat_to_do (0) - , repeat_done (0) -{ - -} - -/** Set this piece to repeat a video frame a given number of times */ -void -Piece::set_repeat (IncomingVideo video, int num) -{ - repeat_video = video; - repeat_to_do = num; - repeat_done = 0; -} - -void -Piece::reset_repeat () -{ - repeat_video.image.reset (); - repeat_to_do = 0; - repeat_done = 0; -} - -bool -Piece::repeating () const -{ - return repeat_done != repeat_to_do; -} - -void -Piece::repeat (Player* player) -{ - player->process_video ( - repeat_video.weak_piece, - repeat_video.image, - repeat_video.eyes, - repeat_video.part, - repeat_done > 0, - repeat_video.frame, - (repeat_done + 1) * (TIME_HZ / player->_film->video_frame_rate ()) - ); - - ++repeat_done; -} - diff --git a/src/lib/piece.h b/src/lib/piece.h index 17b87b884..976409381 100644 --- a/src/lib/piece.h +++ b/src/lib/piece.h @@ -25,42 +25,19 @@ class Content; class Decoder; -class Piece; -class ImageProxy; -class Player; - -struct IncomingVideo -{ -public: - boost::weak_ptr<Piece> weak_piece; - boost::shared_ptr<const ImageProxy> image; - Eyes eyes; - Part part; - bool same; - VideoContent::Frame frame; - Time extra; -}; class Piece { public: - Piece (boost::shared_ptr<Content> c); - Piece (boost::shared_ptr<Content> c, boost::shared_ptr<Decoder> d); - void set_repeat (IncomingVideo video, int num); - void reset_repeat (); - bool repeating () const; - void repeat (Player* player); - + Piece (boost::shared_ptr<Content> c, boost::shared_ptr<Decoder> d, FrameRateChange f) + : content (c) + , decoder (d) + , frc (f) + {} + boost::shared_ptr<Content> content; boost::shared_ptr<Decoder> decoder; - /** Time of the last video we emitted relative to the start of the DCP */ - Time video_position; - /** Time of the last audio we emitted relative to the start of the DCP */ - Time audio_position; - - IncomingVideo repeat_video; - int repeat_to_do; - int repeat_done; + FrameRateChange frc; }; #endif diff --git a/src/lib/player.cc b/src/lib/player.cc index 8063d1212..db99cd2ad 100644 --- a/src/lib/player.cc +++ b/src/lib/player.cc @@ -17,26 +17,38 @@ */ -#include <stdint.h> #include "player.h" #include "film.h" #include "ffmpeg_decoder.h" +#include "audio_buffers.h" #include "ffmpeg_content.h" #include "image_decoder.h" #include "image_content.h" #include "sndfile_decoder.h" #include "sndfile_content.h" #include "subtitle_content.h" +#include "subrip_decoder.h" +#include "subrip_content.h" +#include "dcp_content.h" #include "playlist.h" #include "job.h" #include "image.h" -#include "image_proxy.h" +#include "raw_image_proxy.h" #include "ratio.h" -#include "resampler.h" #include "log.h" #include "scaler.h" -#include "player_video_frame.h" +#include "render_subtitles.h" +#include "config.h" +#include "content_video.h" +#include "player_video.h" #include "frame_rate_change.h" +#include "dcp_content.h" +#include "dcp_decoder.h" +#include "dcp_subtitle_content.h" +#include "dcp_subtitle_decoder.h" +#include <boost/foreach.hpp> +#include <stdint.h> +#include <algorithm> #define LOG_GENERAL(...) _film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); @@ -44,23 +56,21 @@ using std::list; using std::cout; using std::min; using std::max; +using std::min; using std::vector; using std::pair; using std::map; +using std::make_pair; using boost::shared_ptr; using boost::weak_ptr; using boost::dynamic_pointer_cast; +using boost::optional; Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p) : _film (f) , _playlist (p) - , _video (true) - , _audio (true) , _have_valid_pieces (false) - , _video_position (0) - , _audio_position (0) - , _audio_merger (f->audio_channels(), bind (&Film::time_to_audio_frames, f.get(), _1), bind (&Film::audio_frames_to_time, f.get(), _1)) - , _last_emit_was_black (false) + , _approximate_size (false) { _playlist_changed_connection = _playlist->Changed.connect (bind (&Player::playlist_changed, this)); _playlist_content_changed_connection = _playlist->ContentChanged.connect (bind (&Player::content_changed, this, _1, _2, _3)); @@ -69,579 +79,508 @@ Player::Player (shared_ptr<const Film> f, shared_ptr<const Playlist> p) } void -Player::disable_video () -{ - _video = false; -} - -void -Player::disable_audio () +Player::setup_pieces () { - _audio = false; -} + list<shared_ptr<Piece> > old_pieces = _pieces; + _pieces.clear (); -bool -Player::pass () -{ - if (!_have_valid_pieces) { - setup_pieces (); - } + ContentList content = _playlist->content (); - Time earliest_t = TIME_MAX; - shared_ptr<Piece> earliest; - enum { - VIDEO, - AUDIO - } type = VIDEO; + for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { - for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { - if ((*i)->decoder->done () || (*i)->content->length_after_trim() == 0) { + if (!(*i)->paths_valid ()) { continue; } - - shared_ptr<VideoDecoder> vd = dynamic_pointer_cast<VideoDecoder> ((*i)->decoder); - shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder); - - if (_video && vd) { - if ((*i)->video_position < earliest_t) { - earliest_t = (*i)->video_position; - earliest = *i; - type = VIDEO; + + shared_ptr<Decoder> decoder; + optional<FrameRateChange> frc; + + /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */ + DCPTime best_overlap_t; + shared_ptr<VideoContent> best_overlap; + for (ContentList::iterator j = content.begin(); j != content.end(); ++j) { + shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j); + if (!vc) { + continue; + } + + DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end()); + if (overlap > best_overlap_t) { + best_overlap = vc; + best_overlap_t = overlap; } } - if (_audio && ad && ad->has_audio ()) { - if ((*i)->audio_position < earliest_t) { - earliest_t = (*i)->audio_position; - earliest = *i; - type = AUDIO; - } + optional<FrameRateChange> best_overlap_frc; + if (best_overlap) { + best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ()); + } else { + /* No video overlap; e.g. if the DCP is just audio */ + best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ()); } - } - if (!earliest) { - flush (); - return true; - } + /* FFmpeg */ + shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i); + if (fc) { + decoder.reset (new FFmpegDecoder (fc, _film->log())); + frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate()); + } - switch (type) { - case VIDEO: - if (earliest_t > _video_position) { - emit_black (); - } else { - if (earliest->repeating ()) { - earliest->repeat (this); - } else { - earliest->decoder->pass (); - } + shared_ptr<const DCPContent> dc = dynamic_pointer_cast<const DCPContent> (*i); + if (dc) { + decoder.reset (new DCPDecoder (dc, _film->log ())); + frc = FrameRateChange (dc->video_frame_rate(), _film->video_frame_rate()); } - break; - case AUDIO: - if (earliest_t > _audio_position) { - emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position)); - } else { - earliest->decoder->pass (); - - if (earliest->decoder->done()) { - shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (earliest->content); - assert (ac); - shared_ptr<Resampler> re = resampler (ac, false); - if (re) { - shared_ptr<const AudioBuffers> b = re->flush (); - if (b->frames ()) { - process_audio ( - earliest, - b, - ac->audio_length() * ac->output_audio_frame_rate() / ac->content_audio_frame_rate(), - true - ); - } + /* ImageContent */ + shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i); + if (ic) { + /* See if we can re-use an old ImageDecoder */ + for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) { + shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder); + if (imd && imd->content() == ic) { + decoder = imd; } } - } - break; - } - if (_audio) { - boost::optional<Time> audio_done_up_to; - for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { - if ((*i)->decoder->done ()) { - continue; + if (!decoder) { + decoder.reset (new ImageDecoder (ic)); } - shared_ptr<AudioDecoder> ad = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder); - if (ad && ad->has_audio ()) { - audio_done_up_to = min (audio_done_up_to.get_value_or (TIME_MAX), (*i)->audio_position); - } + frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate()); } - if (audio_done_up_to) { - TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to.get ()); - Audio (tb.audio, tb.time); - _audio_position += _film->audio_frames_to_time (tb.audio->frames ()); + /* SndfileContent */ + shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i); + if (sc) { + decoder.reset (new SndfileDecoder (sc)); + frc = best_overlap_frc; } - } - - return false; -} -/** @param extra Amount of extra time to add to the content frame's time (for repeat) */ -void -Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const ImageProxy> image, Eyes eyes, Part part, bool same, VideoContent::Frame frame, Time extra) -{ - /* Keep a note of what came in so that we can repeat it if required */ - _last_incoming_video.weak_piece = weak_piece; - _last_incoming_video.image = image; - _last_incoming_video.eyes = eyes; - _last_incoming_video.part = part; - _last_incoming_video.same = same; - _last_incoming_video.frame = frame; - _last_incoming_video.extra = extra; - - shared_ptr<Piece> piece = weak_piece.lock (); - if (!piece) { - return; - } - - shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content); - assert (content); - - FrameRateChange frc (content->video_frame_rate(), _film->video_frame_rate()); - if (frc.skip && (frame % 2) == 1) { - return; - } - - Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate()); - if (content->trimmed (relative_time)) { - return; - } - - Time const time = content->position() + relative_time + extra - content->trim_start (); - libdcp::Size const image_size = content->scale().size (content, _video_container_size, _film->frame_size ()); - - shared_ptr<PlayerVideoFrame> pi ( - new PlayerVideoFrame ( - image, - content->crop(), - image_size, - _video_container_size, - _film->scaler(), - eyes, - part, - content->colour_conversion() - ) - ); - - if (_film->with_subtitles ()) { - for (list<Subtitle>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - if (i->covers (time)) { - /* This may be true for more than one of _subtitles, but the last (latest-starting) - one is the one we want to use, so that's ok. - */ - Position<int> const container_offset ( - (_video_container_size.width - image_size.width) / 2, - (_video_container_size.height - image_size.width) / 2 - ); - - pi->set_subtitle (i->out_image(), i->out_position() + container_offset); - } + /* SubRipContent */ + shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i); + if (rc) { + decoder.reset (new SubRipDecoder (rc)); + frc = best_overlap_frc; } - } - /* Clear out old subtitles */ - for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ) { - list<Subtitle>::iterator j = i; - ++j; - - if (i->ends_before (time)) { - _subtitles.erase (i); + /* DCPSubtitleContent */ + shared_ptr<const DCPSubtitleContent> dsc = dynamic_pointer_cast<const DCPSubtitleContent> (*i); + if (dsc) { + decoder.reset (new DCPSubtitleDecoder (dsc)); + frc = best_overlap_frc; } - i = j; + _pieces.push_back (shared_ptr<Piece> (new Piece (*i, decoder, frc.get ()))); } -#ifdef DCPOMATIC_DEBUG - _last_video = piece->content; -#endif + _have_valid_pieces = true; +} - Video (pi, same, time); +void +Player::content_changed (weak_ptr<Content> w, int property, bool frequent) +{ + shared_ptr<Content> c = w.lock (); + if (!c) { + return; + } - _last_emit_was_black = false; - _video_position = piece->video_position = (time + TIME_HZ / _film->video_frame_rate()); + if ( + property == ContentProperty::POSITION || + property == ContentProperty::LENGTH || + property == ContentProperty::TRIM_START || + property == ContentProperty::TRIM_END || + property == ContentProperty::PATH || + property == VideoContentProperty::VIDEO_FRAME_TYPE || + property == DCPContentProperty::CAN_BE_PLAYED + ) { + + _have_valid_pieces = false; + Changed (frequent); - if (frc.repeat > 1 && !piece->repeating ()) { - piece->set_repeat (_last_incoming_video, frc.repeat - 1); + } else if ( + property == SubtitleContentProperty::USE_SUBTITLES || + property == SubtitleContentProperty::SUBTITLE_X_OFFSET || + property == SubtitleContentProperty::SUBTITLE_Y_OFFSET || + property == SubtitleContentProperty::SUBTITLE_X_SCALE || + property == SubtitleContentProperty::SUBTITLE_Y_SCALE || + property == VideoContentProperty::VIDEO_CROP || + property == VideoContentProperty::VIDEO_SCALE || + property == VideoContentProperty::VIDEO_FRAME_RATE + ) { + + Changed (frequent); } } /** @param already_resampled true if this data has already been through the chain up to the resampler */ void -Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame, bool already_resampled) +Player::playlist_changed () { - shared_ptr<Piece> piece = weak_piece.lock (); - if (!piece) { - return; - } + _have_valid_pieces = false; + Changed (false); +} - shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content); - assert (content); +void +Player::set_video_container_size (dcp::Size s) +{ + _video_container_size = s; - if (!already_resampled) { - /* Gain */ - if (content->audio_gain() != 0) { - shared_ptr<AudioBuffers> gain (new AudioBuffers (audio)); - gain->apply_gain (content->audio_gain ()); - audio = gain; - } - - /* Resample */ - if (content->content_audio_frame_rate() != content->output_audio_frame_rate()) { - shared_ptr<Resampler> r = resampler (content, true); - pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> ro = r->run (audio, frame); - audio = ro.first; - frame = ro.second; - } - } - - Time const relative_time = _film->audio_frames_to_time (frame); + _black_image.reset (new Image (PIX_FMT_RGB24, _video_container_size, true)); + _black_image->make_black (); +} - if (content->trimmed (relative_time)) { - return; - } +void +Player::film_changed (Film::Property p) +{ + /* Here we should notice Film properties that affect our output, and + alert listeners that our output now would be different to how it was + last time we were run. + */ - Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time - content->trim_start (); - - /* Remap channels */ - shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames())); - dcp_mapped->make_silent (); - - AudioMapping map = content->audio_mapping (); - for (int i = 0; i < map.content_channels(); ++i) { - for (int j = 0; j < _film->audio_channels(); ++j) { - if (map.get (i, static_cast<libdcp::Channel> (j)) > 0) { - dcp_mapped->accumulate_channel ( - audio.get(), - i, - static_cast<libdcp::Channel> (j), - map.get (i, static_cast<libdcp::Channel> (j)) - ); - } - } + if (p == Film::SCALER || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) { + Changed (false); } +} - audio = dcp_mapped; - - /* We must cut off anything that comes before the start of all time */ - if (time < 0) { - int const frames = - time * _film->audio_frame_rate() / TIME_HZ; - if (frames >= audio->frames ()) { - return; +list<PositionImage> +Player::transform_image_subtitles (list<ImageSubtitle> subs) const +{ + list<PositionImage> all; + + for (list<ImageSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) { + if (!i->image) { + continue; } - shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames)); - trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0); - - audio = trimmed; - time = 0; + /* We will scale the subtitle up to fit _video_container_size */ + dcp::Size scaled_size (i->rectangle.width * _video_container_size.width, i->rectangle.height * _video_container_size.height); + + /* Then we need a corrective translation, consisting of two parts: + * + * 1. that which is the result of the scaling of the subtitle by _video_container_size; this will be + * rect.x * _video_container_size.width and rect.y * _video_container_size.height. + * + * 2. that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be + * (width_before_subtitle_scale * (1 - subtitle_x_scale) / 2) and + * (height_before_subtitle_scale * (1 - subtitle_y_scale) / 2). + * + * Combining these two translations gives these expressions. + */ + + all.push_back ( + PositionImage ( + i->image->scale ( + scaled_size, + Scaler::from_id ("bicubic"), + i->image->pixel_format (), + true + ), + Position<int> ( + rint (_video_container_size.width * i->rectangle.x), + rint (_video_container_size.height * i->rectangle.y) + ) + ) + ); } - _audio_merger.push (audio, time); - piece->audio_position += _film->audio_frames_to_time (audio->frames ()); + return all; } void -Player::flush () +Player::set_approximate_size () { - TimedAudioBuffers<Time> tb = _audio_merger.flush (); - if (_audio && tb.audio) { - Audio (tb.audio, tb.time); - _audio_position += _film->audio_frames_to_time (tb.audio->frames ()); - } - - while (_video && _video_position < _audio_position) { - emit_black (); - } + _approximate_size = true; +} - while (_audio && _audio_position < _video_position) { - emit_silence (_film->time_to_audio_frames (_video_position - _audio_position)); - } - +shared_ptr<PlayerVideo> +Player::black_player_video_frame (DCPTime time) const +{ + return shared_ptr<PlayerVideo> ( + new PlayerVideo ( + shared_ptr<const ImageProxy> (new RawImageProxy (_black_image, _film->log ())), + time, + Crop (), + optional<float> (), + _video_container_size, + _video_container_size, + Scaler::from_id ("bicubic"), + EYES_BOTH, + PART_WHOLE, + Config::instance()->colour_conversions().front().conversion + ) + ); } -/** Seek so that the next pass() will yield (approximately) the requested frame. - * Pass accurate = true to try harder to get close to the request. - * @return true on error - */ -void -Player::seek (Time t, bool accurate) +/** @return All PlayerVideos at the given time (there may be two frames for 3D) */ +list<shared_ptr<PlayerVideo> > +Player::get_video (DCPTime time, bool accurate) { if (!_have_valid_pieces) { setup_pieces (); } + + list<shared_ptr<Piece> > ov = overlaps<VideoContent> ( + time, + time + DCPTime::from_frames (1, _film->video_frame_rate ()) + ); - if (_pieces.empty ()) { - return; - } + list<shared_ptr<PlayerVideo> > pvf; - for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { - shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> ((*i)->content); - if (!vc) { - continue; + if (ov.empty ()) { + /* No video content at this time */ + pvf.push_back (black_player_video_frame (time)); + } else { + /* Create a PlayerVideo from the content's video at this time */ + + shared_ptr<Piece> piece = ov.back (); + shared_ptr<VideoDecoder> decoder = dynamic_pointer_cast<VideoDecoder> (piece->decoder); + assert (decoder); + shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content); + assert (content); + + list<ContentVideo> content_video = decoder->get_video (dcp_to_content_video (piece, time), accurate); + if (content_video.empty ()) { + pvf.push_back (black_player_video_frame (time)); + return pvf; } + + dcp::Size image_size = content->scale().size (content, _video_container_size, _film->frame_size (), _approximate_size ? 4 : 1); + if (_approximate_size) { + image_size.width &= ~3; + image_size.height &= ~3; + } + + for (list<ContentVideo>::const_iterator i = content_video.begin(); i != content_video.end(); ++i) { + pvf.push_back ( + shared_ptr<PlayerVideo> ( + new PlayerVideo ( + i->image, + content_video_to_dcp (piece, i->frame), + content->crop (), + content->fade (i->frame), + image_size, + _video_container_size, + _film->scaler(), + i->eyes, + i->part, + content->colour_conversion () + ) + ) + ); + } + } - /* s is the offset of t from the start position of this content */ - Time s = t - vc->position (); - s = max (static_cast<Time> (0), s); - s = min (vc->length_after_trim(), s); - - /* Hence set the piece positions to the `global' time */ - (*i)->video_position = (*i)->audio_position = vc->position() + s; + /* Add subtitles (for possible burn-in) to whatever PlayerVideos we got */ - /* And seek the decoder */ - dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek ( - vc->time_to_content_video_frames (s + vc->trim_start ()), accurate - ); + PlayerSubtitles ps = get_subtitles (time, DCPTime::from_frames (1, _film->video_frame_rate ()), false); - (*i)->reset_repeat (); - } + list<PositionImage> sub_images; - _video_position = _audio_position = t; + /* Image subtitles */ + list<PositionImage> c = transform_image_subtitles (ps.image); + copy (c.begin(), c.end(), back_inserter (sub_images)); - /* XXX: don't seek audio because we don't need to... */ + /* Text subtitles (rendered to images) */ + sub_images.push_back (render_subtitles (ps.text, _video_container_size)); + + if (!sub_images.empty ()) { + for (list<shared_ptr<PlayerVideo> >::const_iterator i = pvf.begin(); i != pvf.end(); ++i) { + (*i)->set_subtitle (merge (sub_images)); + } + } + + return pvf; } -void -Player::setup_pieces () +shared_ptr<AudioBuffers> +Player::get_audio (DCPTime time, DCPTime length, bool accurate) { - list<shared_ptr<Piece> > old_pieces = _pieces; + if (!_have_valid_pieces) { + setup_pieces (); + } - _pieces.clear (); + AudioFrame const length_frames = length.frames (_film->audio_frame_rate ()); - ContentList content = _playlist->content (); - sort (content.begin(), content.end(), ContentSorter ()); + shared_ptr<AudioBuffers> audio (new AudioBuffers (_film->audio_channels(), length_frames)); + audio->make_silent (); + + list<shared_ptr<Piece> > ov = overlaps<AudioContent> (time, time + length); + if (ov.empty ()) { + return audio; + } - for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { + for (list<shared_ptr<Piece> >::iterator i = ov.begin(); i != ov.end(); ++i) { - if (!(*i)->paths_valid ()) { + shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> ((*i)->content); + assert (content); + shared_ptr<AudioDecoder> decoder = dynamic_pointer_cast<AudioDecoder> ((*i)->decoder); + assert (decoder); + + if (content->audio_frame_rate() == 0) { + /* This AudioContent has no audio (e.g. if it is an FFmpegContent with no + * audio stream). + */ continue; } - shared_ptr<Piece> piece (new Piece (*i)); + /* The time that we should request from the content */ + DCPTime request = time - DCPTime::from_seconds (content->audio_delay() / 1000.0); + DCPTime offset; + if (request < DCPTime ()) { + /* We went off the start of the content, so we will need to offset + the stuff we get back. + */ + offset = -request; + request = DCPTime (); + } - /* XXX: into content? */ + AudioFrame const content_frame = dcp_to_content_audio (*i, request); - shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i); - if (fc) { - shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio)); - - fd->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0)); - fd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2, false)); - fd->Subtitle.connect (bind (&Player::process_subtitle, this, weak_ptr<Piece> (piece), _1, _2, _3, _4)); + /* Audio from this piece's decoder (which might be more or less than what we asked for) */ + shared_ptr<ContentAudio> all = decoder->get_audio (content_frame, length_frames, accurate); - fd->seek (fc->time_to_content_video_frames (fc->trim_start ()), true); - piece->decoder = fd; + /* Gain */ + if (content->audio_gain() != 0) { + shared_ptr<AudioBuffers> gain (new AudioBuffers (all->audio)); + gain->apply_gain (content->audio_gain ()); + all->audio = gain; } - - shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i); - if (ic) { - bool reusing = false; - - /* See if we can re-use an old ImageDecoder */ - for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) { - shared_ptr<ImageDecoder> imd = dynamic_pointer_cast<ImageDecoder> ((*j)->decoder); - if (imd && imd->content() == ic) { - piece = *j; - reusing = true; - } - } - if (!reusing) { - shared_ptr<ImageDecoder> id (new ImageDecoder (_film, ic)); - id->Video.connect (bind (&Player::process_video, this, weak_ptr<Piece> (piece), _1, _2, _3, _4, _5, 0)); - piece->decoder = id; + /* Remap channels */ + shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), all->audio->frames())); + dcp_mapped->make_silent (); + AudioMapping map = content->audio_mapping (); + for (int i = 0; i < map.content_channels(); ++i) { + for (int j = 0; j < _film->audio_channels(); ++j) { + if (map.get (i, static_cast<dcp::Channel> (j)) > 0) { + dcp_mapped->accumulate_channel ( + all->audio.get(), + i, + j, + map.get (i, static_cast<dcp::Channel> (j)) + ); + } } } + + all->audio = dcp_mapped; - shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i); - if (sc) { - shared_ptr<AudioDecoder> sd (new SndfileDecoder (_film, sc)); - sd->Audio.connect (bind (&Player::process_audio, this, weak_ptr<Piece> (piece), _1, _2, false)); - - piece->decoder = sd; - } - - _pieces.push_back (piece); + audio->accumulate_frames ( + all->audio.get(), + content_frame - all->frame, + offset.frames (_film->audio_frame_rate()), + min (AudioFrame (all->audio->frames()), length_frames) - offset.frames (_film->audio_frame_rate ()) + ); } - _have_valid_pieces = true; + return audio; } -void -Player::content_changed (weak_ptr<Content> w, int property, bool frequent) +VideoFrame +Player::dcp_to_content_video (shared_ptr<const Piece> piece, DCPTime t) const { - shared_ptr<Content> c = w.lock (); - if (!c) { - return; - } + /* s is the offset of t from the start position of this content */ + DCPTime s = t - piece->content->position (); + s = DCPTime (max (DCPTime::Type (0), s.get ())); + s = DCPTime (min (piece->content->length_after_trim().get(), s.get())); - if ( - property == ContentProperty::POSITION || property == ContentProperty::LENGTH || - property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END || - property == VideoContentProperty::VIDEO_FRAME_TYPE - ) { - - _have_valid_pieces = false; - Changed (frequent); - - } else if ( - property == SubtitleContentProperty::SUBTITLE_X_OFFSET || - property == SubtitleContentProperty::SUBTITLE_Y_OFFSET || - property == SubtitleContentProperty::SUBTITLE_X_SCALE || - property == SubtitleContentProperty::SUBTITLE_Y_SCALE - ) { - - for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - i->update (_film, _video_container_size); - } - - Changed (frequent); - - } else if ( - property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_SCALE || - property == VideoContentProperty::VIDEO_FRAME_RATE - ) { - - Changed (frequent); - - } else if (property == ContentProperty::PATH) { - - _have_valid_pieces = false; - Changed (frequent); - } + /* Convert this to the content frame */ + return DCPTime (s + piece->content->trim_start()).frames (_film->video_frame_rate()) * piece->frc.factor (); } -void -Player::playlist_changed () +DCPTime +Player::content_video_to_dcp (shared_ptr<const Piece> piece, VideoFrame f) const { - _have_valid_pieces = false; - Changed (false); + DCPTime t = DCPTime::from_frames (f / piece->frc.factor (), _film->video_frame_rate()) - piece->content->trim_start () + piece->content->position (); + if (t < DCPTime ()) { + t = DCPTime (); + } + + return t; } -void -Player::set_video_container_size (libdcp::Size s) +AudioFrame +Player::dcp_to_content_audio (shared_ptr<const Piece> piece, DCPTime t) const { - _video_container_size = s; + /* s is the offset of t from the start position of this content */ + DCPTime s = t - piece->content->position (); + s = DCPTime (max (DCPTime::Type (0), s.get ())); + s = DCPTime (min (piece->content->length_after_trim().get(), s.get())); - shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true)); - im->make_black (); - - _black_frame.reset ( - new PlayerVideoFrame ( - shared_ptr<ImageProxy> (new RawImageProxy (im, _film->log ())), - Crop(), - _video_container_size, - _video_container_size, - Scaler::from_id ("bicubic"), - EYES_BOTH, - PART_WHOLE, - ColourConversion () - ) - ); + /* Convert this to the content frame */ + return DCPTime (s + piece->content->trim_start()).frames (_film->audio_frame_rate()); } -shared_ptr<Resampler> -Player::resampler (shared_ptr<AudioContent> c, bool create) +ContentTime +Player::dcp_to_content_subtitle (shared_ptr<const Piece> piece, DCPTime t) const { - map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c); - if (i != _resamplers.end ()) { - return i->second; - } - - if (!create) { - return shared_ptr<Resampler> (); - } - - LOG_GENERAL ( - "Creating new resampler for %1 to %2 with %3 channels", c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels() - ); + /* s is the offset of t from the start position of this content */ + DCPTime s = t - piece->content->position (); + s = DCPTime (max (DCPTime::Type (0), s.get ())); + s = DCPTime (min (piece->content->length_after_trim().get(), s.get())); - shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels())); - _resamplers[c] = r; - return r; + return ContentTime (s + piece->content->trim_start(), piece->frc); } void -Player::emit_black () +PlayerStatistics::dump (shared_ptr<Log> log) const { -#ifdef DCPOMATIC_DEBUG - _last_video.reset (); -#endif - - Video (_black_frame, _last_emit_was_black, _video_position); - _video_position += _film->video_frames_to_time (1); - _last_emit_was_black = true; + log->log (String::compose ("Video: %1 good %2 skipped %3 black %4 repeat", video.good, video.skip, video.black, video.repeat), Log::TYPE_GENERAL); + log->log (String::compose ("Audio: %1 good %2 skipped %3 silence", audio.good, audio.skip, audio.silence.seconds()), Log::TYPE_GENERAL); } -void -Player::emit_silence (OutputAudioFrame most) +PlayerStatistics const & +Player::statistics () const { - if (most == 0) { - return; - } - - OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2); - shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N)); - silence->make_silent (); - Audio (silence, _audio_position); - _audio_position += _film->audio_frames_to_time (N); + return _statistics; } -void -Player::film_changed (Film::Property p) +PlayerSubtitles +Player::get_subtitles (DCPTime time, DCPTime length, bool starting) { - /* Here we should notice Film properties that affect our output, and - alert listeners that our output now would be different to how it was - last time we were run. - */ + list<shared_ptr<Piece> > subs = overlaps<SubtitleContent> (time, time + length); - if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER || p == Film::VIDEO_FRAME_RATE) { - Changed (false); - } -} + PlayerSubtitles ps (time, length); -void -Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to) -{ - if (!image) { - /* A null image means that we should stop any current subtitles at `from' */ - for (list<Subtitle>::iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { - i->set_stop (from); + for (list<shared_ptr<Piece> >::const_iterator j = subs.begin(); j != subs.end(); ++j) { + shared_ptr<SubtitleContent> subtitle_content = dynamic_pointer_cast<SubtitleContent> ((*j)->content); + if (!subtitle_content->use_subtitles ()) { + continue; } - } else { - _subtitles.push_back (Subtitle (_film, _video_container_size, weak_piece, image, rect, from, to)); - } -} -/** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles. - * @return false if this could not be done. - */ -bool -Player::repeat_last_video () -{ - if (!_last_incoming_video.image || !_have_valid_pieces) { - return false; - } + shared_ptr<SubtitleDecoder> subtitle_decoder = dynamic_pointer_cast<SubtitleDecoder> ((*j)->decoder); + ContentTime const from = dcp_to_content_subtitle (*j, time); + /* XXX: this video_frame_rate() should be the rate that the subtitle content has been prepared for */ + ContentTime const to = from + ContentTime::from_frames (1, _film->video_frame_rate ()); - process_video ( - _last_incoming_video.weak_piece, - _last_incoming_video.image, - _last_incoming_video.eyes, - _last_incoming_video.part, - _last_incoming_video.same, - _last_incoming_video.frame, - _last_incoming_video.extra - ); + list<ContentImageSubtitle> image = subtitle_decoder->get_image_subtitles (ContentTimePeriod (from, to), starting); + for (list<ContentImageSubtitle>::iterator i = image.begin(); i != image.end(); ++i) { + + /* Apply content's subtitle offsets */ + i->sub.rectangle.x += subtitle_content->subtitle_x_offset (); + i->sub.rectangle.y += subtitle_content->subtitle_y_offset (); + + /* Apply content's subtitle scale */ + i->sub.rectangle.width *= subtitle_content->subtitle_x_scale (); + i->sub.rectangle.height *= subtitle_content->subtitle_y_scale (); + + /* Apply a corrective translation to keep the subtitle centred after that scale */ + i->sub.rectangle.x -= i->sub.rectangle.width * (subtitle_content->subtitle_x_scale() - 1); + i->sub.rectangle.y -= i->sub.rectangle.height * (subtitle_content->subtitle_y_scale() - 1); + + ps.image.push_back (i->sub); + } + + list<ContentTextSubtitle> text = subtitle_decoder->get_text_subtitles (ContentTimePeriod (from, to), starting); + BOOST_FOREACH (ContentTextSubtitle& ts, text) { + BOOST_FOREACH (dcp::SubtitleString& s, ts.subs) { + s.set_v_position (s.v_position() + subtitle_content->subtitle_y_offset ()); + s.set_size (s.size() * max (subtitle_content->subtitle_x_scale(), subtitle_content->subtitle_y_scale())); + ps.text.push_back (s); + } + } + } - return true; + return ps; } diff --git a/src/lib/player.h b/src/lib/player.h index 6e70ad707..a3b745c8e 100644 --- a/src/lib/player.h +++ b/src/lib/player.h @@ -27,10 +27,13 @@ #include "content.h" #include "film.h" #include "rect.h" -#include "audio_merger.h" #include "audio_content.h" +#include "dcpomatic_time.h" +#include "content_subtitle.h" +#include "position_image.h" #include "piece.h" -#include "subtitle.h" +#include "content_video.h" +#include "player_subtitles.h" class Job; class Film; @@ -38,42 +41,60 @@ class Playlist; class AudioContent; class Piece; class Image; +class Decoder; class Resampler; -class PlayerVideoFrame; +class PlayerVideo; class ImageProxy; +class PlayerStatistics +{ +public: + struct Video { + Video () + : black (0) + , repeat (0) + , good (0) + , skip (0) + {} + + int black; + int repeat; + int good; + int skip; + } video; + + struct Audio { + Audio () + : silence (0) + , good (0) + , skip (0) + {} + + DCPTime silence; + int64_t good; + int64_t skip; + } audio; + + void dump (boost::shared_ptr<Log>) const; +}; + /** @class Player - * @brief A class which can `play' a Playlist; emitting its audio and video. + * @brief A class which can `play' a Playlist. */ class Player : public boost::enable_shared_from_this<Player>, public boost::noncopyable { public: Player (boost::shared_ptr<const Film>, boost::shared_ptr<const Playlist>); - void disable_video (); - void disable_audio (); - - bool pass (); - void seek (Time, bool); + std::list<boost::shared_ptr<PlayerVideo> > get_video (DCPTime time, bool accurate); + boost::shared_ptr<AudioBuffers> get_audio (DCPTime time, DCPTime length, bool accurate); + PlayerSubtitles get_subtitles (DCPTime time, DCPTime length, bool starting); - Time video_position () const { - return _video_position; - } - - void set_video_container_size (libdcp::Size); - - bool repeat_last_video (); + void set_video_container_size (dcp::Size); + void set_approximate_size (); - /** Emitted when a video frame is ready. - * First parameter is the video image. - * Second parameter is true if the frame is the same as the last one that was emitted. - * Third parameter is the time. - */ - boost::signals2::signal<void (boost::shared_ptr<PlayerVideoFrame>, bool, Time)> Video; + PlayerStatistics const & statistics () const; - /** Emitted when some audio data is ready */ - boost::signals2::signal<void (boost::shared_ptr<const AudioBuffers>, Time)> Audio; - /** Emitted when something has changed such that if we went back and emitted * the last frame again it would look different. This is not emitted after * a seek. @@ -85,51 +106,58 @@ public: private: friend class PlayerWrapper; friend class Piece; + friend struct player_overlaps_test; - void process_video (boost::weak_ptr<Piece>, boost::shared_ptr<const ImageProxy>, Eyes, Part, bool, VideoContent::Frame, Time); - void process_audio (boost::weak_ptr<Piece>, boost::shared_ptr<const AudioBuffers>, AudioContent::Frame, bool); - void process_subtitle (boost::weak_ptr<Piece>, boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time); void setup_pieces (); void playlist_changed (); void content_changed (boost::weak_ptr<Content>, int, bool); - void do_seek (Time, bool); void flush (); - void emit_black (); - void emit_silence (OutputAudioFrame); - boost::shared_ptr<Resampler> resampler (boost::shared_ptr<AudioContent>, bool); void film_changed (Film::Property); - void update_subtitle (); - + std::list<PositionImage> transform_image_subtitles (std::list<ImageSubtitle>) const; + void update_subtitle_from_text (); + VideoFrame dcp_to_content_video (boost::shared_ptr<const Piece> piece, DCPTime t) const; + DCPTime content_video_to_dcp (boost::shared_ptr<const Piece> piece, VideoFrame f) const; + AudioFrame dcp_to_content_audio (boost::shared_ptr<const Piece> piece, DCPTime t) const; + ContentTime dcp_to_content_subtitle (boost::shared_ptr<const Piece> piece, DCPTime t) const; + boost::shared_ptr<PlayerVideo> black_player_video_frame (DCPTime) const; + + /** @return Pieces of content type C that overlap a specified time range in the DCP */ + template<class C> + std::list<boost::shared_ptr<Piece> > + overlaps (DCPTime from, DCPTime to) + { + if (!_have_valid_pieces) { + setup_pieces (); + } + + std::list<boost::shared_ptr<Piece> > overlaps; + for (typename std::list<boost::shared_ptr<Piece> >::const_iterator i = _pieces.begin(); i != _pieces.end(); ++i) { + if (!boost::dynamic_pointer_cast<C> ((*i)->content)) { + continue; + } + + if ((*i)->content->position() <= to && (*i)->content->end() >= from) { + overlaps.push_back (*i); + } + } + + return overlaps; + } + boost::shared_ptr<const Film> _film; boost::shared_ptr<const Playlist> _playlist; - - bool _video; - bool _audio; /** Our pieces are ready to go; if this is false the pieces must be (re-)created before they are used */ bool _have_valid_pieces; std::list<boost::shared_ptr<Piece> > _pieces; - /** The time after the last video that we emitted */ - Time _video_position; - /** The time after the last audio that we emitted */ - Time _audio_position; - - AudioMerger<Time, AudioContent::Frame> _audio_merger; - - libdcp::Size _video_container_size; - boost::shared_ptr<PlayerVideoFrame> _black_frame; - std::map<boost::shared_ptr<AudioContent>, boost::shared_ptr<Resampler> > _resamplers; - - std::list<Subtitle> _subtitles; - -#ifdef DCPOMATIC_DEBUG - boost::shared_ptr<Content> _last_video; -#endif + dcp::Size _video_container_size; + boost::shared_ptr<Image> _black_image; - bool _last_emit_was_black; + bool _approximate_size; + bool _burn_subtitles; - IncomingVideo _last_incoming_video; + PlayerStatistics _statistics; boost::signals2::scoped_connection _playlist_changed_connection; boost::signals2::scoped_connection _playlist_content_changed_connection; diff --git a/src/lib/player_subtitles.h b/src/lib/player_subtitles.h new file mode 100644 index 000000000..46994ea3b --- /dev/null +++ b/src/lib/player_subtitles.h @@ -0,0 +1,42 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_PLAYER_SUBTITLES_H +#define DCPOMATIC_PLAYER_SUBTITLES_H + +#include <dcp/subtitle_string.h> +#include "image_subtitle.h" + +class PlayerSubtitles +{ +public: + PlayerSubtitles (DCPTime f, DCPTime t) + : from (f) + , to (t) + {} + + DCPTime from; + DCPTime to; + + /** ImageSubtitles, with their rectangles transformed as specified by their content */ + std::list<ImageSubtitle> image; + std::list<dcp::SubtitleString> text; +}; + +#endif diff --git a/src/lib/player_video.cc b/src/lib/player_video.cc new file mode 100644 index 000000000..2feb52f42 --- /dev/null +++ b/src/lib/player_video.cc @@ -0,0 +1,209 @@ +/* + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/raw_convert.h> +#include "player_video.h" +#include "image.h" +#include "image_proxy.h" +#include "j2k_image_proxy.h" +#include "scaler.h" + +using std::string; +using std::cout; +using dcp::raw_convert; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +PlayerVideo::PlayerVideo ( + shared_ptr<const ImageProxy> in, + DCPTime time, + Crop crop, + boost::optional<float> fade, + dcp::Size inter_size, + dcp::Size out_size, + Scaler const * scaler, + Eyes eyes, + Part part, + ColourConversion colour_conversion + ) + : _in (in) + , _time (time) + , _crop (crop) + , _fade (fade) + , _inter_size (inter_size) + , _out_size (out_size) + , _scaler (scaler) + , _eyes (eyes) + , _part (part) + , _colour_conversion (colour_conversion) +{ + +} + +PlayerVideo::PlayerVideo (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket, shared_ptr<Log> log) +{ + _time = DCPTime (node->number_child<DCPTime::Type> ("Time")); + _crop = Crop (node); + _fade = node->optional_number_child<float> ("Fade"); + + _inter_size = dcp::Size (node->number_child<int> ("InterWidth"), node->number_child<int> ("InterHeight")); + _out_size = dcp::Size (node->number_child<int> ("OutWidth"), node->number_child<int> ("OutHeight")); + _scaler = Scaler::from_id (node->string_child ("Scaler")); + _eyes = (Eyes) node->number_child<int> ("Eyes"); + _part = (Part) node->number_child<int> ("Part"); + _colour_conversion = ColourConversion (node); + + _in = image_proxy_factory (node->node_child ("In"), socket, log); + + if (node->optional_number_child<int> ("SubtitleX")) { + + _subtitle.position = Position<int> (node->number_child<int> ("SubtitleX"), node->number_child<int> ("SubtitleY")); + + _subtitle.image.reset ( + new Image (PIX_FMT_RGBA, dcp::Size (node->number_child<int> ("SubtitleWidth"), node->number_child<int> ("SubtitleHeight")), true) + ); + + _subtitle.image->read_from_socket (socket); + } +} + +void +PlayerVideo::set_subtitle (PositionImage image) +{ + _subtitle = image; +} + +shared_ptr<Image> +PlayerVideo::image (bool burn_subtitle) const +{ + shared_ptr<Image> im = _in->image (); + + Crop total_crop = _crop; + switch (_part) { + case PART_LEFT_HALF: + total_crop.right += im->size().width / 2; + break; + case PART_RIGHT_HALF: + total_crop.left += im->size().width / 2; + break; + case PART_TOP_HALF: + total_crop.bottom += im->size().height / 2; + break; + case PART_BOTTOM_HALF: + total_crop.top += im->size().height / 2; + break; + default: + break; + } + + shared_ptr<Image> out = im->crop_scale_window (total_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, true); + + if (burn_subtitle && _subtitle.image) { + out->alpha_blend (_subtitle.image, _subtitle.position); + } + + if (_fade) { + out->fade (_fade.get ()); + } + + return out; +} + +void +PlayerVideo::add_metadata (xmlpp::Node* node, bool send_subtitles) const +{ + node->add_child("Time")->add_child_text (raw_convert<string> (_time.get ())); + _crop.as_xml (node); + if (_fade) { + node->add_child("Fade")->add_child_text (raw_convert<string> (_fade.get ())); + } + _in->add_metadata (node->add_child ("In")); + node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width)); + node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height)); + node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width)); + node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height)); + node->add_child("Scaler")->add_child_text (_scaler->id ()); + node->add_child("Eyes")->add_child_text (raw_convert<string> (_eyes)); + node->add_child("Part")->add_child_text (raw_convert<string> (_part)); + _colour_conversion.as_xml (node); + if (send_subtitles && _subtitle.image) { + node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_subtitle.image->size().width)); + node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_subtitle.image->size().height)); + node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_subtitle.position.x)); + node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_subtitle.position.y)); + } +} + +void +PlayerVideo::send_binary (shared_ptr<Socket> socket, bool send_subtitles) const +{ + _in->send_binary (socket); + if (send_subtitles && _subtitle.image) { + _subtitle.image->write_to_socket (socket); + } +} + +bool +PlayerVideo::has_j2k () const +{ + /* XXX: burnt-in subtitle; maybe other things */ + + shared_ptr<const J2KImageProxy> j2k = dynamic_pointer_cast<const J2KImageProxy> (_in); + if (!j2k) { + return false; + } + + return _crop == Crop () && _inter_size == j2k->size(); +} + +shared_ptr<EncodedData> +PlayerVideo::j2k () const +{ + shared_ptr<const J2KImageProxy> j2k = dynamic_pointer_cast<const J2KImageProxy> (_in); + assert (j2k); + return j2k->j2k (); +} + +Position<int> +PlayerVideo::inter_position () const +{ + return Position<int> ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.height) / 2); +} + +/** @return true if this PlayerVideo is definitely the same as another + * (apart from _time), false if it is probably not + */ +bool +PlayerVideo::same (shared_ptr<const PlayerVideo> other) const +{ + if (_in != other->_in || + _crop != other->_crop || + _fade.get_value_or(0) != other->_fade.get_value_or(0) || + _inter_size != other->_inter_size || + _out_size != other->_out_size || + _scaler != other->_scaler || + _eyes != other->_eyes || + _part != other->_part || + _colour_conversion != other->_colour_conversion || + !_subtitle.same (other->_subtitle)) { + return false; + } + + return _in->same (other->_in); +} diff --git a/src/lib/player_video_frame.h b/src/lib/player_video.h index b085cb609..0f5e83b10 100644 --- a/src/lib/player_video_frame.h +++ b/src/lib/player_video.h @@ -21,29 +21,50 @@ #include "types.h" #include "position.h" #include "colour_conversion.h" +#include "position_image.h" class Image; class ImageProxy; class Scaler; class Socket; class Log; +class EncodedData; /** Everything needed to describe a video frame coming out of the player, but with the * bits still their raw form. We may want to combine the bits on a remote machine, * or maybe not even bother to combine them at all. */ -class PlayerVideoFrame +class PlayerVideo { public: - PlayerVideoFrame (boost::shared_ptr<const ImageProxy>, Crop, libdcp::Size, libdcp::Size, Scaler const *, Eyes, Part, ColourConversion); - PlayerVideoFrame (boost::shared_ptr<cxml::Node>, boost::shared_ptr<Socket>, boost::shared_ptr<Log>); + PlayerVideo ( + boost::shared_ptr<const ImageProxy>, + DCPTime, + Crop, + boost::optional<float>, + dcp::Size, + dcp::Size, + Scaler const *, + Eyes, + Part, + ColourConversion + ); + + PlayerVideo (boost::shared_ptr<cxml::Node>, boost::shared_ptr<Socket>, boost::shared_ptr<Log>); - void set_subtitle (boost::shared_ptr<const Image>, Position<int>); + void set_subtitle (PositionImage); - boost::shared_ptr<Image> image () const; + boost::shared_ptr<Image> image (bool burn_subtitle) const; + + void add_metadata (xmlpp::Node* node, bool send_subtitles) const; + void send_binary (boost::shared_ptr<Socket> socket, bool send_subtitles) const; - void add_metadata (xmlpp::Node* node) const; - void send_binary (boost::shared_ptr<Socket> socket) const; + bool has_j2k () const; + boost::shared_ptr<EncodedData> j2k () const; + + DCPTime time () const { + return _time; + } Eyes eyes () const { return _eyes; @@ -53,15 +74,26 @@ public: return _colour_conversion; } + /** @return Position of the content within the overall image once it has been scaled up */ + Position<int> inter_position () const; + + /** @return Size of the content within the overall image once it has been scaled up */ + dcp::Size inter_size () const { + return _inter_size; + } + + bool same (boost::shared_ptr<const PlayerVideo> other) const; + private: boost::shared_ptr<const ImageProxy> _in; + DCPTime _time; Crop _crop; - libdcp::Size _inter_size; - libdcp::Size _out_size; + boost::optional<float> _fade; + dcp::Size _inter_size; + dcp::Size _out_size; Scaler const * _scaler; Eyes _eyes; Part _part; ColourConversion _colour_conversion; - boost::shared_ptr<const Image> _subtitle_image; - Position<int> _subtitle_position; + PositionImage _subtitle; }; diff --git a/src/lib/player_video_frame.cc b/src/lib/player_video_frame.cc deleted file mode 100644 index 94760e495..000000000 --- a/src/lib/player_video_frame.cc +++ /dev/null @@ -1,148 +0,0 @@ -/* - Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - 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 <libdcp/raw_convert.h> -#include "player_video_frame.h" -#include "image.h" -#include "image_proxy.h" -#include "scaler.h" - -using std::string; -using std::cout; -using boost::shared_ptr; -using libdcp::raw_convert; - -PlayerVideoFrame::PlayerVideoFrame ( - shared_ptr<const ImageProxy> in, - Crop crop, - libdcp::Size inter_size, - libdcp::Size out_size, - Scaler const * scaler, - Eyes eyes, - Part part, - ColourConversion colour_conversion - ) - : _in (in) - , _crop (crop) - , _inter_size (inter_size) - , _out_size (out_size) - , _scaler (scaler) - , _eyes (eyes) - , _part (part) - , _colour_conversion (colour_conversion) -{ - -} - -PlayerVideoFrame::PlayerVideoFrame (shared_ptr<cxml::Node> node, shared_ptr<Socket> socket, shared_ptr<Log> log) -{ - _crop = Crop (node); - - _inter_size = libdcp::Size (node->number_child<int> ("InterWidth"), node->number_child<int> ("InterHeight")); - _out_size = libdcp::Size (node->number_child<int> ("OutWidth"), node->number_child<int> ("OutHeight")); - _scaler = Scaler::from_id (node->string_child ("Scaler")); - _eyes = (Eyes) node->number_child<int> ("Eyes"); - _part = (Part) node->number_child<int> ("Part"); - _colour_conversion = ColourConversion (node); - - _in = image_proxy_factory (node->node_child ("In"), socket, log); - - if (node->optional_number_child<int> ("SubtitleX")) { - - _subtitle_position = Position<int> (node->number_child<int> ("SubtitleX"), node->number_child<int> ("SubtitleY")); - - shared_ptr<Image> image ( - new Image (PIX_FMT_RGBA, libdcp::Size (node->number_child<int> ("SubtitleWidth"), node->number_child<int> ("SubtitleHeight")), true) - ); - - image->read_from_socket (socket); - _subtitle_image = image; - } -} - -void -PlayerVideoFrame::set_subtitle (shared_ptr<const Image> image, Position<int> pos) -{ - _subtitle_image = image; - _subtitle_position = pos; -} - -shared_ptr<Image> -PlayerVideoFrame::image () const -{ - shared_ptr<Image> im = _in->image (); - - Crop total_crop = _crop; - switch (_part) { - case PART_LEFT_HALF: - total_crop.right += im->size().width / 2; - break; - case PART_RIGHT_HALF: - total_crop.left += im->size().width / 2; - break; - case PART_TOP_HALF: - total_crop.bottom += im->size().height / 2; - break; - case PART_BOTTOM_HALF: - total_crop.top += im->size().height / 2; - break; - default: - break; - } - - shared_ptr<Image> out = im->crop_scale_window (total_crop, _inter_size, _out_size, _scaler, PIX_FMT_RGB24, false); - - Position<int> const container_offset ((_out_size.width - _inter_size.width) / 2, (_out_size.height - _inter_size.width) / 2); - - if (_subtitle_image) { - out->alpha_blend (_subtitle_image, _subtitle_position); - } - - return out; -} - -void -PlayerVideoFrame::add_metadata (xmlpp::Node* node) const -{ - _crop.as_xml (node); - _in->add_metadata (node->add_child ("In")); - node->add_child("InterWidth")->add_child_text (raw_convert<string> (_inter_size.width)); - node->add_child("InterHeight")->add_child_text (raw_convert<string> (_inter_size.height)); - node->add_child("OutWidth")->add_child_text (raw_convert<string> (_out_size.width)); - node->add_child("OutHeight")->add_child_text (raw_convert<string> (_out_size.height)); - node->add_child("Scaler")->add_child_text (_scaler->id ()); - node->add_child("Eyes")->add_child_text (raw_convert<string> (_eyes)); - node->add_child("Part")->add_child_text (raw_convert<string> (_part)); - _colour_conversion.as_xml (node); - if (_subtitle_image) { - node->add_child ("SubtitleWidth")->add_child_text (raw_convert<string> (_subtitle_image->size().width)); - node->add_child ("SubtitleHeight")->add_child_text (raw_convert<string> (_subtitle_image->size().height)); - node->add_child ("SubtitleX")->add_child_text (raw_convert<string> (_subtitle_position.x)); - node->add_child ("SubtitleY")->add_child_text (raw_convert<string> (_subtitle_position.y)); - } -} - -void -PlayerVideoFrame::send_binary (shared_ptr<Socket> socket) const -{ - _in->send_binary (socket); - if (_subtitle_image) { - _subtitle_image->write_to_socket (socket); - } -} diff --git a/src/lib/playlist.cc b/src/lib/playlist.cc index c3e430082..22412da4a 100644 --- a/src/lib/playlist.cc +++ b/src/lib/playlist.cc @@ -79,20 +79,20 @@ Playlist::maybe_sequence_video () _sequencing_video = true; ContentList cl = _content; - Time next_left = 0; - Time next_right = 0; + DCPTime next_left; + DCPTime next_right; for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) { shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i); if (!vc) { continue; } - + if (vc->video_frame_type() == VIDEO_FRAME_TYPE_3D_RIGHT) { vc->set_position (next_right); - next_right = vc->end() + 1; + next_right = vc->end() + DCPTime::delta (); } else { vc->set_position (next_left); - next_left = vc->end() + 1; + next_left = vc->end() + DCPTime::delta (); } } @@ -120,7 +120,7 @@ Playlist::video_identifier () const /** @param node <Playlist> node */ void -Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int version, list<string>& notes) +Playlist::set_from_xml (shared_ptr<const Film> film, cxml::ConstNodePtr node, int version, list<string>& notes) { list<cxml::NodePtr> c = node->node_children ("Content"); for (list<cxml::NodePtr>::iterator i = c.begin(); i != c.end(); ++i) { @@ -185,19 +185,6 @@ Playlist::remove (ContentList c) Changed (); } -bool -Playlist::has_subtitles () const -{ - for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { - shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (*i); - if (fc && !fc->subtitle_streams().empty()) { - return true; - } - } - - return false; -} - class FrameRateCandidate { public: @@ -261,12 +248,12 @@ Playlist::best_dcp_frame_rate () const return best->dcp; } -Time +DCPTime Playlist::length () const { - Time len = 0; + DCPTime len; for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { - len = max (len, (*i)->end() + 1); + len = max (len, (*i)->end() + DCPTime::delta ()); } return len; @@ -286,10 +273,10 @@ Playlist::reconnect () } } -Time +DCPTime Playlist::video_end () const { - Time end = 0; + DCPTime end; for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { if (dynamic_pointer_cast<const VideoContent> (*i)) { end = max (end, (*i)->end ()); @@ -299,6 +286,23 @@ Playlist::video_end () const return end; } +FrameRateChange +Playlist::active_frame_rate_change (DCPTime t, int dcp_video_frame_rate) const +{ + for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { + shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i); + if (!vc) { + continue; + } + + if (vc->position() >= t && t < vc->end()) { + return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate); + } + } + + return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate); +} + void Playlist::set_sequence_video (bool s) { @@ -321,7 +325,7 @@ Playlist::content () const void Playlist::repeat (ContentList c, int n) { - pair<Time, Time> range (TIME_MAX, 0); + pair<DCPTime, DCPTime> range (DCPTime::max (), DCPTime ()); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { range.first = min (range.first, (*i)->position ()); range.second = max (range.second, (*i)->position ()); @@ -329,7 +333,7 @@ Playlist::repeat (ContentList c, int n) range.second = max (range.second, (*i)->end ()); } - Time pos = range.second; + DCPTime pos = range.second; for (int i = 0; i < n; ++i) { for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { shared_ptr<Content> copy = (*i)->clone (); @@ -363,7 +367,7 @@ Playlist::move_earlier (shared_ptr<Content> c) } - Time const p = (*previous)->position (); + DCPTime const p = (*previous)->position (); (*previous)->set_position (p + c->length_after_trim ()); c->set_position (p); sort (_content.begin(), _content.end(), ContentSorter ()); @@ -392,20 +396,3 @@ Playlist::move_later (shared_ptr<Content> c) c->set_position (c->position() + c->length_after_trim ()); sort (_content.begin(), _content.end(), ContentSorter ()); } - -FrameRateChange -Playlist::active_frame_rate_change (Time t, int dcp_video_frame_rate) const -{ - for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { - shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i); - if (!vc) { - continue; - } - - if (vc->position() >= t && t < vc->end()) { - return FrameRateChange (vc->video_frame_rate(), dcp_video_frame_rate); - } - } - - return FrameRateChange (dcp_video_frame_rate, dcp_video_frame_rate); -} diff --git a/src/lib/playlist.h b/src/lib/playlist.h index 12380696b..9e3dbb6df 100644 --- a/src/lib/playlist.h +++ b/src/lib/playlist.h @@ -25,6 +25,7 @@ #include <boost/enable_shared_from_this.hpp> #include "ffmpeg_content.h" #include "audio_mapping.h" +#include "util.h" #include "frame_rate_change.h" class Content; @@ -38,18 +39,15 @@ class Job; class Film; class Region; -/** @class Playlist - * @brief A set of content files (video and audio), with knowledge of how they should be arranged into - * a DCP. - * - * This class holds Content objects, and it knows how they should be arranged. - */ - struct ContentSorter { bool operator() (boost::shared_ptr<Content> a, boost::shared_ptr<Content> b); }; +/** @class Playlist + * @brief A set of Content objects with knowledge of how they should be arranged into + * a DCP. + */ class Playlist : public boost::noncopyable { public: @@ -57,7 +55,7 @@ public: ~Playlist (); void as_xml (xmlpp::Node *); - void set_from_xml (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int, std::list<std::string> &); + void set_from_xml (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int, std::list<std::string> &); void add (boost::shared_ptr<Content>); void remove (boost::shared_ptr<Content>); @@ -65,17 +63,15 @@ public: void move_earlier (boost::shared_ptr<Content>); void move_later (boost::shared_ptr<Content>); - bool has_subtitles () const; - ContentList content () const; std::string video_identifier () const; - Time length () const; + DCPTime length () const; int best_dcp_frame_rate () const; - Time video_end () const; - FrameRateChange active_frame_rate_change (Time, int dcp_frame_rate) const; + DCPTime video_end () const; + FrameRateChange active_frame_rate_change (DCPTime, int dcp_frame_rate) const; void set_sequence_video (bool); void maybe_sequence_video (); diff --git a/src/lib/position.h b/src/lib/position.h index f9bd0987c..3c561d85c 100644 --- a/src/lib/position.h +++ b/src/lib/position.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,7 +21,7 @@ #define DCPOMATIC_POSITION_H /** @struct Position - * @brief A position. + * @brief A position (x and y coordinates) */ template <class T> class Position @@ -50,4 +50,25 @@ operator+ (Position<T> const & a, Position<T> const & b) return Position<T> (a.x + b.x, a.y + b.y); } +template<class T> +Position<T> +operator- (Position<T> const & a, Position<T> const & b) +{ + return Position<T> (a.x - b.x, a.y - b.y); +} + +template<class T> +bool +operator== (Position<T> const & a, Position<T> const & b) +{ + return a.x == b.x && a.y == b.y; +} + +template<class T> +bool +operator!= (Position<T> const & a, Position<T> const & b) +{ + return a.x != b.x || a.y != b.y; +} + #endif diff --git a/src/lib/decoder.cc b/src/lib/position_image.cc index 3f4cda6eb..44c1262b3 100644 --- a/src/lib/decoder.cc +++ b/src/lib/position_image.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,22 +17,21 @@ */ -/** @file src/decoder.cc - * @brief Parent class for decoders of content. - */ +#include "position_image.h" +#include "image.h" -#include "film.h" -#include "decoder.h" +using std::cout; -#include "i18n.h" - -using boost::shared_ptr; - -/** @param f Film. - * @param o Decode options. - */ -Decoder::Decoder (shared_ptr<const Film> f) - : _film (f) +bool +PositionImage::same (PositionImage const & other) const { + if (image != other.image || position != other.position) { + return false; + } + + if (!image) { + return true; + } + return *image == *(other.image); } diff --git a/src/lib/position_image.h b/src/lib/position_image.h new file mode 100644 index 000000000..c0c65d1da --- /dev/null +++ b/src/lib/position_image.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_POSITION_IMAGE_H +#define DCPOMATIC_POSITION_IMAGE_H + +#include "position.h" +#include <boost/shared_ptr.hpp> + +class Image; + +class PositionImage +{ +public: + PositionImage () {} + + PositionImage (boost::shared_ptr<Image> i, Position<int> p) + : image (i) + , position (p) + {} + + boost::shared_ptr<Image> image; + Position<int> position; + + bool same (PositionImage const & other) const; +}; + +#endif diff --git a/src/lib/ratio.cc b/src/lib/ratio.cc index 554d3c36c..fc36415c5 100644 --- a/src/lib/ratio.cc +++ b/src/lib/ratio.cc @@ -17,7 +17,7 @@ */ -#include <libdcp/types.h> +#include <dcp/types.h> #include "ratio.h" #include "util.h" diff --git a/src/lib/ratio.h b/src/lib/ratio.h index ab157a9bc..69e3726c8 100644 --- a/src/lib/ratio.h +++ b/src/lib/ratio.h @@ -22,7 +22,7 @@ #include <vector> #include <boost/utility.hpp> -#include <libdcp/util.h> +#include <dcp/util.h> class Ratio : public boost::noncopyable { diff --git a/src/lib/raw_image_proxy.cc b/src/lib/raw_image_proxy.cc new file mode 100644 index 000000000..7e0688d13 --- /dev/null +++ b/src/lib/raw_image_proxy.cc @@ -0,0 +1,71 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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. + +*/ + +extern "C" { +#include <libavutil/pixfmt.h> +} +#include <libcxml/cxml.h> +#include <dcp/util.h> +#include <dcp/raw_convert.h> +#include "raw_image_proxy.h" +#include "image.h" + +#include "i18n.h" + +using std::string; +using boost::shared_ptr; + +RawImageProxy::RawImageProxy (shared_ptr<Image> image, shared_ptr<Log> log) + : ImageProxy (log) + , _image (image) +{ + +} + +RawImageProxy::RawImageProxy (shared_ptr<cxml::Node> xml, shared_ptr<Socket> socket, shared_ptr<Log> log) + : ImageProxy (log) +{ + dcp::Size size ( + xml->number_child<int> ("Width"), xml->number_child<int> ("Height") + ); + + _image.reset (new Image (static_cast<AVPixelFormat> (xml->number_child<int> ("PixelFormat")), size, true)); + _image->read_from_socket (socket); +} + +shared_ptr<Image> +RawImageProxy::image () const +{ + return _image; +} + +void +RawImageProxy::add_metadata (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text (N_("Raw")); + node->add_child("Width")->add_child_text (dcp::raw_convert<string> (_image->size().width)); + node->add_child("Height")->add_child_text (dcp::raw_convert<string> (_image->size().height)); + node->add_child("PixelFormat")->add_child_text (dcp::raw_convert<string> (_image->pixel_format ())); +} + +void +RawImageProxy::send_binary (shared_ptr<Socket> socket) const +{ + _image->write_to_socket (socket); +} diff --git a/src/lib/raw_image_proxy.h b/src/lib/raw_image_proxy.h new file mode 100644 index 000000000..6707f689c --- /dev/null +++ b/src/lib/raw_image_proxy.h @@ -0,0 +1,34 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "image_proxy.h" + +class RawImageProxy : public ImageProxy +{ +public: + RawImageProxy (boost::shared_ptr<Image>, boost::shared_ptr<Log> log); + RawImageProxy (boost::shared_ptr<cxml::Node> xml, boost::shared_ptr<Socket> socket, boost::shared_ptr<Log> log); + + boost::shared_ptr<Image> image () const; + void add_metadata (xmlpp::Node *) const; + void send_binary (boost::shared_ptr<Socket>) const; + +private: + boost::shared_ptr<Image> _image; +}; diff --git a/src/lib/rect.h b/src/lib/rect.h index 6f4709c08..1feb8ad4f 100644 --- a/src/lib/rect.h +++ b/src/lib/rect.h @@ -42,6 +42,13 @@ public: , height (0) {} + Rect (Position<T> p, T w_, T h_) + : x (p.x) + , y (p.y) + , width (w_) + , height (h_) + {} + Rect (T x_, T y_, T w_, T h_) : x (x_) , y (y_) @@ -54,11 +61,13 @@ public: T width; T height; - Position<T> position () const { + Position<T> position () const + { return Position<T> (x, y); } - Rect<T> intersection (Rect<T> const & other) const { + Rect<T> intersection (Rect<T> const & other) const + { T const tx = max (x, other.x); T const ty = max (y, other.y); @@ -69,7 +78,16 @@ public: ); } - bool contains (Position<T> p) const { + void extend (Rect<T> const & other) + { + x = std::min (x, other.x); + y = std::min (y, other.y); + width = std::max (x + width, other.x + other.width) - x; + height = std::max (y + height, other.y + other.height) - y; + } + + bool contains (Position<T> p) const + { return (p.x >= x && p.x <= (x + width) && p.y >= y && p.y <= (y + height)); } }; diff --git a/src/lib/render_subtitles.cc b/src/lib/render_subtitles.cc new file mode 100644 index 000000000..6f103246c --- /dev/null +++ b/src/lib/render_subtitles.cc @@ -0,0 +1,152 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <cairomm/cairomm.h> +#include <pangomm.h> +#include "render_subtitles.h" +#include "types.h" +#include "image.h" + +using std::list; +using std::cout; +using std::string; +using std::min; +using std::max; +using std::pair; +using boost::shared_ptr; +using boost::optional; + +static int +calculate_position (dcp::VAlign v_align, double v_position, int target_height, int offset) +{ + switch (v_align) { + case dcp::TOP: + return v_position * target_height - offset; + case dcp::CENTER: + return (0.5 + v_position) * target_height - offset; + case dcp::BOTTOM: + return (1.0 - v_position) * target_height - offset; + } + + return 0; +} + +PositionImage +render_subtitles (list<dcp::SubtitleString> subtitles, dcp::Size target) +{ + if (subtitles.empty ()) { + return PositionImage (); + } + + /* Estimate height that the subtitle image needs to be */ + optional<int> top; + optional<int> bottom; + for (list<dcp::SubtitleString>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) { + int const b = calculate_position (i->v_align(), i->v_position(), target.height, 0); + int const t = b - i->size() * target.height / (11 * 72); + + top = min (top.get_value_or (t), t); + bottom = max (bottom.get_value_or (b), b); + } + + top = top.get() - 32; + bottom = bottom.get() + 32; + + shared_ptr<Image> image (new Image (PIX_FMT_RGBA, dcp::Size (target.width, bottom.get() - top.get ()), false)); + image->make_black (); + + Cairo::RefPtr<Cairo::ImageSurface> surface = Cairo::ImageSurface::create ( + image->data()[0], + Cairo::FORMAT_ARGB32, + image->size().width, + image->size().height, + Cairo::ImageSurface::format_stride_for_width (Cairo::FORMAT_ARGB32, image->size().width) + ); + + Cairo::RefPtr<Cairo::Context> context = Cairo::Context::create (surface); + Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (context); + + layout->set_width (image->size().width * PANGO_SCALE); + layout->set_alignment (Pango::ALIGN_CENTER); + + context->set_line_width (1); + + for (list<dcp::SubtitleString>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) { + string f = i->font (); + if (f.empty ()) { + f = "Arial"; + } + Pango::FontDescription font (f); + font.set_absolute_size (i->size_in_pixels (target.height) * PANGO_SCALE); + if (i->italic ()) { + font.set_style (Pango::STYLE_ITALIC); + } + layout->set_font_description (font); + layout->set_text (i->text ()); + + /* Compute fade factor */ + /* XXX */ + float fade_factor = 1; +#if 0 + dcp::Time now (time * 1000 / (4 * TIME_HZ)); + dcp::Time end_fade_up = i->in() + i->fade_up_time (); + dcp::Time start_fade_down = i->out() - i->fade_down_time (); + if (now < end_fade_up) { + fade_factor = (now - i->in()) / i->fade_up_time(); + } else if (now > start_fade_down) { + fade_factor = 1.0 - ((now - start_fade_down) / i->fade_down_time ()); + } +#endif + + layout->update_from_cairo_context (context); + + /* Work out position */ + + int const x = 0; + int const y = calculate_position (i->v_align (), i->v_position (), target.height, (layout->get_baseline() / PANGO_SCALE) + top.get ()); + + if (i->effect() == dcp::SHADOW) { + /* Drop-shadow effect */ + dcp::Color const ec = i->effect_color (); + context->set_source_rgba (float(ec.r) / 255, float(ec.g) / 255, float(ec.b) / 255, fade_factor); + context->move_to (x + 4, y + 4); + layout->add_to_cairo_context (context); + context->fill (); + } + + /* The actual subtitle */ + context->move_to (x, y); + dcp::Color const c = i->color (); + context->set_source_rgba (float(c.r) / 255, float(c.g) / 255, float(c.b) / 255, fade_factor); + layout->add_to_cairo_context (context); + context->fill (); + + if (i->effect() == dcp::BORDER) { + /* Border effect */ + context->move_to (x, y); + dcp::Color ec = i->effect_color (); + context->set_source_rgba (float(ec.r) / 255, float(ec.g) / 255, float(ec.b) / 255, fade_factor); + layout->add_to_cairo_context (context); + context->stroke (); + } + } + + return PositionImage (image, Position<int> (0, top.get ())); +} + diff --git a/src/lib/render_subtitles.h b/src/lib/render_subtitles.h new file mode 100644 index 000000000..d83dc119a --- /dev/null +++ b/src/lib/render_subtitles.h @@ -0,0 +1,24 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/subtitle_string.h> +#include <dcp/util.h> +#include "position_image.h" + +PositionImage render_subtitles (std::list<dcp::SubtitleString>, dcp::Size); diff --git a/src/lib/resampler.cc b/src/lib/resampler.cc index e7f50c4b7..e414436e8 100644 --- a/src/lib/resampler.cc +++ b/src/lib/resampler.cc @@ -60,11 +60,9 @@ Resampler::~Resampler () swr_free (&_swr_context); } -pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> -Resampler::run (shared_ptr<const AudioBuffers> in, AudioContent::Frame frame) +shared_ptr<const AudioBuffers> +Resampler::run (shared_ptr<const AudioBuffers> in) { - AudioContent::Frame const resamp_time = swr_next_pts (_swr_context, frame * _out_rate) / _in_rate; - /* Compute the resampled frames count and add 32 for luck */ int const max_resampled_frames = ceil ((double) in->frames() * _out_rate / _in_rate) + 32; shared_ptr<AudioBuffers> resampled (new AudioBuffers (_channels, max_resampled_frames)); @@ -80,7 +78,7 @@ Resampler::run (shared_ptr<const AudioBuffers> in, AudioContent::Frame frame) } resampled->set_frames (resampled_frames); - return make_pair (resampled, resamp_time); + return resampled; } shared_ptr<const AudioBuffers> diff --git a/src/lib/resampler.h b/src/lib/resampler.h index 69ec83ba9..4ee11a7f0 100644 --- a/src/lib/resampler.h +++ b/src/lib/resampler.h @@ -33,7 +33,7 @@ public: Resampler (int, int, int); ~Resampler (); - std::pair<boost::shared_ptr<const AudioBuffers>, AudioContent::Frame> run (boost::shared_ptr<const AudioBuffers>, AudioContent::Frame); + boost::shared_ptr<const AudioBuffers> run (boost::shared_ptr<const AudioBuffers>); boost::shared_ptr<const AudioBuffers> flush (); private: diff --git a/src/lib/safe_stringstream.h b/src/lib/safe_stringstream.h index e455de964..0ffcb6224 100644 --- a/src/lib/safe_stringstream.h +++ b/src/lib/safe_stringstream.h @@ -84,6 +84,11 @@ public: _stream.width (w); } + void fill (int f) + { + _stream.fill (f); + } + void precision (int p) { _stream.precision (p); diff --git a/src/lib/send_kdm_email_job.cc b/src/lib/send_kdm_email_job.cc index 164dfe987..541307f5a 100644 --- a/src/lib/send_kdm_email_job.cc +++ b/src/lib/send_kdm_email_job.cc @@ -34,7 +34,7 @@ SendKDMEmailJob::SendKDMEmailJob ( boost::filesystem::path dcp, boost::posix_time::ptime from, boost::posix_time::ptime to, - libdcp::KDM::Formulation formulation + dcp::Formulation formulation ) : Job (f) , _screens (screens) diff --git a/src/lib/send_kdm_email_job.h b/src/lib/send_kdm_email_job.h index 778d3927a..af84a13af 100644 --- a/src/lib/send_kdm_email_job.h +++ b/src/lib/send_kdm_email_job.h @@ -18,7 +18,7 @@ */ #include <boost/filesystem.hpp> -#include <libdcp/kdm.h> +#include <dcp/types.h> #include "job.h" class Screen; @@ -32,7 +32,7 @@ public: boost::filesystem::path, boost::posix_time::ptime, boost::posix_time::ptime, - libdcp::KDM::Formulation + dcp::Formulation ); std::string name () const; @@ -43,5 +43,5 @@ private: boost::filesystem::path _dcp; boost::posix_time::ptime _from; boost::posix_time::ptime _to; - libdcp::KDM::Formulation _formulation; + dcp::Formulation _formulation; }; diff --git a/src/lib/server.cc b/src/lib/server.cc index 9428ba611..a699be577 100644 --- a/src/lib/server.cc +++ b/src/lib/server.cc @@ -28,15 +28,16 @@ #include <boost/algorithm/string.hpp> #include <boost/scoped_array.hpp> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "server.h" #include "util.h" #include "scaler.h" #include "image.h" -#include "dcp_video_frame.h" +#include "dcp_video.h" #include "config.h" #include "cross.h" -#include "player_video_frame.h" +#include "player_video.h" +#include "encoded_data.h" #include "safe_stringstream.h" #include "i18n.h" @@ -61,16 +62,37 @@ using boost::thread; using boost::bind; using boost::scoped_array; using boost::optional; -using libdcp::Size; -using libdcp::raw_convert; +using dcp::Size; +using dcp::raw_convert; Server::Server (shared_ptr<Log> log, bool verbose) - : _log (log) + : _terminate (false) + , _log (log) , _verbose (verbose) + , _acceptor (_io_service, boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v4(), Config::instance()->server_port_base())) { } +Server::~Server () +{ + { + boost::mutex::scoped_lock lm (_worker_mutex); + _terminate = true; + _empty_condition.notify_all (); + } + + for (vector<boost::thread*>::iterator i = _worker_threads.begin(); i != _worker_threads.end(); ++i) { + (*i)->join (); + delete *i; + } + + _io_service.stop (); + + _broadcast.io_service.stop (); + _broadcast.thread->join (); +} + /** @param after_read Filled in with gettimeofday() after reading the input from the network. * @param after_encode Filled in with gettimeofday() after encoding the image. */ @@ -90,9 +112,9 @@ Server::process (shared_ptr<Socket> socket, struct timeval& after_read, struct t return -1; } - shared_ptr<PlayerVideoFrame> pvf (new PlayerVideoFrame (xml, socket, _log)); + shared_ptr<PlayerVideo> pvf (new PlayerVideo (xml, socket, _log)); - DCPVideoFrame dcp_video_frame (pvf, xml, _log); + DCPVideo dcp_video_frame (pvf, xml, _log); gettimeofday (&after_read, 0); @@ -116,10 +138,14 @@ Server::worker_thread () { while (true) { boost::mutex::scoped_lock lock (_worker_mutex); - while (_queue.empty ()) { + while (_queue.empty () && !_terminate) { _empty_condition.wait (lock); } + if (_terminate) { + return; + } + shared_ptr<Socket> socket = _queue.front (); _queue.pop_front (); @@ -186,39 +212,18 @@ Server::run (int num_threads) _broadcast.thread = new thread (bind (&Server::broadcast_thread, this)); - boost::asio::io_service io_service; - - boost::asio::ip::tcp::acceptor acceptor ( - io_service, - boost::asio::ip::tcp::endpoint (boost::asio::ip::tcp::v4(), Config::instance()->server_port_base ()) - ); - - while (true) { - shared_ptr<Socket> socket (new Socket); - acceptor.accept (socket->socket ()); - - boost::mutex::scoped_lock lock (_worker_mutex); - - /* Wait until the queue has gone down a bit */ - while (int (_queue.size()) >= num_threads * 2) { - _full_condition.wait (lock); - } - - _queue.push_back (socket); - _empty_condition.notify_all (); - } + start_accept (); + _io_service.run (); } void Server::broadcast_thread () try { - boost::asio::io_service io_service; - boost::asio::ip::address address = boost::asio::ip::address_v4::any (); boost::asio::ip::udp::endpoint listen_endpoint (address, Config::instance()->server_port_base() + 1); - _broadcast.socket = new boost::asio::ip::udp::socket (io_service); + _broadcast.socket = new boost::asio::ip::udp::socket (_broadcast.io_service); _broadcast.socket->open (listen_endpoint.protocol ()); _broadcast.socket->bind (listen_endpoint); @@ -228,7 +233,7 @@ try boost::bind (&Server::broadcast_received, this) ); - io_service.run (); + _broadcast.io_service.run (); } catch (...) { @@ -265,3 +270,35 @@ Server::broadcast_received () _broadcast.send_endpoint, boost::bind (&Server::broadcast_received, this) ); } + +void +Server::start_accept () +{ + if (_terminate) { + return; + } + + shared_ptr<Socket> socket (new Socket); + _acceptor.async_accept (socket->socket (), boost::bind (&Server::handle_accept, this, socket, boost::asio::placeholders::error)); +} + +void +Server::handle_accept (shared_ptr<Socket> socket, boost::system::error_code const & error) +{ + if (error) { + return; + } + + boost::mutex::scoped_lock lock (_worker_mutex); + + /* Wait until the queue has gone down a bit */ + while (_queue.size() >= _worker_threads.size() * 2 && !_terminate) { + _full_condition.wait (lock); + } + + _queue.push_back (socket); + _empty_condition.notify_all (); + + start_accept (); +} + diff --git a/src/lib/server.h b/src/lib/server.h index b925031eb..e2e1d46ec 100644 --- a/src/lib/server.h +++ b/src/lib/server.h @@ -90,6 +90,7 @@ class Server : public ExceptionStore, public boost::noncopyable { public: Server (boost::shared_ptr<Log> log, bool verbose); + ~Server (); void run (int num_threads); @@ -98,6 +99,10 @@ private: int process (boost::shared_ptr<Socket> socket, struct timeval &, struct timeval &); void broadcast_thread (); void broadcast_received (); + void start_accept (); + void handle_accept (boost::shared_ptr<Socket>, boost::system::error_code const &); + + bool _terminate; std::vector<boost::thread *> _worker_threads; std::list<boost::shared_ptr<Socket> > _queue; @@ -107,6 +112,9 @@ private: boost::shared_ptr<Log> _log; bool _verbose; + boost::asio::io_service _io_service; + boost::asio::ip::tcp::acceptor _acceptor; + struct Broadcast { Broadcast () @@ -118,6 +126,7 @@ private: boost::asio::ip::udp::socket* socket; char buffer[64]; boost::asio::ip::udp::endpoint send_endpoint; + boost::asio::io_service io_service; } _broadcast; }; diff --git a/src/lib/server_finder.cc b/src/lib/server_finder.cc index a082f3bab..637103591 100644 --- a/src/lib/server_finder.cc +++ b/src/lib/server_finder.cc @@ -18,7 +18,7 @@ */ #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "server_finder.h" #include "exceptions.h" #include "util.h" @@ -32,7 +32,7 @@ using std::vector; using std::cout; using boost::shared_ptr; using boost::scoped_array; -using libdcp::raw_convert; +using dcp::raw_convert; ServerFinder* ServerFinder::_instance = 0; diff --git a/src/lib/single_stream_audio_content.cc b/src/lib/single_stream_audio_content.cc new file mode 100644 index 000000000..521597606 --- /dev/null +++ b/src/lib/single_stream_audio_content.cc @@ -0,0 +1,106 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/raw_convert.h> +#include "single_stream_audio_content.h" +#include "audio_examiner.h" +#include "film.h" + +using std::string; +using std::cout; +using boost::shared_ptr; +using dcp::raw_convert; + +SingleStreamAudioContent::SingleStreamAudioContent (shared_ptr<const Film> f) + : Content (f) + , AudioContent (f) + , _audio_channels (0) + , _audio_length (0) + , _audio_frame_rate (0) +{ + +} + +SingleStreamAudioContent::SingleStreamAudioContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , AudioContent (f, p) + , _audio_channels (0) + , _audio_length (0) + , _audio_frame_rate (0) +{ + +} + +SingleStreamAudioContent::SingleStreamAudioContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) + : Content (f, node) + , AudioContent (f, node) + , _audio_mapping (node->node_child ("AudioMapping"), version) +{ + _audio_channels = node->number_child<int> ("AudioChannels"); + _audio_length = ContentTime (node->number_child<ContentTime::Type> ("AudioLength")); + _audio_frame_rate = node->number_child<int> ("AudioFrameRate"); +} + +void +SingleStreamAudioContent::set_audio_mapping (AudioMapping m) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_mapping = m; + } + + AudioContent::set_audio_mapping (m); +} + + +void +SingleStreamAudioContent::as_xml (xmlpp::Node* node) const +{ + AudioContent::as_xml (node); + node->add_child("AudioChannels")->add_child_text (raw_convert<string> (audio_channels ())); + node->add_child("AudioLength")->add_child_text (raw_convert<string> (audio_length().get ())); + node->add_child("AudioFrameRate")->add_child_text (raw_convert<string> (audio_frame_rate ())); + _audio_mapping.as_xml (node->add_child("AudioMapping")); +} + +void +SingleStreamAudioContent::take_from_audio_examiner (shared_ptr<AudioExaminer> examiner) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_channels = examiner->audio_channels (); + _audio_length = examiner->audio_length (); + _audio_frame_rate = examiner->audio_frame_rate (); + } + + signal_changed (AudioContentProperty::AUDIO_CHANNELS); + signal_changed (AudioContentProperty::AUDIO_LENGTH); + signal_changed (AudioContentProperty::AUDIO_FRAME_RATE); + + int const p = processed_audio_channels (); + + { + boost::mutex::scoped_lock lm (_mutex); + /* XXX: do this in signal_changed...? */ + _audio_mapping = AudioMapping (p); + _audio_mapping.make_default (); + } + + signal_changed (AudioContentProperty::AUDIO_MAPPING); +} diff --git a/src/lib/single_stream_audio_content.h b/src/lib/single_stream_audio_content.h new file mode 100644 index 000000000..fcfaf14ca --- /dev/null +++ b/src/lib/single_stream_audio_content.h @@ -0,0 +1,74 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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. + +*/ + +/** @file src/lib/single_stream_audio_content.h + * @brief SingleStreamAudioContent class. + */ + +#ifndef DCPOMATIC_SINGLE_STREAM_AUDIO_CONTENT_H +#define DCPOMATIC_SINGLE_STREAM_AUDIO_CONTENT_H + +#include "audio_content.h" + +class AudioExaminer; + +/** @class SingleStreamAudioContent + * @brief A piece of AudioContent that has a single audio stream. + */ +class SingleStreamAudioContent : public AudioContent +{ +public: + SingleStreamAudioContent (boost::shared_ptr<const Film>); + SingleStreamAudioContent (boost::shared_ptr<const Film>, boost::filesystem::path); + SingleStreamAudioContent (boost::shared_ptr<const Film> f, cxml::ConstNodePtr node, int version); + + void as_xml (xmlpp::Node* node) const; + + int audio_channels () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_channels; + } + + ContentTime audio_length () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_length; + } + + int audio_frame_rate () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_frame_rate; + } + + AudioMapping audio_mapping () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_mapping; + } + + void set_audio_mapping (AudioMapping); + + void take_from_audio_examiner (boost::shared_ptr<AudioExaminer>); + +protected: + int _audio_channels; + ContentTime _audio_length; + int _audio_frame_rate; + AudioMapping _audio_mapping; +}; + +#endif diff --git a/src/lib/sndfile_content.cc b/src/lib/sndfile_content.cc index ba3bd0a77..1a1797665 100644 --- a/src/lib/sndfile_content.cc +++ b/src/lib/sndfile_content.cc @@ -18,7 +18,7 @@ */ #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "sndfile_content.h" #include "sndfile_decoder.h" #include "film.h" @@ -32,28 +32,31 @@ using std::string; using std::cout; using boost::shared_ptr; -using libdcp::raw_convert; +using dcp::raw_convert; SndfileContent::SndfileContent (shared_ptr<const Film> f, boost::filesystem::path p) : Content (f, p) - , AudioContent (f, p) - , _audio_channels (0) - , _audio_length (0) - , _audio_frame_rate (0) + , SingleStreamAudioContent (f, p) { } -SndfileContent::SndfileContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version) +SndfileContent::SndfileContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) : Content (f, node) - , AudioContent (f, node) - , _audio_mapping (node->node_child ("AudioMapping"), version) + , SingleStreamAudioContent (f, node, version) { - _audio_channels = node->number_child<int> ("AudioChannels"); - _audio_length = node->number_child<AudioContent::Frame> ("AudioLength"); - _audio_frame_rate = node->number_child<int> ("AudioFrameRate"); + } +void +SndfileContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("Sndfile"); + Content::as_xml (node); + SingleStreamAudioContent::as_xml (node); +} + + string SndfileContent::summary () const { @@ -81,8 +84,8 @@ SndfileContent::information () const s << String::compose ( _("%1 channels, %2kHz, %3 samples"), audio_channels(), - content_audio_frame_rate() / 1000.0, - audio_length() + audio_frame_rate() / 1000.0, + audio_length().frames (audio_frame_rate ()) ); return s.str (); @@ -102,69 +105,15 @@ SndfileContent::examine (shared_ptr<Job> job) { job->set_progress_unknown (); Content::examine (job); - - shared_ptr<const Film> film = _film.lock (); - assert (film); - - SndfileDecoder dec (film, shared_from_this()); - - { - boost::mutex::scoped_lock lm (_mutex); - _audio_channels = dec.audio_channels (); - _audio_length = dec.audio_length (); - _audio_frame_rate = dec.audio_frame_rate (); - } - - signal_changed (AudioContentProperty::AUDIO_CHANNELS); - signal_changed (AudioContentProperty::AUDIO_LENGTH); - signal_changed (AudioContentProperty::AUDIO_FRAME_RATE); - - { - boost::mutex::scoped_lock lm (_mutex); - /* XXX: do this in signal_changed...? */ - _audio_mapping = AudioMapping (_audio_channels); - _audio_mapping.make_default (); - } - - signal_changed (AudioContentProperty::AUDIO_MAPPING); -} - -void -SndfileContent::as_xml (xmlpp::Node* node) const -{ - node->add_child("Type")->add_child_text ("Sndfile"); - Content::as_xml (node); - AudioContent::as_xml (node); - - node->add_child("AudioChannels")->add_child_text (raw_convert<string> (audio_channels ())); - node->add_child("AudioLength")->add_child_text (raw_convert<string> (audio_length ())); - node->add_child("AudioFrameRate")->add_child_text (raw_convert<string> (content_audio_frame_rate ())); - _audio_mapping.as_xml (node->add_child("AudioMapping")); + shared_ptr<AudioExaminer> dec (new SndfileDecoder (shared_from_this())); + take_from_audio_examiner (dec); } -Time +DCPTime SndfileContent::full_length () const { shared_ptr<const Film> film = _film.lock (); assert (film); - - FrameRateChange frc = film->active_frame_rate_change (position ()); - - OutputAudioFrame const len = divide_with_round ( - audio_length() * output_audio_frame_rate() * frc.source, - content_audio_frame_rate() * film->video_frame_rate() - ); - - return film->audio_frames_to_time (len); + return DCPTime (audio_length(), film->active_frame_rate_change (position ())); } -void -SndfileContent::set_audio_mapping (AudioMapping m) -{ - { - boost::mutex::scoped_lock lm (_mutex); - _audio_mapping = m; - } - - signal_changed (AudioContentProperty::AUDIO_MAPPING); -} diff --git a/src/lib/sndfile_content.h b/src/lib/sndfile_content.h index a32043c5c..75c723518 100644 --- a/src/lib/sndfile_content.h +++ b/src/lib/sndfile_content.h @@ -23,59 +23,31 @@ extern "C" { #include <libavutil/audioconvert.h> } -#include "audio_content.h" +#include "single_stream_audio_content.h" namespace cxml { class Node; } -class SndfileContent : public AudioContent +class SndfileContent : public SingleStreamAudioContent { public: SndfileContent (boost::shared_ptr<const Film>, boost::filesystem::path); - SndfileContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int); + SndfileContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int); boost::shared_ptr<SndfileContent> shared_from_this () { return boost::dynamic_pointer_cast<SndfileContent> (Content::shared_from_this ()); } + DCPTime full_length () const; + void examine (boost::shared_ptr<Job>); std::string summary () const; std::string technical_summary () const; std::string information () const; void as_xml (xmlpp::Node *) const; - Time full_length () const; - - /* AudioContent */ - int audio_channels () const { - boost::mutex::scoped_lock lm (_mutex); - return _audio_channels; - } - - AudioContent::Frame audio_length () const { - boost::mutex::scoped_lock lm (_mutex); - return _audio_length; - } - - int content_audio_frame_rate () const { - boost::mutex::scoped_lock lm (_mutex); - return _audio_frame_rate; - } - - AudioMapping audio_mapping () const { - boost::mutex::scoped_lock lm (_mutex); - return _audio_mapping; - } - - void set_audio_mapping (AudioMapping); static bool valid_file (boost::filesystem::path); - -private: - int _audio_channels; - AudioContent::Frame _audio_length; - int _audio_frame_rate; - AudioMapping _audio_mapping; }; #endif diff --git a/src/lib/sndfile_decoder.cc b/src/lib/sndfile_decoder.cc index f66a7c7dc..602014d58 100644 --- a/src/lib/sndfile_decoder.cc +++ b/src/lib/sndfile_decoder.cc @@ -25,7 +25,6 @@ #include <sndfile.h> #include "sndfile_content.h" #include "sndfile_decoder.h" -#include "film.h" #include "exceptions.h" #include "audio_buffers.h" @@ -37,9 +36,8 @@ using std::min; using std::cout; using boost::shared_ptr; -SndfileDecoder::SndfileDecoder (shared_ptr<const Film> f, shared_ptr<const SndfileContent> c) - : Decoder (f) - , AudioDecoder (f, c) +SndfileDecoder::SndfileDecoder (shared_ptr<const SndfileContent> c) + : AudioDecoder (c) , _sndfile_content (c) , _deinterleave_buffer (0) { @@ -66,13 +64,17 @@ SndfileDecoder::~SndfileDecoder () delete[] _deinterleave_buffer; } -void +bool SndfileDecoder::pass () { + if (_remaining == 0) { + return true; + } + /* 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 = _sndfile_content->content_audio_frame_rate() / 2; + sf_count_t const block = _sndfile_content->audio_frame_rate() / 2; sf_count_t const this_time = min (block, _remaining); int const channels = _sndfile_content->audio_channels (); @@ -101,9 +103,11 @@ SndfileDecoder::pass () } data->set_frames (this_time); - audio (data, _done); + audio (data, ContentTime::from_frames (_done, audio_frame_rate ())); _done += this_time; _remaining -= this_time; + + return _remaining == 0; } int @@ -112,10 +116,10 @@ SndfileDecoder::audio_channels () const return _info.channels; } -AudioContent::Frame +ContentTime SndfileDecoder::audio_length () const { - return _info.frames; + return ContentTime::from_frames (_info.frames, audio_frame_rate ()); } int @@ -124,8 +128,11 @@ SndfileDecoder::audio_frame_rate () const return _info.samplerate; } -bool -SndfileDecoder::done () const +void +SndfileDecoder::seek (ContentTime t, bool accurate) { - return _audio_position >= _sndfile_content->audio_length (); + AudioDecoder::seek (t, accurate); + + _done = t.frames (audio_frame_rate ()); + _remaining = _info.frames - _done; } diff --git a/src/lib/sndfile_decoder.h b/src/lib/sndfile_decoder.h index 77fa6d177..41d5faf08 100644 --- a/src/lib/sndfile_decoder.h +++ b/src/lib/sndfile_decoder.h @@ -20,27 +20,29 @@ #include <sndfile.h> #include "decoder.h" #include "audio_decoder.h" +#include "audio_examiner.h" class SndfileContent; -class SndfileDecoder : public AudioDecoder +class SndfileDecoder : public AudioDecoder, public AudioExaminer { public: - SndfileDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const SndfileContent>); + SndfileDecoder (boost::shared_ptr<const SndfileContent> c); ~SndfileDecoder (); - void pass (); - bool done () const; + void seek (ContentTime, bool); int audio_channels () const; - AudioContent::Frame audio_length () const; + ContentTime audio_length () const; int audio_frame_rate () const; private: + bool pass (); + boost::shared_ptr<const SndfileContent> _sndfile_content; SNDFILE* _sndfile; SF_INFO _info; - AudioContent::Frame _done; - AudioContent::Frame _remaining; + int64_t _done; + int64_t _remaining; float* _deinterleave_buffer; }; diff --git a/src/lib/subrip.cc b/src/lib/subrip.cc new file mode 100644 index 000000000..8a7423698 --- /dev/null +++ b/src/lib/subrip.cc @@ -0,0 +1,51 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "subrip.h" +#include "cross.h" +#include "exceptions.h" +#include "subrip_content.h" +#include <libsub/subrip_reader.h> +#include <libsub/collect.h> + +#include "i18n.h" + +using std::vector; +using boost::shared_ptr; + +SubRip::SubRip (shared_ptr<const SubRipContent> content) +{ + FILE* f = fopen_boost (content->path (0), "r"); + if (!f) { + throw OpenFileError (content->path (0)); + } + + sub::SubripReader reader (f); + _subtitles = sub::collect<vector<sub::Subtitle> > (reader.subtitles ()); +} + +ContentTime +SubRip::length () const +{ + if (_subtitles.empty ()) { + return ContentTime (); + } + + return ContentTime::from_seconds (_subtitles.back().to.metric().get().all_as_seconds ()); +} diff --git a/src/lib/subrip.h b/src/lib/subrip.h new file mode 100644 index 000000000..14bc360c0 --- /dev/null +++ b/src/lib/subrip.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_SUBRIP_H +#define DCPOMATIC_SUBRIP_H + +#include "subrip_subtitle.h" +#include <libsub/subtitle.h> + +class SubRipContent; +class subrip_time_test; +class subrip_coordinate_test; +class subrip_content_test; +class subrip_parse_test; + +class SubRip +{ +public: + SubRip (boost::shared_ptr<const SubRipContent>); + + ContentTime length () const; + +protected: + std::vector<sub::Subtitle> _subtitles; +}; + +#endif diff --git a/src/lib/subrip_content.cc b/src/lib/subrip_content.cc new file mode 100644 index 000000000..14cb50b86 --- /dev/null +++ b/src/lib/subrip_content.cc @@ -0,0 +1,98 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "subrip_content.h" +#include "util.h" +#include "subrip.h" +#include "film.h" +#include <dcp/raw_convert.h> + +#include "i18n.h" + +using std::string; +using std::cout; +using dcp::raw_convert; +using boost::shared_ptr; +using boost::lexical_cast; + +SubRipContent::SubRipContent (shared_ptr<const Film> film, boost::filesystem::path path) + : Content (film, path) + , SubtitleContent (film, path) +{ + +} + +SubRipContent::SubRipContent (shared_ptr<const Film> film, cxml::ConstNodePtr node, int version) + : Content (film, node) + , SubtitleContent (film, node, version) + , _length (node->number_child<DCPTime::Type> ("Length")) +{ + +} + +void +SubRipContent::examine (boost::shared_ptr<Job> job) +{ + Content::examine (job); + SubRip s (shared_from_this ()); + + shared_ptr<const Film> film = _film.lock (); + assert (film); + + DCPTime len (s.length (), film->active_frame_rate_change (position ())); + + boost::mutex::scoped_lock lm (_mutex); + _length = len; +} + +string +SubRipContent::summary () const +{ + return path_summary() + " " + _("[subtitles]"); +} + +string +SubRipContent::technical_summary () const +{ + return Content::technical_summary() + " - " + _("SubRip subtitles"); +} + +string +SubRipContent::information () const +{ + +} + +void +SubRipContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("SubRip"); + Content::as_xml (node); + SubtitleContent::as_xml (node); + node->add_child("Length")->add_child_text (raw_convert<string> (_length.get ())); +} + +DCPTime +SubRipContent::full_length () const +{ + /* XXX: this assumes that the timing of the SubRip file is appropriate + for the DCP's frame rate. + */ + return _length; +} diff --git a/src/lib/subrip_content.h b/src/lib/subrip_content.h new file mode 100644 index 000000000..d2dcdee00 --- /dev/null +++ b/src/lib/subrip_content.h @@ -0,0 +1,47 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "subtitle_content.h" + +class SubRipContent : public SubtitleContent +{ +public: + SubRipContent (boost::shared_ptr<const Film>, boost::filesystem::path); + SubRipContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int); + + boost::shared_ptr<SubRipContent> shared_from_this () { + return boost::dynamic_pointer_cast<SubRipContent> (Content::shared_from_this ()); + } + + /* Content */ + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + std::string information () const; + void as_xml (xmlpp::Node *) const; + DCPTime full_length () const; + + /* SubtitleContent */ + bool has_subtitles () const { + return true; + } + +private: + DCPTime _length; +}; diff --git a/src/lib/subrip_decoder.cc b/src/lib/subrip_decoder.cc new file mode 100644 index 000000000..411e7542d --- /dev/null +++ b/src/lib/subrip_decoder.cc @@ -0,0 +1,105 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <dcp/subtitle_string.h> +#include "subrip_decoder.h" +#include "subrip_content.h" + +using std::list; +using std::vector; +using boost::shared_ptr; + +SubRipDecoder::SubRipDecoder (shared_ptr<const SubRipContent> content) + : SubtitleDecoder (content) + , SubRip (content) + , _next (0) +{ + +} + +void +SubRipDecoder::seek (ContentTime time, bool accurate) +{ + SubtitleDecoder::seek (time, accurate); + + _next = 0; + while (_next < _subtitles.size() && ContentTime::from_seconds (_subtitles[_next].from.metric().get().all_as_seconds ()) < time) { + ++_next; + } +} + +bool +SubRipDecoder::pass () +{ + if (_next >= _subtitles.size ()) { + return true; + } + + /* XXX: we are ignoring positioning specified in the file */ + + list<dcp::SubtitleString> out; + for (list<sub::Line>::const_iterator i = _subtitles[_next].lines.begin(); i != _subtitles[_next].lines.end(); ++i) { + for (list<sub::Block>::const_iterator j = i->blocks.begin(); j != i->blocks.end(); ++j) { + out.push_back ( + dcp::SubtitleString ( + "Arial", + j->italic, + dcp::Color (255, 255, 255), + /* .srt files don't specify size, so this is an arbitrary value */ + 48, + dcp::Time (rint (_subtitles[_next].from.metric().get().all_as_milliseconds() / 4)), + dcp::Time (rint (_subtitles[_next].to.metric().get().all_as_milliseconds() / 4)), + i->vertical_position.line.get() * (1.5 / 22) + 0.8, + dcp::TOP, + j->text, + dcp::NONE, + dcp::Color (255, 255, 255), + 0, + 0 + ) + ); + } + } + + text_subtitle (out); + ++_next; + return false; +} + +list<ContentTimePeriod> +SubRipDecoder::subtitles_during (ContentTimePeriod p, bool starting) const +{ + /* XXX: inefficient */ + + list<ContentTimePeriod> d; + + for (vector<sub::Subtitle>::const_iterator i = _subtitles.begin(); i != _subtitles.end(); ++i) { + + ContentTimePeriod t ( + ContentTime::from_seconds (i->from.metric().get().all_as_seconds()), + ContentTime::from_seconds (i->to.metric().get().all_as_seconds()) + ); + + if ((starting && p.contains (t.from)) || (!starting && p.overlaps (t))) { + d.push_back (t); + } + } + + return d; +} diff --git a/src/lib/subrip_decoder.h b/src/lib/subrip_decoder.h new file mode 100644 index 000000000..ad9d04e40 --- /dev/null +++ b/src/lib/subrip_decoder.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_SUBRIP_DECODER_H +#define DCPOMATIC_SUBRIP_DECODER_H + +#include "subtitle_decoder.h" +#include "subrip.h" + +class SubRipContent; + +class SubRipDecoder : public SubtitleDecoder, public SubRip +{ +public: + SubRipDecoder (boost::shared_ptr<const SubRipContent>); + +protected: + void seek (ContentTime time, bool accurate); + bool pass (); + +private: + std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const; + + size_t _next; +}; + +#endif diff --git a/src/lib/subrip_subtitle.h b/src/lib/subrip_subtitle.h new file mode 100644 index 000000000..646fc1f7a --- /dev/null +++ b/src/lib/subrip_subtitle.h @@ -0,0 +1,53 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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_SUBRIP_SUBTITLE_H +#define DCPOMATIC_SUBRIP_SUBTITLE_H + +#include <boost/optional.hpp> +#include <dcp/types.h> +#include "types.h" +#include "dcpomatic_time.h" + +struct SubRipSubtitlePiece +{ + SubRipSubtitlePiece () + : bold (false) + , italic (false) + , underline (false) + {} + + std::string text; + bool bold; + bool italic; + bool underline; + dcp::Color color; +}; + +struct SubRipSubtitle +{ + ContentTimePeriod period; + boost::optional<int> x1; + boost::optional<int> x2; + boost::optional<int> y1; + boost::optional<int> y2; + std::list<SubRipSubtitlePiece> pieces; +}; + +#endif diff --git a/src/lib/subtitle.h b/src/lib/subtitle.h deleted file mode 100644 index c74f5c1b9..000000000 --- a/src/lib/subtitle.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - 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 <boost/shared_ptr.hpp> -#include <boost/weak_ptr.hpp> -#include <boost/optional.hpp> -#include <libdcp/util.h> -#include "rect.h" -#include "types.h" - -class Film; -class Piece; -class Image; - -class Subtitle -{ -public: - - Subtitle (boost::shared_ptr<const Film>, libdcp::Size, boost::weak_ptr<Piece>, boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time); - - void update (boost::shared_ptr<const Film>, libdcp::Size); - void set_stop (Time t) { - _stop = t; - check_out_to (); - } - - bool covers (Time t) const; - bool ends_before (Time t) const { - return _out_to < t; - } - - boost::shared_ptr<Image> out_image () const { - return _out_image; - } - - Position<int> out_position () const { - return _out_position; - } - -private: - void check_out_to (); - - boost::weak_ptr<Piece> _piece; - boost::shared_ptr<Image> _in_image; - dcpomatic::Rect<double> _in_rect; - Time _in_from; - Time _in_to; - - boost::shared_ptr<Image> _out_image; - Position<int> _out_position; - Time _out_from; - Time _out_to; - - /** Time at which this subtitle should stop (overriding _out_to) */ - boost::optional<Time> _stop; -}; diff --git a/src/lib/subtitle_content.cc b/src/lib/subtitle_content.cc index 3702eef41..5b370847b 100644 --- a/src/lib/subtitle_content.cc +++ b/src/lib/subtitle_content.cc @@ -18,26 +18,41 @@ */ #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "subtitle_content.h" #include "util.h" #include "exceptions.h" +#include "safe_stringstream.h" #include "i18n.h" using std::string; using std::vector; +using std::cout; using boost::shared_ptr; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; int const SubtitleContentProperty::SUBTITLE_X_OFFSET = 500; int const SubtitleContentProperty::SUBTITLE_Y_OFFSET = 501; int const SubtitleContentProperty::SUBTITLE_X_SCALE = 502; int const SubtitleContentProperty::SUBTITLE_Y_SCALE = 503; +int const SubtitleContentProperty::USE_SUBTITLES = 504; + +SubtitleContent::SubtitleContent (shared_ptr<const Film> f) + : Content (f) + , _use_subtitles (false) + , _subtitle_x_offset (0) + , _subtitle_y_offset (0) + , _subtitle_x_scale (1) + , _subtitle_y_scale (1) +{ + +} SubtitleContent::SubtitleContent (shared_ptr<const Film> f, boost::filesystem::path p) : Content (f, p) + , _use_subtitles (false) , _subtitle_x_offset (0) , _subtitle_y_offset (0) , _subtitle_x_scale (1) @@ -46,13 +61,20 @@ SubtitleContent::SubtitleContent (shared_ptr<const Film> f, boost::filesystem::p } -SubtitleContent::SubtitleContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version) +SubtitleContent::SubtitleContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) : Content (f, node) + , _use_subtitles (false) , _subtitle_x_offset (0) , _subtitle_y_offset (0) , _subtitle_x_scale (1) , _subtitle_y_scale (1) { + if (version >= 32) { + _use_subtitles = node->bool_child ("UseSubtitles"); + } else { + _use_subtitles = false; + } + if (version >= 7) { _subtitle_x_offset = node->number_child<float> ("SubtitleXOffset"); _subtitle_y_offset = node->number_child<float> ("SubtitleYOffset"); @@ -77,6 +99,10 @@ SubtitleContent::SubtitleContent (shared_ptr<const Film> f, vector<shared_ptr<Co for (size_t i = 0; i < c.size(); ++i) { shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (c[i]); + if (sc->use_subtitles() != ref->use_subtitles()) { + throw JoinError (_("Content to be joined must have the same 'use subtitles' setting.")); + } + if (sc->subtitle_x_offset() != ref->subtitle_x_offset()) { throw JoinError (_("Content to be joined must have the same subtitle X offset.")); } @@ -94,6 +120,7 @@ SubtitleContent::SubtitleContent (shared_ptr<const Film> f, vector<shared_ptr<Co } } + _use_subtitles = ref->use_subtitles (); _subtitle_x_offset = ref->subtitle_x_offset (); _subtitle_y_offset = ref->subtitle_y_offset (); _subtitle_x_scale = ref->subtitle_x_scale (); @@ -103,6 +130,7 @@ SubtitleContent::SubtitleContent (shared_ptr<const Film> f, vector<shared_ptr<Co void SubtitleContent::as_xml (xmlpp::Node* root) const { + root->add_child("UseSubtitles")->add_child_text (raw_convert<string> (_use_subtitles)); root->add_child("SubtitleXOffset")->add_child_text (raw_convert<string> (_subtitle_x_offset)); root->add_child("SubtitleYOffset")->add_child_text (raw_convert<string> (_subtitle_y_offset)); root->add_child("SubtitleXScale")->add_child_text (raw_convert<string> (_subtitle_x_scale)); @@ -110,6 +138,16 @@ SubtitleContent::as_xml (xmlpp::Node* root) const } void +SubtitleContent::set_use_subtitles (bool u) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _use_subtitles = u; + } + signal_changed (SubtitleContentProperty::USE_SUBTITLES); +} + +void SubtitleContent::set_subtitle_x_offset (double o) { { @@ -148,3 +186,16 @@ SubtitleContent::set_subtitle_y_scale (double s) } signal_changed (SubtitleContentProperty::SUBTITLE_Y_SCALE); } + +string +SubtitleContent::identifier () const +{ + SafeStringStream s; + s << Content::identifier() + << "_" << raw_convert<string> (subtitle_x_scale()) + << "_" << raw_convert<string> (subtitle_y_scale()) + << "_" << raw_convert<string> (subtitle_x_offset()) + << "_" << raw_convert<string> (subtitle_y_offset()); + + return s.str (); +} diff --git a/src/lib/subtitle_content.h b/src/lib/subtitle_content.h index 329368e44..c3c25232f 100644 --- a/src/lib/subtitle_content.h +++ b/src/lib/subtitle_content.h @@ -29,22 +29,39 @@ public: static int const SUBTITLE_Y_OFFSET; static int const SUBTITLE_X_SCALE; static int const SUBTITLE_Y_SCALE; + static int const USE_SUBTITLES; }; +/** @class SubtitleContent + * @brief Parent for content which has the potential to include subtitles. + * + * Although inheriting from this class indicates that the content could + * have subtitles, it may not. ::has_subtitles() will tell you. + */ class SubtitleContent : public virtual Content { public: + SubtitleContent (boost::shared_ptr<const Film>); SubtitleContent (boost::shared_ptr<const Film>, boost::filesystem::path); - SubtitleContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int version); + SubtitleContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int version); SubtitleContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >); - + void as_xml (xmlpp::Node *) const; + std::string identifier () const; + + virtual bool has_subtitles () const = 0; + void set_use_subtitles (bool); void set_subtitle_x_offset (double); void set_subtitle_y_offset (double); void set_subtitle_x_scale (double); void set_subtitle_y_scale (double); + bool use_subtitles () const { + boost::mutex::scoped_lock lm (_mutex); + return _use_subtitles; + } + double subtitle_x_offset () const { boost::mutex::scoped_lock lm (_mutex); return _subtitle_x_offset; @@ -64,10 +81,11 @@ public: boost::mutex::scoped_lock lm (_mutex); return _subtitle_y_scale; } - + private: - friend class ffmpeg_pts_offset_test; + friend struct ffmpeg_pts_offset_test; + bool _use_subtitles; /** x offset for placing subtitles, as a proportion of the container width; * +ve is further right, -ve is further left. */ diff --git a/src/lib/subtitle_decoder.cc b/src/lib/subtitle_decoder.cc index c06f3d718..e1485e221 100644 --- a/src/lib/subtitle_decoder.cc +++ b/src/lib/subtitle_decoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,21 +19,84 @@ #include <boost/shared_ptr.hpp> #include "subtitle_decoder.h" +#include "subtitle_content.h" +using std::list; +using std::cout; using boost::shared_ptr; +using boost::optional; -SubtitleDecoder::SubtitleDecoder (shared_ptr<const Film> f) - : Decoder (f) +SubtitleDecoder::SubtitleDecoder (shared_ptr<const SubtitleContent> c) + : _subtitle_content (c) { } - -/** Called by subclasses when a subtitle is ready. +/** Called by subclasses when an image subtitle is ready. * Image may be 0 to say that there is no current subtitle. */ void -SubtitleDecoder::subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to) +SubtitleDecoder::image_subtitle (ContentTimePeriod period, shared_ptr<Image> image, dcpomatic::Rect<double> rect) +{ + _decoded_image_subtitles.push_back (ContentImageSubtitle (period, image, rect)); +} + +void +SubtitleDecoder::text_subtitle (list<dcp::SubtitleString> s) +{ + _decoded_text_subtitles.push_back (ContentTextSubtitle (s)); +} + +template <class T> +list<T> +SubtitleDecoder::get (list<T> const & subs, ContentTimePeriod period, bool starting) +{ + /* Get the full periods of the subtitles that are showing or starting during the specified period */ + list<ContentTimePeriod> sp = subtitles_during (period, starting); + if (sp.empty ()) { + /* Nothing in this period */ + return list<T> (); + } + + /* Seek if what we want is before what we have, or more than a reasonable amount after */ + if (subs.empty() || sp.back().to < subs.front().period().from || sp.front().from > (subs.back().period().to + ContentTime::from_seconds (5))) { + seek (sp.front().from, true); + } + + /* Now enough pass() calls will either: + * (a) give us what we want, or + * (b) hit the end of the decoder. + */ + while (!pass() && (subs.empty() || (subs.back().period().to < sp.back().to))) {} + + /* Now look for what we wanted in the data we have collected */ + /* XXX: inefficient */ + + list<T> out; + for (typename list<T>::const_iterator i = subs.begin(); i != subs.end(); ++i) { + if ((starting && period.contains (i->period().from)) || (!starting && period.overlaps (i->period ()))) { + out.push_back (*i); + } + } + + return out; +} + +list<ContentTextSubtitle> +SubtitleDecoder::get_text_subtitles (ContentTimePeriod period, bool starting) +{ + return get<ContentTextSubtitle> (_decoded_text_subtitles, period, starting); +} + +list<ContentImageSubtitle> +SubtitleDecoder::get_image_subtitles (ContentTimePeriod period, bool starting) +{ + return get<ContentImageSubtitle> (_decoded_image_subtitles, period, starting); +} + +void +SubtitleDecoder::seek (ContentTime, bool) { - Subtitle (image, rect, from, to); + _decoded_text_subtitles.clear (); + _decoded_image_subtitles.clear (); } diff --git a/src/lib/subtitle_decoder.h b/src/lib/subtitle_decoder.h index eeeadbd3f..142cfa42b 100644 --- a/src/lib/subtitle_decoder.h +++ b/src/lib/subtitle_decoder.h @@ -17,22 +17,43 @@ */ -#include <boost/signals2.hpp> +#ifndef DCPOMATIC_SUBTITLE_DECODER_H +#define DCPOMATIC_SUBTITLE_DECODER_H + +#include <dcp/subtitle_string.h> #include "decoder.h" #include "rect.h" #include "types.h" +#include "content_subtitle.h" class Film; -class TimedSubtitle; +class DCPTimedSubtitle; class Image; class SubtitleDecoder : public virtual Decoder { public: - SubtitleDecoder (boost::shared_ptr<const Film>); + SubtitleDecoder (boost::shared_ptr<const SubtitleContent>); - boost::signals2::signal<void (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time)> Subtitle; + std::list<ContentImageSubtitle> get_image_subtitles (ContentTimePeriod period, bool starting); + std::list<ContentTextSubtitle> get_text_subtitles (ContentTimePeriod period, bool starting); protected: - void subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time); + void seek (ContentTime, bool); + + void image_subtitle (ContentTimePeriod period, boost::shared_ptr<Image>, dcpomatic::Rect<double>); + void text_subtitle (std::list<dcp::SubtitleString>); + + std::list<ContentImageSubtitle> _decoded_image_subtitles; + std::list<ContentTextSubtitle> _decoded_text_subtitles; + +private: + template <class T> + std::list<T> get (std::list<T> const & subs, ContentTimePeriod period, bool starting); + + virtual std::list<ContentTimePeriod> subtitles_during (ContentTimePeriod, bool starting) const = 0; + + boost::shared_ptr<const SubtitleContent> _subtitle_content; }; + +#endif diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc index 831c74b3b..23a46d06d 100644 --- a/src/lib/transcode_job.cc +++ b/src/lib/transcode_job.cc @@ -37,6 +37,7 @@ using std::string; using std::fixed; using std::setprecision; +using std::cout; using boost::shared_ptr; /** @param s Film to use. @@ -100,6 +101,7 @@ TranscodeJob::status () const return s.str (); } +/** @return Approximate remaining time in seconds */ int TranscodeJob::remaining_time () const { @@ -117,6 +119,5 @@ TranscodeJob::remaining_time () const } /* Compute approximate proposed length here, as it's only here that we need it */ - OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - t->video_frames_out(); - return left / fps; + return (_film->length().frames (_film->video_frame_rate ()) - t->video_frames_out()) / fps; } diff --git a/src/lib/transcoder.cc b/src/lib/transcoder.cc index b11ce8be5..9d8eebe25 100644 --- a/src/lib/transcoder.cc +++ b/src/lib/transcoder.cc @@ -33,51 +33,52 @@ #include "audio_decoder.h" #include "player.h" #include "job.h" +#include "writer.h" using std::string; +using std::cout; +using std::list; using boost::shared_ptr; using boost::weak_ptr; using boost::dynamic_pointer_cast; -static void -video_proxy (weak_ptr<Encoder> encoder, shared_ptr<PlayerVideoFrame> pvf, bool same) -{ - shared_ptr<Encoder> e = encoder.lock (); - if (e) { - e->process_video (pvf, same); - } -} - -static void -audio_proxy (weak_ptr<Encoder> encoder, shared_ptr<const AudioBuffers> audio) -{ - shared_ptr<Encoder> e = encoder.lock (); - if (e) { - e->process_audio (audio); - } -} - /** Construct a transcoder using a Decoder that we create and a supplied Encoder. * @param f Film that we are transcoding. * @param e Encoder to use. */ Transcoder::Transcoder (shared_ptr<const Film> f, shared_ptr<Job> j) - : _player (f->make_player ()) - , _encoder (new Encoder (f, j)) + : _film (f) + , _player (f->make_player ()) + , _writer (new Writer (f, j)) + , _encoder (new Encoder (f, j, _writer)) , _finishing (false) { - _player->Video.connect (bind (video_proxy, _encoder, _1, _2)); - _player->Audio.connect (bind (audio_proxy, _encoder, _1)); + } void Transcoder::go () { - _encoder->process_begin (); - while (!_player->pass ()) {} + _encoder->begin (); + + DCPTime const frame = DCPTime::from_frames (1, _film->video_frame_rate ()); + DCPTime const length = _film->length (); + for (DCPTime t; t < length; t += frame) { + list<shared_ptr<PlayerVideo> > v = _player->get_video (t, true); + for (list<shared_ptr<PlayerVideo> >::const_iterator i = v.begin(); i != v.end(); ++i) { + _encoder->enqueue (*i); + } + _writer->write (_player->get_audio (t, frame, true)); + if (!_film->burn_subtitles ()) { + _writer->write (_player->get_subtitles (t, frame, true)); + } + } _finishing = true; - _encoder->process_end (); + _encoder->end (); + _writer->finish (); + + _player->statistics().dump (_film->log ()); } float diff --git a/src/lib/transcoder.h b/src/lib/transcoder.h index d7736d4e8..ed0a6b1b5 100644 --- a/src/lib/transcoder.h +++ b/src/lib/transcoder.h @@ -42,7 +42,9 @@ public: } private: + boost::shared_ptr<const Film> _film; boost::shared_ptr<Player> _player; + boost::shared_ptr<Writer> _writer; boost::shared_ptr<Encoder> _encoder; bool _finishing; }; diff --git a/src/lib/types.cc b/src/lib/types.cc index 83bbf41e4..d052b2a9a 100644 --- a/src/lib/types.cc +++ b/src/lib/types.cc @@ -19,14 +19,14 @@ #include <libxml++/libxml++.h> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "types.h" using std::max; using std::min; using std::string; using boost::shared_ptr; -using libdcp::raw_convert; +using dcp::raw_convert; bool operator== (Crop const & a, Crop const & b) { diff --git a/src/lib/types.h b/src/lib/types.h index 4eb3d927e..9a6a30b86 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -23,7 +23,9 @@ #include <vector> #include <stdint.h> #include <boost/shared_ptr.hpp> -#include <libdcp/util.h> +#include <dcp/util.h> +#include "dcpomatic_time.h" +#include "position.h" class Content; class VideoContent; @@ -46,31 +48,29 @@ namespace xmlpp { */ #define SERVER_LINK_VERSION 2 -typedef int64_t Time; -#define TIME_MAX INT64_MAX -#define TIME_HZ ((Time) 96000) -typedef int64_t OutputAudioFrame; -typedef int OutputVideoFrame; typedef std::vector<boost::shared_ptr<Content> > ContentList; typedef std::vector<boost::shared_ptr<VideoContent> > VideoContentList; typedef std::vector<boost::shared_ptr<AudioContent> > AudioContentList; typedef std::vector<boost::shared_ptr<SubtitleContent> > SubtitleContentList; typedef std::vector<boost::shared_ptr<FFmpegContent> > FFmpegContentList; -template<class T> +typedef int64_t VideoFrame; +typedef int64_t AudioFrame; + +/* XXX -> DCPAudio */ struct TimedAudioBuffers { TimedAudioBuffers () : time (0) {} - TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, T t) + TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, DCPTime t) : audio (a) , time (t) {} boost::shared_ptr<AudioBuffers> audio; - T time; + DCPTime time; }; enum VideoFrameType @@ -120,7 +120,7 @@ struct Crop /** Number of pixels to remove from the bottom */ int bottom; - libdcp::Size apply (libdcp::Size s, int minimum = 4) const { + dcp::Size apply (dcp::Size s, int minimum = 4) const { s.width -= left + right; s.height -= top + bottom; diff --git a/src/lib/update.cc b/src/lib/update.cc index 7bec061e9..c50022091 100644 --- a/src/lib/update.cc +++ b/src/lib/update.cc @@ -21,7 +21,7 @@ #include <boost/algorithm/string.hpp> #include <curl/curl.h> #include <libcxml/cxml.h> -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "update.h" #include "version.h" #include "ui_signaller.h" @@ -32,8 +32,9 @@ using std::cout; using std::min; using std::string; -using libdcp::raw_convert; +using dcp::raw_convert; +/** Singleton instance */ UpdateChecker* UpdateChecker::_instance = 0; static size_t @@ -42,6 +43,9 @@ write_callback_wrapper (void* data, size_t size, size_t nmemb, void* user) return reinterpret_cast<UpdateChecker*>(user)->write_callback (data, size, nmemb); } +/** Construct an UpdateChecker. This sets things up and starts a thread to + * do the work. + */ UpdateChecker::UpdateChecker () : _buffer (new char[BUFFER_SIZE]) , _offset (0) @@ -73,6 +77,7 @@ UpdateChecker::~UpdateChecker () delete[] _buffer; } +/** Start running the update check */ void UpdateChecker::run () { @@ -85,6 +90,7 @@ void UpdateChecker::thread () { while (true) { + /* Block until there is something to do */ boost::mutex::scoped_lock lock (_process_mutex); while (_to_do == 0) { _condition.wait (lock); @@ -94,12 +100,16 @@ UpdateChecker::thread () try { _offset = 0; + + /* Perform the request */ int r = curl_easy_perform (_curl); if (r != CURLE_OK) { set_state (FAILED); return; } + + /* Parse the reply */ _buffer[_offset] = '\0'; string s (_buffer); diff --git a/src/lib/update.h b/src/lib/update.h index e96ccec31..c86adb873 100644 --- a/src/lib/update.h +++ b/src/lib/update.h @@ -17,12 +17,17 @@ */ +/** @file src/lib/update.h + * @brief UpdateChecker class. + */ + #include <boost/signals2.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/condition.hpp> #include <boost/thread.hpp> #include <curl/curl.h> +/** Class to check for the existance of an update for DCP-o-matic on a remote server */ class UpdateChecker { public: @@ -32,28 +37,31 @@ public: void run (); enum State { - YES, - FAILED, - NO, - NOT_RUN + YES, ///< there is an update + FAILED, ///< the check failed, so we don't know + NO, ///< there is no update + NOT_RUN ///< the check has not been run (yet) }; + /** @return state of the checker */ State state () { boost::mutex::scoped_lock lm (_data_mutex); return _state; } + /** @return the version string of the latest stable version (if _state == YES or NO) */ std::string stable () { boost::mutex::scoped_lock lm (_data_mutex); return _stable; } + /** @return the version string of the latest test version (if _state == YES or NO) */ std::string test () { boost::mutex::scoped_lock lm (_data_mutex); return _test; } - /** @return true if the list signal emission was the first */ + /** @return true if the last signal emission was the first */ bool last_emit_was_first () const { boost::mutex::scoped_lock lm (_data_mutex); return _emits == 1; diff --git a/src/lib/upmixer_a.cc b/src/lib/upmixer_a.cc new file mode 100644 index 000000000..dce08fe37 --- /dev/null +++ b/src/lib/upmixer_a.cc @@ -0,0 +1,109 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "upmixer_a.h" +#include "audio_buffers.h" + +#include "i18n.h" + +using std::string; +using boost::shared_ptr; + +UpmixerA::UpmixerA (int sampling_rate) + : _left (0.02, 1900.0 / sampling_rate, 4800.0 / sampling_rate) + , _right (0.02, 1900.0 / sampling_rate, 4800.0 / sampling_rate) + , _centre (0.02, 150.0 / sampling_rate, 1900.0 / sampling_rate) + , _lfe (0.02, 20.0 / sampling_rate, 150.0 / sampling_rate) + , _ls (0.02, 4800.0 / sampling_rate, 20000.0 / sampling_rate) + , _rs (0.02, 4800.0 / sampling_rate, 20000.0 / sampling_rate) +{ + +} + +string +UpmixerA::name () const +{ + return _("Stereo to 5.1 up-mixer A"); +} + + +string +UpmixerA::id () const +{ + return N_("stereo-5.1-upmix-a"); +} + +ChannelCount +UpmixerA::in_channels () const +{ + return ChannelCount (2); +} + +int +UpmixerA::out_channels (int) const +{ + return 6; +} + +shared_ptr<AudioProcessor> +UpmixerA::clone (int sampling_rate) const +{ + return shared_ptr<AudioProcessor> (new UpmixerA (sampling_rate)); +} + +shared_ptr<AudioBuffers> +UpmixerA::run (shared_ptr<const AudioBuffers> in) +{ + /* Input L and R */ + shared_ptr<AudioBuffers> in_L = in->channel (0); + shared_ptr<AudioBuffers> in_R = in->channel (1); + + /* Mix of L and R */ + shared_ptr<AudioBuffers> in_LR = in_L->clone (); + in_LR->accumulate_frames (in_R.get(), 0, 0, in_R->frames ()); + in_LR->apply_gain (0.5); + + /* Run filters */ + shared_ptr<AudioBuffers> L = _left.run (in_L); + shared_ptr<AudioBuffers> R = _right.run (in_R); + shared_ptr<AudioBuffers> C = _centre.run (in_LR); + shared_ptr<AudioBuffers> Lfe = _lfe.run (in_LR); + shared_ptr<AudioBuffers> Ls = _ls.run (in_L); + shared_ptr<AudioBuffers> Rs = _rs.run (in_R); + + shared_ptr<AudioBuffers> out (new AudioBuffers (6, in->frames ())); + out->copy_channel_from (L.get(), 0, 0); + out->copy_channel_from (R.get(), 0, 1); + out->copy_channel_from (C.get(), 0, 2); + out->copy_channel_from (Lfe.get(), 0, 3); + out->copy_channel_from (Ls.get(), 0, 4); + out->copy_channel_from (Rs.get(), 0, 5); + return out; +} + +void +UpmixerA::flush () +{ + _left.flush (); + _right.flush (); + _centre.flush (); + _lfe.flush (); + _ls.flush (); + _rs.flush (); +} diff --git a/src/lib/upmixer_a.h b/src/lib/upmixer_a.h new file mode 100644 index 000000000..32e3f5fb6 --- /dev/null +++ b/src/lib/upmixer_a.h @@ -0,0 +1,43 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "audio_processor.h" +#include "audio_filter.h" + +class UpmixerA : public AudioProcessor +{ +public: + UpmixerA (int sampling_rate); + + std::string name () const; + std::string id () const; + ChannelCount in_channels () const; + int out_channels (int) const; + boost::shared_ptr<AudioProcessor> clone (int) const; + boost::shared_ptr<AudioBuffers> run (boost::shared_ptr<const AudioBuffers>); + void flush (); + +private: + BandPassAudioFilter _left; + BandPassAudioFilter _right; + BandPassAudioFilter _centre; + BandPassAudioFilter _lfe; + BandPassAudioFilter _ls; + BandPassAudioFilter _rs; +}; diff --git a/src/lib/util.cc b/src/lib/util.cc index 290dd20ef..e0db5de2e 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -43,13 +43,13 @@ #endif #include <glib.h> #include <openjpeg.h> +#include <pangomm/init.h> #include <magick/MagickCore.h> #include <magick/version.h> -#include <libdcp/version.h> -#include <libdcp/util.h> -#include <libdcp/signer_chain.h> -#include <libdcp/signer.h> -#include <libdcp/raw_convert.h> +#include <dcp/version.h> +#include <dcp/util.h> +#include <dcp/signer.h> +#include <dcp/raw_convert.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> @@ -62,13 +62,15 @@ extern "C" { #include "scaler.h" #include "dcp_content_type.h" #include "filter.h" -#include "sound_processor.h" +#include "cinema_sound_processor.h" #include "config.h" #include "ratio.h" #include "job.h" #include "cross.h" #include "video_content.h" +#include "rect.h" #include "md5_digester.h" +#include "audio_processor.h" #include "safe_stringstream.h" #ifdef DCPOMATIC_WINDOWS #include "stack.hpp" @@ -99,8 +101,8 @@ using std::set_terminate; using boost::shared_ptr; using boost::thread; using boost::optional; -using libdcp::Size; -using libdcp::raw_convert; +using dcp::Size; +using dcp::raw_convert; static boost::thread::id ui_thread; static boost::filesystem::path backtrace_file; @@ -265,24 +267,6 @@ ffmpeg_version_to_string (int v) return s.str (); } -/** Return a user-readable string summarising the versions of our dependencies */ -string -dependency_version_summary () -{ - SafeStringStream s; - s << N_("libopenjpeg ") << opj_version () << N_(", ") - << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ") - << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ") - << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ") - << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ") - << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ") - << MagickVersion << N_(", ") - << N_("libssh ") << ssh_version (0) << N_(", ") - << N_("libdcp ") << libdcp::version << N_(" git ") << libdcp::git_commit; - - return s.str (); -} - double seconds (struct timeval t) { @@ -371,14 +355,16 @@ dcpomatic_setup () set_terminate (terminate); - libdcp::init (); + Pango::init (); + dcp::init (); Ratio::setup_ratios (); VideoContentScale::setup_scales (); DCPContentType::setup_dcp_content_types (); Scaler::setup_scalers (); Filter::setup_filters (); - SoundProcessor::setup_sound_processors (); + CinemaSoundProcessor::setup_cinema_sound_processors (); + AudioProcessor::setup_audio_processors (); ui_thread = boost::this_thread::get_id (); } @@ -401,7 +387,7 @@ mo_path () boost::filesystem::path mo_path () { - return "DCP-o-matic.app/Contents/Resources"; + return "DCP-o-matic 2.app/Contents/Resources"; } #endif @@ -486,7 +472,10 @@ md5_digest (vector<boost::filesystem::path> files, shared_ptr<Job> job) while (remaining > 0) { int const t = min (remaining, buffer_size); - fread (buffer, 1, t, f); + int const r = fread (buffer, 1, t, f); + if (r != t) { + throw ReadFileError (files[i], errno); + } digester.add (buffer, t); remaining -= t; @@ -656,6 +645,17 @@ stride_round_up (int c, int const * stride, int t) return a - (a % t); } +/** @param n A number. + * @param r Rounding `boundary' (must be a power of 2) + * @return n rounded to the nearest r + */ +int +round_to (float n, int r) +{ + assert (r == 1 || r == 2 || r == 4); + return int (n + float(r) / 2) &~ (r - 1); +} + /** Read a sequence of key / value pairs from a text stream; * the keys are the first words on the line, and the values are * the remainder of the line following the key. Lines beginning @@ -760,17 +760,6 @@ ensure_ui_thread () assert (boost::this_thread::get_id() == ui_thread); } -/** @param v Content video frame. - * @param audio_sample_rate Source audio sample rate. - * @param frames_per_second Number of video frames per second. - * @return Equivalent number of audio frames for `v'. - */ -int64_t -video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second) -{ - return ((int64_t) v * audio_sample_rate / frames_per_second); -} - string audio_channel_name (int c) { @@ -821,59 +810,6 @@ tidy_for_filename (string f) return t; } -shared_ptr<const libdcp::Signer> -make_signer () -{ - boost::filesystem::path const sd = Config::instance()->signer_chain_directory (); - - /* Remake the chain if any of it is missing */ - - list<boost::filesystem::path> files; - files.push_back ("ca.self-signed.pem"); - files.push_back ("intermediate.signed.pem"); - files.push_back ("leaf.signed.pem"); - files.push_back ("leaf.key"); - - list<boost::filesystem::path>::const_iterator i = files.begin(); - while (i != files.end()) { - boost::filesystem::path p (sd); - p /= *i; - if (!boost::filesystem::exists (p)) { - boost::filesystem::remove_all (sd); - boost::filesystem::create_directories (sd); - libdcp::make_signer_chain (sd, openssl_path ()); - break; - } - - ++i; - } - - libdcp::CertificateChain chain; - - { - boost::filesystem::path p (sd); - p /= "ca.self-signed.pem"; - chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p))); - } - - { - boost::filesystem::path p (sd); - p /= "intermediate.signed.pem"; - chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p))); - } - - { - boost::filesystem::path p (sd); - p /= "leaf.signed.pem"; - chain.add (shared_ptr<libdcp::Certificate> (new libdcp::Certificate (p))); - } - - boost::filesystem::path signer_key (sd); - signer_key /= "leaf.key"; - - return shared_ptr<const libdcp::Signer> (new libdcp::Signer (chain, signer_key)); -} - map<string, string> split_get_request (string url) { @@ -920,14 +856,14 @@ split_get_request (string url) return r; } -libdcp::Size -fit_ratio_within (float ratio, libdcp::Size full_frame) +dcp::Size +fit_ratio_within (float ratio, dcp::Size full_frame, int round) { if (ratio < full_frame.ratio ()) { - return libdcp::Size (rint (full_frame.height * ratio), full_frame.height); + return dcp::Size (round_to (full_frame.height * ratio, round), full_frame.height); } - return libdcp::Size (full_frame.width, rint (full_frame.width / ratio)); + return dcp::Size (full_frame.width, round_to (full_frame.width / ratio, round)); } void * @@ -958,12 +894,34 @@ divide_with_round (int64_t a, int64_t b) } } +/** Return a user-readable string summarising the versions of our dependencies */ +string +dependency_version_summary () +{ + SafeStringStream s; + s << N_("libopenjpeg ") << opj_version () << N_(", ") + << N_("libavcodec ") << ffmpeg_version_to_string (avcodec_version()) << N_(", ") + << N_("libavfilter ") << ffmpeg_version_to_string (avfilter_version()) << N_(", ") + << N_("libavformat ") << ffmpeg_version_to_string (avformat_version()) << N_(", ") + << N_("libavutil ") << ffmpeg_version_to_string (avutil_version()) << N_(", ") + << N_("libswscale ") << ffmpeg_version_to_string (swscale_version()) << N_(", ") + << MagickVersion << N_(", ") + << N_("libssh ") << ssh_version (0) << N_(", ") + << N_("libdcp ") << dcp::version << N_(" git ") << dcp::git_commit; + + return s.str (); +} + +/** Construct a ScopedTemporary. A temporary filename is decided but the file is not opened + * until ::open() is called. + */ ScopedTemporary::ScopedTemporary () : _open (0) { _file = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path (); } +/** Close and delete the temporary file */ ScopedTemporary::~ScopedTemporary () { close (); @@ -971,12 +929,16 @@ ScopedTemporary::~ScopedTemporary () boost::filesystem::remove (_file, ec); } +/** @return temporary filename */ char const * ScopedTemporary::c_str () const { return _file.string().c_str (); } +/** Open the temporary file. + * @return File's FILE pointer. + */ FILE* ScopedTemporary::open (char const * params) { @@ -984,6 +946,7 @@ ScopedTemporary::open (char const * params) return _open; } +/** Close the file */ void ScopedTemporary::close () { @@ -992,3 +955,16 @@ ScopedTemporary::close () _open = 0; } } + +ContentTimePeriod +subtitle_period (AVSubtitle const & sub) +{ + ContentTime const packet_time = ContentTime::from_seconds (static_cast<double> (sub.pts) / AV_TIME_BASE); + + ContentTimePeriod period ( + packet_time + ContentTime::from_seconds (sub.start_display_time / 1e3), + packet_time + ContentTime::from_seconds (sub.end_display_time / 1e3) + ); + + return period; +} diff --git a/src/lib/util.h b/src/lib/util.h index 675c8d03e..724e8937c 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -31,7 +31,7 @@ #include <boost/asio.hpp> #include <boost/optional.hpp> #include <boost/filesystem.hpp> -#include <libdcp/util.h> +#include <dcp/util.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavfilter/avfilter.h> @@ -47,11 +47,8 @@ extern "C" { #define DCPOMATIC_HELLO "Boys, you gotta learn not to talk to nuns that way" #define HISTORY_SIZE 10 -namespace libdcp { - class Signer; -} - class Job; +struct AVSubtitle; extern std::string seconds_to_hms (int); extern std::string seconds_to_approximate_hms (int); @@ -69,12 +66,12 @@ extern bool valid_image_file (boost::filesystem::path); extern boost::filesystem::path mo_path (); #endif extern std::string tidy_for_filename (std::string); -extern boost::shared_ptr<const libdcp::Signer> make_signer (); -extern libdcp::Size fit_ratio_within (float ratio, libdcp::Size); +extern dcp::Size fit_ratio_within (float ratio, dcp::Size, int); extern std::string entities_to_text (std::string e); extern std::map<std::string, std::string> split_get_request (std::string url); extern int dcp_audio_frame_rate (int); extern int stride_round_up (int, int const *, int); +extern int round_to (float n, int r); extern std::multimap<std::string, std::string> read_key_value (std::istream& s); extern int get_required_int (std::multimap<std::string, std::string> const & kv, std::string k); extern float get_required_float (std::multimap<std::string, std::string> const & kv, std::string k); @@ -83,6 +80,7 @@ extern int get_optional_int (std::multimap<std::string, std::string> const & kv, extern std::string get_optional_string (std::multimap<std::string, std::string> const & kv, std::string k); extern void* wrapped_av_malloc (size_t); extern int64_t divide_with_round (int64_t a, int64_t b); +extern ContentTimePeriod subtitle_period (AVSubtitle const &); /** @class Socket * @brief A class to wrap a boost::asio::ip::tcp::socket with some things @@ -125,16 +123,20 @@ private: extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second); +/** @class ScopedTemporary + * @brief A temporary file which is deleted when the ScopedTemporary object goes out of scope. + */ class ScopedTemporary { public: ScopedTemporary (); ~ScopedTemporary (); + /** @return temporary filename */ boost::filesystem::path file () const { return _file; } - + char const * c_str () const; FILE* open (char const *); void close (); diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc index 13f2cf516..ba656e4c2 100644 --- a/src/lib/video_content.cc +++ b/src/lib/video_content.cc @@ -19,8 +19,8 @@ #include <iomanip> #include <libcxml/cxml.h> -#include <libdcp/colour_matrix.h> -#include <libdcp/raw_convert.h> +#include <dcp/colour_matrix.h> +#include <dcp/raw_convert.h> #include "video_content.h" #include "video_examiner.h" #include "compose.hpp" @@ -31,16 +31,21 @@ #include "film.h" #include "exceptions.h" #include "frame_rate_change.h" +#include "log.h" #include "safe_stringstream.h" #include "i18n.h" +#define LOG_GENERAL(...) film->log()->log (String::compose (__VA_ARGS__), Log::TYPE_GENERAL); + int const VideoContentProperty::VIDEO_SIZE = 0; int const VideoContentProperty::VIDEO_FRAME_RATE = 1; int const VideoContentProperty::VIDEO_FRAME_TYPE = 2; int const VideoContentProperty::VIDEO_CROP = 3; int const VideoContentProperty::VIDEO_SCALE = 4; int const VideoContentProperty::COLOUR_CONVERSION = 5; +int const VideoContentProperty::VIDEO_FADE_IN = 6; +int const VideoContentProperty::VIDEO_FADE_OUT = 7; using std::string; using std::setprecision; @@ -51,12 +56,11 @@ using std::max; using boost::shared_ptr; using boost::optional; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; VideoContent::VideoContent (shared_ptr<const Film> f) : Content (f) , _video_length (0) - , _original_video_frame_rate (0) , _video_frame_rate (0) , _video_frame_type (VIDEO_FRAME_TYPE_2D) , _scale (Config::instance()->default_scale ()) @@ -64,10 +68,9 @@ VideoContent::VideoContent (shared_ptr<const Film> f) setup_default_colour_conversion (); } -VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Frame len) +VideoContent::VideoContent (shared_ptr<const Film> f, DCPTime s, ContentTime len) : Content (f, s) , _video_length (len) - , _original_video_frame_rate (0) , _video_frame_rate (0) , _video_frame_type (VIDEO_FRAME_TYPE_2D) , _scale (Config::instance()->default_scale ()) @@ -78,7 +81,6 @@ VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Fram VideoContent::VideoContent (shared_ptr<const Film> f, boost::filesystem::path p) : Content (f, p) , _video_length (0) - , _original_video_frame_rate (0) , _video_frame_rate (0) , _video_frame_type (VIDEO_FRAME_TYPE_2D) , _scale (Config::instance()->default_scale ()) @@ -86,14 +88,20 @@ VideoContent::VideoContent (shared_ptr<const Film> f, boost::filesystem::path p) setup_default_colour_conversion (); } -VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node, int version) +VideoContent::VideoContent (shared_ptr<const Film> f, cxml::ConstNodePtr node, int version) : Content (f, node) { - _video_length = node->number_child<VideoContent::Frame> ("VideoLength"); _video_size.width = node->number_child<int> ("VideoWidth"); _video_size.height = node->number_child<int> ("VideoHeight"); _video_frame_rate = node->number_child<float> ("VideoFrameRate"); - _original_video_frame_rate = node->optional_number_child<float> ("OriginalVideoFrameRate").get_value_or (_video_frame_rate); + + if (version < 32) { + /* DCP-o-matic 1.0 branch */ + _video_length = ContentTime::from_frames (node->number_child<int64_t> ("VideoLength"), _video_frame_rate); + } else { + _video_length = ContentTime (node->number_child<ContentTime::Type> ("VideoLength")); + } + _video_frame_type = static_cast<VideoFrameType> (node->number_child<int> ("VideoFrameType")); _crop.left = node->number_child<int> ("LeftCrop"); _crop.right = node->number_child<int> ("RightCrop"); @@ -110,6 +118,10 @@ VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Nod } _colour_conversion = ColourConversion (node->node_child ("ColourConversion")); + if (version >= 32) { + _fade_in = ContentTime (node->number_child<int64_t> ("FadeIn")); + _fade_out = ContentTime (node->number_child<int64_t> ("FadeOut")); + } } VideoContent::VideoContent (shared_ptr<const Film> f, vector<shared_ptr<Content> > c) @@ -146,55 +158,67 @@ VideoContent::VideoContent (shared_ptr<const Film> f, vector<shared_ptr<Content> throw JoinError (_("Content to be joined must have the same colour conversion.")); } + if (vc->fade_in() != ref->fade_in() || vc->fade_out() != ref->fade_out()) { + throw JoinError (_("Content to be joined must have the same fades.")); + } + _video_length += vc->video_length (); } _video_size = ref->video_size (); - _original_video_frame_rate = ref->original_video_frame_rate (); _video_frame_rate = ref->video_frame_rate (); _video_frame_type = ref->video_frame_type (); _crop = ref->crop (); _scale = ref->scale (); _colour_conversion = ref->colour_conversion (); + _fade_in = ref->fade_in (); + _fade_out = ref->fade_out (); } void VideoContent::as_xml (xmlpp::Node* node) const { boost::mutex::scoped_lock lm (_mutex); - node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length)); + node->add_child("VideoLength")->add_child_text (raw_convert<string> (_video_length.get ())); node->add_child("VideoWidth")->add_child_text (raw_convert<string> (_video_size.width)); node->add_child("VideoHeight")->add_child_text (raw_convert<string> (_video_size.height)); node->add_child("VideoFrameRate")->add_child_text (raw_convert<string> (_video_frame_rate)); - node->add_child("OriginalVideoFrameRate")->add_child_text (raw_convert<string> (_original_video_frame_rate)); node->add_child("VideoFrameType")->add_child_text (raw_convert<string> (static_cast<int> (_video_frame_type))); _crop.as_xml (node); _scale.as_xml (node->add_child("Scale")); _colour_conversion.as_xml (node->add_child("ColourConversion")); + node->add_child("FadeIn")->add_child_text (raw_convert<string> (_fade_in.get ())); + node->add_child("FadeOut")->add_child_text (raw_convert<string> (_fade_out.get ())); } void VideoContent::setup_default_colour_conversion () { - _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, libdcp::colour_matrix::srgb_to_xyz, 2.6).conversion; + _colour_conversion = PresetColourConversion (_("sRGB"), 2.4, true, dcp::colour_matrix::srgb_to_xyz, 2.6).conversion; } void VideoContent::take_from_video_examiner (shared_ptr<VideoExaminer> d) { /* These examiner calls could call other content methods which take a lock on the mutex */ - libdcp::Size const vs = d->video_size (); + dcp::Size const vs = d->video_size (); float const vfr = d->video_frame_rate (); - + ContentTime vl = d->video_length (); + { boost::mutex::scoped_lock lm (_mutex); _video_size = vs; _video_frame_rate = vfr; - _original_video_frame_rate = vfr; + _video_length = vl; } + + shared_ptr<const Film> film = _film.lock (); + assert (film); + LOG_GENERAL ("Video length obtained from header as %1 frames", _video_length.frames (_video_frame_rate)); signal_changed (VideoContentProperty::VIDEO_SIZE); signal_changed (VideoContentProperty::VIDEO_FRAME_RATE); + signal_changed (ContentProperty::LENGTH); } @@ -325,14 +349,17 @@ VideoContent::technical_summary () const { return String::compose ( "video: length %1, size %2x%3, rate %4", - video_length_after_3d_combine(), video_size().width, video_size().height, video_frame_rate() + video_length_after_3d_combine().seconds(), + video_size().width, + video_size().height, + video_frame_rate() ); } -libdcp::Size +dcp::Size VideoContent::video_size_after_3d_split () const { - libdcp::Size const s = video_size (); + dcp::Size const s = video_size (); switch (video_frame_type ()) { case VIDEO_FRAME_TYPE_2D: case VIDEO_FRAME_TYPE_3D_ALTERNATE: @@ -340,9 +367,9 @@ VideoContent::video_size_after_3d_split () const case VIDEO_FRAME_TYPE_3D_RIGHT: return s; case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT: - return libdcp::Size (s.width / 2, s.height); + return dcp::Size (s.width / 2, s.height); case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM: - return libdcp::Size (s.width, s.height / 2); + return dcp::Size (s.width, s.height / 2); } assert (false); @@ -359,29 +386,44 @@ VideoContent::set_colour_conversion (ColourConversion c) signal_changed (VideoContentProperty::COLOUR_CONVERSION); } +void +VideoContent::set_fade_in (ContentTime t) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _fade_in = t; + } + + signal_changed (VideoContentProperty::VIDEO_FADE_IN); +} + +void +VideoContent::set_fade_out (ContentTime t) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _fade_out = t; + } + + signal_changed (VideoContentProperty::VIDEO_FADE_OUT); +} + /** @return Video size after 3D split and crop */ -libdcp::Size +dcp::Size VideoContent::video_size_after_crop () const { return crop().apply (video_size_after_3d_split ()); } /** @param t A time offset from the start of this piece of content. - * @return Corresponding frame index. + * @return Corresponding time with respect to the content. */ -VideoContent::Frame -VideoContent::time_to_content_video_frames (Time t) const +ContentTime +VideoContent::dcp_time_to_content_time (DCPTime t) const { shared_ptr<const Film> film = _film.lock (); assert (film); - - FrameRateChange frc (video_frame_rate(), film->video_frame_rate()); - - /* Here we are converting from time (in the DCP) to a frame number in the content. - Hence we need to use the DCP's frame rate and the double/skip correction, not - the source's rate. - */ - return t * film->video_frame_rate() / (frc.factor() * TIME_HZ); + return ContentTime (t, FrameRateChange (video_frame_rate(), film->video_frame_rate())); } void @@ -425,3 +467,19 @@ VideoContent::set_video_frame_rate (float r) signal_changed (VideoContentProperty::VIDEO_FRAME_RATE); } +optional<float> +VideoContent::fade (VideoFrame f) const +{ + assert (f >= 0); + + if (f < fade_in().frames (video_frame_rate ())) { + return float (f) / _fade_in.frames (video_frame_rate ()); + } + + VideoFrame fade_out_start = ContentTime (video_length() - fade_out()).frames (video_frame_rate ()); + if (f >= fade_out_start) { + return 1 - float (f - fade_out_start) / fade_out().frames (video_frame_rate ()); + } + + return optional<float> (); +} diff --git a/src/lib/video_content.h b/src/lib/video_content.h index 3a7b44306..e88fb0227 100644 --- a/src/lib/video_content.h +++ b/src/lib/video_content.h @@ -36,6 +36,8 @@ public: static int const VIDEO_CROP; static int const VIDEO_SCALE; static int const COLOUR_CONVERSION; + static int const VIDEO_FADE_IN; + static int const VIDEO_FADE_OUT; }; class VideoContent : public virtual Content @@ -44,9 +46,9 @@ public: typedef int Frame; VideoContent (boost::shared_ptr<const Film>); - VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame); + VideoContent (boost::shared_ptr<const Film>, DCPTime, ContentTime); VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path); - VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int); + VideoContent (boost::shared_ptr<const Film>, cxml::ConstNodePtr, int); VideoContent (boost::shared_ptr<const Film>, std::vector<boost::shared_ptr<Content> >); void as_xml (xmlpp::Node *) const; @@ -54,21 +56,21 @@ public: virtual std::string information () const; virtual std::string identifier () const; - VideoContent::Frame video_length () const { + ContentTime video_length () const { boost::mutex::scoped_lock lm (_mutex); return _video_length; } - VideoContent::Frame video_length_after_3d_combine () const { + ContentTime video_length_after_3d_combine () const { boost::mutex::scoped_lock lm (_mutex); if (_video_frame_type == VIDEO_FRAME_TYPE_3D_ALTERNATE) { - return _video_length / 2; + return ContentTime (_video_length.get() / 2); } return _video_length; } - libdcp::Size video_size () const { + dcp::Size video_size () const { boost::mutex::scoped_lock lm (_mutex); return _video_size; } @@ -78,11 +80,6 @@ public: return _video_frame_rate; } - float original_video_frame_rate () const { - boost::mutex::scoped_lock lm (_mutex); - return _original_video_frame_rate; - } - void set_video_frame_type (VideoFrameType); void set_video_frame_rate (float); @@ -93,6 +90,9 @@ public: void set_scale (VideoContentScale); void set_colour_conversion (ColourConversion); + + void set_fade_in (ContentTime); + void set_fade_out (ContentTime); VideoFrameType video_frame_type () const { boost::mutex::scoped_lock lm (_mutex); @@ -135,10 +135,22 @@ public: return _colour_conversion; } - libdcp::Size video_size_after_3d_split () const; - libdcp::Size video_size_after_crop () const; + ContentTime fade_in () const { + boost::mutex::scoped_lock lm (_mutex); + return _fade_in; + } + + ContentTime fade_out () const { + boost::mutex::scoped_lock lm (_mutex); + return _fade_out; + } + + dcp::Size video_size_after_3d_split () const; + dcp::Size video_size_after_crop () const; + + ContentTime dcp_time_to_content_time (DCPTime) const; - VideoContent::Frame time_to_content_video_frames (Time) const; + boost::optional<float> fade (VideoFrame) const; void scale_and_crop_to_fit_width (); void scale_and_crop_to_fit_height (); @@ -146,23 +158,24 @@ public: protected: void take_from_video_examiner (boost::shared_ptr<VideoExaminer>); - VideoContent::Frame _video_length; - float _original_video_frame_rate; + ContentTime _video_length; float _video_frame_rate; private: - friend class ffmpeg_pts_offset_test; - friend class best_dcp_frame_rate_test_single; - friend class best_dcp_frame_rate_test_double; - friend class audio_sampling_rate_test; + friend struct ffmpeg_pts_offset_test; + friend struct best_dcp_frame_rate_test_single; + friend struct best_dcp_frame_rate_test_double; + friend struct audio_sampling_rate_test; void setup_default_colour_conversion (); - libdcp::Size _video_size; + dcp::Size _video_size; VideoFrameType _video_frame_type; Crop _crop; VideoContentScale _scale; ColourConversion _colour_conversion; + ContentTime _fade_in; + ContentTime _fade_out; }; #endif diff --git a/src/lib/video_content_scale.cc b/src/lib/video_content_scale.cc index e603582b8..418c46eec 100644 --- a/src/lib/video_content_scale.cc +++ b/src/lib/video_content_scale.cc @@ -123,24 +123,24 @@ VideoContentScale::from_id (string id) /** @param display_container Size of the container that we are displaying this content in. * @param film_container The size of the film's image. */ -libdcp::Size -VideoContentScale::size (shared_ptr<const VideoContent> c, libdcp::Size display_container, libdcp::Size film_container) const +dcp::Size +VideoContentScale::size (shared_ptr<const VideoContent> c, dcp::Size display_container, dcp::Size film_container, int round) const { if (_ratio) { - return fit_ratio_within (_ratio->ratio (), display_container); + return fit_ratio_within (_ratio->ratio (), display_container, round); } - libdcp::Size const ac = c->video_size_after_crop (); + dcp::Size const ac = c->video_size_after_crop (); /* Force scale if the film_container is smaller than the content's image */ if (_scale || film_container.width < ac.width || film_container.height < ac.height) { - return fit_ratio_within (ac.ratio (), display_container); + return fit_ratio_within (ac.ratio (), display_container, round); } /* Scale the image so that it will be in the right place in film_container, even if display_container is a different size. */ - return libdcp::Size ( + return dcp::Size ( c->video_size().width * float(display_container.width) / film_container.width, c->video_size().height * float(display_container.height) / film_container.height ); diff --git a/src/lib/video_content_scale.h b/src/lib/video_content_scale.h index 87dd2f1fa..6b718d574 100644 --- a/src/lib/video_content_scale.h +++ b/src/lib/video_content_scale.h @@ -22,7 +22,7 @@ #include <vector> #include <boost/shared_ptr.hpp> -#include <libdcp/util.h> +#include <dcp/util.h> namespace cxml { class Node; @@ -43,7 +43,7 @@ public: VideoContentScale (bool); VideoContentScale (boost::shared_ptr<cxml::Node>); - libdcp::Size size (boost::shared_ptr<const VideoContent>, libdcp::Size, libdcp::Size) const; + dcp::Size size (boost::shared_ptr<const VideoContent>, dcp::Size, dcp::Size, int round) const; std::string id () const; std::string name () const; void as_xml (xmlpp::Node *) const; diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc index 5867ac925..64c66ea55 100644 --- a/src/lib/video_decoder.cc +++ b/src/lib/video_decoder.cc @@ -19,46 +19,164 @@ #include "video_decoder.h" #include "image.h" +#include "image_proxy.h" +#include "content_video.h" #include "i18n.h" using std::cout; +using std::list; +using std::max; using boost::shared_ptr; +using boost::optional; -VideoDecoder::VideoDecoder (shared_ptr<const Film> f, shared_ptr<const VideoContent> c) - : Decoder (f) +VideoDecoder::VideoDecoder (shared_ptr<const VideoContent> c) +#ifdef DCPOMATIC_DEBUG + : test_gaps (0) , _video_content (c) - , _video_position (0) +#else + : _video_content (c) +#endif + , _same (false) { } +list<ContentVideo> +VideoDecoder::decoded_video (VideoFrame frame) +{ + list<ContentVideo> output; + + for (list<ContentVideo>::const_iterator i = _decoded_video.begin(); i != _decoded_video.end(); ++i) { + if (i->frame == frame) { + output.push_back (*i); + } + } + + return output; +} + +/** Get all frames which exist in the content at a given frame index. + * @param frame Frame index. + * @param accurate true to try hard to return frames at the precise time that was requested, otherwise frames nearby may be returned. + * @return Frames; there may be none (if there is no video there), 1 for 2D or 2 for 3D. + */ +list<ContentVideo> +VideoDecoder::get_video (VideoFrame frame, bool accurate) +{ + /* At this stage, if we have get_video()ed before, _decoded_video will contain the last frame that this + method returned (and possibly a few more). If the requested frame is not in _decoded_video and it is not the next + one after the end of _decoded_video we need to seek. + */ + + if (_decoded_video.empty() || frame < _decoded_video.front().frame || frame > (_decoded_video.back().frame + 1)) { + seek (ContentTime::from_frames (frame, _video_content->video_frame_rate()), accurate); + } + + list<ContentVideo> dec; + + /* Now enough pass() calls should either: + * (a) give us what we want, or + * (b) give us something after what we want, indicating that we will never get what we want, or + * (c) hit the end of the decoder. + */ + if (accurate) { + /* We are being accurate, so we want the right frame. + * This could all be one statement but it's split up for clarity. + */ + while (true) { + if (!decoded_video(frame).empty ()) { + /* We got what we want */ + break; + } + + if (pass ()) { + /* The decoder has nothing more for us */ + break; + } + + if (!_decoded_video.empty() && _decoded_video.front().frame > frame) { + /* We're never going to get the frame we want. Perhaps the caller is asking + * for a video frame before the content's video starts (if its audio + * begins before its video, for example). + */ + break; + } + } + + dec = decoded_video (frame); + } else { + /* Any frame will do: use the first one that comes out of pass() */ + while (_decoded_video.empty() && !pass ()) {} + if (!_decoded_video.empty ()) { + dec.push_back (_decoded_video.front ()); + } + } + + /* Clean up _decoded_video; keep the frame we are returning, but nothing before that */ + while (!_decoded_video.empty() && _decoded_video.front().frame < dec.front().frame) { + _decoded_video.pop_front (); + } + + return dec; +} + + +/** Called by subclasses when they have a video frame ready */ void -VideoDecoder::video (shared_ptr<const ImageProxy> image, bool same, VideoContent::Frame frame) +VideoDecoder::video (shared_ptr<const ImageProxy> image, VideoFrame frame) { + /* We may receive the same frame index twice for 3D, and we need to know + when that happens. + */ + _same = (!_decoded_video.empty() && frame == _decoded_video.back().frame); + + /* Fill in gaps */ + /* XXX: 3D */ + + while (!_decoded_video.empty () && (_decoded_video.back().frame + 1) < frame) { +#ifdef DCPOMATIC_DEBUG + test_gaps++; +#endif + _decoded_video.push_back ( + ContentVideo ( + _decoded_video.back().image, + _decoded_video.back().eyes, + _decoded_video.back().part, + _decoded_video.back().frame + 1 + ) + ); + } + switch (_video_content->video_frame_type ()) { case VIDEO_FRAME_TYPE_2D: - Video (image, EYES_BOTH, PART_WHOLE, same, frame); + _decoded_video.push_back (ContentVideo (image, EYES_BOTH, PART_WHOLE, frame)); break; case VIDEO_FRAME_TYPE_3D_ALTERNATE: - Video (image, (frame % 2) ? EYES_RIGHT : EYES_LEFT, PART_WHOLE, same, frame / 2); + _decoded_video.push_back (ContentVideo (image, _same ? EYES_RIGHT : EYES_LEFT, PART_WHOLE, frame)); break; case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT: - Video (image, EYES_LEFT, PART_LEFT_HALF, same, frame); - Video (image, EYES_RIGHT, PART_RIGHT_HALF, same, frame); + _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_LEFT_HALF, frame)); + _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_RIGHT_HALF, frame)); break; case VIDEO_FRAME_TYPE_3D_TOP_BOTTOM: - Video (image, EYES_LEFT, PART_TOP_HALF, same, frame); - Video (image, EYES_RIGHT, PART_BOTTOM_HALF, same, frame); + _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_TOP_HALF, frame)); + _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_BOTTOM_HALF, frame)); break; case VIDEO_FRAME_TYPE_3D_LEFT: - Video (image, EYES_LEFT, PART_WHOLE, same, frame); + _decoded_video.push_back (ContentVideo (image, EYES_LEFT, PART_WHOLE, frame)); break; case VIDEO_FRAME_TYPE_3D_RIGHT: - Video (image, EYES_RIGHT, PART_WHOLE, same, frame); + _decoded_video.push_back (ContentVideo (image, EYES_RIGHT, PART_WHOLE, frame)); break; + default: + assert (false); } - - _video_position = frame + 1; +} + +void +VideoDecoder::seek (ContentTime, bool) +{ + _decoded_video.clear (); } diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h index 42add42aa..f5c3cd743 100644 --- a/src/lib/video_decoder.h +++ b/src/lib/video_decoder.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/lib/video_decoder.h + * @brief VideoDecoder class. + */ + #ifndef DCPOMATIC_VIDEO_DECODER_H #define DCPOMATIC_VIDEO_DECODER_H @@ -25,37 +29,38 @@ #include "decoder.h" #include "video_content.h" #include "util.h" +#include "content_video.h" class VideoContent; class ImageProxy; +/** @class VideoDecoder + * @brief Parent for classes which decode video. + */ class VideoDecoder : public virtual Decoder { public: - VideoDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const VideoContent>); - - /** Seek so that the next pass() will yield (approximately) the requested frame. - * Pass accurate = true to try harder to get close to the request. - */ - virtual void seek (VideoContent::Frame frame, bool accurate) = 0; - - /** Emitted when a video frame is ready. - * First parameter is the video image. - * Second parameter is the eye(s) which should see this image. - * Third parameter is the part of this image that should be used. - * Fourth parameter is true if the image is the same as the last one that was emitted for this Eyes value. - * Fourth parameter is the frame within our source. - */ - boost::signals2::signal<void (boost::shared_ptr<const ImageProxy>, Eyes, Part, bool, VideoContent::Frame)> Video; - + VideoDecoder (boost::shared_ptr<const VideoContent> c); + + std::list<ContentVideo> get_video (VideoFrame frame, bool accurate); + + boost::shared_ptr<const VideoContent> video_content () const { + return _video_content; + } + +#ifdef DCPOMATIC_DEBUG + int test_gaps; +#endif + protected: - void video (boost::shared_ptr<const ImageProxy>, bool, VideoContent::Frame); + void seek (ContentTime time, bool accurate); + void video (boost::shared_ptr<const ImageProxy>, VideoFrame frame); + std::list<ContentVideo> decoded_video (VideoFrame frame); + boost::shared_ptr<const VideoContent> _video_content; - /** This is in frames without taking 3D into account (e.g. if we are doing 3D alternate, - * this would equal 2 on the left-eye second frame (not 1)). - */ - VideoContent::Frame _video_position; + std::list<ContentVideo> _decoded_video; + bool _same; }; #endif diff --git a/src/lib/video_examiner.h b/src/lib/video_examiner.h index 039c494b5..87e9a0428 100644 --- a/src/lib/video_examiner.h +++ b/src/lib/video_examiner.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,15 +17,22 @@ */ -#include <libdcp/types.h> +/** @file src/lib/video_examiner.h + * @brief VideoExaminer class. + */ + +#include <dcp/types.h> #include "types.h" #include "video_content.h" +/** @class VideoExaminer + * @brief Parent for classes which examine video sources and obtain information about them. + */ class VideoExaminer { public: virtual ~VideoExaminer () {} virtual float video_frame_rate () const = 0; - virtual libdcp::Size video_size () const = 0; - virtual VideoContent::Frame video_length () const = 0; + virtual dcp::Size video_size () const = 0; + virtual ContentTime video_length () const = 0; }; diff --git a/src/lib/writer.cc b/src/lib/writer.cc index 5af1aea1e..6262525c8 100644 --- a/src/lib/writer.cc +++ b/src/lib/writer.cc @@ -19,25 +19,32 @@ #include <fstream> #include <cerrno> -#include <libdcp/mono_picture_asset.h> -#include <libdcp/stereo_picture_asset.h> -#include <libdcp/sound_asset.h> -#include <libdcp/reel.h> -#include <libdcp/dcp.h> -#include <libdcp/cpl.h> +#include <dcp/mono_picture_mxf.h> +#include <dcp/stereo_picture_mxf.h> +#include <dcp/sound_mxf.h> +#include <dcp/sound_mxf_writer.h> +#include <dcp/reel.h> +#include <dcp/reel_mono_picture_asset.h> +#include <dcp/reel_stereo_picture_asset.h> +#include <dcp/reel_sound_asset.h> +#include <dcp/reel_subtitle_asset.h> +#include <dcp/dcp.h> +#include <dcp/cpl.h> +#include <dcp/signer.h> #include "writer.h" #include "compose.hpp" #include "film.h" #include "ratio.h" #include "log.h" -#include "dcp_video_frame.h" +#include "dcp_video.h" #include "dcp_content_type.h" -#include "player.h" #include "audio_mapping.h" #include "config.h" #include "job.h" #include "cross.h" +#include "audio_buffers.h" #include "md5_digester.h" +#include "encoded_data.h" #include "version.h" #include "i18n.h" @@ -57,6 +64,7 @@ using std::list; using std::cout; using boost::shared_ptr; using boost::weak_ptr; +using boost::dynamic_pointer_cast; int const Writer::_maximum_frames_in_memory = Config::instance()->num_local_encoding_threads() + 4; @@ -71,7 +79,6 @@ Writer::Writer (shared_ptr<const Film> f, weak_ptr<Job> j) , _last_written_eyes (EYES_RIGHT) , _full_written (0) , _fake_written (0) - , _repeat_written (0) , _pushed_to_disk (0) { /* Remove any old DCP */ @@ -89,36 +96,39 @@ Writer::Writer (shared_ptr<const Film> f, weak_ptr<Job> j) */ if (_film->three_d ()) { - _picture_asset.reset (new libdcp::StereoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ())); + _picture_mxf.reset (new dcp::StereoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1))); } else { - _picture_asset.reset (new libdcp::MonoPictureAsset (_film->internal_video_mxf_dir (), _film->internal_video_mxf_filename ())); + _picture_mxf.reset (new dcp::MonoPictureMXF (dcp::Fraction (_film->video_frame_rate (), 1))); } - _picture_asset->set_edit_rate (_film->video_frame_rate ()); - _picture_asset->set_size (_film->frame_size ()); - _picture_asset->set_interop (_film->interop ()); + _picture_mxf->set_size (_film->frame_size ()); if (_film->encrypted ()) { - _picture_asset->set_key (_film->key ()); + _picture_mxf->set_key (_film->key ()); } - _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0); + _picture_mxf_writer = _picture_mxf->start_write ( + _film->internal_video_mxf_dir() / _film->internal_video_mxf_filename(), + _film->interop() ? dcp::INTEROP : dcp::SMPTE, + _first_nonexistant_frame > 0 + ); if (_film->audio_channels ()) { - _sound_asset.reset (new libdcp::SoundAsset (_film->directory (), _film->audio_mxf_filename ())); - _sound_asset->set_edit_rate (_film->video_frame_rate ()); - _sound_asset->set_channels (_film->audio_channels ()); - _sound_asset->set_sampling_rate (_film->audio_frame_rate ()); - _sound_asset->set_interop (_film->interop ()); + _sound_mxf.reset (new dcp::SoundMXF (dcp::Fraction (_film->video_frame_rate(), 1), _film->audio_frame_rate (), _film->audio_channels ())); if (_film->encrypted ()) { - _sound_asset->set_key (_film->key ()); + _sound_mxf->set_key (_film->key ()); } - - /* Write the sound asset into the film directory so that we leave the creation + + /* Write the sound MXF into the film directory so that we leave the creation of the DCP directory until the last minute. */ - _sound_asset_writer = _sound_asset->start_write (); + _sound_mxf_writer = _sound_mxf->start_write (_film->directory() / _film->audio_mxf_filename(), _film->interop() ? dcp::INTEROP : dcp::SMPTE); + } + + /* Check that the signer is OK if we need one */ + if (_film->is_signed() && !Config::instance()->signer()->valid ()) { + throw InvalidSignerError (); } _thread = new boost::thread (boost::bind (&Writer::thread, this)); @@ -175,7 +185,7 @@ Writer::fake_write (int frame, Eyes eyes) } FILE* ifi = fopen_boost (_film->info_path (frame, eyes), "r"); - libdcp::FrameInfo info (ifi); + dcp::FrameInfo info (ifi); fclose (ifi); QueueItem qi; @@ -200,8 +210,8 @@ Writer::fake_write (int frame, Eyes eyes) void Writer::write (shared_ptr<const AudioBuffers> audio) { - if (_sound_asset) { - _sound_asset_writer->write (audio->data(), audio->frames()); + if (_sound_mxf_writer) { + _sound_mxf_writer->write (audio->data(), audio->frames()); } } @@ -277,7 +287,7 @@ try qi.encoded.reset (new EncodedData (_film->j2c_path (qi.frame, qi.eyes, false))); } - libdcp::FrameInfo fin = _picture_asset_writer->write (qi.encoded->data(), qi.encoded->size()); + dcp::FrameInfo fin = _picture_mxf_writer->write (qi.encoded->data(), qi.encoded->size()); qi.encoded->write_info (_film, qi.frame, qi.eyes, fin); _last_written[qi.eyes] = qi.encoded; ++_full_written; @@ -285,39 +295,27 @@ try } case QueueItem::FAKE: LOG_GENERAL (N_("Writer FAKE-writes %1 to MXF"), qi.frame); - _picture_asset_writer->fake_write (qi.size); + _picture_mxf_writer->fake_write (qi.size); _last_written[qi.eyes].reset (); ++_fake_written; break; - case QueueItem::REPEAT: - { - LOG_GENERAL (N_("Writer REPEAT-writes %1 to MXF"), qi.frame); - libdcp::FrameInfo fin = _picture_asset_writer->write ( - _last_written[qi.eyes]->data(), - _last_written[qi.eyes]->size() - ); - - _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin); - ++_repeat_written; - break; - } } lock.lock (); _last_written_frame = qi.frame; _last_written_eyes = qi.eyes; - if (_film->length()) { - shared_ptr<Job> job = _job.lock (); - assert (job); - int total = _film->time_to_video_frames (_film->length ()); - if (_film->three_d ()) { - /* _full_written and so on are incremented for each eye, so we need to double the total - frames to get the correct progress. - */ - total *= 2; - } - job->set_progress (float (_full_written + _fake_written + _repeat_written) / total); + shared_ptr<Job> job = _job.lock (); + assert (job); + int64_t total = _film->length().frames (_film->video_frame_rate ()); + if (_film->three_d ()) { + /* _full_written and so on are incremented for each eye, so we need to double the total + frames to get the correct progress. + */ + total *= 2; + } + if (total) { + job->set_progress (float (_full_written + _fake_written) / total); } } @@ -392,15 +390,11 @@ Writer::finish () terminate_thread (true); - _picture_asset_writer->finalize (); - if (_sound_asset_writer) { - _sound_asset_writer->finalize (); + _picture_mxf_writer->finalize (); + if (_sound_mxf_writer) { + _sound_mxf_writer->finalize (); } - int const frames = _last_written_frame + 1; - - _picture_asset->set_duration (frames); - /* Hard-link the video MXF into the DCP */ boost::filesystem::path video_from; video_from /= _film->internal_video_mxf_dir(); @@ -421,14 +415,11 @@ Writer::finish () } } - /* And update the asset */ - - _picture_asset->set_directory (_film->dir (_film->dcp_name ())); - _picture_asset->set_file_name (_film->video_mxf_filename ()); + _picture_mxf->set_file (video_to); /* Move the audio MXF into the DCP */ - if (_sound_asset) { + if (_sound_mxf) { boost::filesystem::path audio_to; audio_to /= _film->dir (_film->dcp_name ()); audio_to /= _film->audio_mxf_filename (); @@ -439,80 +430,86 @@ Writer::finish () String::compose (_("could not move audio MXF into the DCP (%1)"), ec.value ()), _film->file (_film->audio_mxf_filename ()) ); } - - _sound_asset->set_directory (_film->dir (_film->dcp_name ())); - _sound_asset->set_duration (frames); + + _sound_mxf->set_file (audio_to); } - - libdcp::DCP dcp (_film->dir (_film->dcp_name())); - shared_ptr<libdcp::CPL> cpl ( - new libdcp::CPL ( - _film->dir (_film->dcp_name()), + dcp::DCP dcp (_film->dir (_film->dcp_name())); + + shared_ptr<dcp::CPL> cpl ( + new dcp::CPL ( _film->dcp_name(), - _film->dcp_content_type()->libdcp_kind (), - frames, - _film->video_frame_rate () + _film->dcp_content_type()->libdcp_kind () ) ); - dcp.add_cpl (cpl); + dcp.add (cpl); + + shared_ptr<dcp::Reel> reel (new dcp::Reel ()); + + shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (_picture_mxf); + if (mono) { + reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelMonoPictureAsset (mono, 0))); + dcp.add (mono); + } + + shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (_picture_mxf); + if (stereo) { + reel->add (shared_ptr<dcp::ReelPictureAsset> (new dcp::ReelStereoPictureAsset (stereo, 0))); + dcp.add (stereo); + } + + if (_sound_mxf) { + reel->add (shared_ptr<dcp::ReelSoundAsset> (new dcp::ReelSoundAsset (_sound_mxf, 0))); + dcp.add (_sound_mxf); + } - cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel ( - _picture_asset, - _sound_asset, - shared_ptr<libdcp::SubtitleAsset> () - ) - )); + if (_subtitle_content) { + _subtitle_content->write_xml (_film->dir (_film->dcp_name ()) / _film->subtitle_xml_filename ()); + reel->add (shared_ptr<dcp::ReelSubtitleAsset> ( + new dcp::ReelSubtitleAsset ( + _subtitle_content, + dcp::Fraction (_film->video_frame_rate(), 1), + _picture_mxf->intrinsic_duration (), + 0 + ) + )); + + dcp.add (_subtitle_content); + } + + cpl->add (reel); shared_ptr<Job> job = _job.lock (); assert (job); job->sub (_("Computing image digest")); - _picture_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false)); + _picture_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false)); - if (_sound_asset) { + if (_sound_mxf) { job->sub (_("Computing audio digest")); - _sound_asset->compute_digest (boost::bind (&Job::set_progress, job.get(), _1, false)); + _sound_mxf->hash (boost::bind (&Job::set_progress, job.get(), _1, false)); } - libdcp::XMLMetadata meta; + dcp::XMLMetadata meta; meta.issuer = Config::instance()->dcp_issuer (); meta.creator = String::compose ("DCP-o-matic %1 %2", dcpomatic_version, dcpomatic_git_commit); meta.set_issue_date_now (); - dcp.write_xml (_film->interop (), meta, _film->is_signed() ? make_signer () : shared_ptr<const libdcp::Signer> ()); - - LOG_GENERAL ( - N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk - ); -} -/** Tell the writer that frame `f' should be a repeat of the frame before it */ -void -Writer::repeat (int f, Eyes e) -{ - boost::mutex::scoped_lock lock (_mutex); - - while (_queued_full_in_memory > _maximum_frames_in_memory) { - /* The queue is too big; wait until that is sorted out */ - _full_condition.wait (lock); - } - - QueueItem qi; - qi.type = QueueItem::REPEAT; - qi.frame = f; - if (_film->three_d() && e == EYES_BOTH) { - qi.eyes = EYES_LEFT; - _queue.push_back (qi); - qi.eyes = EYES_RIGHT; - _queue.push_back (qi); - } else { - qi.eyes = e; - _queue.push_back (qi); + shared_ptr<const dcp::Signer> signer; + if (_film->is_signed ()) { + signer = Config::instance()->signer (); + /* We did check earlier, but check again here to be on the safe side */ + if (!signer->valid ()) { + throw InvalidSignerError (); + } } - /* Now there's something to do: wake anything wait()ing on _empty_condition */ - _empty_condition.notify_all (); + dcp.write_xml (_film->interop () ? dcp::INTEROP : dcp::SMPTE, meta, signer); + + LOG_GENERAL ( + N_("Wrote %1 FULL, %2 FAKE, %3 pushed to disk"), _full_written, _fake_written, _pushed_to_disk + ); } bool @@ -525,7 +522,7 @@ Writer::check_existing_picture_mxf_frame (FILE* mxf, int f, Eyes eyes) return false; } - libdcp::FrameInfo info (ifi); + dcp::FrameInfo info (ifi); fclose (ifi); if (info.size == 0) { LOG_GENERAL ("Existing frame %1 has no info file", f); @@ -610,6 +607,24 @@ Writer::can_fake_write (int frame) const return (frame != 0 && frame < _first_nonexistant_frame); } +void +Writer::write (PlayerSubtitles subs) +{ + if (subs.text.empty ()) { + return; + } + + if (!_subtitle_content) { + _subtitle_content.reset ( + new dcp::SubtitleContent (_film->name(), _film->isdcf_metadata().subtitle_language) + ); + } + + for (list<dcp::SubtitleString>::const_iterator i = subs.text.begin(); i != subs.text.end(); ++i) { + _subtitle_content->add (*i); + } +} + bool operator< (QueueItem const & a, QueueItem const & b) { diff --git a/src/lib/writer.h b/src/lib/writer.h index c0699ad44..66fe98ec7 100644 --- a/src/lib/writer.h +++ b/src/lib/writer.h @@ -17,27 +17,33 @@ */ +/** @file src/lib/writer.h + * @brief Writer class. + */ + #include <list> #include <boost/shared_ptr.hpp> #include <boost/thread.hpp> #include <boost/thread/condition.hpp> +#include <dcp/subtitle_content.h> #include "exceptions.h" #include "types.h" +#include "player_subtitles.h" class Film; class EncodedData; class AudioBuffers; class Job; -namespace libdcp { - class MonoPictureAsset; - class MonoPictureAssetWriter; - class StereoPictureAsset; - class StereoPictureAssetWriter; - class PictureAsset; - class PictureAssetWriter; - class SoundAsset; - class SoundAssetWriter; +namespace dcp { + class MonoPictureMXF; + class MonoPictureMXFWriter; + class StereoPictureMXF; + class StereoPictureMXFWriter; + class PictureMXF; + class PictureMXFWriter; + class SoundMXF; + class SoundMXFWriter; } struct QueueItem @@ -51,8 +57,6 @@ public: state but we use the data that is already on disk. */ FAKE, - /** this is a repeat of the last frame to be written */ - REPEAT } type; /** encoded data for FULL */ @@ -67,6 +71,17 @@ public: bool operator< (QueueItem const & a, QueueItem const & b); bool operator== (QueueItem const & a, QueueItem const & b); +/** @class Writer + * @brief Class to manage writing JPEG2000 and audio data to MXFs on disk. + * + * This class creates sound and picture MXFs, then takes EncodedData + * or AudioBuffers objects (containing image or sound data respectively) + * and writes them to the MXFs. + * + * ::write() for EncodedData can be called out of order, and the Writer + * will sort it out. write() for AudioBuffers must be called in order. + */ + class Writer : public ExceptionStore, public boost::noncopyable { public: @@ -78,6 +93,7 @@ public: void write (boost::shared_ptr<const EncodedData>, int, Eyes); void fake_write (int, Eyes); void write (boost::shared_ptr<const AudioBuffers>); + void write (PlayerSubtitles); void repeat (int f, Eyes); void finish (); @@ -123,15 +139,14 @@ private: int _full_written; /** number of FAKE written frames */ int _fake_written; - /** number of REPEAT written frames */ - int _repeat_written; /** number of frames pushed to disk and then recovered due to the limit of frames to be held in memory. */ int _pushed_to_disk; - boost::shared_ptr<libdcp::PictureAsset> _picture_asset; - boost::shared_ptr<libdcp::PictureAssetWriter> _picture_asset_writer; - boost::shared_ptr<libdcp::SoundAsset> _sound_asset; - boost::shared_ptr<libdcp::SoundAssetWriter> _sound_asset_writer; + boost::shared_ptr<dcp::PictureMXF> _picture_mxf; + boost::shared_ptr<dcp::PictureMXFWriter> _picture_mxf_writer; + boost::shared_ptr<dcp::SoundMXF> _sound_mxf; + boost::shared_ptr<dcp::SoundMXFWriter> _sound_mxf_writer; + boost::shared_ptr<dcp::SubtitleContent> _subtitle_content; }; diff --git a/src/lib/wscript b/src/lib/wscript index 6c1da1772..d62c22bae 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -7,26 +7,39 @@ sources = """ audio_buffers.cc audio_content.cc audio_decoder.cc + audio_filter.cc audio_mapping.cc + audio_processor.cc cinema.cc + cinema_sound_processor.cc colour_conversion.cc config.cc content.cc content_factory.cc + content_subtitle.cc cross.cc + dcp_content.cc dcp_content_type.cc - dcp_video_frame.cc - decoder.cc + dcp_decoder.cc + dcp_examiner.cc + dcp_subtitle_content.cc + dcp_subtitle_decoder.cc + dcp_video.cc + dcpomatic_time.cc dolby_cp750.cc encoder.cc + encoded_data.cc examine_content_job.cc exceptions.cc file_group.cc filter_graph.cc ffmpeg.cc + ffmpeg_audio_stream.cc ffmpeg_content.cc ffmpeg_decoder.cc ffmpeg_examiner.cc + ffmpeg_stream.cc + ffmpeg_subtitle_stream.cc film.cc filter.cc frame_rate_change.cc @@ -37,16 +50,21 @@ sources = """ image_examiner.cc image_proxy.cc isdcf_metadata.cc + j2k_image_proxy.cc job.cc job_manager.cc kdm.cc log.cc + magick_image_proxy.cc md5_digester.cc - piece.cc + mid_side_decoder.cc player.cc - player_video_frame.cc + player_video.cc playlist.cc + position_image.cc ratio.cc + raw_image_proxy.cc + render_subtitles.cc resampler.cc safe_stringstream.cc scp_dcp_job.cc @@ -54,10 +72,12 @@ sources = """ send_kdm_email_job.cc server.cc server_finder.cc + single_stream_audio_content.cc sndfile_content.cc sndfile_decoder.cc - sound_processor.cc - subtitle.cc + subrip.cc + subrip_content.cc + subrip_decoder.cc subtitle_content.cc subtitle_decoder.cc timer.cc @@ -66,6 +86,7 @@ sources = """ types.cc ui_signaller.cc update.cc + upmixer_a.cc util.cc video_content.cc video_content_scale.cc @@ -79,13 +100,13 @@ def build(bld): else: obj = bld(features = 'cxx cxxshlib') - obj.name = 'libdcpomatic' + obj.name = 'libdcpomatic2' obj.export_includes = ['..'] obj.uselib = """ AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 - SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XMLPP - CURL ZIP QUICKMAIL XMLSEC + SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++ + CURL ZIP QUICKMAIL PANGOMM CAIROMM XMLSEC SUB """ if bld.env.TARGET_OSX: @@ -99,9 +120,9 @@ def build(bld): if bld.env.BUILD_STATIC: obj.uselib += ' XMLPP' - obj.target = 'dcpomatic' + obj.target = 'dcpomatic2' - i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic', bld) + i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic2', bld) def pot(bld): i18n.pot(os.path.join('src', 'lib'), sources, 'libdcpomatic') diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc index fa89a4871..d08a11ea9 100644 --- a/src/tools/dcpomatic.cc +++ b/src/tools/dcpomatic.cc @@ -30,7 +30,7 @@ #include <wx/stdpaths.h> #include <wx/cmdline.h> #include <wx/preferences.h> -#include <libdcp/exceptions.h> +#include <dcp/exceptions.h> #include "wx/film_viewer.h" #include "wx/film_editor.h" #include "wx/job_manager_view.h" @@ -45,6 +45,7 @@ #include "wx/servers_list_dialog.h" #include "wx/hints_dialog.h" #include "wx/update_dialog.h" +#include "wx/content_panel.h" #include "lib/film.h" #include "lib/config.h" #include "lib/util.h" @@ -72,8 +73,6 @@ using std::exception; using boost::shared_ptr; using boost::dynamic_pointer_cast; -// #define DCPOMATIC_WINDOWS_CONSOLE 1 - class FilmChangedDialog { public: @@ -145,20 +144,24 @@ public: , _history_position (0) , _history_separator (0) { -#if defined(DCPOMATIC_WINDOWS) && defined(DCPOMATIC_WINDOWS_CONSOLE) - AllocConsole(); - - HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE); - int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT); - FILE* hf_out = _fdopen(hCrt, "w"); - setvbuf(hf_out, NULL, _IONBF, 1); - *stdout = *hf_out; - - HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE); - hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT); - FILE* hf_in = _fdopen(hCrt, "r"); - setvbuf(hf_in, NULL, _IONBF, 128); - *stdin = *hf_in; +#if defined(DCPOMATIC_WINDOWS) + if (Config::instance()->win32_console ()) { + AllocConsole(); + + HANDLE handle_out = GetStdHandle(STD_OUTPUT_HANDLE); + int hCrt = _open_osfhandle((intptr_t) handle_out, _O_TEXT); + FILE* hf_out = _fdopen(hCrt, "w"); + setvbuf(hf_out, NULL, _IONBF, 1); + *stdout = *hf_out; + + HANDLE handle_in = GetStdHandle(STD_INPUT_HANDLE); + hCrt = _open_osfhandle((intptr_t) handle_in, _O_TEXT); + FILE* hf_in = _fdopen(hCrt, "r"); + setvbuf(hf_in, NULL, _IONBF, 128); + *stdin = *hf_in; + + cout << "DCP-o-matic is starting." << "\n"; + } #endif wxMenuBar* bar = new wxMenuBar; @@ -188,12 +191,6 @@ public: Bind (wxEVT_CLOSE_WINDOW, boost::bind (&Frame::close, this, _1)); - wxAcceleratorEntry accel[1]; - accel[0].Set (wxACCEL_CTRL, static_cast<int>('A'), ID_add_file); - Bind (wxEVT_MENU, boost::bind (&FilmEditor::content_add_file_clicked, _film_editor), ID_add_file); - wxAcceleratorTable accel_table (1, accel); - SetAcceleratorTable (accel_table); - /* Use a panel as the only child of the Frame so that we avoid the dark-grey background on Windows. */ @@ -219,6 +216,12 @@ public: JobManager::instance()->ActiveJobsChanged.connect (boost::bind (&Frame::set_menu_sensitivity, this)); overall_panel->SetSizer (main_sizer); + + wxAcceleratorEntry accel[1]; + accel[0].Set (wxACCEL_CTRL, static_cast<int>('A'), ID_add_file); + Bind (wxEVT_MENU, boost::bind (&ContentPanel::add_file_clicked, _film_editor->content_panel()), ID_add_file); + wxAcceleratorTable accel_table (1, accel); + SetAcceleratorTable (accel_table); } void new_film (boost::filesystem::path path) @@ -410,7 +413,7 @@ private: shared_ptr<Job> (new SendKDMEmailJob (_film, d->screens (), d->cpl (), d->from (), d->until (), d->formulation ())) ); } - } catch (libdcp::NotEncryptedError& e) { + } catch (dcp::NotEncryptedError& e) { error_dialog (this, _("CPL's content is not encrypted.")); } catch (exception& e) { error_dialog (this, e.what ()); @@ -423,7 +426,7 @@ private: void content_scale_to_fit_width () { - VideoContentList vc = _film_editor->selected_video_content (); + VideoContentList vc = _film_editor->content_panel()->selected_video (); for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) { (*i)->scale_and_crop_to_fit_width (); } @@ -431,7 +434,7 @@ private: void content_scale_to_fit_height () { - VideoContentList vc = _film_editor->selected_video_content (); + VideoContentList vc = _film_editor->content_panel()->selected_video (); for (VideoContentList::iterator i = vc.begin(); i != vc.end(); ++i) { (*i)->scale_and_crop_to_fit_height (); } @@ -542,7 +545,7 @@ private: } bool const dcp_creation = (i != jobs.end ()) && !(*i)->finished (); bool const have_cpl = _film && !_film->cpls().empty (); - bool const have_selected_video_content = !_film_editor->selected_video_content().empty(); + bool const have_selected_video_content = !_film_editor->content_panel()->selected_video().empty(); for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) { @@ -707,6 +710,9 @@ static const wxCmdLineEntryDesc command_line_description[] = { { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 } }; +/** @class App + * @brief The magic App class for wxWidgets. + */ class App : public wxApp { bool OnInit () diff --git a/src/tools/dcpomatic_cli.cc b/src/tools/dcpomatic_cli.cc index 5cb05e11d..8c33b7d83 100644 --- a/src/tools/dcpomatic_cli.cc +++ b/src/tools/dcpomatic_cli.cc @@ -20,7 +20,7 @@ #include <iostream> #include <iomanip> #include <getopt.h> -#include <libdcp/version.h> +#include <dcp/version.h> #include "lib/film.h" #include "lib/filter.h" #include "lib/transcode_job.h" diff --git a/src/tools/dcpomatic_create.cc b/src/tools/dcpomatic_create.cc index 26de1c71f..304f4f697 100644 --- a/src/tools/dcpomatic_create.cc +++ b/src/tools/dcpomatic_create.cc @@ -190,7 +190,7 @@ main (int argc, char* argv[]) for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (*i); if (ic) { - ic->set_video_length (still_length * 24); + ic->set_video_length (ContentTime::from_seconds (still_length)); } } diff --git a/src/tools/dcpomatic_kdm.cc b/src/tools/dcpomatic_kdm.cc index 758060a08..6257d60af 100644 --- a/src/tools/dcpomatic_kdm.cc +++ b/src/tools/dcpomatic_kdm.cc @@ -18,7 +18,7 @@ */ #include <getopt.h> -#include <libdcp/certificates.h> +#include <dcp/certificates.h> #include "lib/film.h" #include "lib/cinema.h" #include "lib/kdm.h" @@ -41,8 +41,8 @@ help () cerr << "Syntax: " << program_name << " [OPTION] [<FILM>]\n" " -h, --help show this help\n" " -o, --output output file or directory\n" - " -f, --valid-from valid from time (e.g. \"2013-09-28 01:41:51\") or \"now\"\n" - " -t, --valid-to valid to time (e.g. \"2014-09-28 01:41:51\")\n" + " -f, --valid-from valid from time (in local time zone) (e.g. \"2013-09-28 01:41:51\") or \"now\"\n" + " -t, --valid-to valid to time (in local time zone) (e.g. \"2014-09-28 01:41:51\")\n" " -d, --valid-duration valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")\n" " --formulation modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]\n" " -z, --zip ZIP each cinema's KDMs into its own file\n" @@ -111,7 +111,7 @@ int main (int argc, char* argv[]) bool cinemas = false; string duration_string; bool verbose = false; - libdcp::KDM::Formulation formulation = libdcp::KDM::MODIFIED_TRANSITIONAL_1; + dcp::Formulation formulation = dcp::MODIFIED_TRANSITIONAL_1; program_name = argv[0]; @@ -171,13 +171,13 @@ int main (int argc, char* argv[]) break; case 'C': if (string (optarg) == "modified-transitional-1") { - formulation = libdcp::KDM::MODIFIED_TRANSITIONAL_1; + formulation = dcp::MODIFIED_TRANSITIONAL_1; } else if (string (optarg) == "dci-any") { - formulation = libdcp::KDM::DCI_ANY; + formulation = dcp::DCI_ANY; } else if (string (optarg) == "dci-specific") { - formulation = libdcp::KDM::DCI_SPECIFIC; + formulation = dcp::DCI_SPECIFIC; } else { - error ("unrecognised KDM formulation " + formulation); + error ("unrecognised KDM formulation " + string (optarg)); } } } @@ -248,8 +248,8 @@ int main (int argc, char* argv[]) error ("you must specify --output"); } - shared_ptr<libdcp::Certificate> certificate (new libdcp::Certificate (boost::filesystem::path (certificate_file))); - libdcp::KDM kdm = film->make_kdm (certificate, cpl, valid_from.get(), valid_to.get(), formulation); + dcp::Certificate certificate (dcp::file_to_string (certificate_file)); + dcp::EncryptedKDM kdm = film->make_kdm (certificate, cpl, valid_from.get(), valid_to.get(), formulation); kdm.as_xml (output); if (verbose) { cout << "Generated KDM " << output << " for certificate.\n"; @@ -273,12 +273,18 @@ int main (int argc, char* argv[]) try { if (zip) { - write_kdm_zip_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), formulation, output); + write_kdm_zip_files ( + film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), formulation, output + ); + if (verbose) { cout << "Wrote ZIP files to " << output << "\n"; } } else { - write_kdm_files (film, (*i)->screens(), cpl, valid_from.get(), valid_to.get(), formulation, output); + write_kdm_files ( + film, (*i)->screens(), cpl, dcp::LocalTime (valid_from.get()), dcp::LocalTime (valid_to.get()), formulation, output + ); + if (verbose) { cout << "Wrote KDM files to " << output << "\n"; } diff --git a/src/tools/dcpomatic_server_cli.cc b/src/tools/dcpomatic_server_cli.cc index f35797954..b816460a3 100644 --- a/src/tools/dcpomatic_server_cli.cc +++ b/src/tools/dcpomatic_server_cli.cc @@ -32,7 +32,7 @@ #include <boost/thread/mutex.hpp> #include <boost/thread/condition.hpp> #include "lib/config.h" -#include "lib/dcp_video_frame.h" +#include "lib/dcp_video.h" #include "lib/exceptions.h" #include "lib/util.h" #include "lib/config.h" diff --git a/src/tools/server_test.cc b/src/tools/server_test.cc index a5d31fc08..9223efb3e 100644 --- a/src/tools/server_test.cc +++ b/src/tools/server_test.cc @@ -27,14 +27,15 @@ #include "lib/util.h" #include "lib/scaler.h" #include "lib/server.h" -#include "lib/dcp_video_frame.h" +#include "lib/dcp_video.h" #include "lib/decoder.h" #include "lib/exceptions.h" #include "lib/scaler.h" #include "lib/log.h" #include "lib/video_decoder.h" #include "lib/player.h" -#include "lib/player_video_frame.h" +#include "lib/player_video.h" +#include "lib/encoded_data.h" using std::cout; using std::cerr; @@ -45,18 +46,18 @@ using boost::shared_ptr; static shared_ptr<Film> film; static ServerDescription* server; static shared_ptr<FileLog> log_ (new FileLog ("servomatictest.log")); -static int frame = 0; +static int frame_count = 0; void -process_video (shared_ptr<PlayerVideoFrame> pvf) +process_video (shared_ptr<PlayerVideo> pvf) { - shared_ptr<DCPVideoFrame> local (new DCPVideoFrame (pvf, frame, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_)); - shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (pvf, frame, film->video_frame_rate(), 250000000, RESOLUTION_2K, log_)); + shared_ptr<DCPVideo> local (new DCPVideo (pvf, frame_count, film->video_frame_rate(), 250000000, RESOLUTION_2K, true, log_)); + shared_ptr<DCPVideo> remote (new DCPVideo (pvf, frame_count, film->video_frame_rate(), 250000000, RESOLUTION_2K, true, log_)); - cout << "Frame " << frame << ": "; + cout << "Frame " << frame_count << ": "; cout.flush (); - ++frame; + ++frame_count; shared_ptr<EncodedData> local_encoded = local->encode_locally (); shared_ptr<EncodedData> remote_encoded; @@ -144,12 +145,10 @@ main (int argc, char* argv[]) film->read_metadata (); shared_ptr<Player> player = film->make_player (); - player->disable_audio (); - player->Video.connect (boost::bind (process_video, _1)); - bool done = false; - while (!done) { - done = player->pass (); + DCPTime const frame = DCPTime::from_frames (1, film->video_frame_rate ()); + for (DCPTime t; t < film->length(); t += frame) { + process_video (player->get_video(t, true).front ()); } } catch (std::exception& e) { cerr << "Error: " << e.what() << "\n"; diff --git a/src/tools/wscript b/src/tools/wscript index c4ea1530f..34d8be059 100644 --- a/src/tools/wscript +++ b/src/tools/wscript @@ -13,9 +13,9 @@ def build(bld): obj = bld(features = 'cxx cxxprogram') obj.uselib = 'BOOST_THREAD BOOST_DATETIME OPENJPEG DCP CXML AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC WXWIDGETS QUICKMAIL' obj.includes = ['..'] - obj.use = ['libdcpomatic'] + obj.use = ['libdcpomatic2'] obj.source = '%s.cc' % t - obj.target = t + obj.target = t.replace('dcpomatic', 'dcpomatic2') if t == 'server_test': obj.install_path = None @@ -26,13 +26,13 @@ def build(bld): if bld.env.BUILD_STATIC: obj.uselib += ' GTK' obj.includes = ['..'] - obj.use = ['libdcpomatic', 'libdcpomatic-wx'] + obj.use = ['libdcpomatic2', 'libdcpomatic2-wx'] obj.source = '%s.cc' % t if bld.env.TARGET_WINDOWS: obj.source += ' ../../platform/windows/dcpomatic.rc' - obj.target = t + obj.target = t.replace('dcpomatic', 'dcpomatic2') - i18n.po_to_mo(os.path.join('src', 'tools'), 'dcpomatic', bld) + i18n.po_to_mo(os.path.join('src', 'tools'), 'dcpomatic2', bld) def pot(bld): i18n.pot(os.path.join('src', 'tools'), 'dcpomatic.cc dcpomatic_batch.cc', 'dcpomatic') diff --git a/src/wx/about_dialog.cc b/src/wx/about_dialog.cc index 58f299723..15b667f9b 100644 --- a/src/wx/about_dialog.cc +++ b/src/wx/about_dialog.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,10 @@ */ +/** @file src/wx/about_dialog.cc + * @brief The "about DCP-o-matic" dialogue box. + */ + #include <wx/notebook.h> #include <wx/hyperlink.h> #include "lib/version.h" @@ -220,6 +224,10 @@ AboutDialog::AboutDialog (wxWindow* parent) SetSizerAndFit (overall_sizer); } +/** Add a section of credits. + * @param name Name of section. + * @param credits List of names. + */ void AboutDialog::add_section (wxString name, wxArrayString credits) { diff --git a/src/wx/about_dialog.h b/src/wx/about_dialog.h index a78abb93e..4901cf990 100644 --- a/src/wx/about_dialog.h +++ b/src/wx/about_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,10 +17,17 @@ */ +/** @file src/wx/about_dialog.h + * @brief The "about DCP-o-matic" dialogue box. + */ + #include <wx/wx.h> class wxNotebook; +/** @class AboutDialog + * @brief The "about DCP-o-matic" dialogue box. + */ class AboutDialog : public wxDialog { public: @@ -29,6 +36,6 @@ public: private: void add_section (wxString, wxArrayString); - wxNotebook* _notebook; + wxNotebook* _notebook; ///< notebook used to keep each list of names for the credits }; diff --git a/src/wx/audio_mapping_view.cc b/src/wx/audio_mapping_view.cc index 6c1508aee..8e92400bd 100644 --- a/src/wx/audio_mapping_view.cc +++ b/src/wx/audio_mapping_view.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,16 +17,21 @@ */ +/** @file src/wx/audio_mapping_view.cc + * @brief AudioMappingView class and helpers. + */ + #include <wx/wx.h> #include <wx/renderer.h> #include <wx/grid.h> -#include <libdcp/types.h> -#include <libdcp/raw_convert.h> +#include <dcp/types.h> +#include <dcp/raw_convert.h> #include "lib/audio_mapping.h" #include "lib/util.h" #include "audio_mapping_view.h" #include "wx_util.h" #include "audio_gain_dialog.h" +#include <boost/lexical_cast.hpp> using std::cout; using std::list; @@ -53,6 +58,9 @@ public: } }; +/** @class ValueRenderer + * @brief wxGridCellRenderer for a gain value. + */ class ValueRenderer : public wxGridCellRenderer { public: @@ -155,7 +163,7 @@ AudioMappingView::left_click (wxGridEvent& ev) return; } - libdcp::Channel d = static_cast<libdcp::Channel> (ev.GetCol() - 1); + dcp::Channel d = static_cast<dcp::Channel> (ev.GetCol() - 1); if (_map.get (ev.GetRow(), d) > 0) { _map.set (ev.GetRow(), d, 0); @@ -181,28 +189,28 @@ AudioMappingView::right_click (wxGridEvent& ev) void AudioMappingView::off () { - _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 0); + _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 0); map_changed (); } void AudioMappingView::full () { - _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), 1); + _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), 1); map_changed (); } void AudioMappingView::minus6dB () { - _map.set (_menu_row, static_cast<libdcp::Channel> (_menu_column - 1), pow (10, -6.0 / 20)); + _map.set (_menu_row, static_cast<dcp::Channel> (_menu_column - 1), pow (10, -6.0 / 20)); map_changed (); } void AudioMappingView::edit () { - libdcp::Channel d = static_cast<libdcp::Channel> (_menu_column - 1); + dcp::Channel d = static_cast<dcp::Channel> (_menu_column - 1); AudioGainDialog* dialog = new AudioGainDialog (this, _menu_row, _menu_column - 1, _map.get (_menu_row, d)); if (dialog->ShowModal () == wxID_OK) { @@ -239,7 +247,7 @@ AudioMappingView::update_cells () _grid->SetCellValue (i, 0, wxString::Format (wxT("%d"), i + 1)); for (int j = 1; j < _grid->GetNumberCols(); ++j) { - _grid->SetCellValue (i, j, std_to_wx (libdcp::raw_convert<string> (_map.get (i, static_cast<libdcp::Channel> (j - 1))))); + _grid->SetCellValue (i, j, std_to_wx (dcp::raw_convert<string> (_map.get (i, static_cast<dcp::Channel> (j - 1))))); } } @@ -343,7 +351,7 @@ AudioMappingView::mouse_moved (wxMouseEvent& ev) if (row != _last_tooltip_row || column != _last_tooltip_column) { wxString s; - float const gain = _map.get (row, static_cast<libdcp::Channel> (column - 1)); + float const gain = _map.get (row, static_cast<dcp::Channel> (column - 1)); if (gain == 0) { s = wxString::Format (_("No audio will be passed from content channel %d to DCP channel %d."), row + 1, column); } else if (gain == 1) { diff --git a/src/wx/audio_mapping_view.h b/src/wx/audio_mapping_view.h index 98375eb9e..7ed699463 100644 --- a/src/wx/audio_mapping_view.h +++ b/src/wx/audio_mapping_view.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,6 +17,13 @@ */ +/** @file src/wx/audio_mapping_view.h + * @brief AudioMappingView class + * + * This class displays the mapping of one set of audio channels to another, + * with gain values on each node of the map. + */ + #include <boost/signals2.hpp> #include <wx/wx.h> #include <wx/grid.h> diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc index 917775181..82604763c 100644 --- a/src/wx/audio_panel.cc +++ b/src/wx/audio_panel.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,24 +20,27 @@ #include <boost/lexical_cast.hpp> #include <wx/spinctrl.h> #include "lib/config.h" -#include "lib/sound_processor.h" #include "lib/ffmpeg_content.h" +#include "lib/ffmpeg_audio_stream.h" +#include "lib/audio_processor.h" +#include "lib/cinema_sound_processor.h" #include "audio_dialog.h" #include "audio_panel.h" #include "audio_mapping_view.h" #include "wx_util.h" #include "gain_calculator_dialog.h" -#include "film_editor.h" +#include "content_panel.h" using std::vector; using std::cout; using std::string; +using std::list; using boost::dynamic_pointer_cast; using boost::lexical_cast; using boost::shared_ptr; -AudioPanel::AudioPanel (FilmEditor* e) - : FilmEditorPanel (e, _("Audio")) +AudioPanel::AudioPanel (ContentPanel* p) + : ContentSubPanel (p, _("Audio")) , _audio_dialog (0) { wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); @@ -80,8 +83,13 @@ AudioPanel::AudioPanel (FilmEditor* e) add_label_to_grid_bag_sizer (grid, this, _("Stream"), true, wxGBPosition (r, 0)); _stream = new wxChoice (this, wxID_ANY); - grid->Add (_stream, wxGBPosition (r, 1)); - _description = add_label_to_grid_bag_sizer (grid, this, "", false, wxGBPosition (r, 3)); + grid->Add (_stream, wxGBPosition (r, 1), wxGBSpan (1, 3), wxEXPAND); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Process with"), true, wxGBPosition (r, 0)); + _processor = new wxChoice (this, wxID_ANY); + setup_processors (); + grid->Add (_processor, wxGBPosition (r, 1), wxGBSpan (1, 3), wxEXPAND); ++r; _mapping = new AudioMappingView (this); @@ -92,9 +100,10 @@ AudioPanel::AudioPanel (FilmEditor* e) _gain->wrapped()->SetIncrement (0.5); _delay->wrapped()->SetRange (-1000, 1000); - _stream->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&AudioPanel::stream_changed, this)); - _show->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&AudioPanel::show_clicked, this)); - _gain_calculate_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&AudioPanel::gain_calculate_button_clicked, this)); + _stream->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&AudioPanel::stream_changed, this)); + _show->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&AudioPanel::show_clicked, this)); + _gain_calculate_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&AudioPanel::gain_calculate_button_clicked, this)); + _processor->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&AudioPanel::processor_changed, this)); _mapping->Changed.connect (boost::bind (&AudioPanel::mapping_changed, this, _1)); } @@ -105,7 +114,7 @@ AudioPanel::film_changed (Film::Property property) { switch (property) { case Film::AUDIO_CHANNELS: - _mapping->set_channels (_editor->film()->audio_channels ()); + _mapping->set_channels (_parent->film()->audio_channels ()); _sizer->Layout (); break; default: @@ -116,7 +125,7 @@ AudioPanel::film_changed (Film::Property property) void AudioPanel::film_content_changed (int property) { - AudioContentList ac = _editor->selected_audio_content (); + AudioContentList ac = _parent->selected_audio (); shared_ptr<AudioContent> acs; shared_ptr<FFmpegContent> fcs; if (ac.size() == 1) { @@ -128,7 +137,6 @@ AudioPanel::film_content_changed (int property) _mapping->set (acs ? acs->audio_mapping () : AudioMapping ()); _sizer->Layout (); } else if (property == FFmpegContentProperty::AUDIO_STREAM) { - setup_stream_description (); _mapping->set (acs ? acs->audio_mapping () : AudioMapping ()); _sizer->Layout (); } else if (property == FFmpegContentProperty::AUDIO_STREAMS) { @@ -141,9 +149,14 @@ AudioPanel::film_content_changed (int property) if (fcs->audio_stream()) { checked_set (_stream, fcs->audio_stream()->identifier ()); - setup_stream_description (); } } + } else if (property == AudioContentProperty::AUDIO_PROCESSOR) { + if (acs) { + checked_set (_processor, acs->audio_processor() ? acs->audio_processor()->id() : N_("none")); + } else { + checked_set (_processor, N_("none")); + } } } @@ -159,7 +172,7 @@ AudioPanel::gain_calculate_button_clicked () } _gain->wrapped()->SetValue ( - Config::instance()->sound_processor()->db_for_fader_change ( + Config::instance()->cinema_sound_processor()->db_for_fader_change ( d->wanted_fader (), d->actual_fader () ) @@ -181,7 +194,7 @@ AudioPanel::show_clicked () _audio_dialog = 0; } - AudioContentList ac = _editor->selected_audio_content (); + AudioContentList ac = _parent->selected_audio (); if (ac.size() != 1) { return; } @@ -194,7 +207,7 @@ AudioPanel::show_clicked () void AudioPanel::stream_changed () { - FFmpegContentList fc = _editor->selected_ffmpeg_content (); + FFmpegContentList fc = _parent->selected_ffmpeg (); if (fc.size() != 1) { return; } @@ -215,39 +228,27 @@ AudioPanel::stream_changed () if (i != a.end ()) { fcs->set_audio_stream (*i); } - - setup_stream_description (); } void -AudioPanel::setup_stream_description () +AudioPanel::processor_changed () { - FFmpegContentList fc = _editor->selected_ffmpeg_content (); - if (fc.size() != 1) { - _description->SetLabel (""); - return; + string const s = string_client_data (_processor->GetClientObject (_processor->GetSelection ())); + AudioProcessor const * p = 0; + if (s != wx_to_std (N_("none"))) { + p = AudioProcessor::from_id (s); } - - shared_ptr<FFmpegContent> fcs = fc.front (); - - if (!fcs->audio_stream ()) { - _description->SetLabel (wxT ("")); - } else { - wxString s; - if (fcs->audio_channels() == 1) { - s << _("1 channel"); - } else { - s << fcs->audio_channels() << wxT (" ") << _("channels"); - } - s << wxT (", ") << fcs->content_audio_frame_rate() << _("Hz"); - _description->SetLabel (s); + + AudioContentList c = _parent->selected_audio (); + for (AudioContentList::const_iterator i = c.begin(); i != c.end(); ++i) { + (*i)->set_audio_processor (p); } } void AudioPanel::mapping_changed (AudioMapping m) { - AudioContentList c = _editor->selected_audio_content (); + AudioContentList c = _parent->selected_audio (); if (c.size() == 1) { c.front()->set_audio_mapping (m); } @@ -256,7 +257,7 @@ AudioPanel::mapping_changed (AudioMapping m) void AudioPanel::content_selection_changed () { - AudioContentList sel = _editor->selected_audio_content (); + AudioContentList sel = _parent->selected_audio (); if (_audio_dialog && sel.size() == 1) { _audio_dialog->set_content (sel.front ()); @@ -265,11 +266,37 @@ AudioPanel::content_selection_changed () _gain->set_content (sel); _delay->set_content (sel); + _gain_calculate_button->Enable (sel.size() == 1); _show->Enable (sel.size() == 1); _stream->Enable (sel.size() == 1); + _processor->Enable (!sel.empty()); _mapping->Enable (sel.size() == 1); + setup_processors (); + film_content_changed (AudioContentProperty::AUDIO_MAPPING); + film_content_changed (AudioContentProperty::AUDIO_PROCESSOR); film_content_changed (FFmpegContentProperty::AUDIO_STREAM); film_content_changed (FFmpegContentProperty::AUDIO_STREAMS); } + +void +AudioPanel::setup_processors () +{ + AudioContentList sel = _parent->selected_audio (); + + _processor->Clear (); + list<AudioProcessor const *> ap = AudioProcessor::all (); + _processor->Append (_("None"), new wxStringClientData (N_("none"))); + for (list<AudioProcessor const *>::const_iterator i = ap.begin(); i != ap.end(); ++i) { + + AudioContentList::const_iterator j = sel.begin(); + while (j != sel.end() && (*i)->in_channels().includes ((*j)->audio_channels ())) { + ++j; + } + + if (j == sel.end ()) { + _processor->Append (std_to_wx ((*i)->name ()), new wxStringClientData (std_to_wx ((*i)->id ()))); + } + } +} diff --git a/src/wx/audio_panel.h b/src/wx/audio_panel.h index 2ba5a9ffc..d5821d26a 100644 --- a/src/wx/audio_panel.h +++ b/src/wx/audio_panel.h @@ -18,7 +18,7 @@ */ #include "lib/audio_mapping.h" -#include "film_editor_panel.h" +#include "content_sub_panel.h" #include "content_widget.h" class wxSpinCtrlDouble; @@ -28,10 +28,10 @@ class wxStaticText; class AudioMappingView; class AudioDialog; -class AudioPanel : public FilmEditorPanel +class AudioPanel : public ContentSubPanel { public: - AudioPanel (FilmEditor *); + AudioPanel (ContentPanel *); void film_changed (Film::Property); void film_content_changed (int); @@ -42,14 +42,15 @@ private: void show_clicked (); void stream_changed (); void mapping_changed (AudioMapping); - void setup_stream_description (); + void processor_changed (); + void setup_processors (); ContentSpinCtrlDouble<AudioContent>* _gain; wxButton* _gain_calculate_button; wxButton* _show; ContentSpinCtrl<AudioContent>* _delay; wxChoice* _stream; - wxStaticText* _description; + wxChoice* _processor; AudioMappingView* _mapping; AudioDialog* _audio_dialog; }; diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc index 816602355..8d8f44b4e 100644 --- a/src/wx/config_dialog.cc +++ b/src/wx/config_dialog.cc @@ -28,13 +28,19 @@ #include <wx/preferences.h> #include <wx/filepicker.h> #include <wx/spinctrl.h> -#include <libdcp/colour_matrix.h> +#include <dcp/colour_matrix.h> +#include <dcp/exceptions.h> +#include <dcp/signer.h> #include "lib/config.h" #include "lib/ratio.h" #include "lib/scaler.h" #include "lib/filter.h" #include "lib/dcp_content_type.h" #include "lib/colour_conversion.h" +#include "lib/log.h" +#include "lib/util.h" +#include "lib/cross.h" +#include "lib/exceptions.h" #include "config_dialog.h" #include "wx_util.h" #include "editable_list.h" @@ -43,6 +49,7 @@ #include "isdcf_metadata_dialog.h" #include "preset_colour_conversion_dialog.h" #include "server_dialog.h" +#include "make_signer_chain_dialog.h" using std::vector; using std::string; @@ -112,7 +119,6 @@ public: _num_local_encoding_threads = new wxSpinCtrl (panel); table->Add (_num_local_encoding_threads, 1); - _check_for_updates = new wxCheckBox (panel, wxID_ANY, _("Check for updates on startup")); table->Add (_check_for_updates, 1, wxEXPAND | wxALL); table->AddSpacer (0); @@ -530,6 +536,339 @@ private: } }; +class KeysPage : public wxPreferencesPage, public Page +{ +public: + KeysPage (wxSize panel_size, int border) + : Page (panel_size, border) + {} + + wxString GetName () const + { + return _("Keys"); + } + +#ifdef DCPOMATIC_OSX + wxBitmap GetLargeIcon () const + { + return wxBitmap ("keys", wxBITMAP_TYPE_PNG_RESOURCE); + } +#endif + + wxWindow* CreateWindow (wxWindow* parent) + { + _panel = new wxPanel (parent, wxID_ANY, wxDefaultPosition, _panel_size); + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + _panel->SetSizer (overall_sizer); + + wxStaticText* m = new wxStaticText (_panel, wxID_ANY, _("Certificate chain for signing DCPs and KDMs:")); + overall_sizer->Add (m, 0, wxALL, _border); + + wxBoxSizer* certificates_sizer = new wxBoxSizer (wxHORIZONTAL); + overall_sizer->Add (certificates_sizer, 0, wxLEFT | wxRIGHT, _border); + + _certificates = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (400, 200), wxLC_REPORT | wxLC_SINGLE_SEL); + + { + wxListItem ip; + ip.SetId (0); + ip.SetText (_("Type")); + ip.SetWidth (100); + _certificates->InsertColumn (0, ip); + } + + { + wxListItem ip; + ip.SetId (1); + ip.SetText (_("Thumbprint")); + ip.SetWidth (300); + + wxFont font = ip.GetFont (); + font.SetFamily (wxFONTFAMILY_TELETYPE); + ip.SetFont (font); + + _certificates->InsertColumn (1, ip); + } + + certificates_sizer->Add (_certificates, 1, wxEXPAND); + + { + wxSizer* s = new wxBoxSizer (wxVERTICAL); + _add_certificate = new wxButton (_panel, wxID_ANY, _("Add...")); + s->Add (_add_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP); + _remove_certificate = new wxButton (_panel, wxID_ANY, _("Remove")); + s->Add (_remove_certificate, 0, wxTOP | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP); + certificates_sizer->Add (s, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); + } + + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + overall_sizer->Add (table, 1, wxALL | wxEXPAND, _border); + + _remake_certificates = new wxButton (_panel, wxID_ANY, _("Re-make certificates...")); + table->Add (_remake_certificates, 0); + table->AddSpacer (0); + + add_label_to_sizer (table, _panel, _("Private key for leaf certificate"), true); + { + wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _signer_private_key = new wxStaticText (_panel, wxID_ANY, wxT ("")); + wxFont font = _signer_private_key->GetFont (); + font.SetFamily (wxFONTFAMILY_TELETYPE); + _signer_private_key->SetFont (font); + s->Add (_signer_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP); + _load_signer_private_key = new wxButton (_panel, wxID_ANY, _("Load...")); + s->Add (_load_signer_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); + table->Add (s, 0); + } + + add_label_to_sizer (table, _panel, _("Certificate for decrypting DCPs"), true); + { + wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _decryption_certificate = new wxStaticText (_panel, wxID_ANY, wxT ("")); + wxFont font = _decryption_certificate->GetFont (); + font.SetFamily (wxFONTFAMILY_TELETYPE); + _decryption_certificate->SetFont (font); + s->Add (_decryption_certificate, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP); + _load_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Load...")); + s->Add (_load_decryption_certificate, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); + table->Add (s, 0); + } + + add_label_to_sizer (table, _panel, _("Private key for decrypting DCPs"), true); + { + wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _decryption_private_key = new wxStaticText (_panel, wxID_ANY, wxT ("")); + wxFont font = _decryption_private_key->GetFont (); + font.SetFamily (wxFONTFAMILY_TELETYPE); + _decryption_private_key->SetFont (font); + s->Add (_decryption_private_key, 1, wxLEFT | wxRIGHT | wxALIGN_CENTER_VERTICAL, DCPOMATIC_SIZER_X_GAP); + _load_decryption_private_key = new wxButton (_panel, wxID_ANY, _("Load...")); + s->Add (_load_decryption_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); + table->Add (s, 0); + } + + _export_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Export DCP decryption certificate...")); + table->Add (_export_decryption_certificate); + table->AddSpacer (0); + + _add_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::add_certificate, this)); + _remove_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remove_certificate, this)); + _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&KeysPage::update_sensitivity, this)); + _certificates->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&KeysPage::update_sensitivity, this)); + _remake_certificates->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remake_certificates, this)); + _load_signer_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_signer_private_key, this)); + _load_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_certificate, this)); + _load_decryption_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_private_key, this)); + _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this)); + + _signer.reset (new dcp::Signer (*Config::instance()->signer().get ())); + + update_certificate_list (); + update_signer_private_key (); + update_decryption_certificate (); + update_decryption_private_key (); + update_sensitivity (); + + return _panel; + } + +private: + void add_certificate () + { + wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File")); + + if (d->ShowModal() == wxID_OK) { + try { + dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ()))); + _signer->certificates().add (c); + Config::instance()->set_signer (_signer); + update_certificate_list (); + } catch (dcp::MiscError& e) { + error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ())); + } + } + + d->Destroy (); + + update_sensitivity (); + } + + void remove_certificate () + { + int i = _certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (i == -1) { + return; + } + + _certificates->DeleteItem (i); + _signer->certificates().remove (i); + Config::instance()->set_signer (_signer); + + update_sensitivity (); + } + + void update_certificate_list () + { + _certificates->DeleteAllItems (); + dcp::CertificateChain::List certs = _signer->certificates().root_to_leaf (); + size_t n = 0; + for (dcp::CertificateChain::List::const_iterator i = certs.begin(); i != certs.end(); ++i) { + wxListItem item; + item.SetId (n); + _certificates->InsertItem (item); + _certificates->SetItem (n, 1, std_to_wx (i->thumbprint ())); + + if (n == 0) { + _certificates->SetItem (n, 0, _("Root")); + } else if (n == (certs.size() - 1)) { + _certificates->SetItem (n, 0, _("Leaf")); + } else { + _certificates->SetItem (n, 0, _("Intermediate")); + } + + ++n; + } + } + + void remake_certificates () + { + MakeSignerChainDialog* d = new MakeSignerChainDialog (_panel); + if (d->ShowModal () == wxID_OK) { + _signer.reset ( + new dcp::Signer ( + openssl_path (), + d->organisation (), + d->organisational_unit (), + d->root_common_name (), + d->intermediate_common_name (), + d->leaf_common_name () + ) + ); + + Config::instance()->set_signer (_signer); + update_certificate_list (); + update_signer_private_key (); + } + + d->Destroy (); + } + + void update_sensitivity () + { + _remove_certificate->Enable (_certificates->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED) != -1); + } + + void update_signer_private_key () + { + _signer_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (_signer->key ()))); + } + + void load_signer_private_key () + { + wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File")); + + if (d->ShowModal() == wxID_OK) { + try { + boost::filesystem::path p (wx_to_std (d->GetPath ())); + if (boost::filesystem::file_size (p) > 1024) { + error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), std_to_wx (p.string ()))); + return; + } + + _signer->set_key (dcp::file_to_string (p)); + Config::instance()->set_signer (_signer); + update_signer_private_key (); + } catch (dcp::MiscError& e) { + error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ())); + } + } + + d->Destroy (); + + update_sensitivity (); + + } + + void load_decryption_certificate () + { + wxFileDialog* d = new wxFileDialog (_panel, _("Select Certificate File")); + + if (d->ShowModal() == wxID_OK) { + try { + dcp::Certificate c (dcp::file_to_string (wx_to_std (d->GetPath ()))); + Config::instance()->set_decryption_certificate (c); + update_decryption_certificate (); + } catch (dcp::MiscError& e) { + error_dialog (_panel, wxString::Format (_("Could not read certificate file (%s)"), e.what ())); + } + } + + d->Destroy (); + } + + void update_decryption_certificate () + { + _decryption_certificate->SetLabel (std_to_wx (Config::instance()->decryption_certificate().thumbprint ())); + } + + void load_decryption_private_key () + { + wxFileDialog* d = new wxFileDialog (_panel, _("Select Key File")); + + if (d->ShowModal() == wxID_OK) { + try { + boost::filesystem::path p (wx_to_std (d->GetPath ())); + Config::instance()->set_decryption_private_key (dcp::file_to_string (p)); + update_decryption_private_key (); + } catch (dcp::MiscError& e) { + error_dialog (_panel, wxString::Format (_("Could not read key file (%s)"), e.what ())); + } + } + + d->Destroy (); + } + + void update_decryption_private_key () + { + _decryption_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (Config::instance()->decryption_private_key()))); + } + + void export_decryption_certificate () + { + wxFileDialog* d = new wxFileDialog ( + _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + if (d->ShowModal () == wxID_OK) { + FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w"); + if (!f) { + throw OpenFileError (wx_to_std (d->GetPath ())); + } + + string const s = Config::instance()->decryption_certificate().certificate (true); + fwrite (s.c_str(), 1, s.length(), f); + fclose (f); + } + d->Destroy (); + } + + wxPanel* _panel; + wxListCtrl* _certificates; + wxButton* _add_certificate; + wxButton* _remove_certificate; + wxButton* _remake_certificates; + wxStaticText* _signer_private_key; + wxButton* _load_signer_private_key; + wxStaticText* _decryption_certificate; + wxButton* _load_decryption_certificate; + wxStaticText* _decryption_private_key; + wxButton* _load_decryption_private_key; + wxButton* _export_decryption_certificate; + shared_ptr<dcp::Signer> _signer; +}; + class TMSPage : public wxPreferencesPage, public Page { public: @@ -770,6 +1109,9 @@ private: wxButton* _reset_kdm_email; }; +/** @class AdvancedPage + * @brief Advanced page of the preferences dialog. + */ class AdvancedPage : public wxStockPreferencesPage, public Page { public: @@ -821,6 +1163,12 @@ public: table->Add (t, 0, wxALL, 6); } +#ifdef DCPOMATIC_WINDOWS + _win32_console = new wxCheckBox (panel, wxID_ANY, _("Open console window")); + table->Add (_win32_console, 1, wxEXPAND | wxALL); + table->AddSpacer (0); +#endif + Config* config = Config::instance (); _maximum_j2k_bandwidth->SetRange (1, 500); @@ -836,6 +1184,10 @@ public: _log_error->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this)); _log_timing->SetValue (config->log_types() & Log::TYPE_TIMING); _log_timing->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::log_changed, this)); +#ifdef DCPOMATIC_WINDOWS + _win32_console->SetValue (config->win32_console()); + _win32_console->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AdvancedPage::win32_console_changed, this)); +#endif return panel; } @@ -869,6 +1221,13 @@ private: } Config::instance()->set_log_types (types); } + +#ifdef DCPOMATIC_WINDOWS + void win32_console_changed () + { + Config::instance()->set_win32_console (_win32_console->GetValue ()); + } +#endif wxSpinCtrl* _maximum_j2k_bandwidth; wxCheckBox* _allow_any_dcp_frame_rate; @@ -876,6 +1235,9 @@ private: wxCheckBox* _log_warning; wxCheckBox* _log_error; wxCheckBox* _log_timing; +#ifdef DCPOMATIC_WINDOWS + wxCheckBox* _win32_console; +#endif }; wxPreferencesEditor* @@ -899,6 +1261,7 @@ create_config_dialog () e->AddPage (new DefaultsPage (ps, border)); e->AddPage (new EncodingServersPage (ps, border)); e->AddPage (new ColourConversionsPage (ps, border)); + e->AddPage (new KeysPage (ps, border)); e->AddPage (new TMSPage (ps, border)); e->AddPage (new KDMEmailPage (ps, border)); e->AddPage (new AdvancedPage (ps, border)); diff --git a/src/wx/content_menu.cc b/src/wx/content_menu.cc index b91c82ab1..3e3c462b2 100644 --- a/src/wx/content_menu.cc +++ b/src/wx/content_menu.cc @@ -26,6 +26,7 @@ #include "lib/examine_content_job.h" #include "lib/job_manager.h" #include "lib/exceptions.h" +#include "lib/dcp_content.h" #include "content_menu.h" #include "repeat_dialog.h" #include "wx_util.h" @@ -40,6 +41,8 @@ enum { ID_repeat = 1, ID_join, ID_find_missing, + ID_re_examine, + ID_kdm, ID_remove }; @@ -50,12 +53,16 @@ ContentMenu::ContentMenu (wxWindow* p) _repeat = _menu->Append (ID_repeat, _("Repeat...")); _join = _menu->Append (ID_join, _("Join")); _find_missing = _menu->Append (ID_find_missing, _("Find missing...")); + _re_examine = _menu->Append (ID_re_examine, _("Re-examine...")); + _kdm = _menu->Append (ID_kdm, _("Add KDM...")); _menu->AppendSeparator (); _remove = _menu->Append (ID_remove, _("Remove")); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::repeat, this), ID_repeat); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::join, this), ID_join); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::find_missing, this), ID_find_missing); + _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::re_examine, this), ID_re_examine); + _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::kdm, this), ID_kdm); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::remove, this), ID_remove); } @@ -81,6 +88,15 @@ ContentMenu::popup (weak_ptr<Film> f, ContentList c, wxPoint p) _join->Enable (n > 1); _find_missing->Enable (_content.size() == 1 && !_content.front()->paths_valid ()); + _re_examine->Enable (!_content.empty ()); + + if (_content.size() == 1) { + shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ()); + _kdm->Enable (dcp && dcp->encrypted ()); + } else { + _kdm->Enable (false); + } + _remove->Enable (!_content.empty ()); _parent->PopupMenu (_menu, p); } @@ -207,6 +223,19 @@ ContentMenu::find_missing () } void +ContentMenu::re_examine () +{ + shared_ptr<Film> film = _film.lock (); + if (!film) { + return; + } + + for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) { + film->examine_content (*i); + } +} + +void ContentMenu::maybe_found_missing (weak_ptr<Job> j, weak_ptr<Content> oc, weak_ptr<Content> nc) { shared_ptr<Job> job = j.lock (); @@ -226,3 +255,22 @@ ContentMenu::maybe_found_missing (weak_ptr<Job> j, weak_ptr<Content> oc, weak_pt old_content->set_path (new_content->path (0)); } + +void +ContentMenu::kdm () +{ + assert (!_content.empty ()); + shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_content.front ()); + assert (dcp); + + wxFileDialog* d = new wxFileDialog (_parent, _("Select KDM")); + + if (d->ShowModal() == wxID_OK) { + dcp->add_kdm (dcp::EncryptedKDM (dcp::file_to_string (wx_to_std (d->GetPath ())))); + shared_ptr<Film> film = _film.lock (); + assert (film); + film->examine_content (dcp); + } + + d->Destroy (); +} diff --git a/src/wx/content_menu.h b/src/wx/content_menu.h index a9f9093c6..77cf29a30 100644 --- a/src/wx/content_menu.h +++ b/src/wx/content_menu.h @@ -30,15 +30,17 @@ class Film; class ContentMenu { public: - ContentMenu (wxWindow *); + ContentMenu (wxWindow* p); ~ContentMenu (); - + void popup (boost::weak_ptr<Film>, ContentList, wxPoint); private: void repeat (); void join (); void find_missing (); + void re_examine (); + void kdm (); void remove (); void maybe_found_missing (boost::weak_ptr<Job>, boost::weak_ptr<Content>, boost::weak_ptr<Content>); @@ -50,6 +52,8 @@ private: wxMenuItem* _repeat; wxMenuItem* _join; wxMenuItem* _find_missing; + wxMenuItem* _re_examine; + wxMenuItem* _kdm; wxMenuItem* _remove; }; diff --git a/src/wx/content_panel.cc b/src/wx/content_panel.cc new file mode 100644 index 000000000..b4b9f13da --- /dev/null +++ b/src/wx/content_panel.cc @@ -0,0 +1,473 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <wx/wx.h> +#include <wx/notebook.h> +#include <wx/listctrl.h> +#include "lib/audio_content.h" +#include "lib/subtitle_content.h" +#include "lib/video_content.h" +#include "lib/ffmpeg_content.h" +#include "lib/content_factory.h" +#include "lib/image_content.h" +#include "lib/dcp_content.h" +#include "lib/playlist.h" +#include "content_panel.h" +#include "wx_util.h" +#include "video_panel.h" +#include "audio_panel.h" +#include "subtitle_panel.h" +#include "timing_panel.h" +#include "timeline_dialog.h" + +using std::list; +using std::string; +using std::cout; +using boost::shared_ptr; +using boost::weak_ptr; +using boost::dynamic_pointer_cast; + +ContentPanel::ContentPanel (wxNotebook* n, boost::shared_ptr<Film> f) + : _timeline_dialog (0) + , _film (f) + , _generally_sensitive (true) +{ + _panel = new wxPanel (n); + _sizer = new wxBoxSizer (wxVERTICAL); + _panel->SetSizer (_sizer); + + _menu = new ContentMenu (_panel); + + { + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + + _content = new wxListCtrl (_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER); + s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6); + + _content->InsertColumn (0, wxT("")); + _content->SetColumnWidth (0, 512); + + wxBoxSizer* b = new wxBoxSizer (wxVERTICAL); + _add_file = new wxButton (_panel, wxID_ANY, _("Add file(s)...")); + b->Add (_add_file, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + _add_folder = new wxButton (_panel, wxID_ANY, _("Add folder...")); + b->Add (_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + _remove = new wxButton (_panel, wxID_ANY, _("Remove")); + b->Add (_remove, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + _earlier = new wxButton (_panel, wxID_ANY, _("Up")); + b->Add (_earlier, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + _later = new wxButton (_panel, wxID_ANY, _("Down")); + b->Add (_later, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + _timeline = new wxButton (_panel, wxID_ANY, _("Timeline...")); + b->Add (_timeline, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); + + s->Add (b, 0, wxALL, 4); + + _sizer->Add (s, 0, wxEXPAND | wxALL, 6); + } + + _sequence_video = new wxCheckBox (_panel, wxID_ANY, _("Keep video in sequence")); + _sizer->Add (_sequence_video); + + _notebook = new wxNotebook (_panel, wxID_ANY); + _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6); + + _video_panel = new VideoPanel (this); + _panels.push_back (_video_panel); + _audio_panel = new AudioPanel (this); + _panels.push_back (_audio_panel); + _subtitle_panel = new SubtitlePanel (this); + _panels.push_back (_subtitle_panel); + _timing_panel = new TimingPanel (this); + _panels.push_back (_timing_panel); + + _content->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&ContentPanel::selection_changed, this)); + _content->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&ContentPanel::selection_changed, this)); + _content->Bind (wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, boost::bind (&ContentPanel::right_click, this, _1)); + _content->Bind (wxEVT_DROP_FILES, boost::bind (&ContentPanel::files_dropped, this, _1)); + _add_file->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::add_file_clicked, this)); + _add_folder->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::add_folder_clicked, this)); + _remove->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::remove_clicked, this)); + _earlier->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::earlier_clicked, this)); + _later->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::later_clicked, this)); + _timeline->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ContentPanel::timeline_clicked, this)); + _sequence_video->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ContentPanel::sequence_video_changed, this)); +} + +ContentList +ContentPanel::selected () +{ + ContentList sel; + long int s = -1; + while (true) { + s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (s == -1) { + break; + } + + if (s < int (_film->content().size ())) { + sel.push_back (_film->content()[s]); + } + } + + return sel; +} + +VideoContentList +ContentPanel::selected_video () +{ + ContentList c = selected (); + VideoContentList vc; + + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + shared_ptr<VideoContent> t = dynamic_pointer_cast<VideoContent> (*i); + if (t) { + vc.push_back (t); + } + } + + return vc; +} + +AudioContentList +ContentPanel::selected_audio () +{ + ContentList c = selected (); + AudioContentList ac; + + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + shared_ptr<AudioContent> t = dynamic_pointer_cast<AudioContent> (*i); + if (t) { + ac.push_back (t); + } + } + + return ac; +} + +SubtitleContentList +ContentPanel::selected_subtitle () +{ + ContentList c = selected (); + SubtitleContentList sc; + + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + shared_ptr<SubtitleContent> t = dynamic_pointer_cast<SubtitleContent> (*i); + if (t) { + sc.push_back (t); + } + } + + return sc; +} + +FFmpegContentList +ContentPanel::selected_ffmpeg () +{ + ContentList c = selected (); + FFmpegContentList sc; + + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (*i); + if (t) { + sc.push_back (t); + } + } + + return sc; +} + +void +ContentPanel::sequence_video_changed () +{ + if (!_film) { + return; + } + + _film->set_sequence_video (_sequence_video->GetValue ()); +} + +void +ContentPanel::film_changed (Film::Property p) +{ + switch (p) { + case Film::CONTENT: + setup (); + break; + case Film::SEQUENCE_VIDEO: + checked_set (_sequence_video, _film->sequence_video ()); + break; + default: + break; + } + + for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { + (*i)->film_changed (p); + } +} + +void +ContentPanel::selection_changed () +{ + setup_sensitivity (); + + for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { + (*i)->content_selection_changed (); + } +} + +void +ContentPanel::add_file_clicked () +{ + /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using + non-Latin filenames or paths. + */ + wxFileDialog* d = new wxFileDialog (_panel, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE | wxFD_CHANGE_DIR); + int const r = d->ShowModal (); + + if (r != wxID_OK) { + d->Destroy (); + return; + } + + wxArrayString paths; + d->GetPaths (paths); + + /* XXX: check for lots of files here and do something */ + + for (unsigned int i = 0; i < paths.GetCount(); ++i) { + _film->examine_and_add_content (content_factory (_film, wx_to_std (paths[i]))); + } + + d->Destroy (); +} + +void +ContentPanel::add_folder_clicked () +{ + wxDirDialog* d = new wxDirDialog (_panel, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST); + int const r = d->ShowModal (); + d->Destroy (); + + if (r != wxID_OK) { + return; + } + + shared_ptr<Content> content; + + try { + content.reset (new ImageContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ())))); + } catch (...) { + try { + content.reset (new DCPContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ())))); + } catch (...) { + error_dialog (_panel, _("Could not find any images nor a DCP in that folder")); + return; + } + } + + if (content) { + _film->examine_and_add_content (content); + } +} + +void +ContentPanel::remove_clicked () +{ + ContentList c = selected (); + if (c.size() == 1) { + _film->remove_content (c.front ()); + } + + selection_changed (); +} + +void +ContentPanel::timeline_clicked () +{ + if (_timeline_dialog) { + _timeline_dialog->Destroy (); + _timeline_dialog = 0; + } + + _timeline_dialog = new TimelineDialog (this, _film); + _timeline_dialog->Show (); +} + +void +ContentPanel::right_click (wxListEvent& ev) +{ + _menu->popup (_film, selected (), ev.GetPoint ()); +} + +/** Set up broad sensitivity based on the type of content that is selected */ +void +ContentPanel::setup_sensitivity () +{ + _add_file->Enable (_generally_sensitive); + _add_folder->Enable (_generally_sensitive); + + ContentList selection = selected (); + VideoContentList video_selection = selected_video (); + AudioContentList audio_selection = selected_audio (); + + _remove->Enable (selection.size() == 1 && _generally_sensitive); + _earlier->Enable (selection.size() == 1 && _generally_sensitive); + _later->Enable (selection.size() == 1 && _generally_sensitive); + _timeline->Enable (!_film->content().empty() && _generally_sensitive); + + _video_panel->Enable (video_selection.size() > 0 && _generally_sensitive); + _audio_panel->Enable (audio_selection.size() > 0 && _generally_sensitive); + _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<SubtitleContent> (selection.front()) && _generally_sensitive); + _timing_panel->Enable (selection.size() == 1 && _generally_sensitive); +} + +void +ContentPanel::set_film (shared_ptr<Film> f) +{ + _film = f; + + film_changed (Film::CONTENT); + selection_changed (); +} + +void +ContentPanel::set_general_sensitivity (bool s) +{ + _generally_sensitive = s; + + _content->Enable (s); + _add_file->Enable (s); + _add_folder->Enable (s); + _remove->Enable (s); + _earlier->Enable (s); + _later->Enable (s); + _timeline->Enable (s); + _sequence_video->Enable (s); + + /* Set the panels in the content notebook */ + for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { + (*i)->Enable (s); + } +} + +void +ContentPanel::earlier_clicked () +{ + ContentList sel = selected (); + if (sel.size() == 1) { + _film->move_content_earlier (sel.front ()); + selection_changed (); + } +} + +void +ContentPanel::later_clicked () +{ + ContentList sel = selected (); + if (sel.size() == 1) { + _film->move_content_later (sel.front ()); + selection_changed (); + } +} + +void +ContentPanel::set_selection (weak_ptr<Content> wc) +{ + ContentList content = _film->content (); + for (size_t i = 0; i < content.size(); ++i) { + if (content[i] == wc.lock ()) { + _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); + } else { + _content->SetItemState (i, 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED); + } + } +} + +void +ContentPanel::film_content_changed (int property) +{ + if (property == ContentProperty::PATH || property == ContentProperty::POSITION || property == DCPContentProperty::CAN_BE_PLAYED) { + setup (); + } + + for (list<ContentSubPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { + (*i)->film_content_changed (property); + } +} + +void +ContentPanel::setup () +{ + string selected_summary; + int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (s != -1) { + selected_summary = wx_to_std (_content->GetItemText (s)); + } + + _content->DeleteAllItems (); + + ContentList content = _film->content (); + sort (content.begin(), content.end(), ContentSorter ()); + + for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { + int const t = _content->GetItemCount (); + bool const valid = (*i)->paths_valid (); + shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (*i); + bool const needs_kdm = dcp && !dcp->can_be_played (); + + string s = (*i)->summary (); + + if (!valid) { + s = _("MISSING: ") + s; + } + + if (needs_kdm) { + s = _("NEEDS KDM: ") + s; + } + + _content->InsertItem (t, std_to_wx (s)); + + if ((*i)->summary() == selected_summary) { + _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); + } + + if (!valid || needs_kdm) { + _content->SetItemTextColour (t, *wxRED); + } + } + + if (selected_summary.empty () && !content.empty ()) { + /* Select the item of content if none was selected before */ + _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); + } +} + +void +ContentPanel::files_dropped (wxDropFilesEvent& event) +{ + if (!_film) { + return; + } + + wxString* paths = event.GetFiles (); + for (int i = 0; i < event.GetNumberOfFiles(); i++) { + _film->examine_and_add_content (content_factory (_film, wx_to_std (paths[i]))); + } +} diff --git a/src/wx/content_panel.h b/src/wx/content_panel.h new file mode 100644 index 000000000..ab198411d --- /dev/null +++ b/src/wx/content_panel.h @@ -0,0 +1,103 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <list> +#include <boost/shared_ptr.hpp> +#include "lib/types.h" +#include "lib/film.h" +#include "content_menu.h" + +class wxNotebook; +class wxPanel; +class wxSizer; +class wxListCtrl; +class wxListEvent; +class TimelineDialog; +class FilmEditor; +class ContentSubPanel; +class Film; + +class ContentPanel +{ +public: + ContentPanel (wxNotebook *, boost::shared_ptr<Film>); + + boost::shared_ptr<Film> film () const { + return _film; + } + + void set_film (boost::shared_ptr<Film> f); + void set_general_sensitivity (bool s); + void set_selection (boost::weak_ptr<Content>); + + void film_changed (Film::Property p); + void film_content_changed (int p); + + wxPanel* panel () const { + return _panel; + } + + wxNotebook* notebook () const { + return _notebook; + } + + ContentList selected (); + VideoContentList selected_video (); + AudioContentList selected_audio (); + SubtitleContentList selected_subtitle (); + FFmpegContentList selected_ffmpeg (); + + void add_file_clicked (); + +private: + void sequence_video_changed (); + void selection_changed (); + void add_folder_clicked (); + void remove_clicked (); + void earlier_clicked (); + void later_clicked (); + void right_click (wxListEvent &); + void files_dropped (wxDropFilesEvent &); + void timeline_clicked (); + + void setup (); + void setup_sensitivity (); + + wxPanel* _panel; + wxSizer* _sizer; + wxNotebook* _notebook; + wxListCtrl* _content; + wxButton* _add_file; + wxButton* _add_folder; + wxButton* _remove; + wxButton* _earlier; + wxButton* _later; + wxButton* _timeline; + wxCheckBox* _sequence_video; + ContentSubPanel* _video_panel; + ContentSubPanel* _audio_panel; + ContentSubPanel* _subtitle_panel; + ContentSubPanel* _timing_panel; + std::list<ContentSubPanel *> _panels; + ContentMenu* _menu; + TimelineDialog* _timeline_dialog; + + boost::shared_ptr<Film> _film; + bool _generally_sensitive; +}; diff --git a/src/wx/film_editor_panel.cc b/src/wx/content_sub_panel.cc index a637df1fe..7ea17c15c 100644 --- a/src/wx/film_editor_panel.cc +++ b/src/wx/content_sub_panel.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,17 +18,17 @@ */ #include <wx/notebook.h> -#include "film_editor_panel.h" -#include "film_editor.h" +#include "content_sub_panel.h" +#include "content_panel.h" using boost::shared_ptr; -FilmEditorPanel::FilmEditorPanel (FilmEditor* e, wxString name) - : wxPanel (e->content_notebook (), wxID_ANY) - , _editor (e) +ContentSubPanel::ContentSubPanel (ContentPanel* p, wxString name) + : wxPanel (p->notebook(), wxID_ANY) + , _parent (p) , _sizer (new wxBoxSizer (wxVERTICAL)) { - e->content_notebook()->AddPage (this, name, false); + p->notebook()->AddPage (this, name, false); SetSizer (_sizer); } diff --git a/src/wx/film_editor_panel.h b/src/wx/content_sub_panel.h index e0514ba99..5a1b739ef 100644 --- a/src/wx/film_editor_panel.h +++ b/src/wx/content_sub_panel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,20 +17,20 @@ */ -#ifndef DCPOMATIC_FILM_EDITOR_PANEL_H -#define DCPOMATIC_FILM_EDITOR_PANEL_H +#ifndef DCPOMATIC_CONTENT_SUB_PANEL_H +#define DCPOMATIC_CONTENT_SUB_PANEL_H #include <boost/shared_ptr.hpp> #include <wx/wx.h> #include "lib/film.h" -class FilmEditor; +class ContentPanel; class Content; -class FilmEditorPanel : public wxPanel +class ContentSubPanel : public wxPanel { public: - FilmEditorPanel (FilmEditor *, wxString); + ContentSubPanel (ContentPanel *, wxString); virtual void film_changed (Film::Property) {} /** Called when a given property of one of the selected Contents changes */ @@ -39,7 +39,7 @@ public: virtual void content_selection_changed () = 0; protected: - FilmEditor* _editor; + ContentPanel* _parent; wxSizer* _sizer; }; diff --git a/src/wx/content_widget.h b/src/wx/content_widget.h index ca9485006..b4d06286e 100644 --- a/src/wx/content_widget.h +++ b/src/wx/content_widget.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,16 +17,22 @@ */ +/** @file src/wx/content_widget.h + * @brief ContentWidget class. + */ + #ifndef DCPOMATIC_MULTIPLE_WIDGET_H #define DCPOMATIC_MULTIPLE_WIDGET_H #include <vector> #include <wx/wx.h> #include <wx/gbsizer.h> +#include <wx/spinctrl.h> #include <boost/function.hpp> #include "wx_util.h" -/** A widget which represents some Content state and which can be used +/** @class ContentWidget + * @brief A widget which represents some Content state and which can be used * when multiple pieces of content are selected. * * @param S Type containing the content being represented (e.g. VideoContent) @@ -97,11 +103,12 @@ public: } /** Add this widget to a wxGridBagSizer */ - void add (wxGridBagSizer* sizer, wxGBPosition position) + void add (wxGridBagSizer* sizer, wxGBPosition position, wxGBSpan span = wxDefaultSpan) { _sizer = sizer; _position = position; - _sizer->Add (_wrapped, _position); + _span = span; + _sizer->Add (_wrapped, _position, _span); } /** Update the view from the model */ @@ -145,7 +152,7 @@ private: _sizer->Detach (_button); _button->Hide (); - _sizer->Add (_wrapped, _position); + _sizer->Add (_wrapped, _position, _span); _wrapped->Show (); _sizer->Layout (); } @@ -159,7 +166,7 @@ private: _wrapped->Hide (); _sizer->Detach (_wrapped); _button->Show (); - _sizer->Add (_button, _position); + _sizer->Add (_button, _position, _span); _sizer->Layout (); } @@ -181,6 +188,7 @@ private: T* _wrapped; wxGridBagSizer* _sizer; wxGBPosition _position; + wxGBSpan _span; wxButton* _button; List _content; int _property; diff --git a/src/wx/dcp_panel.cc b/src/wx/dcp_panel.cc new file mode 100644 index 000000000..f042e5eb1 --- /dev/null +++ b/src/wx/dcp_panel.cc @@ -0,0 +1,633 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "dcp_panel.h" +#include "wx_util.h" +#include "isdcf_metadata_dialog.h" +#include "lib/ratio.h" +#include "lib/scaler.h" +#include "lib/config.h" +#include "lib/dcp_content_type.h" +#include "lib/util.h" +#include "lib/film.h" +#include "lib/ffmpeg_content.h" +#include <wx/wx.h> +#include <wx/notebook.h> +#include <wx/gbsizer.h> +#include <wx/spinctrl.h> +#include <boost/lexical_cast.hpp> + +using std::cout; +using std::list; +using std::string; +using std::vector; +using boost::lexical_cast; +using boost::shared_ptr; + +DCPPanel::DCPPanel (wxNotebook* n, boost::shared_ptr<Film> f) + : _film (f) + , _generally_sensitive (true) +{ + _panel = new wxPanel (n); + _sizer = new wxBoxSizer (wxVERTICAL); + _panel->SetSizer (_sizer); + + wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _sizer->Add (grid, 0, wxEXPAND | wxALL, 8); + + int r = 0; + + add_label_to_grid_bag_sizer (grid, _panel, _("Name"), true, wxGBPosition (r, 0)); + _name = new wxTextCtrl (_panel, wxID_ANY); + grid->Add (_name, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND | wxLEFT | wxRIGHT); + ++r; + + int flags = wxALIGN_CENTER_VERTICAL; +#ifdef __WXOSX__ + flags |= wxALIGN_RIGHT; +#endif + + _use_isdcf_name = new wxCheckBox (_panel, wxID_ANY, _("Use ISDCF name")); + grid->Add (_use_isdcf_name, wxGBPosition (r, 0), wxDefaultSpan, flags); + _edit_isdcf_button = new wxButton (_panel, wxID_ANY, _("Details...")); + grid->Add (_edit_isdcf_button, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, _panel, _("DCP Name"), true, wxGBPosition (r, 0)); + _dcp_name = new wxStaticText (_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END); + grid->Add (_dcp_name, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL | wxEXPAND); + ++r; + + add_label_to_grid_bag_sizer (grid, _panel, _("Content Type"), true, wxGBPosition (r, 0)); + _dcp_content_type = new wxChoice (_panel, wxID_ANY); + grid->Add (_dcp_content_type, wxGBPosition (r, 1)); + ++r; + + _notebook = new wxNotebook (_panel, wxID_ANY); + _sizer->Add (_notebook, 1, wxEXPAND | wxTOP, 6); + + _notebook->AddPage (make_video_panel (), _("Video"), false); + _notebook->AddPage (make_audio_panel (), _("Audio"), false); + + _signed = new wxCheckBox (_panel, wxID_ANY, _("Signed")); + grid->Add (_signed, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; + + _encrypted = new wxCheckBox (_panel, wxID_ANY, _("Encrypted")); + grid->Add (_encrypted, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; + + add_label_to_grid_bag_sizer (grid, _panel, _("Standard"), true, wxGBPosition (r, 0)); + _standard = new wxChoice (_panel, wxID_ANY); + grid->Add (_standard, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + ++r; + + _name->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&DCPPanel::name_changed, this)); + _use_isdcf_name->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&DCPPanel::use_isdcf_name_toggled, this)); + _edit_isdcf_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DCPPanel::edit_isdcf_button_clicked, this)); + _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::dcp_content_type_changed, this)); + _signed->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&DCPPanel::signed_toggled, this)); + _encrypted->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&DCPPanel::encrypted_toggled, this)); + _standard->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::standard_changed, this)); + + vector<DCPContentType const *> const ct = DCPContentType::all (); + for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) { + _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ())); + } + + _standard->Append (_("SMPTE")); + _standard->Append (_("Interop")); + + Config::instance()->Changed.connect (boost::bind (&DCPPanel::config_changed, this)); +} + +void +DCPPanel::name_changed () +{ + if (!_film) { + return; + } + + _film->set_name (string (_name->GetValue().mb_str())); +} + +void +DCPPanel::j2k_bandwidth_changed () +{ + if (!_film) { + return; + } + + _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000); +} + +void +DCPPanel::signed_toggled () +{ + if (!_film) { + return; + } + + _film->set_signed (_signed->GetValue ()); +} + +void +DCPPanel::burn_subtitles_toggled () +{ + if (!_film) { + return; + } + + _film->set_burn_subtitles (_burn_subtitles->GetValue ()); +} + +void +DCPPanel::encrypted_toggled () +{ + if (!_film) { + return; + } + + _film->set_encrypted (_encrypted->GetValue ()); +} + +/** Called when the frame rate choice widget has been changed */ +void +DCPPanel::frame_rate_choice_changed () +{ + if (!_film) { + return; + } + + _film->set_video_frame_rate ( + boost::lexical_cast<int> ( + wx_to_std (_frame_rate_choice->GetString (_frame_rate_choice->GetSelection ())) + ) + ); +} + +/** Called when the frame rate spin widget has been changed */ +void +DCPPanel::frame_rate_spin_changed () +{ + if (!_film) { + return; + } + + _film->set_video_frame_rate (_frame_rate_spin->GetValue ()); +} + +void +DCPPanel::audio_channels_changed () +{ + if (!_film) { + return; + } + + _film->set_audio_channels (_audio_channels->GetValue ()); +} + +void +DCPPanel::resolution_changed () +{ + if (!_film) { + return; + } + + _film->set_resolution (_resolution->GetSelection() == 0 ? RESOLUTION_2K : RESOLUTION_4K); +} + +void +DCPPanel::standard_changed () +{ + if (!_film) { + return; + } + + _film->set_interop (_standard->GetSelection() == 1); +} + +void +DCPPanel::film_changed (int p) +{ + switch (p) { + case Film::NONE: + break; + case Film::CONTAINER: + setup_container (); + break; + case Film::NAME: + checked_set (_name, _film->name()); + setup_dcp_name (); + break; + case Film::DCP_CONTENT_TYPE: + checked_set (_dcp_content_type, DCPContentType::as_index (_film->dcp_content_type ())); + setup_dcp_name (); + break; + case Film::SCALER: + checked_set (_scaler, Scaler::as_index (_film->scaler ())); + break; + case Film::BURN_SUBTITLES: + checked_set (_burn_subtitles, _film->burn_subtitles ()); + break; + case Film::SIGNED: + checked_set (_signed, _film->is_signed ()); + break; + case Film::ENCRYPTED: + checked_set (_encrypted, _film->encrypted ()); + if (_film->encrypted ()) { + _film->set_signed (true); + _signed->Enable (false); + } else { + _signed->Enable (_generally_sensitive); + } + break; + case Film::RESOLUTION: + checked_set (_resolution, _film->resolution() == RESOLUTION_2K ? 0 : 1); + setup_dcp_name (); + break; + case Film::J2K_BANDWIDTH: + checked_set (_j2k_bandwidth, _film->j2k_bandwidth() / 1000000); + break; + case Film::USE_ISDCF_NAME: + { + checked_set (_use_isdcf_name, _film->use_isdcf_name ()); + setup_dcp_name (); + bool const i = _film->use_isdcf_name (); + if (!i) { + _film->set_name (_film->isdcf_name (true)); + } + _edit_isdcf_button->Enable (i); + break; + } + case Film::ISDCF_METADATA: + setup_dcp_name (); + break; + case Film::VIDEO_FRAME_RATE: + { + bool done = false; + for (unsigned int i = 0; i < _frame_rate_choice->GetCount(); ++i) { + if (wx_to_std (_frame_rate_choice->GetString(i)) == boost::lexical_cast<string> (_film->video_frame_rate())) { + checked_set (_frame_rate_choice, i); + done = true; + break; + } + } + + if (!done) { + checked_set (_frame_rate_choice, -1); + } + + _frame_rate_spin->SetValue (_film->video_frame_rate ()); + + _best_frame_rate->Enable (_film->best_video_frame_rate () != _film->video_frame_rate ()); + break; + } + case Film::AUDIO_CHANNELS: + checked_set (_audio_channels, _film->audio_channels ()); + setup_dcp_name (); + break; + case Film::THREE_D: + checked_set (_three_d, _film->three_d ()); + setup_dcp_name (); + break; + case Film::INTEROP: + checked_set (_standard, _film->interop() ? 1 : 0); + break; + default: + break; + } +} + +void +DCPPanel::film_content_changed (int property) +{ + if (property == FFmpegContentProperty::AUDIO_STREAM || + property == SubtitleContentProperty::USE_SUBTITLES || + property == VideoContentProperty::VIDEO_SCALE) { + setup_dcp_name (); + } +} + + +void +DCPPanel::setup_container () +{ + int n = 0; + vector<Ratio const *> ratios = Ratio::all (); + vector<Ratio const *>::iterator i = ratios.begin (); + while (i != ratios.end() && *i != _film->container ()) { + ++i; + ++n; + } + + if (i == ratios.end()) { + checked_set (_container, -1); + } else { + checked_set (_container, n); + } + + setup_dcp_name (); +} + +/** Called when the container widget has been changed */ +void +DCPPanel::container_changed () +{ + if (!_film) { + return; + } + + int const n = _container->GetSelection (); + if (n >= 0) { + vector<Ratio const *> ratios = Ratio::all (); + assert (n < int (ratios.size())); + _film->set_container (ratios[n]); + } +} + +/** Called when the DCP content type widget has been changed */ +void +DCPPanel::dcp_content_type_changed () +{ + if (!_film) { + return; + } + + int const n = _dcp_content_type->GetSelection (); + if (n != wxNOT_FOUND) { + _film->set_dcp_content_type (DCPContentType::from_index (n)); + } +} + +void +DCPPanel::set_film (shared_ptr<Film> film) +{ + _film = film; + + film_changed (Film::NAME); + film_changed (Film::USE_ISDCF_NAME); + film_changed (Film::CONTENT); + film_changed (Film::DCP_CONTENT_TYPE); + film_changed (Film::CONTAINER); + film_changed (Film::RESOLUTION); + film_changed (Film::SCALER); + film_changed (Film::SIGNED); + film_changed (Film::BURN_SUBTITLES); + film_changed (Film::ENCRYPTED); + film_changed (Film::J2K_BANDWIDTH); + film_changed (Film::ISDCF_METADATA); + film_changed (Film::VIDEO_FRAME_RATE); + film_changed (Film::AUDIO_CHANNELS); + film_changed (Film::SEQUENCE_VIDEO); + film_changed (Film::THREE_D); + film_changed (Film::INTEROP); +} + +void +DCPPanel::set_general_sensitivity (bool s) +{ + _name->Enable (s); + _use_isdcf_name->Enable (s); + _edit_isdcf_button->Enable (s); + _dcp_content_type->Enable (s); + + bool si = s; + if (_film && _film->encrypted ()) { + si = false; + } + _burn_subtitles->Enable (s); + _signed->Enable (si); + + _encrypted->Enable (s); + _frame_rate_choice->Enable (s); + _frame_rate_spin->Enable (s); + _audio_channels->Enable (s); + _j2k_bandwidth->Enable (s); + _container->Enable (s); + _best_frame_rate->Enable (s && _film && _film->best_video_frame_rate () != _film->video_frame_rate ()); + _resolution->Enable (s); + _scaler->Enable (s); + _three_d->Enable (s); + _standard->Enable (s); +} + +/** Called when the scaler widget has been changed */ +void +DCPPanel::scaler_changed () +{ + if (!_film) { + return; + } + + int const n = _scaler->GetSelection (); + if (n >= 0) { + _film->set_scaler (Scaler::from_index (n)); + } +} + +void +DCPPanel::use_isdcf_name_toggled () +{ + if (!_film) { + return; + } + + _film->set_use_isdcf_name (_use_isdcf_name->GetValue ()); +} + +void +DCPPanel::edit_isdcf_button_clicked () +{ + if (!_film) { + return; + } + + ISDCFMetadataDialog* d = new ISDCFMetadataDialog (_panel, _film->isdcf_metadata ()); + d->ShowModal (); + _film->set_isdcf_metadata (d->isdcf_metadata ()); + d->Destroy (); +} + +void +DCPPanel::setup_dcp_name () +{ + string s = _film->dcp_name (true); + if (s.length() > 28) { + _dcp_name->SetLabel (std_to_wx (s.substr (0, 28)) + N_("...")); + _dcp_name->SetToolTip (std_to_wx (s)); + } else { + _dcp_name->SetLabel (std_to_wx (s)); + } +} + +void +DCPPanel::best_frame_rate_clicked () +{ + if (!_film) { + return; + } + + _film->set_video_frame_rate (_film->best_video_frame_rate ()); +} + +void +DCPPanel::three_d_changed () +{ + if (!_film) { + return; + } + + _film->set_three_d (_three_d->GetValue ()); +} + +void +DCPPanel::config_changed () +{ + _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000); + setup_frame_rate_widget (); +} + +void +DCPPanel::setup_frame_rate_widget () +{ + if (Config::instance()->allow_any_dcp_frame_rate ()) { + _frame_rate_choice->Hide (); + _frame_rate_spin->Show (); + } else { + _frame_rate_choice->Show (); + _frame_rate_spin->Hide (); + } + + _frame_rate_sizer->Layout (); +} + +wxPanel * +DCPPanel::make_video_panel () +{ + wxPanel* panel = new wxPanel (_notebook); + wxSizer* sizer = new wxBoxSizer (wxVERTICAL); + wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + sizer->Add (grid, 0, wxALL, 8); + panel->SetSizer (sizer); + + int r = 0; + + add_label_to_grid_bag_sizer (grid, panel, _("Container"), true, wxGBPosition (r, 0)); + _container = new wxChoice (panel, wxID_ANY); + grid->Add (_container, wxGBPosition (r, 1)); + ++r; + + { + add_label_to_grid_bag_sizer (grid, panel, _("Frame Rate"), true, wxGBPosition (r, 0)); + _frame_rate_sizer = new wxBoxSizer (wxHORIZONTAL); + _frame_rate_choice = new wxChoice (panel, wxID_ANY); + _frame_rate_sizer->Add (_frame_rate_choice, 1, wxALIGN_CENTER_VERTICAL); + _frame_rate_spin = new wxSpinCtrl (panel, wxID_ANY); + _frame_rate_sizer->Add (_frame_rate_spin, 1, wxALIGN_CENTER_VERTICAL); + setup_frame_rate_widget (); + _best_frame_rate = new wxButton (panel, wxID_ANY, _("Use best")); + _frame_rate_sizer->Add (_best_frame_rate, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND); + grid->Add (_frame_rate_sizer, wxGBPosition (r, 1)); + } + ++r; + + _burn_subtitles = new wxCheckBox (panel, wxID_ANY, _("Burn subtitles into image")); + grid->Add (_burn_subtitles, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; + + _three_d = new wxCheckBox (panel, wxID_ANY, _("3D")); + grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; + + add_label_to_grid_bag_sizer (grid, panel, _("Resolution"), true, wxGBPosition (r, 0)); + _resolution = new wxChoice (panel, wxID_ANY); + grid->Add (_resolution, wxGBPosition (r, 1)); + ++r; + + { + add_label_to_grid_bag_sizer (grid, panel, _("JPEG2000 bandwidth"), true, wxGBPosition (r, 0)); + wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _j2k_bandwidth = new wxSpinCtrl (panel, wxID_ANY); + s->Add (_j2k_bandwidth, 1); + add_label_to_sizer (s, panel, _("Mbit/s"), false); + grid->Add (s, wxGBPosition (r, 1)); + } + ++r; + + add_label_to_grid_bag_sizer (grid, panel, _("Scaler"), true, wxGBPosition (r, 0)); + _scaler = new wxChoice (panel, wxID_ANY); + grid->Add (_scaler, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); + ++r; + + _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::container_changed, this)); + _scaler->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::scaler_changed, this)); + _frame_rate_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::frame_rate_choice_changed, this)); + _frame_rate_spin->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DCPPanel::frame_rate_spin_changed, this)); + _best_frame_rate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DCPPanel::best_frame_rate_clicked, this)); + _burn_subtitles->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&DCPPanel::burn_subtitles_toggled, this)); + _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DCPPanel::j2k_bandwidth_changed, this)); + _resolution->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&DCPPanel::resolution_changed, this)); + _three_d->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&DCPPanel::three_d_changed, this)); + + vector<Scaler const *> const sc = Scaler::all (); + for (vector<Scaler const *>::const_iterator i = sc.begin(); i != sc.end(); ++i) { + _scaler->Append (std_to_wx ((*i)->name())); + } + + vector<Ratio const *> const ratio = Ratio::all (); + for (vector<Ratio const *>::const_iterator i = ratio.begin(); i != ratio.end(); ++i) { + _container->Append (std_to_wx ((*i)->nickname ())); + } + + list<int> const dfr = Config::instance()->allowed_dcp_frame_rates (); + for (list<int>::const_iterator i = dfr.begin(); i != dfr.end(); ++i) { + _frame_rate_choice->Append (std_to_wx (boost::lexical_cast<string> (*i))); + } + + _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000); + _frame_rate_spin->SetRange (1, 480); + + _resolution->Append (_("2K")); + _resolution->Append (_("4K")); + + return panel; +} + +wxPanel * +DCPPanel::make_audio_panel () +{ + wxPanel* panel = new wxPanel (_notebook); + wxSizer* sizer = new wxBoxSizer (wxVERTICAL); + wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + sizer->Add (grid, 0, wxALL, 8); + panel->SetSizer (sizer); + + int r = 0; + add_label_to_grid_bag_sizer (grid, panel, _("Channels"), true, wxGBPosition (r, 0)); + _audio_channels = new wxSpinCtrl (panel, wxID_ANY); + grid->Add (_audio_channels, wxGBPosition (r, 1)); + ++r; + + _audio_channels->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&DCPPanel::audio_channels_changed, this)); + + _audio_channels->SetRange (0, MAX_DCP_AUDIO_CHANNELS); + + return panel; +} diff --git a/src/wx/dcp_panel.h b/src/wx/dcp_panel.h new file mode 100644 index 000000000..88a9c4c51 --- /dev/null +++ b/src/wx/dcp_panel.h @@ -0,0 +1,106 @@ +/* + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <boost/shared_ptr.hpp> + +class wxNotebook; +class wxPanel; +class wxBoxSizer; +class wxTextCtrl; +class wxStaticText; +class wxCheckBox; +class wxChoice; +class wxButton; +class wxSpinCtrl; +class wxSizer; + +class Film; + +class DCPPanel +{ +public: + DCPPanel (wxNotebook *, boost::shared_ptr<Film>); + + void set_film (boost::shared_ptr<Film>); + void set_general_sensitivity (bool); + + void film_changed (int); + void film_content_changed (int); + + wxPanel* panel () const { + return _panel; + } + +private: + void name_changed (); + void use_isdcf_name_toggled (); + void edit_isdcf_button_clicked (); + void container_changed (); + void dcp_content_type_changed (); + void scaler_changed (); + void j2k_bandwidth_changed (); + void frame_rate_choice_changed (); + void frame_rate_spin_changed (); + void best_frame_rate_clicked (); + void content_timeline_clicked (); + void audio_channels_changed (); + void resolution_changed (); + void three_d_changed (); + void standard_changed (); + void signed_toggled (); + void burn_subtitles_toggled (); + void encrypted_toggled (); + + void setup_frame_rate_widget (); + void setup_container (); + void setup_dcp_name (); + + wxPanel* make_general_panel (); + wxPanel* make_video_panel (); + wxPanel* make_audio_panel (); + + void config_changed (); + + wxPanel* _panel; + wxNotebook* _notebook; + wxBoxSizer* _sizer; + + wxTextCtrl* _name; + wxStaticText* _dcp_name; + wxCheckBox* _use_isdcf_name; + wxChoice* _container; + wxButton* _edit_isdcf_button; + wxChoice* _scaler; + wxSpinCtrl* _j2k_bandwidth; + wxChoice* _dcp_content_type; + wxChoice* _frame_rate_choice; + wxSpinCtrl* _frame_rate_spin; + wxSizer* _frame_rate_sizer; + wxSpinCtrl* _audio_channels; + wxButton* _best_frame_rate; + wxCheckBox* _three_d; + wxChoice* _resolution; + wxChoice* _standard; + wxCheckBox* _signed; + wxCheckBox* _burn_subtitles; + wxCheckBox* _encrypted; + + boost::shared_ptr<Film> _film; + bool _generally_sensitive; +}; diff --git a/src/wx/editable_list.h b/src/wx/editable_list.h index 5772f6391..481a14741 100644 --- a/src/wx/editable_list.h +++ b/src/wx/editable_list.h @@ -43,7 +43,7 @@ public: wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); table->AddGrowableCol (0, 1); - s->Add (table, 1, wxALL | wxEXPAND, 8); + s->Add (table, 1, wxEXPAND); _list = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (columns.size() * 200, height), wxLC_REPORT | wxLC_SINGLE_SEL); diff --git a/src/wx/film_editor.cc b/src/wx/film_editor.cc index e73b27267..7f9461d94 100644 --- a/src/wx/film_editor.cc +++ b/src/wx/film_editor.cc @@ -40,21 +40,22 @@ #include "lib/ffmpeg_content.h" #include "lib/sndfile_content.h" #include "lib/dcp_content_type.h" -#include "lib/sound_processor.h" #include "lib/scaler.h" #include "lib/playlist.h" #include "lib/content.h" #include "lib/content_factory.h" +#include "lib/dcp_content.h" #include "lib/safe_stringstream.h" #include "timecode.h" #include "wx_util.h" #include "film_editor.h" -#include "isdcf_metadata_dialog.h" #include "timeline_dialog.h" #include "timing_panel.h" #include "subtitle_panel.h" #include "audio_panel.h" #include "video_panel.h" +#include "content_panel.h" +#include "dcp_panel.h" using std::string; using std::cout; @@ -72,343 +73,26 @@ using boost::lexical_cast; /** @param f Film to edit */ FilmEditor::FilmEditor (wxWindow* parent) : wxPanel (parent) - , _menu (this) - , _generally_sensitive (true) - , _timeline_dialog (0) { wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); _main_notebook = new wxNotebook (this, wxID_ANY); s->Add (_main_notebook, 1); - make_content_panel (); - _main_notebook->AddPage (_content_panel, _("Content"), true); - make_dcp_panel (); - _main_notebook->AddPage (_dcp_panel, _("DCP"), false); + _content_panel = new ContentPanel (_main_notebook, _film); + _main_notebook->AddPage (_content_panel->panel (), _("Content"), true); + _dcp_panel = new DCPPanel (_main_notebook, _film); + _main_notebook->AddPage (_dcp_panel->panel (), _("DCP"), false); - connect_to_widgets (); - JobManager::instance()->ActiveJobsChanged.connect ( bind (&FilmEditor::active_jobs_changed, this, _1) ); - Config::instance()->Changed.connect (boost::bind (&FilmEditor::config_changed, this)); - set_film (shared_ptr<Film> ()); - SetSizerAndFit (s); -} - -void -FilmEditor::make_dcp_panel () -{ - _dcp_panel = new wxPanel (_main_notebook); - _dcp_sizer = new wxBoxSizer (wxVERTICAL); - _dcp_panel->SetSizer (_dcp_sizer); - - wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); - _dcp_sizer->Add (grid, 0, wxEXPAND | wxALL, 8); - - int r = 0; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Name"), true, wxGBPosition (r, 0)); - _name = new wxTextCtrl (_dcp_panel, wxID_ANY); - grid->Add (_name, wxGBPosition(r, 1), wxDefaultSpan, wxEXPAND | wxLEFT | wxRIGHT); - ++r; - - int flags = wxALIGN_CENTER_VERTICAL; -#ifdef __WXOSX__ - flags |= wxALIGN_RIGHT; -#endif - - _use_isdcf_name = new wxCheckBox (_dcp_panel, wxID_ANY, _("Use ISDCF name")); - grid->Add (_use_isdcf_name, wxGBPosition (r, 0), wxDefaultSpan, flags); - _edit_isdcf_button = new wxButton (_dcp_panel, wxID_ANY, _("Details...")); - grid->Add (_edit_isdcf_button, wxGBPosition (r, 1), wxDefaultSpan); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("DCP Name"), true, wxGBPosition (r, 0)); - _dcp_name = new wxStaticText (_dcp_panel, wxID_ANY, wxT ("")); - grid->Add (_dcp_name, wxGBPosition(r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Container"), true, wxGBPosition (r, 0)); - _container = new wxChoice (_dcp_panel, wxID_ANY); - grid->Add (_container, wxGBPosition (r, 1), wxDefaultSpan, wxEXPAND); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Content Type"), true, wxGBPosition (r, 0)); - _dcp_content_type = new wxChoice (_dcp_panel, wxID_ANY); - grid->Add (_dcp_content_type, wxGBPosition (r, 1)); - ++r; - - { - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Frame Rate"), true, wxGBPosition (r, 0)); - _frame_rate_sizer = new wxBoxSizer (wxHORIZONTAL); - _frame_rate_choice = new wxChoice (_dcp_panel, wxID_ANY); - _frame_rate_sizer->Add (_frame_rate_choice, 1, wxALIGN_CENTER_VERTICAL); - _frame_rate_spin = new wxSpinCtrl (_dcp_panel, wxID_ANY); - _frame_rate_sizer->Add (_frame_rate_spin, 1, wxALIGN_CENTER_VERTICAL); - setup_frame_rate_widget (); - _best_frame_rate = new wxButton (_dcp_panel, wxID_ANY, _("Use best")); - _frame_rate_sizer->Add (_best_frame_rate, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND); - grid->Add (_frame_rate_sizer, wxGBPosition (r, 1)); - } - ++r; - - _signed = new wxCheckBox (_dcp_panel, wxID_ANY, _("Signed")); - grid->Add (_signed, wxGBPosition (r, 0), wxGBSpan (1, 2)); - ++r; - _encrypted = new wxCheckBox (_dcp_panel, wxID_ANY, _("Encrypted")); - grid->Add (_encrypted, wxGBPosition (r, 0), wxGBSpan (1, 2)); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Audio channels"), true, wxGBPosition (r, 0)); - _audio_channels = new wxSpinCtrl (_dcp_panel, wxID_ANY); - grid->Add (_audio_channels, wxGBPosition (r, 1)); - ++r; - - _three_d = new wxCheckBox (_dcp_panel, wxID_ANY, _("3D")); - grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2)); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Resolution"), true, wxGBPosition (r, 0)); - _resolution = new wxChoice (_dcp_panel, wxID_ANY); - grid->Add (_resolution, wxGBPosition (r, 1)); - ++r; - - { - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("JPEG2000 bandwidth"), true, wxGBPosition (r, 0)); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _j2k_bandwidth = new wxSpinCtrl (_dcp_panel, wxID_ANY); - s->Add (_j2k_bandwidth, 1); - add_label_to_sizer (s, _dcp_panel, _("Mbit/s"), false); - grid->Add (s, wxGBPosition (r, 1)); - } - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Standard"), true, wxGBPosition (r, 0)); - _standard = new wxChoice (_dcp_panel, wxID_ANY); - grid->Add (_standard, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - ++r; - - add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Scaler"), true, wxGBPosition (r, 0)); - _scaler = new wxChoice (_dcp_panel, wxID_ANY); - grid->Add (_scaler, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - ++r; - - vector<Scaler const *> const sc = Scaler::all (); - for (vector<Scaler const *>::const_iterator i = sc.begin(); i != sc.end(); ++i) { - _scaler->Append (std_to_wx ((*i)->name())); - } - - vector<Ratio const *> const ratio = Ratio::all (); - for (vector<Ratio const *>::const_iterator i = ratio.begin(); i != ratio.end(); ++i) { - _container->Append (std_to_wx ((*i)->nickname ())); - } - - vector<DCPContentType const *> const ct = DCPContentType::all (); - for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) { - _dcp_content_type->Append (std_to_wx ((*i)->pretty_name ())); - } - - list<int> const dfr = Config::instance()->allowed_dcp_frame_rates (); - for (list<int>::const_iterator i = dfr.begin(); i != dfr.end(); ++i) { - _frame_rate_choice->Append (std_to_wx (boost::lexical_cast<string> (*i))); - } - - _audio_channels->SetRange (0, MAX_DCP_AUDIO_CHANNELS); - _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000); - _frame_rate_spin->SetRange (1, 480); - - _resolution->Append (_("2K")); - _resolution->Append (_("4K")); - - _standard->Append (_("SMPTE")); - _standard->Append (_("Interop")); -} - -void -FilmEditor::connect_to_widgets () -{ - _name->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&FilmEditor::name_changed, this)); - _use_isdcf_name->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::use_isdcf_name_toggled, this)); - _edit_isdcf_button->Bind(wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::edit_isdcf_button_clicked, this)); - _container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::container_changed, this)); - _content->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&FilmEditor::content_selection_changed, this)); - _content->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&FilmEditor::content_selection_changed, this)); - _content->Bind (wxEVT_COMMAND_LIST_ITEM_RIGHT_CLICK, boost::bind (&FilmEditor::content_right_click, this, _1)); - _content->Bind (wxEVT_DROP_FILES, boost::bind (&FilmEditor::content_files_dropped, this, _1)); - _content_add_file->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_add_file_clicked, this)); - _content_add_folder->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_add_folder_clicked, this)); - _content_remove->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_remove_clicked, this)); - _content_earlier->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_earlier_clicked, this)); - _content_later->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_later_clicked, this)); - _content_timeline->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::content_timeline_clicked, this)); - _scaler->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::scaler_changed, this)); - _dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::dcp_content_type_changed, this)); - _frame_rate_choice->Bind(wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::frame_rate_choice_changed, this)); - _frame_rate_spin->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&FilmEditor::frame_rate_spin_changed, this)); - _best_frame_rate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::best_frame_rate_clicked, this)); - _signed->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::signed_toggled, this)); - _encrypted->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::encrypted_toggled, this)); - _audio_channels->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&FilmEditor::audio_channels_changed, this)); - _j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&FilmEditor::j2k_bandwidth_changed, this)); - _resolution->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::resolution_changed, this)); - _sequence_video->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::sequence_video_changed, this)); - _three_d->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::three_d_changed, this)); - _standard->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::standard_changed, this)); -} - -void -FilmEditor::make_content_panel () -{ - _content_panel = new wxPanel (_main_notebook); - _content_sizer = new wxBoxSizer (wxVERTICAL); - _content_panel->SetSizer (_content_sizer); - - { - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - - _content = new wxListCtrl (_content_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER); - s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6); - - _content->InsertColumn (0, wxT("")); - _content->SetColumnWidth (0, 512); - - wxBoxSizer* b = new wxBoxSizer (wxVERTICAL); - _content_add_file = new wxButton (_content_panel, wxID_ANY, _("Add file(s)...")); - b->Add (_content_add_file, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - _content_add_folder = new wxButton (_content_panel, wxID_ANY, _("Add folder...")); - b->Add (_content_add_folder, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - _content_remove = new wxButton (_content_panel, wxID_ANY, _("Remove")); - b->Add (_content_remove, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - _content_earlier = new wxButton (_content_panel, wxID_ANY, _("Up")); - b->Add (_content_earlier, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - _content_later = new wxButton (_content_panel, wxID_ANY, _("Down")); - b->Add (_content_later, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - _content_timeline = new wxButton (_content_panel, wxID_ANY, _("Timeline...")); - b->Add (_content_timeline, 1, wxEXPAND | wxALL, DCPOMATIC_BUTTON_STACK_GAP); - - s->Add (b, 0, wxALL, 4); - - _content_sizer->Add (s, 0, wxEXPAND | wxALL, 6); - } - - _sequence_video = new wxCheckBox (_content_panel, wxID_ANY, _("Keep video in sequence")); - _content_sizer->Add (_sequence_video); - - _content_notebook = new wxNotebook (_content_panel, wxID_ANY); - _content_sizer->Add (_content_notebook, 1, wxEXPAND | wxTOP, 6); - - _video_panel = new VideoPanel (this); - _panels.push_back (_video_panel); - _audio_panel = new AudioPanel (this); - _panels.push_back (_audio_panel); - _subtitle_panel = new SubtitlePanel (this); - _panels.push_back (_subtitle_panel); - _timing_panel = new TimingPanel (this); - _panels.push_back (_timing_panel); - - _content->DragAcceptFiles (true); -} - -/** Called when the name widget has been changed */ -void -FilmEditor::name_changed () -{ - if (!_film) { - return; - } - - _film->set_name (string (_name->GetValue().mb_str())); -} - -void -FilmEditor::j2k_bandwidth_changed () -{ - if (!_film) { - return; - } - - _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1000000); -} - -void -FilmEditor::signed_toggled () -{ - if (!_film) { - return; - } - - _film->set_signed (_signed->GetValue ()); -} - -void -FilmEditor::encrypted_toggled () -{ - if (!_film) { - return; - } - - _film->set_encrypted (_encrypted->GetValue ()); -} - -/** Called when the frame rate choice widget has been changed */ -void -FilmEditor::frame_rate_choice_changed () -{ - if (!_film) { - return; - } - - _film->set_video_frame_rate ( - boost::lexical_cast<int> ( - wx_to_std (_frame_rate_choice->GetString (_frame_rate_choice->GetSelection ())) - ) - ); -} - -/** Called when the frame rate spin widget has been changed */ -void -FilmEditor::frame_rate_spin_changed () -{ - if (!_film) { - return; - } - - _film->set_video_frame_rate (_frame_rate_spin->GetValue ()); -} - -void -FilmEditor::audio_channels_changed () -{ - if (!_film) { - return; - } - - _film->set_audio_channels (_audio_channels->GetValue ()); -} - -void -FilmEditor::resolution_changed () -{ - if (!_film) { - return; - } - - _film->set_resolution (_resolution->GetSelection() == 0 ? RESOLUTION_2K : RESOLUTION_4K); + SetSizerAndFit (s); } -void -FilmEditor::standard_changed () -{ - if (!_film) { - return; - } - - _film->set_interop (_standard->GetSelection() == 1); -} /** Called when the metadata stored in the Film object has changed; * so that we can update the GUI. @@ -423,97 +107,8 @@ FilmEditor::film_changed (Film::Property p) return; } - SafeStringStream s; - - for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { - (*i)->film_changed (p); - } - - switch (p) { - case Film::NONE: - break; - case Film::CONTENT: - setup_content (); - break; - case Film::CONTAINER: - setup_container (); - break; - case Film::NAME: - checked_set (_name, _film->name()); - setup_dcp_name (); - break; - case Film::WITH_SUBTITLES: - setup_dcp_name (); - break; - case Film::DCP_CONTENT_TYPE: - checked_set (_dcp_content_type, DCPContentType::as_index (_film->dcp_content_type ())); - setup_dcp_name (); - break; - case Film::SCALER: - checked_set (_scaler, Scaler::as_index (_film->scaler ())); - break; - case Film::SIGNED: - checked_set (_signed, _film->is_signed ()); - break; - case Film::ENCRYPTED: - checked_set (_encrypted, _film->encrypted ()); - if (_film->encrypted ()) { - _film->set_signed (true); - _signed->Enable (false); - } else { - _signed->Enable (_generally_sensitive); - } - break; - case Film::RESOLUTION: - checked_set (_resolution, _film->resolution() == RESOLUTION_2K ? 0 : 1); - setup_dcp_name (); - break; - case Film::J2K_BANDWIDTH: - checked_set (_j2k_bandwidth, _film->j2k_bandwidth() / 1000000); - break; - case Film::USE_ISDCF_NAME: - checked_set (_use_isdcf_name, _film->use_isdcf_name ()); - setup_dcp_name (); - use_isdcf_name_changed (); - break; - case Film::ISDCF_METADATA: - setup_dcp_name (); - break; - case Film::VIDEO_FRAME_RATE: - { - bool done = false; - for (unsigned int i = 0; i < _frame_rate_choice->GetCount(); ++i) { - if (wx_to_std (_frame_rate_choice->GetString(i)) == boost::lexical_cast<string> (_film->video_frame_rate())) { - checked_set (_frame_rate_choice, i); - done = true; - break; - } - } - - if (!done) { - checked_set (_frame_rate_choice, -1); - } - - _frame_rate_spin->SetValue (_film->video_frame_rate ()); - - _best_frame_rate->Enable (_film->best_video_frame_rate () != _film->video_frame_rate ()); - break; - } - case Film::AUDIO_CHANNELS: - checked_set (_audio_channels, _film->audio_channels ()); - setup_dcp_name (); - break; - case Film::SEQUENCE_VIDEO: - checked_set (_sequence_video, _film->sequence_video ()); - break; - case Film::THREE_D: - checked_set (_three_d, _film->three_d ()); - setup_dcp_name (); - break; - case Film::INTEROP: - checked_set (_standard, _film->interop() ? 1 : 0); - break; - } + _content_panel->film_changed (p); + _dcp_panel->film_changed (p); } void @@ -528,69 +123,8 @@ FilmEditor::film_content_changed (int property) return; } - for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { - (*i)->film_content_changed (property); - } - - if (property == FFmpegContentProperty::AUDIO_STREAM) { - setup_dcp_name (); - } else if (property == ContentProperty::PATH) { - setup_content (); - } else if (property == ContentProperty::POSITION) { - setup_content (); - } else if (property == VideoContentProperty::VIDEO_SCALE) { - setup_dcp_name (); - } -} - -void -FilmEditor::setup_container () -{ - int n = 0; - vector<Ratio const *> ratios = Ratio::all (); - vector<Ratio const *>::iterator i = ratios.begin (); - while (i != ratios.end() && *i != _film->container ()) { - ++i; - ++n; - } - - if (i == ratios.end()) { - checked_set (_container, -1); - } else { - checked_set (_container, n); - } - - setup_dcp_name (); -} - -/** Called when the container widget has been changed */ -void -FilmEditor::container_changed () -{ - if (!_film) { - return; - } - - int const n = _container->GetSelection (); - if (n >= 0) { - vector<Ratio const *> ratios = Ratio::all (); - assert (n < int (ratios.size())); - _film->set_container (ratios[n]); - } -} - -/** Called when the DCP content type widget has been changed */ -void -FilmEditor::dcp_content_type_changed () -{ - if (!_film) { - return; - } - - int const n = _dcp_content_type->GetSelection (); - if (n != wxNOT_FOUND) { - _film->set_dcp_content_type (DCPContentType::from_index (n)); - } + _content_panel->film_content_changed (property); + _dcp_panel->film_content_changed (property); } /** Sets the Film that we are editing */ @@ -605,6 +139,9 @@ FilmEditor::set_film (shared_ptr<Film> f) _film = f; + _content_panel->set_film (_film); + _dcp_panel->set_film (_film); + if (_film) { _film->Changed.connect (bind (&FilmEditor::film_changed, this, _1)); _film->ContentChanged.connect (bind (&FilmEditor::film_content_changed, this, _2)); @@ -616,121 +153,16 @@ FilmEditor::set_film (shared_ptr<Film> f) FileChanged (""); } - film_changed (Film::NAME); - film_changed (Film::USE_ISDCF_NAME); - film_changed (Film::CONTENT); - film_changed (Film::DCP_CONTENT_TYPE); - film_changed (Film::CONTAINER); - film_changed (Film::RESOLUTION); - film_changed (Film::SCALER); - film_changed (Film::WITH_SUBTITLES); - film_changed (Film::SIGNED); - film_changed (Film::ENCRYPTED); - film_changed (Film::J2K_BANDWIDTH); - film_changed (Film::ISDCF_METADATA); - film_changed (Film::VIDEO_FRAME_RATE); - film_changed (Film::AUDIO_CHANNELS); - film_changed (Film::SEQUENCE_VIDEO); - film_changed (Film::THREE_D); - film_changed (Film::INTEROP); - if (!_film->content().empty ()) { - set_selection (_film->content().front ()); + _content_panel->set_selection (_film->content().front ()); } - - content_selection_changed (); } void FilmEditor::set_general_sensitivity (bool s) { - _generally_sensitive = s; - - /* Stuff in the Content / DCP tabs */ - _name->Enable (s); - _use_isdcf_name->Enable (s); - _edit_isdcf_button->Enable (s); - _content->Enable (s); - _content_add_file->Enable (s); - _content_add_folder->Enable (s); - _content_remove->Enable (s); - _content_earlier->Enable (s); - _content_later->Enable (s); - _content_timeline->Enable (s); - _dcp_content_type->Enable (s); - - bool si = s; - if (_film && _film->encrypted ()) { - si = false; - } - _signed->Enable (si); - - _encrypted->Enable (s); - _frame_rate_choice->Enable (s); - _frame_rate_spin->Enable (s); - _audio_channels->Enable (s); - _j2k_bandwidth->Enable (s); - _container->Enable (s); - _best_frame_rate->Enable (s && _film && _film->best_video_frame_rate () != _film->video_frame_rate ()); - _sequence_video->Enable (s); - _resolution->Enable (s); - _scaler->Enable (s); - _three_d->Enable (s); - _standard->Enable (s); - - /* Set the panels in the content notebook */ - for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { - (*i)->Enable (s); - } -} - -/** Called when the scaler widget has been changed */ -void -FilmEditor::scaler_changed () -{ - if (!_film) { - return; - } - - int const n = _scaler->GetSelection (); - if (n >= 0) { - _film->set_scaler (Scaler::from_index (n)); - } -} - -void -FilmEditor::use_isdcf_name_toggled () -{ - if (!_film) { - return; - } - - _film->set_use_isdcf_name (_use_isdcf_name->GetValue ()); -} - -void -FilmEditor::use_isdcf_name_changed () -{ - bool const i = _film->use_isdcf_name (); - - if (!i) { - _film->set_name (_film->isdcf_name (true)); - } - - _edit_isdcf_button->Enable (i); -} - -void -FilmEditor::edit_isdcf_button_clicked () -{ - if (!_film) { - return; - } - - ISDCFMetadataDialog* d = new ISDCFMetadataDialog (this, _film->isdcf_metadata ()); - d->ShowModal (); - _film->set_isdcf_metadata (d->isdcf_metadata ()); - d->Destroy (); + _content_panel->set_general_sensitivity (s); + _dcp_panel->set_general_sensitivity (s); } void @@ -738,356 +170,3 @@ FilmEditor::active_jobs_changed (bool a) { set_general_sensitivity (!a); } - -void -FilmEditor::setup_dcp_name () -{ - string s = _film->dcp_name (true); - if (s.length() > 28) { - _dcp_name->SetLabel (std_to_wx (s.substr (0, 28)) + N_("...")); - _dcp_name->SetToolTip (std_to_wx (s)); - } else { - _dcp_name->SetLabel (std_to_wx (s)); - } -} - -void -FilmEditor::best_frame_rate_clicked () -{ - if (!_film) { - return; - } - - _film->set_video_frame_rate (_film->best_video_frame_rate ()); -} - -void -FilmEditor::setup_content () -{ - string selected_summary; - int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - if (s != -1) { - selected_summary = wx_to_std (_content->GetItemText (s)); - } - - _content->DeleteAllItems (); - - ContentList content = _film->content (); - sort (content.begin(), content.end(), ContentSorter ()); - - for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { - int const t = _content->GetItemCount (); - bool const valid = (*i)->paths_valid (); - - string s = (*i)->summary (); - if (!valid) { - s = _("MISSING: ") + s; - } - - _content->InsertItem (t, std_to_wx (s)); - - if ((*i)->summary() == selected_summary) { - _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); - } - - if (!valid) { - _content->SetItemTextColour (t, *wxRED); - } - } - - if (selected_summary.empty () && !content.empty ()) { - /* Select the item of content if none was selected before */ - _content->SetItemState (0, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); - } -} - -void -FilmEditor::content_add_file_clicked () -{ - /* The wxFD_CHANGE_DIR here prevents a `could not set working directory' error 123 on Windows when using - non-Latin filenames or paths. - */ - wxFileDialog* d = new wxFileDialog (this, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE | wxFD_CHANGE_DIR); - int const r = d->ShowModal (); - - if (r != wxID_OK) { - d->Destroy (); - return; - } - - wxArrayString paths; - d->GetPaths (paths); - - /* XXX: check for lots of files here and do something */ - - for (unsigned int i = 0; i < paths.GetCount(); ++i) { - _film->examine_and_add_content (content_factory (_film, wx_to_std (paths[i]))); - } - - d->Destroy (); -} - -void -FilmEditor::content_add_folder_clicked () -{ - wxDirDialog* d = new wxDirDialog (this, _("Choose a folder"), wxT (""), wxDD_DIR_MUST_EXIST); - int const r = d->ShowModal (); - d->Destroy (); - - if (r != wxID_OK) { - return; - } - - shared_ptr<ImageContent> ic; - - try { - ic.reset (new ImageContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ())))); - } catch (FileError& e) { - error_dialog (this, std_to_wx (e.what ())); - return; - } - - _film->examine_and_add_content (ic); -} - -void -FilmEditor::content_remove_clicked () -{ - ContentList c = selected_content (); - for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - _film->remove_content (*i); - } - - content_selection_changed (); -} - -void -FilmEditor::content_selection_changed () -{ - setup_content_sensitivity (); - - for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { - (*i)->content_selection_changed (); - } -} - -/** Set up broad sensitivity based on the type of content that is selected */ -void -FilmEditor::setup_content_sensitivity () -{ - _content_add_file->Enable (_generally_sensitive); - _content_add_folder->Enable (_generally_sensitive); - - ContentList selection = selected_content (); - VideoContentList video_selection = selected_video_content (); - AudioContentList audio_selection = selected_audio_content (); - - _content_remove->Enable (!selection.empty() && _generally_sensitive); - _content_earlier->Enable (selection.size() == 1 && _generally_sensitive); - _content_later->Enable (selection.size() == 1 && _generally_sensitive); - _content_timeline->Enable (!_film->content().empty() && _generally_sensitive); - - _video_panel->Enable (!video_selection.empty() && _generally_sensitive); - _audio_panel->Enable (!audio_selection.empty() && _generally_sensitive); - _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<FFmpegContent> (selection.front()) && _generally_sensitive); - _timing_panel->Enable (!selection.empty() && _generally_sensitive); -} - -ContentList -FilmEditor::selected_content () -{ - ContentList sel; - - if (!_film) { - return sel; - } - - /* The list was populated using a sorted content list, so we must sort it here too - so that we can look up by index and get the right thing. - */ - ContentList content = _film->content (); - sort (content.begin(), content.end(), ContentSorter ()); - - long int s = -1; - while (true) { - s = _content->GetNextItem (s, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - if (s == -1) { - break; - } - - if (s < int (_film->content().size ())) { - sel.push_back (content[s]); - } - } - - return sel; -} - -VideoContentList -FilmEditor::selected_video_content () -{ - ContentList c = selected_content (); - VideoContentList vc; - - for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - shared_ptr<VideoContent> t = dynamic_pointer_cast<VideoContent> (*i); - if (t) { - vc.push_back (t); - } - } - - return vc; -} - -AudioContentList -FilmEditor::selected_audio_content () -{ - ContentList c = selected_content (); - AudioContentList ac; - - for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - shared_ptr<AudioContent> t = dynamic_pointer_cast<AudioContent> (*i); - if (t) { - ac.push_back (t); - } - } - - return ac; -} - -SubtitleContentList -FilmEditor::selected_subtitle_content () -{ - ContentList c = selected_content (); - SubtitleContentList sc; - - for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - shared_ptr<SubtitleContent> t = dynamic_pointer_cast<SubtitleContent> (*i); - if (t) { - sc.push_back (t); - } - } - - return sc; -} - -FFmpegContentList -FilmEditor::selected_ffmpeg_content () -{ - ContentList c = selected_content (); - FFmpegContentList sc; - - for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - shared_ptr<FFmpegContent> t = dynamic_pointer_cast<FFmpegContent> (*i); - if (t) { - sc.push_back (t); - } - } - - return sc; -} - -void -FilmEditor::content_timeline_clicked () -{ - if (_timeline_dialog) { - _timeline_dialog->Destroy (); - _timeline_dialog = 0; - } - - _timeline_dialog = new TimelineDialog (this, _film); - _timeline_dialog->Show (); -} - -void -FilmEditor::set_selection (weak_ptr<Content> wc) -{ - ContentList content = _film->content (); - for (size_t i = 0; i < content.size(); ++i) { - if (content[i] == wc.lock ()) { - _content->SetItemState (i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); - } else { - _content->SetItemState (i, 0, wxLIST_STATE_SELECTED); - } - } -} - -void -FilmEditor::sequence_video_changed () -{ - if (!_film) { - return; - } - - _film->set_sequence_video (_sequence_video->GetValue ()); -} - -void -FilmEditor::content_right_click (wxListEvent& ev) -{ - _menu.popup (_film, selected_content (), ev.GetPoint ()); -} - -void -FilmEditor::three_d_changed () -{ - if (!_film) { - return; - } - - _film->set_three_d (_three_d->GetValue ()); -} - -void -FilmEditor::content_earlier_clicked () -{ - ContentList sel = selected_content (); - if (sel.size() == 1) { - _film->move_content_earlier (sel.front ()); - content_selection_changed (); - } -} - -void -FilmEditor::content_later_clicked () -{ - ContentList sel = selected_content (); - if (sel.size() == 1) { - _film->move_content_later (sel.front ()); - content_selection_changed (); - } -} - -void -FilmEditor::config_changed () -{ - _j2k_bandwidth->SetRange (1, Config::instance()->maximum_j2k_bandwidth() / 1000000); - setup_frame_rate_widget (); -} - -void -FilmEditor::setup_frame_rate_widget () -{ - if (Config::instance()->allow_any_dcp_frame_rate ()) { - _frame_rate_choice->Hide (); - _frame_rate_spin->Show (); - } else { - _frame_rate_choice->Show (); - _frame_rate_spin->Hide (); - } - - _frame_rate_sizer->Layout (); -} - -void -FilmEditor::content_files_dropped (wxDropFilesEvent& event) -{ - if (!_film) { - return; - } - - wxString* paths = event.GetFiles (); - for (int i = 0; i < event.GetNumberOfFiles(); i++) { - _film->examine_and_add_content (content_factory (_film, wx_to_std (paths[i]))); - } -} diff --git a/src/wx/film_editor.h b/src/wx/film_editor.h index ba9ff6fa0..25749fffa 100644 --- a/src/wx/film_editor.h +++ b/src/wx/film_editor.h @@ -22,23 +22,15 @@ */ #include <wx/wx.h> -#include <wx/spinctrl.h> -#include <wx/filepicker.h> -#include <wx/collpane.h> #include <boost/signals2.hpp> #include "lib/film.h" -#include "content_menu.h" +class wxSpinCtrl; class wxNotebook; -class wxListCtrl; -class wxListEvent; -class wxGridBagSizer; class Film; -class TimelineDialog; class Ratio; -class Timecode; -class FilmEditorPanel; -class SubtitleContent; +class ContentPanel; +class DCPPanel; /** @class FilmEditor * @brief A wx widget to edit a film's metadata, and perform various functions. @@ -49,121 +41,30 @@ public: FilmEditor (wxWindow *); void set_film (boost::shared_ptr<Film>); - void set_selection (boost::weak_ptr<Content>); boost::signals2::signal<void (boost::filesystem::path)> FileChanged; /* Stuff for panels */ - - wxNotebook* content_notebook () const { - return _content_notebook; - } + ContentPanel* content_panel () const { + return _content_panel; + } + boost::shared_ptr<Film> film () const { return _film; } - ContentList selected_content (); - VideoContentList selected_video_content (); - AudioContentList selected_audio_content (); - SubtitleContentList selected_subtitle_content (); - FFmpegContentList selected_ffmpeg_content (); - - void content_add_file_clicked (); - -private: - void make_dcp_panel (); - void make_content_panel (); - void connect_to_widgets (); - - /* Handle changes to the view */ - void name_changed (); - void use_isdcf_name_toggled (); - void edit_isdcf_button_clicked (); - void content_selection_changed (); - void content_add_folder_clicked (); - void content_remove_clicked (); - void content_earlier_clicked (); - void content_later_clicked (); - void content_files_dropped (wxDropFilesEvent& event); - void container_changed (); - void dcp_content_type_changed (); - void scaler_changed (); - void j2k_bandwidth_changed (); - void frame_rate_choice_changed (); - void frame_rate_spin_changed (); - void best_frame_rate_clicked (); - void content_timeline_clicked (); - void audio_channels_changed (); - void resolution_changed (); - void sequence_video_changed (); - void content_right_click (wxListEvent &); - void three_d_changed (); - void standard_changed (); - void signed_toggled (); - void encrypted_toggled (); - /* Handle changes to the model */ void film_changed (Film::Property); void film_content_changed (int); - void use_isdcf_name_changed (); void set_general_sensitivity (bool); - void setup_dcp_name (); - void setup_content (); - void setup_container (); - void setup_content_sensitivity (); - void setup_frame_rate_widget (); - void active_jobs_changed (bool); - void config_changed (); - - FilmEditorPanel* _video_panel; - FilmEditorPanel* _audio_panel; - FilmEditorPanel* _subtitle_panel; - FilmEditorPanel* _timing_panel; - std::list<FilmEditorPanel *> _panels; wxNotebook* _main_notebook; - wxNotebook* _content_notebook; - wxPanel* _dcp_panel; - wxSizer* _dcp_sizer; - wxPanel* _content_panel; - wxSizer* _content_sizer; + ContentPanel* _content_panel; + DCPPanel* _dcp_panel; /** The film we are editing */ boost::shared_ptr<Film> _film; - wxTextCtrl* _name; - wxStaticText* _dcp_name; - wxCheckBox* _use_isdcf_name; - wxChoice* _container; - wxListCtrl* _content; - wxButton* _content_add_file; - wxButton* _content_add_folder; - wxButton* _content_remove; - wxButton* _content_earlier; - wxButton* _content_later; - wxButton* _content_timeline; - wxCheckBox* _sequence_video; - wxButton* _edit_isdcf_button; - wxChoice* _scaler; - wxSpinCtrl* _j2k_bandwidth; - wxChoice* _dcp_content_type; - wxChoice* _frame_rate_choice; - wxSpinCtrl* _frame_rate_spin; - wxSizer* _frame_rate_sizer; - wxSpinCtrl* _audio_channels; - wxButton* _best_frame_rate; - wxCheckBox* _three_d; - wxChoice* _resolution; - wxChoice* _standard; - wxCheckBox* _signed; - wxCheckBox* _encrypted; - - ContentMenu _menu; - - std::vector<Ratio const *> _ratios; - - bool _generally_sensitive; - TimelineDialog* _timeline_dialog; }; diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index 595fd4720..7ecba1903 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -24,6 +24,7 @@ #include <iostream> #include <iomanip> #include <wx/tglbtn.h> +#include <dcp/exceptions.h> #include "lib/film.h" #include "lib/ratio.h" #include "lib/util.h" @@ -34,9 +35,10 @@ #include "lib/examine_content_job.h" #include "lib/filter.h" #include "lib/player.h" -#include "lib/player_video_frame.h" +#include "lib/player_video.h" #include "lib/video_content.h" #include "lib/video_decoder.h" +#include "lib/timer.h" #include "film_viewer.h" #include "wx_util.h" @@ -51,18 +53,19 @@ using std::make_pair; using boost::shared_ptr; using boost::dynamic_pointer_cast; using boost::weak_ptr; -using libdcp::Size; +using dcp::Size; FilmViewer::FilmViewer (wxWindow* p) : wxPanel (p) , _panel (new wxPanel (this)) + , _outline_content (new wxCheckBox (this, wxID_ANY, _("Outline content"))) , _slider (new wxSlider (this, wxID_ANY, 0, 0, 4096)) , _back_button (new wxButton (this, wxID_ANY, wxT("<"))) , _forward_button (new wxButton (this, wxID_ANY, wxT(">"))) , _frame_number (new wxStaticText (this, wxID_ANY, wxT(""))) , _timecode (new wxStaticText (this, wxID_ANY, wxT(""))) , _play_button (new wxToggleButton (this, wxID_ANY, _("Play"))) - , _got_frame (false) + , _last_get_accurate (true) { #ifndef __WXOSX__ _panel->SetDoubleBuffered (true); @@ -75,6 +78,8 @@ FilmViewer::FilmViewer (wxWindow* p) _v_sizer->Add (_panel, 1, wxEXPAND); + _v_sizer->Add (_outline_content, 0, wxALL, DCPOMATIC_SIZER_GAP); + wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL); wxBoxSizer* time_sizer = new wxBoxSizer (wxVERTICAL); @@ -95,6 +100,7 @@ FilmViewer::FilmViewer (wxWindow* p) _panel->Bind (wxEVT_PAINT, boost::bind (&FilmViewer::paint_panel, this)); _panel->Bind (wxEVT_SIZE, boost::bind (&FilmViewer::panel_sized, this, _1)); + _outline_content->Bind(wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmViewer::refresh_panel, this)); _slider->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind (&FilmViewer::slider_moved, this)); _slider->Bind (wxEVT_SCROLL_PAGEUP, boost::bind (&FilmViewer::slider_moved, this)); _slider->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind (&FilmViewer::slider_moved, this)); @@ -108,6 +114,8 @@ FilmViewer::FilmViewer (wxWindow* p) JobManager::instance()->ActiveJobsChanged.connect ( bind (&FilmViewer::active_jobs_changed, this, _1) ); + + setup_sensitivity (); } void @@ -122,7 +130,7 @@ FilmViewer::set_film (shared_ptr<Film> f) _frame.reset (); _slider->SetValue (0); - set_position_text (0); + set_position_text (); if (!_film) { return; @@ -136,47 +144,73 @@ FilmViewer::set_film (shared_ptr<Film> f) return; } - _player->disable_audio (); - _player->Video.connect (boost::bind (&FilmViewer::process_video, this, _1, _3)); - _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1)); + _film_connection = _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1)); + + _player->set_approximate_size (); + _player_connection = _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1)); calculate_sizes (); - fetch_next_frame (); + get (_position, _last_get_accurate); + + setup_sensitivity (); +} + +void +FilmViewer::refresh_panel () +{ + _panel->Refresh (); + _panel->Update (); } void -FilmViewer::fetch_current_frame_again () +FilmViewer::get (DCPTime p, bool accurate) { if (!_player) { return; } - /* We could do this with a seek and a fetch_next_frame, but this is - a shortcut to make it quicker. - */ - - _got_frame = false; - if (!_player->repeat_last_video ()) { - fetch_next_frame (); + list<shared_ptr<PlayerVideo> > pvf = _player->get_video (p, accurate); + if (!pvf.empty ()) { + try { + _frame = pvf.front()->image (true); + _frame = _frame->scale (_frame->size(), Scaler::from_id ("fastbilinear"), PIX_FMT_RGB24, false); + _position = pvf.front()->time (); + _inter_position = pvf.front()->inter_position (); + _inter_size = pvf.front()->inter_size (); + } catch (dcp::DCPReadError& e) { + /* This can happen on the following sequence of events: + * - load encrypted DCP + * - add KDM + * - DCP is examined again, which sets its "playable" flag to 1 + * - as a side effect of the exam, the viewer is updated using the old pieces + * - the DCPDecoder in the old piece gives us an encrypted frame + * - then, the pieces are re-made (but too late). + * + * I hope there's a better way to handle this ... + */ + _frame.reset (); + _position = p; + } + } else { + _frame.reset (); + _position = p; } - - _panel->Refresh (); - _panel->Update (); + + set_position_text (); + refresh_panel (); + + _last_get_accurate = accurate; } void FilmViewer::timer () { - if (!_player) { - return; - } - - fetch_next_frame (); + get (_position + DCPTime::from_frames (1, _film->video_frame_rate ()), true); - Time const len = _film->length (); + DCPTime const len = _film->length (); - if (len) { - int const new_slider_position = 4096 * _player->video_position() / len; + if (len.get ()) { + int const new_slider_position = 4096 * _position.get() / len.get(); if (new_slider_position != _slider->GetValue()) { _slider->SetValue (new_slider_position); } @@ -212,22 +246,29 @@ FilmViewer::paint_panel () dc.SetPen (p); dc.SetBrush (b); dc.DrawRectangle (0, _out_size.height, _panel_size.width, _panel_size.height - _out_size.height); - } -} + } + if (_outline_content->GetValue ()) { + wxPen p (wxColour (255, 0, 0), 2); + dc.SetPen (p); + dc.SetBrush (*wxTRANSPARENT_BRUSH); + dc.DrawRectangle (_inter_position.x, _inter_position.y, _inter_size.width, _inter_size.height); + } +} void FilmViewer::slider_moved () { - if (_film && _player) { - Time t = _slider->GetValue() * _film->length() / 4096; - /* Ensure that we hit the end of the film at the end of the slider */ - if (t >= _film->length ()) { - t = _film->length() - _film->video_frames_to_time (1); - } - _player->seek (t, false); - fetch_next_frame (); + if (!_film) { + return; + } + + DCPTime t (_slider->GetValue() * _film->length().get() / 4096); + /* Ensure that we hit the end of the film at the end of the slider */ + if (t >= _film->length ()) { + t = _film->length() - DCPTime::from_frames (1, _film->video_frame_rate ()); } + get (t, false); } void @@ -235,8 +276,9 @@ FilmViewer::panel_sized (wxSizeEvent& ev) { _panel_size.width = ev.GetSize().GetWidth(); _panel_size.height = ev.GetSize().GetHeight(); + calculate_sizes (); - fetch_current_frame_again (); + get (_position, _last_get_accurate); } void @@ -254,17 +296,24 @@ FilmViewer::calculate_sizes () if (panel_ratio < film_ratio) { /* panel is less widscreen than the film; clamp width */ _out_size.width = _panel_size.width; - _out_size.height = _out_size.width / film_ratio; + _out_size.height = rint (_out_size.width / film_ratio); } else { /* panel is more widescreen than the film; clamp height */ _out_size.height = _panel_size.height; - _out_size.width = _out_size.height * film_ratio; + _out_size.width = rint (_out_size.height * film_ratio); } /* Catch silly values */ _out_size.width = max (64, _out_size.width); _out_size.height = max (64, _out_size.height); + /* The player will round its image size down to the next lowest 4 pixels + to speed up its scale, so do similar here to avoid black borders + around things. This is a bit of a hack. + */ + _out_size.width &= ~3; + _out_size.height &= ~3; + _player->set_video_container_size (_out_size); } @@ -289,20 +338,7 @@ FilmViewer::check_play_state () } void -FilmViewer::process_video (shared_ptr<PlayerVideoFrame> pvf, Time t) -{ - if (pvf->eyes() == EYES_RIGHT) { - return; - } - - _frame = pvf->image (); - _got_frame = true; - - set_position_text (t); -} - -void -FilmViewer::set_position_text (Time t) +FilmViewer::set_position_text () { if (!_film) { _frame_number->SetLabel ("0"); @@ -312,9 +348,9 @@ FilmViewer::set_position_text (Time t) double const fps = _film->video_frame_rate (); /* Count frame number from 1 ... not sure if this is the best idea */ - _frame_number->SetLabel (wxString::Format (wxT("%d"), int (rint (t * fps / TIME_HZ)) + 1)); + _frame_number->SetLabel (wxString::Format (wxT("%d"), int (rint (_position.seconds() * fps)) + 1)); - double w = static_cast<double>(t) / TIME_HZ; + double w = _position.seconds (); int const h = (w / 3600); w -= h * 3600; int const m = (w / 60); @@ -325,35 +361,6 @@ FilmViewer::set_position_text (Time t) _timecode->SetLabel (wxString::Format (wxT("%02d:%02d:%02d.%02d"), h, m, s, f)); } -/** Ask the player to emit its next frame, then update our display */ -void -FilmViewer::fetch_next_frame () -{ - /* Clear our frame in case we don't get a new one */ - _frame.reset (); - - if (!_player) { - return; - } - - _got_frame = false; - - try { - while (!_got_frame && !_player->pass ()) {} - } catch (DecodeError& e) { - _play_button->SetValue (false); - check_play_state (); - error_dialog (this, wxString::Format (_("Could not decode video for view (%s)"), std_to_wx(e.what()).data())); - } catch (OpenFileError& e) { - /* There was a problem opening a content file; we'll let this slide as it - probably means a missing content file, which we're already taking care of. - */ - } - - _panel->Refresh (); - _panel->Update (); -} - void FilmViewer::active_jobs_changed (bool a) { @@ -377,31 +384,18 @@ FilmViewer::active_jobs_changed (bool a) void FilmViewer::back_clicked () { - if (!_player) { - return; + DCPTime p = _position - DCPTime::from_frames (1, _film->video_frame_rate ()); + if (p < DCPTime ()) { + p = DCPTime (); } - /* Player::video_position is the time after the last frame that we received. - We want to see the one before it, so we need to go back 2. - */ - - Time p = _player->video_position() - _film->video_frames_to_time (2); - if (p < 0) { - p = 0; - } - - _player->seek (p, true); - fetch_next_frame (); + get (p, true); } void FilmViewer::forward_clicked () { - if (!_player) { - return; - } - - fetch_next_frame (); + get (_position + DCPTime::from_frames (1, _film->video_frame_rate ()), true); } void @@ -412,5 +406,27 @@ FilmViewer::player_changed (bool frequent) } calculate_sizes (); - fetch_current_frame_again (); + get (_position, _last_get_accurate); +} + +void +FilmViewer::setup_sensitivity () +{ + bool const c = _film && !_film->content().empty (); + + _slider->Enable (c); + _back_button->Enable (c); + _forward_button->Enable (c); + _play_button->Enable (c); + _outline_content->Enable (c); + _frame_number->Enable (c); + _timecode->Enable (c); +} + +void +FilmViewer::film_changed (Film::Property p) +{ + if (p == Film::CONTENT) { + setup_sensitivity (); + } } diff --git a/src/wx/film_viewer.h b/src/wx/film_viewer.h index 337b68446..e502c6f45 100644 --- a/src/wx/film_viewer.h +++ b/src/wx/film_viewer.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,23 +28,10 @@ class wxToggleButton; class FFmpegPlayer; class Image; class RGBPlusAlphaImage; -class PlayerVideoFrame; +class PlayerVideo; /** @class FilmViewer * @brief A wx widget to view a preview of a Film. - * - * The film takes the following path through the viewer: - * - * 1. fetch_next_frame() asks our _player to decode some data. If it does, process_video() - * will be called. - * - * 2. process_video() takes the image from the player (_frame). - * - * 3. fetch_next_frame() calls _panel->Refresh() and _panel->Update() which results in - * paint_panel() being called; this creates frame_bitmap from _frame and blits it to the display. - * - * fetch_current_frame_again() asks the player to re-emit its current frame on the next pass(), and then - * starts from step #1. */ class FilmViewer : public wxPanel { @@ -59,22 +46,24 @@ private: void slider_moved (); void play_clicked (); void timer (); - void process_video (boost::shared_ptr<PlayerVideoFrame>, Time); void calculate_sizes (); void check_play_state (); - void fetch_current_frame_again (); - void fetch_next_frame (); void active_jobs_changed (bool); void back_clicked (); void forward_clicked (); void player_changed (bool); - void set_position_text (Time); + void set_position_text (); + void get (DCPTime, bool); + void refresh_panel (); + void setup_sensitivity (); + void film_changed (Film::Property); boost::shared_ptr<Film> _film; boost::shared_ptr<Player> _player; wxSizer* _v_sizer; wxPanel* _panel; + wxCheckBox* _outline_content; wxSlider* _slider; wxButton* _back_button; wxButton* _forward_button; @@ -84,10 +73,20 @@ private: wxTimer _timer; boost::shared_ptr<const Image> _frame; - bool _got_frame; + DCPTime _position; + Position<int> _inter_position; + dcp::Size _inter_size; /** Size of our output (including padding if we have any) */ - libdcp::Size _out_size; + dcp::Size _out_size; /** Size of the panel that we have available */ - libdcp::Size _panel_size; + dcp::Size _panel_size; + /** true if the last call to ::get() was specified to be accurate; + * this is used so that when re-fetching the current frame we + * can get the same one that we got last time. + */ + bool _last_get_accurate; + + boost::signals2::scoped_connection _film_connection; + boost::signals2::scoped_connection _player_connection; }; diff --git a/src/wx/kdm_dialog.cc b/src/wx/kdm_dialog.cc index ebecd234c..4334fd446 100644 --- a/src/wx/kdm_dialog.cc +++ b/src/wx/kdm_dialog.cc @@ -161,10 +161,10 @@ KDMDialog::KDMDialog (wxWindow* parent, boost::shared_ptr<const Film> film) add_label_to_sizer (table, this, _("KDM type"), true); _type = new wxChoice (this, wxID_ANY); - _type->Append ("Modified Transitional 1", ((void *) libdcp::KDM::MODIFIED_TRANSITIONAL_1)); + _type->Append ("Modified Transitional 1", ((void *) dcp::MODIFIED_TRANSITIONAL_1)); if (!film->interop ()) { - _type->Append ("DCI Any", ((void *) libdcp::KDM::DCI_ANY)); - _type->Append ("DCI Specific", ((void *) libdcp::KDM::DCI_SPECIFIC)); + _type->Append ("DCI Any", ((void *) dcp::DCI_ANY)); + _type->Append ("DCI Specific", ((void *) dcp::DCI_SPECIFIC)); } table->Add (_type, 1, wxEXPAND); _type->SetSelection (0); @@ -490,10 +490,10 @@ KDMDialog::write_to () const return _write_to->GetValue (); } -libdcp::KDM::Formulation +dcp::Formulation KDMDialog::formulation () const { - return (libdcp::KDM::Formulation) reinterpret_cast<intptr_t> (_type->GetClientData (_type->GetSelection())); + return (dcp::Formulation) reinterpret_cast<intptr_t> (_type->GetClientData (_type->GetSelection())); } void diff --git a/src/wx/kdm_dialog.h b/src/wx/kdm_dialog.h index 13e9196ea..0fc95db84 100644 --- a/src/wx/kdm_dialog.h +++ b/src/wx/kdm_dialog.h @@ -48,7 +48,7 @@ public: boost::filesystem::path cpl () const; boost::filesystem::path directory () const; bool write_to () const; - libdcp::KDM::Formulation formulation () const; + dcp::Formulation formulation () const; private: void add_cinema (boost::shared_ptr<Cinema>); diff --git a/src/wx/make_signer_chain_dialog.cc b/src/wx/make_signer_chain_dialog.cc new file mode 100644 index 000000000..8736f2456 --- /dev/null +++ b/src/wx/make_signer_chain_dialog.cc @@ -0,0 +1,35 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "make_signer_chain_dialog.h" + +MakeSignerChainDialog::MakeSignerChainDialog (wxWindow* parent) + : TableDialog (parent, _("Make certificate chain"), 2, true) +{ + add (_("Organisation"), true); + add (_organisation = new wxTextCtrl (this, wxID_ANY)); + add (_("Organisational unit"), true); + add (_organisational_unit = new wxTextCtrl (this, wxID_ANY)); + add (_("Root common name"), true); + add (_root_common_name = new wxTextCtrl (this, wxID_ANY)); + add (_("Intermediate common name"), true); + add (_intermediate_common_name = new wxTextCtrl (this, wxID_ANY)); + add (_("Leaf common name"), true); + add (_leaf_common_name = new wxTextCtrl (this, wxID_ANY)); +} diff --git a/src/wx/make_signer_chain_dialog.h b/src/wx/make_signer_chain_dialog.h new file mode 100644 index 000000000..fc6391a94 --- /dev/null +++ b/src/wx/make_signer_chain_dialog.h @@ -0,0 +1,56 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "table_dialog.h" +#include "wx_util.h" + +class MakeSignerChainDialog : public TableDialog +{ +public: + MakeSignerChainDialog (wxWindow* parent); + + std::string organisation () const { + return wx_to_std (_organisation->GetValue ()); + } + + std::string organisational_unit () const { + return wx_to_std (_organisational_unit->GetValue ()); + } + + std::string root_common_name () const { + return wx_to_std (_root_common_name->GetValue ()); + } + + std::string intermediate_common_name () const { + return wx_to_std (_intermediate_common_name->GetValue ()); + } + + std::string leaf_common_name () const { + return wx_to_std (_leaf_common_name->GetValue ()); + } + + +private: + wxTextCtrl* _organisation; + wxTextCtrl* _organisational_unit; + wxTextCtrl* _root_common_name; + wxTextCtrl* _intermediate_common_name; + wxTextCtrl* _leaf_common_name; +}; + diff --git a/src/wx/properties_dialog.cc b/src/wx/properties_dialog.cc index 53ca23755..27fc75b1b 100644 --- a/src/wx/properties_dialog.cc +++ b/src/wx/properties_dialog.cc @@ -45,8 +45,7 @@ PropertiesDialog::PropertiesDialog (wxWindow* parent, shared_ptr<Film> film) add (_("Frames already encoded"), true); _encoded = add (new ThreadedStaticText (this, _("counting..."), boost::bind (&PropertiesDialog::frames_already_encoded, this))); _encoded->Finished.connect (boost::bind (&PropertiesDialog::layout, this)); - - _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->time_to_video_frames (_film->length())))); + _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->length().frames (_film->video_frame_rate ())))); double const disk = double (_film->required_disk_space()) / 1073741824.0f; SafeStringStream s; s << fixed << setprecision (1) << disk << wx_to_std (_("Gb")); @@ -64,10 +63,11 @@ PropertiesDialog::frames_already_encoded () const } catch (boost::thread_interrupted &) { return ""; } - - if (_film->length()) { + + uint64_t const frames = _film->length().frames (_film->video_frame_rate ()); + if (frames) { /* XXX: encoded_frames() should check which frames have been encoded */ - u << " (" << (_film->encoded_frames() * 100 / _film->time_to_video_frames (_film->length())) << "%)"; + u << " (" << (_film->encoded_frames() * 100 / frames) << "%)"; } return u.str (); } diff --git a/src/wx/screen_dialog.cc b/src/wx/screen_dialog.cc index c69912716..503745683 100644 --- a/src/wx/screen_dialog.cc +++ b/src/wx/screen_dialog.cc @@ -19,7 +19,7 @@ #include <wx/filepicker.h> #include <wx/validate.h> -#include <libdcp/exceptions.h> +#include <dcp/exceptions.h> #include "lib/compose.hpp" #include "lib/util.h" #include "screen_dialog.h" @@ -29,9 +29,9 @@ using std::string; using std::cout; -using boost::shared_ptr; +using boost::optional; -ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, shared_ptr<libdcp::Certificate> certificate) +ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, optional<dcp::Certificate> certificate) : TableDialog (parent, std_to_wx (title), 2, true) , _certificate (certificate) { @@ -79,7 +79,7 @@ ScreenDialog::name () const return wx_to_std (_name->GetValue()); } -shared_ptr<libdcp::Certificate> +optional<dcp::Certificate> ScreenDialog::certificate () const { return _certificate; @@ -89,9 +89,9 @@ void ScreenDialog::load_certificate (boost::filesystem::path file) { try { - _certificate.reset (new libdcp::Certificate (file)); + _certificate = dcp::Certificate (dcp::file_to_string (file)); _certificate_text->SetValue (_certificate->certificate ()); - } catch (libdcp::MiscError& e) { + } catch (dcp::MiscError& e) { error_dialog (this, wxString::Format (_("Could not read certificate file (%s)"), e.what())); } } diff --git a/src/wx/screen_dialog.h b/src/wx/screen_dialog.h index 3601a8f6c..3e110d230 100644 --- a/src/wx/screen_dialog.h +++ b/src/wx/screen_dialog.h @@ -19,7 +19,7 @@ #include <wx/wx.h> #include <boost/shared_ptr.hpp> -#include <libdcp/certificates.h> +#include <dcp/certificates.h> #include "table_dialog.h" class Progress; @@ -27,10 +27,10 @@ class Progress; class ScreenDialog : public TableDialog { public: - ScreenDialog (wxWindow *, std::string, std::string name = "", boost::shared_ptr<libdcp::Certificate> c = boost::shared_ptr<libdcp::Certificate> ()); + ScreenDialog (wxWindow *, std::string, std::string name = "", boost::optional<dcp::Certificate> c = boost::optional<dcp::Certificate> ()); std::string name () const; - boost::shared_ptr<libdcp::Certificate> certificate () const; + boost::optional<dcp::Certificate> certificate () const; private: void select_certificate (); @@ -44,5 +44,5 @@ private: wxButton* _download_certificate; wxTextCtrl* _certificate_text; - boost::shared_ptr<libdcp::Certificate> _certificate; + boost::optional<dcp::Certificate> _certificate; }; diff --git a/src/wx/subtitle_panel.cc b/src/wx/subtitle_panel.cc index 7953682fc..21d6f8e5b 100644 --- a/src/wx/subtitle_panel.cc +++ b/src/wx/subtitle_panel.cc @@ -20,9 +20,16 @@ #include <boost/lexical_cast.hpp> #include <wx/spinctrl.h> #include "lib/ffmpeg_content.h" +#include "lib/subrip_content.h" +#include "lib/ffmpeg_subtitle_stream.h" +#include "lib/dcp_subtitle_content.h" +#include "lib/subrip_decoder.h" +#include "lib/dcp_subtitle_decoder.h" #include "subtitle_panel.h" #include "film_editor.h" #include "wx_util.h" +#include "subtitle_view.h" +#include "content_panel.h" using std::vector; using std::string; @@ -30,16 +37,17 @@ using boost::shared_ptr; using boost::lexical_cast; using boost::dynamic_pointer_cast; -SubtitlePanel::SubtitlePanel (FilmEditor* e) - : FilmEditorPanel (e, _("Subtitles")) +SubtitlePanel::SubtitlePanel (ContentPanel* p) + : ContentSubPanel (p, _("Subtitles")) + , _view (0) { wxFlexGridSizer* grid = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); _sizer->Add (grid, 0, wxALL, 8); - _with_subtitles = new wxCheckBox (this, wxID_ANY, _("With Subtitles")); - grid->Add (_with_subtitles, 1); + _use = new wxCheckBox (this, wxID_ANY, _("Use subtitles")); + grid->Add (_use); grid->AddSpacer (0); - + { add_label_to_sizer (grid, this, _("X Offset"), true); wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); @@ -79,41 +87,37 @@ SubtitlePanel::SubtitlePanel (FilmEditor* e) add_label_to_sizer (grid, this, _("Stream"), true); _stream = new wxChoice (this, wxID_ANY); grid->Add (_stream, 1, wxEXPAND); + + _view_button = new wxButton (this, wxID_ANY, _("View...")); + grid->Add (_view_button); _x_offset->SetRange (-100, 100); _y_offset->SetRange (-100, 100); _x_scale->SetRange (10, 1000); _y_scale->SetRange (10, 1000); - _with_subtitles->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&SubtitlePanel::with_subtitles_toggled, this)); - _x_offset->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_offset_changed, this)); - _y_offset->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_offset_changed, this)); - _x_scale->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_scale_changed, this)); - _y_scale->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_scale_changed, this)); - _stream->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&SubtitlePanel::stream_changed, this)); + _use->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&SubtitlePanel::use_toggled, this)); + _x_offset->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_offset_changed, this)); + _y_offset->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_offset_changed, this)); + _x_scale->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::x_scale_changed, this)); + _y_scale->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::y_scale_changed, this)); + _stream->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&SubtitlePanel::stream_changed, this)); + _view_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&SubtitlePanel::view_clicked, this)); } void SubtitlePanel::film_changed (Film::Property property) { - switch (property) { - case Film::CONTENT: - setup_sensitivity (); - break; - case Film::WITH_SUBTITLES: - checked_set (_with_subtitles, _editor->film()->with_subtitles ()); + if (property == Film::CONTENT) { setup_sensitivity (); - break; - default: - break; } } void SubtitlePanel::film_content_changed (int property) { - FFmpegContentList fc = _editor->selected_ffmpeg_content (); - SubtitleContentList sc = _editor->selected_subtitle_content (); + FFmpegContentList fc = _parent->selected_ffmpeg (); + SubtitleContentList sc = _parent->selected_subtitle (); shared_ptr<FFmpegContent> fcs; if (fc.size() == 1) { @@ -124,7 +128,7 @@ SubtitlePanel::film_content_changed (int property) if (sc.size() == 1) { scs = sc.front (); } - + if (property == FFmpegContentProperty::SUBTITLE_STREAMS) { _stream->Clear (); if (fcs) { @@ -140,6 +144,9 @@ SubtitlePanel::film_content_changed (int property) } } setup_sensitivity (); + } else if (property == SubtitleContentProperty::USE_SUBTITLES) { + checked_set (_use, scs ? scs->use_subtitles() : false); + setup_sensitivity (); } else if (property == SubtitleContentProperty::SUBTITLE_X_OFFSET) { checked_set (_x_offset, scs ? (scs->subtitle_x_offset() * 100) : 0); } else if (property == SubtitleContentProperty::SUBTITLE_Y_OFFSET) { @@ -152,37 +159,53 @@ SubtitlePanel::film_content_changed (int property) } void -SubtitlePanel::with_subtitles_toggled () +SubtitlePanel::use_toggled () { - if (!_editor->film()) { - return; + SubtitleContentList c = _parent->selected_subtitle (); + for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) { + (*i)->set_use_subtitles (_use->GetValue()); } - - _editor->film()->set_with_subtitles (_with_subtitles->GetValue ()); } void SubtitlePanel::setup_sensitivity () { - bool h = false; - bool j = false; - if (_editor->film()) { - h = _editor->film()->has_subtitles (); - j = _editor->film()->with_subtitles (); + int any_subs = 0; + int ffmpeg_subs = 0; + int subrip_or_dcp_subs = 0; + SubtitleContentList c = _parent->selected_subtitle (); + for (SubtitleContentList::const_iterator i = c.begin(); i != c.end(); ++i) { + shared_ptr<const FFmpegContent> fc = boost::dynamic_pointer_cast<const FFmpegContent> (*i); + shared_ptr<const SubRipContent> sc = boost::dynamic_pointer_cast<const SubRipContent> (*i); + shared_ptr<const DCPSubtitleContent> dsc = boost::dynamic_pointer_cast<const DCPSubtitleContent> (*i); + if (fc) { + if (fc->has_subtitles ()) { + ++ffmpeg_subs; + ++any_subs; + } + } else if (sc || dsc) { + ++subrip_or_dcp_subs; + ++any_subs; + } else { + ++any_subs; + } } + + _use->Enable (any_subs > 0); + bool const use = _use->GetValue (); - _with_subtitles->Enable (h); - _x_offset->Enable (j); - _y_offset->Enable (j); - _x_scale->Enable (j); - _y_scale->Enable (j); - _stream->Enable (j); + _x_offset->Enable (any_subs > 0 && use); + _y_offset->Enable (any_subs > 0 && use); + _x_scale->Enable (any_subs > 0 && use); + _y_scale->Enable (any_subs > 0 && use); + _stream->Enable (ffmpeg_subs == 1); + _view_button->Enable (subrip_or_dcp_subs == 1); } void SubtitlePanel::stream_changed () { - FFmpegContentList fc = _editor->selected_ffmpeg_content (); + FFmpegContentList fc = _parent->selected_ffmpeg (); if (fc.size() != 1) { return; } @@ -204,25 +227,25 @@ SubtitlePanel::stream_changed () void SubtitlePanel::x_offset_changed () { - SubtitleContentList c = _editor->selected_subtitle_content (); - if (c.size() == 1) { - c.front()->set_subtitle_x_offset (_x_offset->GetValue() / 100.0); + SubtitleContentList c = _parent->selected_subtitle (); + for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) { + (*i)->set_subtitle_x_offset (_x_offset->GetValue() / 100.0); } } void SubtitlePanel::y_offset_changed () { - SubtitleContentList c = _editor->selected_subtitle_content (); - if (c.size() == 1) { - c.front()->set_subtitle_y_offset (_y_offset->GetValue() / 100.0); + SubtitleContentList c = _parent->selected_subtitle (); + for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) { + (*i)->set_subtitle_y_offset (_y_offset->GetValue() / 100.0); } } void SubtitlePanel::x_scale_changed () { - SubtitleContentList c = _editor->selected_subtitle_content (); + SubtitleContentList c = _parent->selected_subtitle (); if (c.size() == 1) { c.front()->set_subtitle_x_scale (_x_scale->GetValue() / 100.0); } @@ -231,9 +254,9 @@ SubtitlePanel::x_scale_changed () void SubtitlePanel::y_scale_changed () { - SubtitleContentList c = _editor->selected_subtitle_content (); - if (c.size() == 1) { - c.front()->set_subtitle_y_scale (_y_scale->GetValue() / 100.0); + SubtitleContentList c = _parent->selected_subtitle (); + for (SubtitleContentList::iterator i = c.begin(); i != c.end(); ++i) { + (*i)->set_subtitle_y_scale (_y_scale->GetValue() / 100.0); } } @@ -241,8 +264,38 @@ void SubtitlePanel::content_selection_changed () { film_content_changed (FFmpegContentProperty::SUBTITLE_STREAMS); + film_content_changed (SubtitleContentProperty::USE_SUBTITLES); film_content_changed (SubtitleContentProperty::SUBTITLE_X_OFFSET); film_content_changed (SubtitleContentProperty::SUBTITLE_Y_OFFSET); film_content_changed (SubtitleContentProperty::SUBTITLE_X_SCALE); film_content_changed (SubtitleContentProperty::SUBTITLE_Y_SCALE); } + +void +SubtitlePanel::view_clicked () +{ + if (_view) { + _view->Destroy (); + _view = 0; + } + + SubtitleContentList c = _parent->selected_subtitle (); + assert (c.size() == 1); + + shared_ptr<SubtitleDecoder> decoder; + + shared_ptr<SubRipContent> sr = dynamic_pointer_cast<SubRipContent> (c.front ()); + if (sr) { + decoder.reset (new SubRipDecoder (sr)); + } + + shared_ptr<DCPSubtitleContent> dc = dynamic_pointer_cast<DCPSubtitleContent> (c.front ()); + if (dc) { + decoder.reset (new DCPSubtitleDecoder (dc)); + } + + if (decoder) { + _view = new SubtitleView (this, _parent->film(), decoder, c.front()->position ()); + _view->Show (); + } +} diff --git a/src/wx/subtitle_panel.h b/src/wx/subtitle_panel.h index 7f5d9239d..bcff995a0 100644 --- a/src/wx/subtitle_panel.h +++ b/src/wx/subtitle_panel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,34 +17,38 @@ */ -#include "film_editor_panel.h" +#include "content_sub_panel.h" class wxCheckBox; class wxSpinCtrl; +class SubtitleView; -class SubtitlePanel : public FilmEditorPanel +class SubtitlePanel : public ContentSubPanel { public: - SubtitlePanel (FilmEditor *); + SubtitlePanel (ContentPanel *); void film_changed (Film::Property); void film_content_changed (int); void content_selection_changed (); private: - void with_subtitles_toggled (); + void use_toggled (); void x_offset_changed (); void y_offset_changed (); void x_scale_changed (); void y_scale_changed (); void stream_changed (); + void view_clicked (); void setup_sensitivity (); - wxCheckBox* _with_subtitles; + wxCheckBox* _use; wxSpinCtrl* _x_offset; wxSpinCtrl* _y_offset; wxSpinCtrl* _x_scale; wxSpinCtrl* _y_scale; wxChoice* _stream; + wxButton* _view_button; + SubtitleView* _view; }; diff --git a/src/wx/subtitle_view.cc b/src/wx/subtitle_view.cc new file mode 100644 index 000000000..dc41db2fa --- /dev/null +++ b/src/wx/subtitle_view.cc @@ -0,0 +1,86 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 "lib/subrip_decoder.h" +#include "lib/content_subtitle.h" +#include "lib/film.h" +#include "lib/subrip_content.h" +#include "subtitle_view.h" +#include "wx_util.h" + +using std::list; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +SubtitleView::SubtitleView (wxWindow* parent, shared_ptr<Film> film, shared_ptr<SubtitleDecoder> decoder, DCPTime position) + : wxDialog (parent, wxID_ANY, _("Subtitles")) +{ + _list = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL); + + { + wxListItem ip; + ip.SetId (0); + ip.SetText (_("Start")); + ip.SetWidth (100); + _list->InsertColumn (0, ip); + } + + { + wxListItem ip; + ip.SetId (1); + ip.SetText (_("End")); + ip.SetWidth (100); + _list->InsertColumn (1, ip); + } + + { + wxListItem ip; + ip.SetId (2); + ip.SetText (_("Subtitle")); + ip.SetWidth (640); + _list->InsertColumn (2, ip); + } + + wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); + sizer->Add (_list, 1, wxEXPAND); + + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + if (buttons) { + sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + list<ContentTextSubtitle> subs = decoder->get_text_subtitles (ContentTimePeriod (ContentTime(), ContentTime::max ()), true); + FrameRateChange const frc = film->active_frame_rate_change (position); + int n = 0; + for (list<ContentTextSubtitle>::const_iterator i = subs.begin(); i != subs.end(); ++i) { + for (list<dcp::SubtitleString>::const_iterator j = i->subs.begin(); j != i->subs.end(); ++j) { + wxListItem list_item; + list_item.SetId (n); + _list->InsertItem (list_item); + ContentTimePeriod const p = i->period (); + _list->SetItem (n, 0, std_to_wx (p.from.timecode (frc.source))); + _list->SetItem (n, 1, std_to_wx (p.to.timecode (frc.source))); + _list->SetItem (n, 2, std_to_wx (j->text ())); + ++n; + } + } + + SetSizerAndFit (sizer); +} + diff --git a/src/wx/subtitle_view.h b/src/wx/subtitle_view.h new file mode 100644 index 000000000..338742afc --- /dev/null +++ b/src/wx/subtitle_view.h @@ -0,0 +1,33 @@ +/* + Copyright (C) 2014 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + 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 <boost/shared_ptr.hpp> +#include <wx/wx.h> +#include <wx/listctrl.h> + +class SubtitleDecoder; + +class SubtitleView : public wxDialog +{ +public: + SubtitleView (wxWindow *, boost::shared_ptr<Film>, boost::shared_ptr<SubtitleDecoder>, DCPTime position); + +private: + wxListCtrl* _list; +}; diff --git a/src/wx/timecode.cc b/src/wx/timecode.cc index 166446d8c..bd0a182c2 100644 --- a/src/wx/timecode.cc +++ b/src/wx/timecode.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,16 +17,16 @@ */ -#include <boost/lexical_cast.hpp> #include "lib/util.h" #include "timecode.h" #include "wx_util.h" +#include <boost/lexical_cast.hpp> using std::string; using std::cout; using boost::lexical_cast; -Timecode::Timecode (wxWindow* parent) +TimecodeBase::TimecodeBase (wxWindow* parent) : wxPanel (parent) { wxClientDC dc (parent); @@ -69,11 +69,11 @@ Timecode::Timecode (wxWindow* parent) _fixed = add_label_to_sizer (_sizer, this, wxT ("42"), false); - _hours->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&Timecode::changed, this)); - _minutes->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&Timecode::changed, this)); - _seconds->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&Timecode::changed, this)); - _frames->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&Timecode::changed, this)); - _set_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&Timecode::set_clicked, this)); + _hours->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TimecodeBase::changed, this)); + _minutes->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TimecodeBase::changed, this)); + _seconds->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TimecodeBase::changed, this)); + _frames->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&TimecodeBase::changed, this)); + _set_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&TimecodeBase::set_clicked, this)); _set_button->Enable (false); @@ -83,46 +83,7 @@ Timecode::Timecode (wxWindow* parent) } void -Timecode::set (Time t, int fps) -{ - /* Do this calculation with frames so that we can round - to a frame boundary at the start rather than the end. - */ - int64_t f = divide_with_round (t * fps, TIME_HZ); - - int const h = f / (3600 * fps); - f -= h * 3600 * fps; - int const m = f / (60 * fps); - f -= m * 60 * fps; - int const s = f / fps; - f -= s * fps; - - checked_set (_hours, lexical_cast<string> (h)); - checked_set (_minutes, lexical_cast<string> (m)); - checked_set (_seconds, lexical_cast<string> (s)); - checked_set (_frames, lexical_cast<string> (f)); - - _fixed->SetLabel (wxString::Format ("%02d:%02d:%02d.%02" wxLongLongFmtSpec "d", h, m, s, f)); -} - -Time -Timecode::get (int fps) const -{ - Time t = 0; - string const h = wx_to_std (_hours->GetValue ()); - t += lexical_cast<int> (h.empty() ? "0" : h) * 3600 * TIME_HZ; - string const m = wx_to_std (_minutes->GetValue()); - t += lexical_cast<int> (m.empty() ? "0" : m) * 60 * TIME_HZ; - string const s = wx_to_std (_seconds->GetValue()); - t += lexical_cast<int> (s.empty() ? "0" : s) * TIME_HZ; - string const f = wx_to_std (_frames->GetValue()); - t += lexical_cast<int> (f.empty() ? "0" : f) * TIME_HZ / fps; - - return t; -} - -void -Timecode::clear () +TimecodeBase::clear () { checked_set (_hours, ""); checked_set (_minutes, ""); @@ -132,20 +93,20 @@ Timecode::clear () } void -Timecode::changed () +TimecodeBase::changed () { _set_button->Enable (true); } void -Timecode::set_clicked () +TimecodeBase::set_clicked () { Changed (); _set_button->Enable (false); } void -Timecode::set_editable (bool e) +TimecodeBase::set_editable (bool e) { _editable->Show (e); _fixed->Show (!e); diff --git a/src/wx/timecode.h b/src/wx/timecode.h index d0e8176f2..7a34b80fe 100644 --- a/src/wx/timecode.h +++ b/src/wx/timecode.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2013-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,24 +17,27 @@ */ -#include <boost/signals2.hpp> -#include <wx/wx.h> +#ifndef DCPOMATIC_WX_TIMECODE_H +#define DCPOMATIC_WX_TIMECODE_H + +#include "wx_util.h" #include "lib/types.h" +#include <wx/wx.h> +#include <boost/signals2.hpp> +#include <boost/lexical_cast.hpp> -class Timecode : public wxPanel +class TimecodeBase : public wxPanel { public: - Timecode (wxWindow *); + TimecodeBase (wxWindow *); - void set (Time, int); - Time get (int) const; void clear (); void set_editable (bool); boost::signals2::signal<void ()> Changed; -private: +protected: void changed (); void set_clicked (); @@ -48,3 +51,46 @@ private: wxStaticText* _fixed; }; +template <class T> +class Timecode : public TimecodeBase +{ +public: + Timecode (wxWindow* parent) + : TimecodeBase (parent) + { + + } + + void set (T t, int fps) + { + int h; + int m; + int s; + int f; + t.split (fps, h, m, s, f); + + checked_set (_hours, boost::lexical_cast<std::string> (h)); + checked_set (_minutes, boost::lexical_cast<std::string> (m)); + checked_set (_seconds, boost::lexical_cast<std::string> (s)); + checked_set (_frames, boost::lexical_cast<std::string> (f)); + + _fixed->SetLabel (std_to_wx (t.timecode (fps))); + } + + T get (int fps) const + { + T t; + std::string const h = wx_to_std (_hours->GetValue ()); + t += T::from_seconds (boost::lexical_cast<int> (h.empty() ? "0" : h) * 3600); + std::string const m = wx_to_std (_minutes->GetValue()); + t += T::from_seconds (boost::lexical_cast<int> (m.empty() ? "0" : m) * 60); + std::string const s = wx_to_std (_seconds->GetValue()); + t += T::from_seconds (boost::lexical_cast<int> (s.empty() ? "0" : s)); + std::string const f = wx_to_std (_frames->GetValue()); + t += T::from_seconds (boost::lexical_cast<double> (f.empty() ? "0" : f) / fps); + + return t; + } +}; + +#endif diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc index eba12c47f..13baef6b7 100644 --- a/src/wx/timeline.cc +++ b/src/wx/timeline.cc @@ -22,8 +22,10 @@ #include <boost/weak_ptr.hpp> #include "lib/film.h" #include "lib/playlist.h" +#include "lib/image_content.h" #include "film_editor.h" #include "timeline.h" +#include "content_panel.h" #include "wx_util.h" using std::list; @@ -35,7 +37,9 @@ using boost::dynamic_pointer_cast; using boost::bind; using boost::optional; -/** Parent class for components of the timeline (e.g. a piece of content or an axis) */ +/** @class View + * @brief Parent class for components of the timeline (e.g. a piece of content or an axis). + */ class View : public boost::noncopyable { public: @@ -64,9 +68,9 @@ public: protected: virtual void do_paint (wxGraphicsContext *) = 0; - int time_x (Time t) const + int time_x (DCPTime t) const { - return _timeline.tracks_position().x + t * _timeline.pixels_per_time_unit().get_value_or (0); + return _timeline.tracks_position().x + t.seconds() * _timeline.pixels_per_second().get_value_or (0); } Timeline& _timeline; @@ -76,7 +80,9 @@ private: }; -/** Parent class for views of pieces of content */ +/** @class ContentView + * @brief Parent class for views of pieces of content. + */ class ContentView : public View { public: @@ -101,7 +107,7 @@ public: return dcpomatic::Rect<int> ( time_x (content->position ()) - 8, y_pos (_track.get()) - 8, - content->length_after_trim () * _timeline.pixels_per_time_unit().get_value_or(0) + 16, + content->length_after_trim().seconds() * _timeline.pixels_per_second().get_value_or(0) + 16, _timeline.track_height() + 16 ); } @@ -132,7 +138,8 @@ public: } virtual wxString type () const = 0; - virtual wxColour colour () const = 0; + virtual wxColour background_colour () const = 0; + virtual wxColour foreground_colour () const = 0; private: @@ -146,18 +153,16 @@ private: return; } - Time const position = cont->position (); - Time const len = cont->length_after_trim (); + DCPTime const position = cont->position (); + DCPTime const len = cont->length_after_trim (); - wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2); + wxColour selected (background_colour().Red() / 2, background_colour().Green() / 2, background_colour().Blue() / 2); - gc->SetPen (*wxBLACK_PEN); - - gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 4, wxPENSTYLE_SOLID)); + gc->SetPen (*wxThePenList->FindOrCreatePen (foreground_colour(), 4, wxPENSTYLE_SOLID)); if (_selected) { gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (selected, wxBRUSHSTYLE_SOLID)); } else { - gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (colour(), wxBRUSHSTYLE_SOLID)); + gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (background_colour(), wxBRUSHSTYLE_SOLID)); } wxGraphicsPath path = gc->CreatePath (); @@ -169,14 +174,15 @@ private: gc->StrokePath (path); gc->FillPath (path); - wxString name = wxString::Format (wxT ("%s [%s]"), std_to_wx (cont->path_summary()).data(), type().data()); + wxString name = wxString::Format (wxT ("%s [%s]"), std_to_wx (cont->summary()).data(), type().data()); wxDouble name_width; wxDouble name_height; wxDouble name_descent; wxDouble name_leading; gc->GetTextExtent (name, &name_width, &name_height, &name_descent, &name_leading); - gc->Clip (wxRegion (time_x (position), y_pos (_track.get()), len * _timeline.pixels_per_time_unit().get_value_or(0), _timeline.track_height())); + gc->Clip (wxRegion (time_x (position), y_pos (_track.get()), len.seconds() * _timeline.pixels_per_second().get_value_or(0), _timeline.track_height())); + gc->SetFont (gc->CreateFont (*wxNORMAL_FONT, foreground_colour ())); gc->DrawText (name, time_x (position) + 12, y_pos (_track.get() + 1) - name_height - 4); gc->ResetClip (); } @@ -195,7 +201,7 @@ private: } if (!frequent) { - _timeline.setup_pixels_per_time_unit (); + _timeline.setup_pixels_per_second (); _timeline.Refresh (); } } @@ -207,6 +213,9 @@ private: boost::signals2::scoped_connection _content_connection; }; +/** @class AudioContentView + * @brief Timeline view for AudioContent. + */ class AudioContentView : public ContentView { public: @@ -220,12 +229,20 @@ private: return _("audio"); } - wxColour colour () const + wxColour background_colour () const { return wxColour (149, 121, 232, 255); } + + wxColour foreground_colour () const + { + return wxColour (0, 0, 0, 255); + } }; +/** @class AudioContentView + * @brief Timeline view for VideoContent. + */ class VideoContentView : public ContentView { public: @@ -237,17 +254,62 @@ private: wxString type () const { - if (dynamic_pointer_cast<FFmpegContent> (content ())) { - return _("video"); - } else { + if (dynamic_pointer_cast<ImageContent> (content ()) && content()->number_of_paths() == 1) { return _("still"); + } else { + return _("video"); } } - wxColour colour () const + wxColour background_colour () const { return wxColour (242, 92, 120, 255); } + + wxColour foreground_colour () const + { + return wxColour (0, 0, 0, 255); + } +}; + +/** @class AudioContentView + * @brief Timeline view for SubtitleContent. + */ +class SubtitleContentView : public ContentView +{ +public: + SubtitleContentView (Timeline& tl, shared_ptr<SubtitleContent> c) + : ContentView (tl, c) + , _subtitle_content (c) + {} + +private: + wxString type () const + { + return _("subtitles"); + } + + wxColour background_colour () const + { + shared_ptr<SubtitleContent> sc = _subtitle_content.lock (); + if (!sc || !sc->use_subtitles ()) { + return wxColour (210, 210, 210, 128); + } + + return wxColour (163, 255, 154, 255); + } + + wxColour foreground_colour () const + { + shared_ptr<SubtitleContent> sc = _subtitle_content.lock (); + if (!sc || !sc->use_subtitles ()) { + return wxColour (180, 180, 180, 128); + } + + return wxColour (0, 0, 0, 255); + } + + boost::weak_ptr<SubtitleContent> _subtitle_content; }; class TimeAxisView : public View @@ -273,26 +335,26 @@ private: void do_paint (wxGraphicsContext* gc) { - if (!_timeline.pixels_per_time_unit()) { + if (!_timeline.pixels_per_second()) { return; } - double const pptu = _timeline.pixels_per_time_unit().get (); + double const pps = _timeline.pixels_per_second().get (); gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 1, wxPENSTYLE_SOLID)); - int mark_interval = rint (128 / (TIME_HZ * pptu)); + double mark_interval = rint (128 / pps); if (mark_interval > 5) { - mark_interval -= mark_interval % 5; + mark_interval -= int (rint (mark_interval)) % 5; } if (mark_interval > 10) { - mark_interval -= mark_interval % 10; + mark_interval -= int (rint (mark_interval)) % 10; } if (mark_interval > 60) { - mark_interval -= mark_interval % 60; + mark_interval -= int (rint (mark_interval)) % 60; } if (mark_interval > 3600) { - mark_interval -= mark_interval % 3600; + mark_interval -= int (rint (mark_interval)) % 3600; } if (mark_interval < 1) { @@ -304,14 +366,17 @@ private: path.AddLineToPoint (_timeline.width(), _y); gc->StrokePath (path); - Time t = 0; - while ((t * pptu) < _timeline.width()) { + gc->SetFont (gc->CreateFont (*wxNORMAL_FONT)); + + /* Time in seconds */ + DCPTime t; + while ((t.seconds() * pps) < _timeline.width()) { wxGraphicsPath path = gc->CreatePath (); path.MoveToPoint (time_x (t), _y - 4); path.AddLineToPoint (time_x (t), _y + 4); gc->StrokePath (path); - int tc = t / TIME_HZ; + double tc = t.seconds (); int const h = tc / 3600; tc -= h * 3600; int const m = tc / 60; @@ -325,12 +390,12 @@ private: wxDouble str_leading; gc->GetTextExtent (str, &str_width, &str_height, &str_descent, &str_leading); - int const tx = _timeline.x_offset() + t * pptu; + int const tx = _timeline.x_offset() + t.seconds() * pps; if ((tx + str_width) < _timeline.width()) { gc->DrawText (str, time_x (t), _y + 16); } - t += mark_interval * TIME_HZ; + t += DCPTime::from_seconds (mark_interval); } } @@ -339,9 +404,9 @@ private: }; -Timeline::Timeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film) +Timeline::Timeline (wxWindow* parent, ContentPanel* cp, shared_ptr<Film> film) : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE) - , _film_editor (ed) + , _content_panel (cp) , _film (film) , _time_axis_view (new TimeAxisView (*this, 32)) , _tracks (0) @@ -380,8 +445,6 @@ Timeline::paint () return; } - gc->SetFont (gc->CreateFont (*wxNORMAL_FONT)); - for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { (*i)->paint (gc); } @@ -411,10 +474,15 @@ Timeline::playlist_changed () if (dynamic_pointer_cast<AudioContent> (*i)) { _views.push_back (shared_ptr<View> (new AudioContentView (*this, *i))); } + + shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (*i); + if (sc && sc->has_subtitles ()) { + _views.push_back (shared_ptr<View> (new SubtitleContentView (*this, sc))); + } } assign_tracks (); - setup_pixels_per_time_unit (); + setup_pixels_per_second (); Refresh (); } @@ -425,7 +493,7 @@ Timeline::playlist_content_changed (int property) if (property == ContentProperty::POSITION) { assign_tracks (); - setup_pixels_per_time_unit (); + setup_pixels_per_second (); Refresh (); } } @@ -495,14 +563,14 @@ Timeline::tracks () const } void -Timeline::setup_pixels_per_time_unit () +Timeline::setup_pixels_per_second () { shared_ptr<const Film> film = _film.lock (); - if (!film || film->length() == 0) { + if (!film || film->length() == DCPTime ()) { return; } - _pixels_per_time_unit = static_cast<double>(width() - x_offset() * 2) / film->length (); + _pixels_per_second = static_cast<double>(width() - x_offset() * 2) / film->length().seconds (); } shared_ptr<View> @@ -545,7 +613,7 @@ Timeline::left_down (wxMouseEvent& ev) } if (view == *i) { - _film_editor->set_selection (cv->content ()); + _content_panel->set_selection (cv->content ()); } } @@ -604,11 +672,11 @@ Timeline::right_down (wxMouseEvent& ev) void Timeline::set_position_from_event (wxMouseEvent& ev) { - if (!_pixels_per_time_unit) { + if (!_pixels_per_second) { return; } - double const pptu = _pixels_per_time_unit.get (); + double const pps = _pixels_per_second.get (); wxPoint const p = ev.GetPosition(); @@ -627,13 +695,13 @@ Timeline::set_position_from_event (wxMouseEvent& ev) return; } - Time new_position = _down_view_position + (p.x - _down_point.x) / pptu; + DCPTime new_position = _down_view_position + DCPTime::from_seconds ((p.x - _down_point.x) / pps); if (_snap) { bool first = true; - Time nearest_distance = TIME_MAX; - Time nearest_new_position = TIME_MAX; + DCPTime nearest_distance = DCPTime::max (); + DCPTime nearest_new_position = DCPTime::max (); /* Find the nearest content edge; this is inefficient */ for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { @@ -644,7 +712,7 @@ Timeline::set_position_from_event (wxMouseEvent& ev) { /* Snap starts to ends */ - Time const d = abs (cv->content()->end() - new_position); + DCPTime const d = DCPTime (cv->content()->end() - new_position).abs (); if (first || d < nearest_distance) { nearest_distance = d; nearest_new_position = cv->content()->end(); @@ -653,7 +721,10 @@ Timeline::set_position_from_event (wxMouseEvent& ev) { /* Snap ends to starts */ - Time const d = abs (cv->content()->position() - (new_position + _down_view->content()->length_after_trim())); + DCPTime const d = DCPTime ( + cv->content()->position() - (new_position + _down_view->content()->length_after_trim()) + ).abs (); + if (d < nearest_distance) { nearest_distance = d; nearest_new_position = cv->content()->position() - _down_view->content()->length_after_trim (); @@ -665,14 +736,14 @@ Timeline::set_position_from_event (wxMouseEvent& ev) if (!first) { /* Snap if it's close; `close' means within a proportion of the time on the timeline */ - if (nearest_distance < (width() / pptu) / 32) { + if (nearest_distance < DCPTime::from_seconds ((width() / pps) / 32)) { new_position = nearest_new_position; } } } - if (new_position < 0) { - new_position = 0; + if (new_position < DCPTime ()) { + new_position = DCPTime (); } _down_view->content()->set_position (new_position); @@ -697,7 +768,7 @@ Timeline::film () const void Timeline::resized () { - setup_pixels_per_time_unit (); + setup_pixels_per_second (); } void diff --git a/src/wx/timeline.h b/src/wx/timeline.h index fafb09c0e..82d10afde 100644 --- a/src/wx/timeline.h +++ b/src/wx/timeline.h @@ -28,13 +28,13 @@ class Film; class View; class ContentView; -class FilmEditor; +class ContentPanel; class TimeAxisView; class Timeline : public wxPanel { public: - Timeline (wxWindow *, FilmEditor *, boost::shared_ptr<Film>); + Timeline (wxWindow *, ContentPanel *, boost::shared_ptr<Film>); boost::shared_ptr<const Film> film () const; @@ -52,8 +52,8 @@ public: return 48; } - boost::optional<double> pixels_per_time_unit () const { - return _pixels_per_time_unit; + boost::optional<double> pixels_per_second () const { + return _pixels_per_second; } Position<int> tracks_position () const { @@ -62,7 +62,7 @@ public: int tracks () const; - void setup_pixels_per_time_unit (); + void setup_pixels_per_second (); void set_snap (bool s) { _snap = s; @@ -92,16 +92,16 @@ private: ContentViewList selected_views () const; ContentList selected_content () const; - FilmEditor* _film_editor; + ContentPanel* _content_panel; boost::weak_ptr<Film> _film; ViewList _views; boost::shared_ptr<TimeAxisView> _time_axis_view; int _tracks; - boost::optional<double> _pixels_per_time_unit; + boost::optional<double> _pixels_per_second; bool _left_down; wxPoint _down_point; boost::shared_ptr<ContentView> _down_view; - Time _down_view_position; + DCPTime _down_view_position; bool _first_move; ContentMenu _menu; bool _snap; diff --git a/src/wx/timeline_dialog.cc b/src/wx/timeline_dialog.cc index dbf7ae232..8ac90b8de 100644 --- a/src/wx/timeline_dialog.cc +++ b/src/wx/timeline_dialog.cc @@ -23,14 +23,15 @@ #include "film_editor.h" #include "timeline_dialog.h" #include "wx_util.h" +#include "content_panel.h" using std::list; using std::cout; using boost::shared_ptr; -TimelineDialog::TimelineDialog (FilmEditor* ed, shared_ptr<Film> film) - : wxDialog (ed, wxID_ANY, _("Timeline"), wxDefaultPosition, wxSize (640, 512), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE) - , _timeline (this, ed, film) +TimelineDialog::TimelineDialog (ContentPanel* cp, shared_ptr<Film> film) + : wxDialog (cp->panel(), wxID_ANY, _("Timeline"), wxDefaultPosition, wxSize (640, 512), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE) + , _timeline (this, cp, film) { wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); diff --git a/src/wx/timeline_dialog.h b/src/wx/timeline_dialog.h index 1e5955003..2f5fa5ec7 100644 --- a/src/wx/timeline_dialog.h +++ b/src/wx/timeline_dialog.h @@ -27,7 +27,7 @@ class Playlist; class TimelineDialog : public wxDialog { public: - TimelineDialog (FilmEditor *, boost::shared_ptr<Film>); + TimelineDialog (ContentPanel *, boost::shared_ptr<Film>); private: void snap_toggled (); diff --git a/src/wx/timing_panel.cc b/src/wx/timing_panel.cc index aa4f70a81..0f86a3f3f 100644 --- a/src/wx/timing_panel.cc +++ b/src/wx/timing_panel.cc @@ -17,42 +17,42 @@ */ -#include <libdcp/raw_convert.h> +#include <dcp/raw_convert.h> #include "lib/content.h" #include "lib/image_content.h" #include "timing_panel.h" #include "wx_util.h" #include "timecode.h" -#include "film_editor.h" +#include "content_panel.h" using std::cout; using std::string; using std::set; using boost::shared_ptr; using boost::dynamic_pointer_cast; -using libdcp::raw_convert; +using dcp::raw_convert; -TimingPanel::TimingPanel (FilmEditor* e) +TimingPanel::TimingPanel (ContentPanel* p) /* horrid hack for apparent lack of context support with wxWidgets i18n code */ - : FilmEditorPanel (e, S_("Timing|Timing")) + : ContentSubPanel (p, S_("Timing|Timing")) { wxFlexGridSizer* grid = new wxFlexGridSizer (2, 4, 4); _sizer->Add (grid, 0, wxALL, 8); add_label_to_sizer (grid, this, _("Position"), true); - _position = new Timecode (this); + _position = new Timecode<DCPTime> (this); grid->Add (_position); add_label_to_sizer (grid, this, _("Full length"), true); - _full_length = new Timecode (this); + _full_length = new Timecode<DCPTime> (this); grid->Add (_full_length); add_label_to_sizer (grid, this, _("Trim from start"), true); - _trim_start = new Timecode (this); + _trim_start = new Timecode<DCPTime> (this); grid->Add (_trim_start); add_label_to_sizer (grid, this, _("Trim from end"), true); - _trim_end = new Timecode (this); + _trim_end = new Timecode<DCPTime> (this); grid->Add (_trim_end); add_label_to_sizer (grid, this, _("Play length"), true); - _play_length = new Timecode (this); + _play_length = new Timecode<DCPTime> (this); grid->Add (_play_length); { @@ -78,8 +78,8 @@ TimingPanel::TimingPanel (FilmEditor* e) void TimingPanel::film_content_changed (int property) { - ContentList cl = _editor->selected_content (); - int const film_video_frame_rate = _editor->film()->video_frame_rate (); + ContentList cl = _parent->selected (); + int const film_video_frame_rate = _parent->film()->video_frame_rate (); /* Here we check to see if we have exactly one different value of various properties, and fill the controls with that value if so. @@ -87,7 +87,7 @@ TimingPanel::film_content_changed (int property) if (property == ContentProperty::POSITION) { - set<Time> check; + set<DCPTime> check; for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) { check.insert ((*i)->position ()); } @@ -104,7 +104,7 @@ TimingPanel::film_content_changed (int property) property == VideoContentProperty::VIDEO_FRAME_TYPE ) { - set<Time> check; + set<DCPTime> check; for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) { check.insert ((*i)->full_length ()); } @@ -117,7 +117,7 @@ TimingPanel::film_content_changed (int property) } else if (property == ContentProperty::TRIM_START) { - set<Time> check; + set<DCPTime> check; for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) { check.insert ((*i)->trim_start ()); } @@ -130,7 +130,7 @@ TimingPanel::film_content_changed (int property) } else if (property == ContentProperty::TRIM_END) { - set<Time> check; + set<DCPTime> check; for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) { check.insert ((*i)->trim_end ()); } @@ -138,7 +138,7 @@ TimingPanel::film_content_changed (int property) if (check.size() == 1) { _trim_end->set (cl.front()->trim_end (), film_video_frame_rate); } else { - _trim_end->set (0, 24); + _trim_end->clear (); } } @@ -150,7 +150,7 @@ TimingPanel::film_content_changed (int property) property == VideoContentProperty::VIDEO_FRAME_TYPE ) { - set<Time> check; + set<DCPTime> check; for (ContentList::const_iterator i = cl.begin (); i != cl.end(); ++i) { check.insert ((*i)->length_after_trim ()); } @@ -197,20 +197,21 @@ TimingPanel::film_content_changed (int property) void TimingPanel::position_changed () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - (*i)->set_position (_position->get (_editor->film()->video_frame_rate ())); + (*i)->set_position (_position->get (_parent->film()->video_frame_rate ())); } } void TimingPanel::full_length_changed () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { shared_ptr<ImageContent> ic = dynamic_pointer_cast<ImageContent> (*i); if (ic && ic->still ()) { - ic->set_video_length (rint (_full_length->get (_editor->film()->video_frame_rate()) * ic->video_frame_rate() / TIME_HZ)); + /* XXX: No effective FRC here... is this right? */ + ic->set_video_length (ContentTime (_full_length->get (_parent->film()->video_frame_rate()), FrameRateChange (1, 1))); } } } @@ -218,9 +219,9 @@ TimingPanel::full_length_changed () void TimingPanel::trim_start_changed () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - (*i)->set_trim_start (_trim_start->get (_editor->film()->video_frame_rate ())); + (*i)->set_trim_start (_trim_start->get (_parent->film()->video_frame_rate ())); } } @@ -228,18 +229,18 @@ TimingPanel::trim_start_changed () void TimingPanel::trim_end_changed () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - (*i)->set_trim_end (_trim_end->get (_editor->film()->video_frame_rate ())); + (*i)->set_trim_end (_trim_end->get (_parent->film()->video_frame_rate ())); } } void TimingPanel::play_length_changed () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { - (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_editor->film()->video_frame_rate()) - (*i)->trim_start()); + (*i)->set_trim_end ((*i)->full_length() - _play_length->get (_parent->film()->video_frame_rate()) - (*i)->trim_start()); } } @@ -252,7 +253,7 @@ TimingPanel::video_frame_rate_changed () void TimingPanel::set_video_frame_rate () { - ContentList c = _editor->selected_content (); + ContentList c = _parent->selected (); for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*i); if (vc) { @@ -265,7 +266,7 @@ TimingPanel::set_video_frame_rate () void TimingPanel::content_selection_changed () { - bool const e = !_editor->selected_content().empty (); + bool const e = !_parent->selected().empty (); _position->Enable (e); _full_length->Enable (e); diff --git a/src/wx/timing_panel.h b/src/wx/timing_panel.h index d9696a201..00b7f84e7 100644 --- a/src/wx/timing_panel.h +++ b/src/wx/timing_panel.h @@ -17,14 +17,13 @@ */ -#include "film_editor_panel.h" +#include "content_sub_panel.h" +#include "timecode.h" -class Timecode; - -class TimingPanel : public FilmEditorPanel +class TimingPanel : public ContentSubPanel { public: - TimingPanel (FilmEditor *); + TimingPanel (ContentPanel *); void film_content_changed (int); void content_selection_changed (); @@ -38,11 +37,11 @@ private: void video_frame_rate_changed (); void set_video_frame_rate (); - Timecode* _position; - Timecode* _full_length; - Timecode* _trim_start; - Timecode* _trim_end; - Timecode* _play_length; + Timecode<DCPTime>* _position; + Timecode<DCPTime>* _full_length; + Timecode<DCPTime>* _trim_start; + Timecode<DCPTime>* _trim_end; + Timecode<DCPTime>* _play_length; wxTextCtrl* _video_frame_rate; wxButton* _set_video_frame_rate; }; diff --git a/src/wx/video_panel.cc b/src/wx/video_panel.cc index b33a97591..a8510cbba 100644 --- a/src/wx/video_panel.cc +++ b/src/wx/video_panel.cc @@ -28,15 +28,16 @@ #include "filter_dialog.h" #include "video_panel.h" #include "wx_util.h" -#include "film_editor.h" #include "content_colour_conversion_dialog.h" #include "content_widget.h" +#include "content_panel.h" using std::vector; using std::string; using std::pair; using std::cout; using std::list; +using std::set; using boost::shared_ptr; using boost::dynamic_pointer_cast; using boost::bind; @@ -64,8 +65,8 @@ scale_to_index (VideoContentScale scale) assert (false); } -VideoPanel::VideoPanel (FilmEditor* e) - : FilmEditorPanel (e, _("Video")) +VideoPanel::VideoPanel (ContentPanel* p) + : ContentSubPanel (p, _("Video")) { wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); _sizer->Add (grid, 0, wxALL, 8); @@ -82,7 +83,7 @@ VideoPanel::VideoPanel (FilmEditor* e) &caster<int, VideoFrameType>, &caster<VideoFrameType, int> ); - _frame_type->add (grid, wxGBPosition (r, 1)); + _frame_type->add (grid, wxGBPosition (r, 1), wxGBSpan (1, 2)); ++r; add_label_to_grid_bag_sizer (grid, this, _("Left crop"), true, wxGBPosition (r, 0)); @@ -94,9 +95,8 @@ VideoPanel::VideoPanel (FilmEditor* e) boost::mem_fn (&VideoContent::set_left_crop) ); _left_crop->add (grid, wxGBPosition (r, 1)); - ++r; - add_label_to_grid_bag_sizer (grid, this, _("Right crop"), true, wxGBPosition (r, 0)); + add_label_to_grid_bag_sizer (grid, this, _("Right crop"), true, wxGBPosition (r, 2)); _right_crop = new ContentSpinCtrl<VideoContent> ( this, new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)), @@ -104,7 +104,8 @@ VideoPanel::VideoPanel (FilmEditor* e) boost::mem_fn (&VideoContent::right_crop), boost::mem_fn (&VideoContent::set_right_crop) ); - _right_crop->add (grid, wxGBPosition (r, 1)); + _right_crop->add (grid, wxGBPosition (r, 3)); + ++r; add_label_to_grid_bag_sizer (grid, this, _("Top crop"), true, wxGBPosition (r, 0)); @@ -115,10 +116,9 @@ VideoPanel::VideoPanel (FilmEditor* e) boost::mem_fn (&VideoContent::top_crop), boost::mem_fn (&VideoContent::set_top_crop) ); - _top_crop->add (grid, wxGBPosition (r,1 )); - ++r; + _top_crop->add (grid, wxGBPosition (r, 1)); - add_label_to_grid_bag_sizer (grid, this, _("Bottom crop"), true, wxGBPosition (r, 0)); + add_label_to_grid_bag_sizer (grid, this, _("Bottom crop"), true, wxGBPosition (r, 2)); _bottom_crop = new ContentSpinCtrl<VideoContent> ( this, new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)), @@ -126,9 +126,20 @@ VideoPanel::VideoPanel (FilmEditor* e) boost::mem_fn (&VideoContent::bottom_crop), boost::mem_fn (&VideoContent::set_bottom_crop) ); - _bottom_crop->add (grid, wxGBPosition (r, 1)); + _bottom_crop->add (grid, wxGBPosition (r, 3)); + ++r; + add_label_to_grid_bag_sizer (grid, this, _("Fade in"), true, wxGBPosition (r, 0)); + _fade_in = new Timecode<ContentTime> (this); + grid->Add (_fade_in, wxGBPosition (r, 1), wxGBSpan (1, 3)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Fade out"), true, wxGBPosition (r, 0)); + _fade_out = new Timecode<ContentTime> (this); + grid->Add (_fade_out, wxGBPosition (r, 1), wxGBSpan (1, 3)); + ++r; + add_label_to_grid_bag_sizer (grid, this, _("Scale to"), true, wxGBPosition (r, 0)); _scale = new ContentChoice<VideoContent, VideoContentScale> ( this, @@ -139,44 +150,29 @@ VideoPanel::VideoPanel (FilmEditor* e) &index_to_scale, &scale_to_index ); - _scale->add (grid, wxGBPosition (r, 1)); + _scale->add (grid, wxGBPosition (r, 1), wxGBSpan (1, 2)); ++r; - { - add_label_to_grid_bag_sizer (grid, this, _("Filters"), true, wxGBPosition (r, 0)); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - - wxClientDC dc (this); - wxSize size = dc.GetTextExtent (wxT ("A quite long name")); - size.SetHeight (-1); - - _filters = new wxStaticText (this, wxID_ANY, _("None"), wxDefaultPosition, size); - s->Add (_filters, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 6); - _filters_button = new wxButton (this, wxID_ANY, _("Edit...")); - s->Add (_filters_button, 0, wxALIGN_CENTER_VERTICAL); - grid->Add (s, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - } + wxClientDC dc (this); + wxSize size = dc.GetTextExtent (wxT ("A quite long name")); + size.SetHeight (-1); + + add_label_to_grid_bag_sizer (grid, this, _("Filters"), true, wxGBPosition (r, 0)); + _filters = new wxStaticText (this, wxID_ANY, _("None"), wxDefaultPosition, size); + grid->Add (_filters, wxGBPosition (r, 1), wxGBSpan (1, 2), wxALIGN_CENTER_VERTICAL); + _filters_button = new wxButton (this, wxID_ANY, _("Edit...")); + grid->Add (_filters_button, wxGBPosition (r, 3), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); ++r; - { - add_label_to_grid_bag_sizer (grid, this, _("Colour conversion"), true, wxGBPosition (r, 0)); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - - wxClientDC dc (this); - wxSize size = dc.GetTextExtent (wxT ("A quite long name")); - size.SetHeight (-1); - - _colour_conversion = new wxStaticText (this, wxID_ANY, wxT (""), wxDefaultPosition, size); - - s->Add (_colour_conversion, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 6); - _colour_conversion_button = new wxButton (this, wxID_ANY, _("Edit...")); - s->Add (_colour_conversion_button, 0, wxALIGN_CENTER_VERTICAL); - grid->Add (s, wxGBPosition (r, 1), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); - } + add_label_to_grid_bag_sizer (grid, this, _("Colour conversion"), true, wxGBPosition (r, 0)); + _colour_conversion = new wxStaticText (this, wxID_ANY, wxT (""), wxDefaultPosition, size); + grid->Add (_colour_conversion, wxGBPosition (r, 1), wxGBSpan (1, 2), wxALIGN_CENTER_VERTICAL); + _colour_conversion_button = new wxButton (this, wxID_ANY, _("Edit...")); + grid->Add (_colour_conversion_button, wxGBPosition (r, 3), wxDefaultSpan, wxALIGN_CENTER_VERTICAL); ++r; _description = new wxStaticText (this, wxID_ANY, wxT ("\n \n \n \n \n"), wxDefaultPosition, wxDefaultSize); - grid->Add (_description, wxGBPosition (r, 0), wxGBSpan (1, 2), wxEXPAND | wxALIGN_CENTER_VERTICAL | wxALL, 6); + grid->Add (_description, wxGBPosition (r, 0), wxGBSpan (1, 4), wxEXPAND | wxALIGN_CENTER_VERTICAL | wxALL, 6); wxFont font = _description->GetFont(); font.SetStyle(wxFONTSTYLE_ITALIC); font.SetPointSize(font.GetPointSize() - 1); @@ -201,6 +197,9 @@ VideoPanel::VideoPanel (FilmEditor* e) _frame_type->wrapped()->Append (_("3D left only")); _frame_type->wrapped()->Append (_("3D right only")); + _fade_in->Changed.connect (boost::bind (&VideoPanel::fade_in_changed, this)); + _fade_out->Changed.connect (boost::bind (&VideoPanel::fade_out_changed, this)); + _filters_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&VideoPanel::edit_filters_clicked, this)); _colour_conversion_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&VideoPanel::edit_colour_conversion_clicked, this)); } @@ -222,7 +221,7 @@ VideoPanel::film_changed (Film::Property property) void VideoPanel::film_content_changed (int property) { - VideoContentList vc = _editor->selected_video_content (); + VideoContentList vc = _parent->selected_video (); shared_ptr<VideoContent> vcs; shared_ptr<FFmpegContent> fcs; if (!vc.empty ()) { @@ -251,6 +250,28 @@ VideoPanel::film_content_changed (int property) _filters->SetLabel (std_to_wx (p)); } } + } else if (property == VideoContentProperty::VIDEO_FADE_IN) { + set<ContentTime> check; + for (VideoContentList::const_iterator i = vc.begin (); i != vc.end(); ++i) { + check.insert ((*i)->fade_in ()); + } + + if (check.size() == 1) { + _fade_in->set (vc.front()->fade_in (), vc.front()->video_frame_rate ()); + } else { + _fade_in->clear (); + } + } else if (property == VideoContentProperty::VIDEO_FADE_OUT) { + set<ContentTime> check; + for (VideoContentList::const_iterator i = vc.begin (); i != vc.end(); ++i) { + check.insert ((*i)->fade_out ()); + } + + if (check.size() == 1) { + _fade_out->set (vc.front()->fade_out (), vc.front()->video_frame_rate ()); + } else { + _fade_out->clear (); + } } } @@ -258,7 +279,7 @@ VideoPanel::film_content_changed (int property) void VideoPanel::edit_filters_clicked () { - FFmpegContentList c = _editor->selected_ffmpeg_content (); + FFmpegContentList c = _parent->selected_ffmpeg (); if (c.size() != 1) { return; } @@ -272,7 +293,7 @@ VideoPanel::edit_filters_clicked () void VideoPanel::setup_description () { - VideoContentList vc = _editor->selected_video_content (); + VideoContentList vc = _parent->selected_video (); if (vc.empty ()) { _description->SetLabel (""); return; @@ -298,8 +319,8 @@ VideoPanel::setup_description () } Crop const crop = vcs->crop (); - if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != libdcp::Size (0, 0)) { - libdcp::Size cropped = vcs->video_size_after_crop (); + if ((crop.left || crop.right || crop.top || crop.bottom) && vcs->video_size() != dcp::Size (0, 0)) { + dcp::Size cropped = vcs->video_size_after_crop (); d << wxString::Format ( _("Cropped to %dx%d (%.2f:1)\n"), cropped.width, cropped.height, @@ -308,8 +329,8 @@ VideoPanel::setup_description () ++lines; } - libdcp::Size const container_size = _editor->film()->frame_size (); - libdcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size); + dcp::Size const container_size = _parent->film()->frame_size (); + dcp::Size const scaled = vcs->scale().size (vcs, container_size, container_size, 1); if (scaled != vcs->video_size_after_crop ()) { d << wxString::Format ( @@ -331,7 +352,7 @@ VideoPanel::setup_description () d << wxString::Format (_("Content frame rate %.4f\n"), vcs->video_frame_rate ()); ++lines; - FrameRateChange frc (vcs->video_frame_rate(), _editor->film()->video_frame_rate ()); + FrameRateChange frc (vcs->video_frame_rate(), _parent->film()->video_frame_rate ()); d << std_to_wx (frc.description ()) << "\n"; ++lines; @@ -346,7 +367,7 @@ VideoPanel::setup_description () void VideoPanel::edit_colour_conversion_clicked () { - VideoContentList vc = _editor->selected_video_content (); + VideoContentList vc = _parent->selected_video (); if (vc.size() != 1) { return; } @@ -363,8 +384,8 @@ VideoPanel::edit_colour_conversion_clicked () void VideoPanel::content_selection_changed () { - VideoContentList video_sel = _editor->selected_video_content (); - FFmpegContentList ffmpeg_sel = _editor->selected_ffmpeg_content (); + VideoContentList video_sel = _parent->selected_video (); + FFmpegContentList ffmpeg_sel = _parent->selected_ffmpeg (); bool const single = video_sel.size() == 1; @@ -381,5 +402,25 @@ VideoPanel::content_selection_changed () film_content_changed (VideoContentProperty::VIDEO_CROP); film_content_changed (VideoContentProperty::VIDEO_FRAME_RATE); film_content_changed (VideoContentProperty::COLOUR_CONVERSION); + film_content_changed (VideoContentProperty::VIDEO_FADE_IN); + film_content_changed (VideoContentProperty::VIDEO_FADE_OUT); film_content_changed (FFmpegContentProperty::FILTERS); } + +void +VideoPanel::fade_in_changed () +{ + VideoContentList vc = _parent->selected_video (); + for (VideoContentList::const_iterator i = vc.begin(); i != vc.end(); ++i) { + (*i)->set_fade_in (_fade_in->get (_parent->film()->video_frame_rate ())); + } +} + +void +VideoPanel::fade_out_changed () +{ + VideoContentList vc = _parent->selected_video (); + for (VideoContentList::const_iterator i = vc.begin(); i != vc.end(); ++i) { + (*i)->set_fade_out (_fade_out->get (_parent->film()->video_frame_rate ())); + } +} diff --git a/src/wx/video_panel.h b/src/wx/video_panel.h index 99633491d..aa0c6ed53 100644 --- a/src/wx/video_panel.h +++ b/src/wx/video_panel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,19 +17,27 @@ */ -#include "lib/film.h" -#include "film_editor_panel.h" +/** @file src/lib/video_panel.h + * @brief VideoPanel class. + */ + +#include "content_sub_panel.h" #include "content_widget.h" +#include "timecode.h" +#include "lib/film.h" class wxChoice; class wxStaticText; class wxSpinCtrl; class wxButton; -class VideoPanel : public FilmEditorPanel +/** @class VideoPanel + * @brief The video tab of the film editor. + */ +class VideoPanel : public ContentSubPanel { public: - VideoPanel (FilmEditor *); + VideoPanel (ContentPanel *); void film_changed (Film::Property); void film_content_changed (int); @@ -38,6 +46,8 @@ public: private: void edit_filters_clicked (); void edit_colour_conversion_clicked (); + void fade_in_changed (); + void fade_out_changed (); void setup_description (); @@ -46,6 +56,8 @@ private: ContentSpinCtrl<VideoContent>* _right_crop; ContentSpinCtrl<VideoContent>* _top_crop; ContentSpinCtrl<VideoContent>* _bottom_crop; + Timecode<ContentTime>* _fade_in; + Timecode<ContentTime>* _fade_out; ContentChoice<VideoContent, VideoContentScale>* _scale; wxStaticText* _description; wxStaticText* _filters; diff --git a/src/wx/wscript b/src/wx/wscript index 8bf2451c2..0f39038a5 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -15,13 +15,15 @@ sources = """ config_dialog.cc content_colour_conversion_dialog.cc content_menu.cc + content_panel.cc + content_sub_panel.cc + dcp_panel.cc isdcf_metadata_dialog.cc dir_picker_ctrl.cc dolby_certificate_dialog.cc doremi_certificate_dialog.cc download_certificate_dialog.cc film_editor.cc - film_editor_panel.cc film_viewer.cc filter_dialog.cc filter_editor.cc @@ -30,6 +32,7 @@ sources = """ job_manager_view.cc job_wrapper.cc kdm_dialog.cc + make_signer_chain_dialog.cc new_film_dialog.cc preset_colour_conversion_dialog.cc properties_dialog.cc @@ -38,6 +41,7 @@ sources = """ server_dialog.cc servers_list_dialog.cc subtitle_panel.cc + subtitle_view.cc table_dialog.cc timecode.cc timeline.cc @@ -83,16 +87,16 @@ def build(bld): else: obj = bld(features = 'cxx cxxshlib') - obj.name = 'libdcpomatic-wx' + obj.name = 'libdcpomatic2-wx' obj.export_includes = ['..'] obj.uselib = 'WXWIDGETS DCP' if bld.env.TARGET_LINUX: obj.uselib += ' GTK' - obj.use = 'libdcpomatic' + obj.use = 'libdcpomatic2' obj.source = sources - obj.target = 'dcpomatic-wx' + obj.target = 'dcpomatic2-wx' - i18n.po_to_mo(os.path.join('src', 'wx'), 'libdcpomatic-wx', bld) + i18n.po_to_mo(os.path.join('src', 'wx'), 'libdcpomatic2-wx', bld) def pot(bld): i18n.pot(os.path.join('src', 'wx'), sources + " editable_list.h", 'libdcpomatic-wx') diff --git a/src/wx/wx_ui_signaller.cc b/src/wx/wx_ui_signaller.cc index f30631960..8fc6670d6 100644 --- a/src/wx/wx_ui_signaller.cc +++ b/src/wx/wx_ui_signaller.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/src/wx/wx_ui_signaller.h b/src/wx/wx_ui_signaller.h index f7df6fca4..63f2049cd 100644 --- a/src/wx/wx_ui_signaller.h +++ b/src/wx/wx_ui_signaller.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-2014 Carl Hetherington <cth@carlh.net> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,6 +21,10 @@ class wxEvtHandler; +/** @class wxUISignaller + * @brief UISignaller for the wxWidgets event loop + */ + class wxUISignaller : public UISignaller { public: diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc index 1e501f54f..003cc222f 100644 --- a/src/wx/wx_util.cc +++ b/src/wx/wx_util.cc @@ -123,7 +123,7 @@ int const ThreadedStaticText::_update_event_id = 10000; * @param initial Initial text for the wxStaticText while the computation is being run. * @param fn Function which works out what the wxStaticText content should be and returns it. */ -ThreadedStaticText::ThreadedStaticText (wxWindow* parent, wxString initial, function<string ()> fn) +ThreadedStaticText::ThreadedStaticText (wxWindow* parent, wxString initial, boost::function<string ()> fn) : wxStaticText (parent, wxID_ANY, initial) { Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ThreadedStaticText::thread_finished, this, _1), _update_event_id); @@ -139,7 +139,7 @@ ThreadedStaticText::~ThreadedStaticText () /** Run our thread and post the result to the GUI thread via AddPendingEvent */ void -ThreadedStaticText::run (function<string ()> fn) +ThreadedStaticText::run (boost::function<string ()> fn) try { wxCommandEvent ev (wxEVT_COMMAND_TEXT_UPDATED, _update_event_id); |
