diff options
| author | Carl Hetherington <cth@carlh.net> | 2013-09-17 23:39:05 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2013-09-17 23:39:05 +0100 |
| commit | 373f010a7f04add1f49169cbaa60cb7ae5f508d4 (patch) | |
| tree | a61fe014cbefc775dcf3a5c9a45d06e391e65b31 /src | |
| parent | 048f9b6b5569f03d1342a04f75c83a2bad340996 (diff) | |
| parent | e888e92f354b9868337b0b022ff9be38b9c36c0f (diff) | |
Merge 1.0 in.
Diffstat (limited to 'src')
237 files changed, 24035 insertions, 9604 deletions
diff --git a/src/lib/ab_transcode_job.cc b/src/lib/ab_transcode_job.cc deleted file mode 100644 index a6233c185..000000000 --- a/src/lib/ab_transcode_job.cc +++ /dev/null @@ -1,66 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <stdexcept> -#include "ab_transcode_job.h" -#include "film.h" -#include "format.h" -#include "filter.h" -#include "ab_transcoder.h" -#include "config.h" -#include "encoder.h" - -using std::string; -using boost::shared_ptr; - -/** @param f Film to compare. - * @param o Options. - */ -ABTranscodeJob::ABTranscodeJob (shared_ptr<Film> f, shared_ptr<const DecodeOptions> od, shared_ptr<const EncodeOptions> oe, shared_ptr<Job> req) - : Job (f, req) - , _decode_opt (od) - , _encode_opt (oe) -{ - _film_b.reset (new Film (*_film)); - _film_b->set_scaler (Config::instance()->reference_scaler ()); - _film_b->set_filters (Config::instance()->reference_filters ()); -} - -string -ABTranscodeJob::name () const -{ - return String::compose ("A/B transcode %1", _film->name()); -} - -void -ABTranscodeJob::run () -{ - try { - /* _film_b is the one with reference filters */ - ABTranscoder w (_film_b, _film, _decode_opt, this, shared_ptr<Encoder> (new Encoder (_film, _encode_opt))); - w.go (); - set_progress (1); - set_state (FINISHED_OK); - - } catch (std::exception& e) { - - set_state (FINISHED_ERROR); - - } -} diff --git a/src/lib/ab_transcode_job.h b/src/lib/ab_transcode_job.h deleted file mode 100644 index 86a2a81b8..000000000 --- a/src/lib/ab_transcode_job.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/ab_transcode_job.h - * @brief Job to run a transcoder which produces output for A/B comparison of various settings. - */ - -#include <boost/shared_ptr.hpp> -#include "job.h" - -class Film; -class DecodeOptions; -class EncodeOptions; - -/** @class ABTranscodeJob - * @brief Job to run a transcoder which produces output for A/B comparison of various settings. - * - * The right half of the frame will be processed using the Film supplied; - * the left half will be processed using the same state but with the reference - * filters and scaler. - */ -class ABTranscodeJob : public Job -{ -public: - ABTranscodeJob ( - boost::shared_ptr<Film> f, - boost::shared_ptr<const DecodeOptions> od, - boost::shared_ptr<const EncodeOptions> oe, - boost::shared_ptr<Job> req - ); - - std::string name () const; - void run (); - -private: - boost::shared_ptr<const DecodeOptions> _decode_opt; - boost::shared_ptr<const EncodeOptions> _encode_opt; - - /** Copy of our Film using the reference filters and scaler */ - boost::shared_ptr<Film> _film_b; -}; diff --git a/src/lib/ab_transcoder.cc b/src/lib/ab_transcoder.cc deleted file mode 100644 index 53af43b5d..000000000 --- a/src/lib/ab_transcoder.cc +++ /dev/null @@ -1,119 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <iostream> -#include <boost/shared_ptr.hpp> -#include "ab_transcoder.h" -#include "film.h" -#include "video_decoder.h" -#include "audio_decoder.h" -#include "encoder.h" -#include "job.h" -#include "options.h" -#include "image.h" -#include "decoder_factory.h" -#include "matcher.h" -#include "delay_line.h" -#include "gain.h" -#include "combiner.h" - -/** @file src/ab_transcoder.cc - * @brief A transcoder which uses one Film for the left half of the screen, and a different one - * for the right half (to facilitate A/B comparisons of settings) - */ - -using std::string; -using boost::shared_ptr; - -/** @param a Film to use for the left half of the screen. - * @param b Film to use for the right half of the screen. - * @param o Decoder options. - * @param j Job that we are associated with. - * @param e Encoder to use. - */ - -ABTranscoder::ABTranscoder ( - shared_ptr<Film> a, shared_ptr<Film> b, shared_ptr<const DecodeOptions> o, Job* j, shared_ptr<Encoder> e) - : _film_a (a) - , _film_b (b) - , _job (j) - , _encoder (e) -{ - _da = decoder_factory (_film_a, o, j); - _db = decoder_factory (_film_b, o, j); - - if (_film_a->audio_stream()) { - shared_ptr<AudioStream> st = _film_a->audio_stream(); - _matcher.reset (new Matcher (_film_a->log(), st->sample_rate(), _film_a->frames_per_second())); - _delay_line.reset (new DelayLine (_film_a->log(), st->channels(), _film_a->audio_delay() * st->sample_rate() / 1000)); - _gain.reset (new Gain (_film_a->log(), _film_a->audio_gain())); - } - - /* Set up the decoder to use the film's set streams */ - _da.video->set_subtitle_stream (_film_a->subtitle_stream ()); - _db.video->set_subtitle_stream (_film_a->subtitle_stream ()); - _da.audio->set_audio_stream (_film_a->audio_stream ()); - - _da.video->Video.connect (bind (&Combiner::process_video, _combiner, _1, _2, _3)); - _db.video->Video.connect (bind (&Combiner::process_video_b, _combiner, _1, _2, _3)); - - if (_matcher) { - _combiner->connect_video (_matcher); - _matcher->connect_video (_encoder); - } else { - _combiner->connect_video (_encoder); - } - - if (_matcher && _delay_line) { - _da.audio->connect_audio (_delay_line); - _delay_line->connect_audio (_matcher); - _matcher->connect_audio (_gain); - _gain->connect_audio (_encoder); - } -} - -void -ABTranscoder::go () -{ - _encoder->process_begin (); - - while (1) { - bool const va = _da.video->pass (); - bool const vb = _db.video->pass (); - bool const a = _da.audio->pass (); - - _da.video->set_progress (); - - if (va && vb && a) { - break; - } - } - - if (_delay_line) { - _delay_line->process_end (); - } - if (_matcher) { - _matcher->process_end (); - } - if (_gain) { - _gain->process_end (); - } - _encoder->process_end (); -} - diff --git a/src/lib/ab_transcoder.h b/src/lib/ab_transcoder.h deleted file mode 100644 index 7bfcb393c..000000000 --- a/src/lib/ab_transcoder.h +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/ab_transcoder.h - * @brief A transcoder which uses one Film for the left half of the screen, and a different one - * for the right half (to facilitate A/B comparisons of settings) - */ - -#include <boost/shared_ptr.hpp> -#include <stdint.h> -#include "util.h" -#include "decoder_factory.h" - -class Job; -class Encoder; -class VideoDecoder; -class AudioDecoder; -class DecodeOptions; -class Image; -class Log; -class Subtitle; -class Film; -class Matcher; -class DelayLine; -class Gain; -class Combiner; - -/** @class ABTranscoder - * @brief A transcoder which uses one Film for the left half of the screen, and a different one - * for the right half (to facilitate A/B comparisons of settings) - */ -class ABTranscoder -{ -public: - ABTranscoder ( - boost::shared_ptr<Film> a, - boost::shared_ptr<Film> b, - boost::shared_ptr<const DecodeOptions> o, - Job* j, - boost::shared_ptr<Encoder> e - ); - - void go (); - -private: - boost::shared_ptr<Film> _film_a; - boost::shared_ptr<Film> _film_b; - Job* _job; - boost::shared_ptr<Encoder> _encoder; - Decoders _da; - Decoders _db; - boost::shared_ptr<Combiner> _combiner; - boost::shared_ptr<Matcher> _matcher; - boost::shared_ptr<DelayLine> _delay_line; - boost::shared_ptr<Gain> _gain; - boost::shared_ptr<Image> _image; -}; diff --git a/src/lib/analyse_audio_job.cc b/src/lib/analyse_audio_job.cc new file mode 100644 index 000000000..8186f9de4 --- /dev/null +++ b/src/lib/analyse_audio_job.cc @@ -0,0 +1,108 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include "audio_analysis.h" +#include "analyse_audio_job.h" +#include "compose.hpp" +#include "film.h" +#include "player.h" + +#include "i18n.h" + +using std::string; +using std::max; +using std::min; +using std::cout; +using boost::shared_ptr; + +int const AnalyseAudioJob::_num_points = 1024; + +AnalyseAudioJob::AnalyseAudioJob (shared_ptr<const Film> f, shared_ptr<AudioContent> c) + : Job (f) + , _content (c) + , _done (0) + , _samples_per_point (1) +{ + +} + +string +AnalyseAudioJob::name () const +{ + return _("Analyse audio"); +} + +void +AnalyseAudioJob::run () +{ + shared_ptr<AudioContent> content = _content.lock (); + if (!content) { + return; + } + + 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); + + _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); + } + + _analysis->write (content->audio_analysis_path ()); + + set_progress (1); + set_state (FINISHED_OK); +} + +void +AnalyseAudioJob::audio (shared_ptr<const AudioBuffers> b, Time) +{ + for (int i = 0; i < b->frames(); ++i) { + for (int j = 0; j < b->channels(); ++j) { + float s = b->data(j)[i]; + if (fabsf (s) < 10e-7) { + /* stringstream can't serialise and recover inf or -inf, so prevent such + values by replacing with this (140dB down) */ + s = 10e-7; + } + _current[j][AudioPoint::RMS] += pow (s, 2); + _current[j][AudioPoint::PEAK] = max (_current[j][AudioPoint::PEAK], fabsf (s)); + + if ((_done % _samples_per_point) == 0) { + _current[j][AudioPoint::RMS] = sqrt (_current[j][AudioPoint::RMS] / _samples_per_point); + _analysis->add_point (j, _current[j]); + + _current[j] = AudioPoint (); + } + } + + ++_done; + } +} + diff --git a/src/lib/make_dcp_job.h b/src/lib/analyse_audio_job.h index 5e4f78a25..3d4881983 100644 --- a/src/lib/make_dcp_job.h +++ b/src/lib/analyse_audio_job.h @@ -17,30 +17,31 @@ */ -/** @file src/make_dcp_job.h - * @brief A job to create DCPs. - */ - #include "job.h" +#include "audio_analysis.h" +#include "types.h" -class EncodeOptions; +class AudioBuffers; +class AudioContent; -/** @class MakeDCPJob - * @brief A job to create DCPs - */ -class MakeDCPJob : public Job +class AnalyseAudioJob : public Job { public: - MakeDCPJob (boost::shared_ptr<Film>, boost::shared_ptr<const EncodeOptions>, boost::shared_ptr<Job> req); + AnalyseAudioJob (boost::shared_ptr<const Film>, boost::shared_ptr<AudioContent>); std::string name () const; void run (); private: - void dcp_progress (float); - std::string j2c_path (int, int) const; - std::string wav_path (libdcp::Channel) const; + void audio (boost::shared_ptr<const AudioBuffers>, Time); + + boost::weak_ptr<AudioContent> _content; + OutputAudioFrame _done; + int64_t _samples_per_point; + std::vector<AudioPoint> _current; + + boost::shared_ptr<AudioAnalysis> _analysis; - boost::shared_ptr<const EncodeOptions> _opt; + static const int _num_points; }; diff --git a/src/lib/audio_analysis.cc b/src/lib/audio_analysis.cc new file mode 100644 index 000000000..bc59bccca --- /dev/null +++ b/src/lib/audio_analysis.cc @@ -0,0 +1,146 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <stdint.h> +#include <cmath> +#include <cassert> +#include <fstream> +#include <boost/filesystem.hpp> +#include "audio_analysis.h" + +using std::ostream; +using std::istream; +using std::string; +using std::ofstream; +using std::ifstream; +using std::vector; +using std::cout; +using std::max; +using std::list; + +AudioPoint::AudioPoint () +{ + for (int i = 0; i < COUNT; ++i) { + _data[i] = 0; + } +} + +AudioPoint::AudioPoint (istream& s) +{ + for (int i = 0; i < COUNT; ++i) { + s >> _data[i]; + } +} + +AudioPoint::AudioPoint (AudioPoint const & other) +{ + for (int i = 0; i < COUNT; ++i) { + _data[i] = other._data[i]; + } +} + +AudioPoint & +AudioPoint::operator= (AudioPoint const & other) +{ + if (this == &other) { + return *this; + } + + for (int i = 0; i < COUNT; ++i) { + _data[i] = other._data[i]; + } + + return *this; +} + +void +AudioPoint::write (ostream& s) const +{ + for (int i = 0; i < COUNT; ++i) { + s << _data[i] << "\n"; + } +} + + +AudioAnalysis::AudioAnalysis (int channels) +{ + _data.resize (channels); +} + +AudioAnalysis::AudioAnalysis (boost::filesystem::path filename) +{ + ifstream f (filename.string().c_str ()); + + int channels; + f >> channels; + _data.resize (channels); + + for (int i = 0; i < channels; ++i) { + int points; + f >> points; + for (int j = 0; j < points; ++j) { + _data[i].push_back (AudioPoint (f)); + } + } +} + +void +AudioAnalysis::add_point (int c, AudioPoint const & p) +{ + assert (c < channels ()); + _data[c].push_back (p); +} + +AudioPoint +AudioAnalysis::get_point (int c, int p) const +{ + assert (p < points (c)); + return _data[c][p]; +} + +int +AudioAnalysis::channels () const +{ + return _data.size (); +} + +int +AudioAnalysis::points (int c) const +{ + assert (c < channels ()); + return _data[c].size (); +} + +void +AudioAnalysis::write (boost::filesystem::path filename) +{ + string tmp = filename.string() + ".tmp"; + + ofstream f (tmp.c_str ()); + f << _data.size() << "\n"; + for (vector<vector<AudioPoint> >::iterator i = _data.begin(); i != _data.end(); ++i) { + f << i->size () << "\n"; + for (vector<AudioPoint>::iterator j = i->begin(); j != i->end(); ++j) { + j->write (f); + } + } + + f.close (); + boost::filesystem::rename (tmp, filename); +} diff --git a/src/lib/external_audio_decoder.h b/src/lib/audio_analysis.h index 2558955eb..cfc170c84 100644 --- a/src/lib/external_audio_decoder.h +++ b/src/lib/audio_analysis.h @@ -17,38 +17,54 @@ */ -#include <sndfile.h> -#include "decoder.h" -#include "audio_decoder.h" -#include "stream.h" +#ifndef DCPOMATIC_AUDIO_ANALYSIS_H +#define DCPOMATIC_AUDIO_ANALYSIS_H -class ExternalAudioStream : public AudioStream +#include <iostream> +#include <vector> +#include <list> +#include <boost/filesystem.hpp> + +class AudioPoint { public: - ExternalAudioStream (int sample_rate, int64_t layout) - : AudioStream (sample_rate, layout) - {} - - std::string to_string () const; + enum Type { + PEAK, + RMS, + COUNT + }; - static boost::shared_ptr<ExternalAudioStream> create (); - static boost::shared_ptr<ExternalAudioStream> create (std::string t, boost::optional<int> v); + AudioPoint (); + AudioPoint (std::istream &); + AudioPoint (AudioPoint const &); + AudioPoint& operator= (AudioPoint const &); -private: - friend class stream_test; + void write (std::ostream &) const; - ExternalAudioStream (); - ExternalAudioStream (std::string t, boost::optional<int> v); + float& operator[] (int t) { + return _data[t]; + } + +private: + float _data[COUNT]; }; -class ExternalAudioDecoder : public AudioDecoder +class AudioAnalysis : public boost::noncopyable { public: - ExternalAudioDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); + AudioAnalysis (int c); + AudioAnalysis (boost::filesystem::path); - bool pass (); + void add_point (int c, AudioPoint const & p); + + AudioPoint get_point (int c, int p) const; + int points (int c) const; + int channels () const; + + void write (boost::filesystem::path); private: - std::vector<SNDFILE*> open_files (sf_count_t &); - void close_files (std::vector<SNDFILE*> const &); + std::vector<std::vector<AudioPoint> > _data; }; + +#endif diff --git a/src/lib/audio_buffers.cc b/src/lib/audio_buffers.cc new file mode 100644 index 000000000..e80142b8e --- /dev/null +++ b/src/lib/audio_buffers.cc @@ -0,0 +1,275 @@ +/* + Copyright (C) 2012-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 <cassert> +#include <cstring> +#include <cmath> +#include <stdexcept> +#include "audio_buffers.h" + +using std::bad_alloc; +using boost::shared_ptr; + +/** Construct an AudioBuffers. Audio data is undefined after this constructor. + * @param channels Number of channels. + * @param frames Number of frames to reserve space for. + */ +AudioBuffers::AudioBuffers (int channels, int frames) +{ + allocate (channels, frames); +} + +/** Copy constructor. + * @param other Other AudioBuffers; data is copied. + */ +AudioBuffers::AudioBuffers (AudioBuffers const & other) +{ + allocate (other._channels, other._frames); + copy_from (&other, other._frames, 0, 0); +} + +AudioBuffers::AudioBuffers (boost::shared_ptr<const AudioBuffers> other) +{ + allocate (other->_channels, other->_frames); + copy_from (other.get(), other->_frames, 0, 0); +} + +AudioBuffers & +AudioBuffers::operator= (AudioBuffers const & other) +{ + if (this == &other) { + return *this; + } + + deallocate (); + allocate (other._channels, other._frames); + copy_from (&other, other._frames, 0, 0); + + return *this; +} + +/** AudioBuffers destructor */ +AudioBuffers::~AudioBuffers () +{ + deallocate (); +} + +void +AudioBuffers::allocate (int channels, int frames) +{ + _channels = channels; + _frames = frames; + _allocated_frames = frames; + + _data = static_cast<float**> (malloc (_channels * sizeof (float *))); + if (!_data) { + throw bad_alloc (); + } + + for (int i = 0; i < _channels; ++i) { + _data[i] = static_cast<float*> (malloc (frames * sizeof (float))); + if (!_data[i]) { + throw bad_alloc (); + } + } +} + +void +AudioBuffers::deallocate () +{ + for (int i = 0; i < _channels; ++i) { + free (_data[i]); + } + + free (_data); +} + +/** @param c Channel index. + * @return Buffer for this channel. + */ +float* +AudioBuffers::data (int c) const +{ + assert (c >= 0 && c < _channels); + return _data[c]; +} + +/** Set the number of frames that these AudioBuffers will report themselves + * as having. If we reduce the number of frames, the `lost' frames will + * be silenced. + * @param f Frames; must be less than or equal to the number of allocated frames. + */ +void +AudioBuffers::set_frames (int f) +{ + assert (f <= _allocated_frames); + + for (int c = 0; c < _channels; ++c) { + for (int i = f; i < _frames; ++i) { + _data[c][i] = 0; + } + } + + _frames = f; +} + +/** Make all samples on all channels silent */ +void +AudioBuffers::make_silent () +{ + for (int i = 0; i < _channels; ++i) { + make_silent (i); + } +} + +/** Make all samples on a given channel silent. + * @param c Channel. + */ +void +AudioBuffers::make_silent (int c) +{ + assert (c >= 0 && c < _channels); + + for (int i = 0; i < _frames; ++i) { + _data[c][i] = 0; + } +} + +void +AudioBuffers::make_silent (int from, int frames) +{ + assert ((from + frames) <= _allocated_frames); + + for (int c = 0; c < _channels; ++c) { + for (int i = from; i < (from + frames); ++i) { + _data[c][i] = 0; + } + } +} + +/** Copy data from another AudioBuffers to this one. All channels are copied. + * @param from AudioBuffers to copy from; must have the same number of channels as this. + * @param frames_to_copy Number of frames to copy. + * @param read_offset Offset to read from in `from'. + * @param write_offset Offset to write to in `to'. + */ +void +AudioBuffers::copy_from (AudioBuffers const * from, int frames_to_copy, int read_offset, int write_offset) +{ + assert (from->channels() == channels()); + + assert (from); + assert (read_offset >= 0 && (read_offset + frames_to_copy) <= from->_allocated_frames); + assert (write_offset >= 0 && (write_offset + frames_to_copy) <= _allocated_frames); + + for (int i = 0; i < _channels; ++i) { + memcpy (_data[i] + write_offset, from->_data[i] + read_offset, frames_to_copy * sizeof(float)); + } +} + +/** Move audio data around. + * @param from Offset to move from. + * @param to Offset to move to. + * @param frames Number of frames to move. + */ + +void +AudioBuffers::move (int from, int to, int frames) +{ + if (frames == 0) { + return; + } + + assert (from >= 0); + assert (from < _frames); + assert (to >= 0); + assert (to < _frames); + assert (frames > 0); + assert (frames <= _frames); + assert ((from + frames) <= _frames); + assert ((to + frames) <= _allocated_frames); + + for (int i = 0; i < _channels; ++i) { + memmove (_data[i] + to, _data[i] + from, frames * sizeof(float)); + } +} + +/** Add data from from `from', `from_channel' to our channel `to_channel' */ +void +AudioBuffers::accumulate_channel (AudioBuffers const * from, int from_channel, int to_channel) +{ + int const N = frames (); + assert (from->frames() == N); + assert (to_channel <= _channels); + + float* s = from->data (from_channel); + float* d = _data[to_channel]; + + for (int i = 0; i < N; ++i) { + *d++ += *s++; + } +} + +/** Ensure we have space for at least a certain number of frames. If we extend + * the buffers, fill the new space with silence. + */ +void +AudioBuffers::ensure_size (int frames) +{ + if (_allocated_frames >= frames) { + return; + } + + for (int i = 0; i < _channels; ++i) { + _data[i] = static_cast<float*> (realloc (_data[i], frames * sizeof (float))); + if (!_data[i]) { + throw bad_alloc (); + } + for (int j = _allocated_frames; j < frames; ++j) { + _data[i][j] = 0; + } + } + + _allocated_frames = frames; +} + +void +AudioBuffers::accumulate_frames (AudioBuffers const * from, int read_offset, int write_offset, int frames) +{ + assert (_channels == from->channels ()); + + for (int i = 0; i < _channels; ++i) { + for (int j = 0; j < frames; ++j) { + _data[i][j + write_offset] += from->data()[i][j + read_offset]; + } + } +} + +/** @param dB gain in dB */ +void +AudioBuffers::apply_gain (float dB) +{ + float const linear = pow (10, dB / 20); + + for (int i = 0; i < _channels; ++i) { + for (int j = 0; j < _frames; ++j) { + _data[i][j] *= linear; + } + } +} diff --git a/src/lib/audio_buffers.h b/src/lib/audio_buffers.h new file mode 100644 index 000000000..75bc686f8 --- /dev/null +++ b/src/lib/audio_buffers.h @@ -0,0 +1,81 @@ +/* + Copyright (C) 2012-2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DVDOMATIC_AUDIO_BUFFERS_H +#define DVDOMATIC_AUDIO_BUFFERS_H + +#include <boost/shared_ptr.hpp> + +/** @class AudioBuffers + * @brief A class to hold multi-channel audio data in float format. + */ +class AudioBuffers +{ +public: + AudioBuffers (int channels, int frames); + AudioBuffers (AudioBuffers const &); + AudioBuffers (boost::shared_ptr<const AudioBuffers>); + ~AudioBuffers (); + + AudioBuffers & operator= (AudioBuffers const &); + + void ensure_size (int); + + float** data () const { + return _data; + } + + float* data (int) const; + + int channels () const { + return _channels; + } + + int frames () const { + return _frames; + } + + void set_frames (int f); + + void make_silent (); + void make_silent (int c); + void make_silent (int from, int frames); + + void apply_gain (float); + + void copy_from (AudioBuffers const * from, int frames_to_copy, int read_offset, int write_offset); + void move (int from, int to, int frames); + void accumulate_channel (AudioBuffers const *, int, int); + void accumulate_frames (AudioBuffers const *, int read_offset, int write_offset, int frames); + +private: + void allocate (int, int); + void deallocate (); + + /** Number of channels */ + int _channels; + /** Number of frames (where a frame is one sample across all channels) */ + int _frames; + /** Number of frames that _data can hold */ + int _allocated_frames; + /** Audio data (so that, e.g. _data[2][6] is channel 2, sample 6) */ + float** _data; +}; + +#endif diff --git a/src/lib/audio_content.cc b/src/lib/audio_content.cc new file mode 100644 index 000000000..100264d44 --- /dev/null +++ b/src/lib/audio_content.cc @@ -0,0 +1,120 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "audio_content.h" +#include "analyse_audio_job.h" +#include "job_manager.h" +#include "film.h" + +using std::string; +using boost::shared_ptr; +using boost::lexical_cast; +using boost::dynamic_pointer_cast; + +int const AudioContentProperty::AUDIO_CHANNELS = 200; +int const AudioContentProperty::AUDIO_LENGTH = 201; +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; + +AudioContent::AudioContent (shared_ptr<const Film> f, Time s) + : Content (f, s) + , _audio_gain (0) + , _audio_delay (0) +{ + +} + +AudioContent::AudioContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , _audio_gain (0) + , _audio_delay (0) +{ + +} + +AudioContent::AudioContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) +{ + _audio_gain = node->number_child<float> ("AudioGain"); + _audio_delay = node->number_child<int> ("AudioDelay"); +} + +void +AudioContent::as_xml (xmlpp::Node* node) const +{ + boost::mutex::scoped_lock lm (_mutex); + node->add_child("AudioGain")->add_child_text (lexical_cast<string> (_audio_gain)); + node->add_child("AudioDelay")->add_child_text (lexical_cast<string> (_audio_delay)); +} + + +void +AudioContent::set_audio_gain (float g) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_gain = g; + } + + signal_changed (AudioContentProperty::AUDIO_GAIN); +} + +void +AudioContent::set_audio_delay (int d) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_delay = d; + } + + signal_changed (AudioContentProperty::AUDIO_DELAY); +} + +void +AudioContent::analyse_audio (boost::function<void()> finished) +{ + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return; + } + + shared_ptr<AnalyseAudioJob> job (new AnalyseAudioJob (film, dynamic_pointer_cast<AudioContent> (shared_from_this()))); + job->Finished.connect (finished); + JobManager::instance()->add (job); +} + +boost::filesystem::path +AudioContent::audio_analysis_path () const +{ + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return boost::filesystem::path (); + } + + return film->audio_analysis_path (dynamic_pointer_cast<const AudioContent> (shared_from_this ())); +} + +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()); +} diff --git a/src/lib/audio_content.h b/src/lib/audio_content.h new file mode 100644 index 000000000..73919105d --- /dev/null +++ b/src/lib/audio_content.h @@ -0,0 +1,83 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_AUDIO_CONTENT_H +#define DCPOMATIC_AUDIO_CONTENT_H + +#include "content.h" +#include "audio_mapping.h" + +namespace cxml { + class Node; +} + +class AudioContentProperty +{ +public: + static int const AUDIO_CHANNELS; + static int const AUDIO_LENGTH; + static int const AUDIO_FRAME_RATE; + static int const AUDIO_GAIN; + static int const AUDIO_DELAY; + static int const AUDIO_MAPPING; +}; + +class AudioContent : public virtual Content +{ +public: + typedef int64_t Frame; + + AudioContent (boost::shared_ptr<const Film>, Time); + AudioContent (boost::shared_ptr<const Film>, boost::filesystem::path); + AudioContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + std::string technical_summary () const; + + virtual int audio_channels () const = 0; + virtual AudioContent::Frame audio_length () const = 0; + virtual int content_audio_frame_rate () const = 0; + virtual int output_audio_frame_rate () const = 0; + virtual AudioMapping audio_mapping () const = 0; + virtual void set_audio_mapping (AudioMapping) = 0; + + void analyse_audio (boost::function<void()>); + boost::filesystem::path audio_analysis_path () const; + + void set_audio_gain (float); + void set_audio_delay (int); + + float audio_gain () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_gain; + } + + int audio_delay () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_delay; + } + +private: + /** Gain to apply to audio in dB */ + float _audio_gain; + /** Delay to apply to audio (positive moves audio later) in milliseconds */ + int _audio_delay; +}; + +#endif diff --git a/src/lib/audio_decoder.cc b/src/lib/audio_decoder.cc index 9d8de971c..1f5868583 100644 --- a/src/lib/audio_decoder.cc +++ b/src/lib/audio_decoder.cc @@ -18,19 +18,30 @@ */ #include "audio_decoder.h" -#include "stream.h" +#include "audio_buffers.h" +#include "exceptions.h" +#include "log.h" +#include "resampler.h" +#include "i18n.h" + +using std::stringstream; +using std::list; +using std::pair; +using std::cout; using boost::optional; using boost::shared_ptr; -AudioDecoder::AudioDecoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j) - : Decoder (f, o, j) +AudioDecoder::AudioDecoder (shared_ptr<const Film> film) + : Decoder (film) + , _audio_position (0) { } void -AudioDecoder::set_audio_stream (shared_ptr<AudioStream> s) +AudioDecoder::audio (shared_ptr<const AudioBuffers> data, AudioContent::Frame frame) { - _audio_stream = s; + Audio (data, frame); + _audio_position = frame + data->frames (); } diff --git a/src/lib/audio_decoder.h b/src/lib/audio_decoder.h index 013a6327f..2ad53da8b 100644 --- a/src/lib/audio_decoder.h +++ b/src/lib/audio_decoder.h @@ -21,38 +21,30 @@ * @brief Parent class for audio decoders. */ -#ifndef DVDOMATIC_AUDIO_DECODER_H -#define DVDOMATIC_AUDIO_DECODER_H +#ifndef DCPOMATIC_AUDIO_DECODER_H +#define DCPOMATIC_AUDIO_DECODER_H -#include "audio_source.h" -#include "stream.h" #include "decoder.h" +#include "content.h" +#include "audio_content.h" + +class AudioBuffers; /** @class AudioDecoder. * @brief Parent class for audio decoders. */ -class AudioDecoder : public AudioSource, public virtual Decoder +class AudioDecoder : public virtual Decoder { public: - AudioDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); - - virtual void set_audio_stream (boost::shared_ptr<AudioStream>); + AudioDecoder (boost::shared_ptr<const Film>); - /** @return Audio stream that we are using */ - boost::shared_ptr<AudioStream> audio_stream () const { - return _audio_stream; - } - - /** @return All available audio streams */ - std::vector<boost::shared_ptr<AudioStream> > audio_streams () const { - return _audio_streams; - } + /** Emitted when some audio data is ready */ + boost::signals2::signal<void (boost::shared_ptr<const AudioBuffers>, AudioContent::Frame)> Audio; protected: - /** Audio stream that we are using */ - boost::shared_ptr<AudioStream> _audio_stream; - /** All available audio streams */ - std::vector<boost::shared_ptr<AudioStream> > _audio_streams; + + void audio (boost::shared_ptr<const AudioBuffers>, AudioContent::Frame); + AudioContent::Frame _audio_position; }; #endif diff --git a/src/lib/audio_mapping.cc b/src/lib/audio_mapping.cc new file mode 100644 index 000000000..7a5da7d2a --- /dev/null +++ b/src/lib/audio_mapping.cc @@ -0,0 +1,117 @@ +/* + 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 <boost/lexical_cast.hpp> +#include <libxml++/libxml++.h> +#include <libcxml/cxml.h> +#include "audio_mapping.h" + +using std::list; +using std::cout; +using std::make_pair; +using std::pair; +using std::string; +using boost::shared_ptr; +using boost::lexical_cast; +using boost::dynamic_pointer_cast; + +AudioMapping::AudioMapping () + : _content_channels (0) +{ + +} + +/** Create a default AudioMapping for a given channel count. + * @param c Number of channels. + */ +AudioMapping::AudioMapping (int c) + : _content_channels (c) +{ + +} + +void +AudioMapping::make_default () +{ + if (_content_channels == 1) { + /* Mono -> Centre */ + add (0, libdcp::CENTRE); + } else { + /* 1:1 mapping */ + for (int i = 0; i < _content_channels; ++i) { + add (i, static_cast<libdcp::Channel> (i)); + } + } +} + +AudioMapping::AudioMapping (shared_ptr<const cxml::Node> node) +{ + _content_channels = node->number_child<int> ("ContentChannels"); + + list<shared_ptr<cxml::Node> > const c = node->node_children ("Map"); + for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) { + add ((*i)->number_child<int> ("ContentIndex"), static_cast<libdcp::Channel> ((*i)->number_child<int> ("DCP"))); + } +} + +void +AudioMapping::add (int c, libdcp::Channel d) +{ + _content_to_dcp.push_back (make_pair (c, d)); +} + +list<int> +AudioMapping::dcp_to_content (libdcp::Channel d) const +{ + list<int> c; + for (list<pair<int, libdcp::Channel> >::const_iterator i = _content_to_dcp.begin(); i != _content_to_dcp.end(); ++i) { + if (i->second == d) { + c.push_back (i->first); + } + } + + return c; +} + +list<libdcp::Channel> +AudioMapping::content_to_dcp (int c) const +{ + assert (c < _content_channels); + + list<libdcp::Channel> d; + for (list<pair<int, libdcp::Channel> >::const_iterator i = _content_to_dcp.begin(); i != _content_to_dcp.end(); ++i) { + if (i->first == c) { + d.push_back (i->second); + } + } + + return d; +} + +void +AudioMapping::as_xml (xmlpp::Node* node) const +{ + node->add_child ("ContentChannels")->add_child_text (lexical_cast<string> (_content_channels)); + + for (list<pair<int, libdcp::Channel> >::const_iterator i = _content_to_dcp.begin(); i != _content_to_dcp.end(); ++i) { + xmlpp::Node* t = node->add_child ("Map"); + t->add_child ("ContentIndex")->add_child_text (lexical_cast<string> (i->first)); + t->add_child ("DCP")->add_child_text (lexical_cast<string> (i->second)); + } +} diff --git a/src/lib/audio_mapping.h b/src/lib/audio_mapping.h new file mode 100644 index 000000000..9a507b550 --- /dev/null +++ b/src/lib/audio_mapping.h @@ -0,0 +1,70 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_AUDIO_MAPPING_H +#define DCPOMATIC_AUDIO_MAPPING_H + +#include <list> +#include <libdcp/types.h> +#include <boost/shared_ptr.hpp> + +namespace xmlpp { + class Node; +} + +namespace cxml { + class Node; +} + +/** 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 can be mapped to zero or + * more DCP channels. + */ +class AudioMapping +{ +public: + AudioMapping (); + AudioMapping (int); + AudioMapping (boost::shared_ptr<const cxml::Node>); + + /* Default copy constructor is fine */ + + void as_xml (xmlpp::Node *) const; + + void add (int, libdcp::Channel); + void make_default (); + + std::list<int> dcp_to_content (libdcp::Channel) const; + std::list<std::pair<int, libdcp::Channel> > content_to_dcp () const { + return _content_to_dcp; + } + + int content_channels () const { + return _content_channels; + } + + std::list<libdcp::Channel> content_to_dcp (int) const; + +private: + int _content_channels; + std::list<std::pair<int, libdcp::Channel> > _content_to_dcp; +}; + +#endif diff --git a/src/lib/audio_merger.h b/src/lib/audio_merger.h new file mode 100644 index 000000000..226601e0e --- /dev/null +++ b/src/lib/audio_merger.h @@ -0,0 +1,109 @@ +/* + 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/check_hashes_job.cc b/src/lib/check_hashes_job.cc deleted file mode 100644 index 701584c74..000000000 --- a/src/lib/check_hashes_job.cc +++ /dev/null @@ -1,123 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <fstream> -#include <boost/lexical_cast.hpp> -#include <boost/filesystem.hpp> -#include "check_hashes_job.h" -#include "options.h" -#include "log.h" -#include "job_manager.h" -#include "ab_transcode_job.h" -#include "transcode_job.h" -#include "film.h" -#include "exceptions.h" - -using std::string; -using std::stringstream; -using std::ifstream; -using boost::shared_ptr; - -CheckHashesJob::CheckHashesJob (shared_ptr<Film> f, shared_ptr<const DecodeOptions> od, shared_ptr<const EncodeOptions> oe, shared_ptr<Job> req) - : Job (f, req) - , _decode_opt (od) - , _encode_opt (oe) - , _bad (0) -{ - -} - -string -CheckHashesJob::name () const -{ - return String::compose ("Check hashes of %1", _film->name()); -} - -void -CheckHashesJob::run () -{ - _bad = 0; - - if (!_film->dcp_length()) { - throw EncodeError ("cannot check hashes of a DCP with unknown length"); - } - - SourceFrame const N = _film->dcp_trim_start() + _film->dcp_length().get(); - DCPFrameRate const dfr = dcp_frame_rate (_film->frames_per_second ()); - - for (SourceFrame i = _film->dcp_trim_start(); i < N; i += dfr.skip) { - string const j2k_file = _encode_opt->frame_out_path (i, false); - string const hash_file = _encode_opt->hash_out_path (i, false); - - if (!boost::filesystem::exists (j2k_file)) { - _film->log()->log (String::compose ("Frame %1 has a missing J2K file.", i)); - boost::filesystem::remove (hash_file); - ++_bad; - } else if (!boost::filesystem::exists (hash_file)) { - _film->log()->log (String::compose ("Frame %1 has a missing hash file.", i)); - boost::filesystem::remove (j2k_file); - ++_bad; - } else { - ifstream ref (hash_file.c_str ()); - string hash; - ref >> hash; - if (hash != md5_digest (j2k_file)) { - _film->log()->log (String::compose ("Frame %1 has wrong hash; deleting.", i)); - boost::filesystem::remove (j2k_file); - boost::filesystem::remove (hash_file); - ++_bad; - } - } - - set_progress (float (i) / N); - } - - if (_bad) { - shared_ptr<Job> tc; - - if (_film->dcp_ab()) { - tc.reset (new ABTranscodeJob (_film, _decode_opt, _encode_opt, shared_from_this())); - } else { - tc.reset (new TranscodeJob (_film, _decode_opt, _encode_opt, shared_from_this())); - } - - JobManager::instance()->add_after (shared_from_this(), tc); - JobManager::instance()->add_after (tc, shared_ptr<Job> (new CheckHashesJob (_film, _decode_opt, _encode_opt, tc))); - } - - set_progress (1); - set_state (FINISHED_OK); -} - -string -CheckHashesJob::status () const -{ - stringstream s; - s << Job::status (); - if (overall_progress() > 0) { - if (_bad == 0) { - s << "; no bad frames found"; - } else if (_bad == 1) { - s << "; 1 bad frame found"; - } else { - s << "; " << _bad << " bad frames found"; - } - } - return s.str (); -} diff --git a/src/lib/colour_conversion.cc b/src/lib/colour_conversion.cc new file mode 100644 index 000000000..ceb302971 --- /dev/null +++ b/src/lib/colour_conversion.cc @@ -0,0 +1,198 @@ +/* + 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 <boost/lexical_cast.hpp> +#include <libxml++/libxml++.h> +#include <libdcp/colour_matrix.h> +#include <libcxml/cxml.h> +#include "config.h" +#include "colour_conversion.h" +#include "util.h" + +#include "i18n.h" + +using std::list; +using std::string; +using std::cout; +using std::vector; +using boost::shared_ptr; +using boost::lexical_cast; +using boost::optional; + +ColourConversion::ColourConversion () + : input_gamma (2.4) + , input_gamma_linearised (true) + , matrix (3, 3) + , output_gamma (2.6) +{ + 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]; + } + } +} + +ColourConversion::ColourConversion (double i, bool il, double const m[3][3], double o) + : input_gamma (i) + , input_gamma_linearised (il) + , matrix (3, 3) + , output_gamma (o) +{ + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + matrix (i, j) = m[i][j]; + } + } +} + +ColourConversion::ColourConversion (shared_ptr<cxml::Node> node) + : matrix (3, 3) +{ + input_gamma = node->number_child<double> ("InputGamma"); + input_gamma_linearised = node->bool_child ("InputGammaLinearised"); + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + matrix (i, j) = 0; + } + } + + list<shared_ptr<cxml::Node> > m = node->node_children ("Matrix"); + for (list<shared_ptr<cxml::Node> >::iterator i = m.begin(); i != m.end(); ++i) { + int const ti = (*i)->number_attribute<int> ("i"); + int const tj = (*i)->number_attribute<int> ("j"); + matrix(ti, tj) = lexical_cast<double> ((*i)->content ()); + } + + output_gamma = node->number_child<double> ("OutputGamma"); +} + +void +ColourConversion::as_xml (xmlpp::Node* node) const +{ + node->add_child("InputGamma")->add_child_text (lexical_cast<string> (input_gamma)); + node->add_child("InputGammaLinearised")->add_child_text (input_gamma_linearised ? "1" : "0"); + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + xmlpp::Element* m = node->add_child("Matrix"); + m->set_attribute ("i", lexical_cast<string> (i)); + m->set_attribute ("j", lexical_cast<string> (j)); + m->add_child_text (lexical_cast<string> (matrix (i, j))); + } + } + + node->add_child("OutputGamma")->add_child_text (lexical_cast<string> (output_gamma)); +} + +optional<size_t> +ColourConversion::preset () const +{ + vector<PresetColourConversion> presets = Config::instance()->colour_conversions (); + size_t i = 0; + while (i < presets.size() && (presets[i].conversion != *this)) { + ++i; + } + + if (i >= presets.size ()) { + return optional<size_t> (); + } + + return i; +} + +string +ColourConversion::identifier () const +{ + double numbers[12]; + + int n = 0; + numbers[n++] = input_gamma; + numbers[n++] = input_gamma_linearised; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + numbers[n++] = matrix (i, j); + } + } + numbers[n++] = output_gamma; + + assert (n == 12); + + return md5_digest (numbers, 12 * sizeof (double)); +} + +PresetColourConversion::PresetColourConversion () + : name (_("Untitled")) +{ + +} + +PresetColourConversion::PresetColourConversion (string n, double i, bool il, double const m[3][3], double o) + : name (n) + , conversion (i, il, m, o) +{ + +} + +PresetColourConversion::PresetColourConversion (shared_ptr<cxml::Node> node) + : conversion (node) +{ + name = node->string_child ("Name"); +} + +void +PresetColourConversion::as_xml (xmlpp::Node* node) const +{ + conversion.as_xml (node); + node->add_child("Name")->add_child_text (name); +} + +static bool +about_equal (double a, double b) +{ + static const double eps = 1e-6; + return fabs (a - b) < eps; +} + +bool +operator== (ColourConversion const & a, ColourConversion const & b) +{ + if ( + !about_equal (a.input_gamma, b.input_gamma) || + a.input_gamma_linearised != b.input_gamma_linearised || + !about_equal (a.output_gamma, b.output_gamma)) { + return false; + } + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + if (!about_equal (a.matrix (i, j), b.matrix (i, j))) { + return false; + } + } + } + + return true; +} + +bool +operator!= (ColourConversion const & a, ColourConversion const & b) +{ + return !(a == b); +} diff --git a/src/lib/colour_conversion.h b/src/lib/colour_conversion.h new file mode 100644 index 000000000..893148466 --- /dev/null +++ b/src/lib/colour_conversion.h @@ -0,0 +1,74 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_COLOUR_CONVERSION_H +#define DCPOMATIC_COLOUR_CONVERSION_H + +/* Hack for OS X compile failure; see https://bugs.launchpad.net/hugin/+bug/910160 */ +#ifdef check +#undef check +#endif + +#include <boost/utility.hpp> +#include <boost/optional.hpp> +#include <boost/numeric/ublas/matrix.hpp> + +namespace cxml { + class Node; +} + +namespace xmlpp { + class Node; +} + +class ColourConversion +{ +public: + ColourConversion (); + ColourConversion (double, bool, double const matrix[3][3], double); + ColourConversion (boost::shared_ptr<cxml::Node>); + + virtual void as_xml (xmlpp::Node *) const; + std::string identifier () const; + + boost::optional<size_t> preset () const; + + double input_gamma; + bool input_gamma_linearised; + boost::numeric::ublas::matrix<double> matrix; + double output_gamma; +}; + +class PresetColourConversion +{ +public: + PresetColourConversion (); + PresetColourConversion (std::string, double, bool, double const matrix[3][3], double); + PresetColourConversion (boost::shared_ptr<cxml::Node>); + + void as_xml (xmlpp::Node *) const; + + std::string name; + ColourConversion conversion; +}; + +bool operator== (ColourConversion const &, ColourConversion const &); +bool operator!= (ColourConversion const &, ColourConversion const &); + +#endif diff --git a/src/lib/combiner.cc b/src/lib/combiner.cc deleted file mode 100644 index 68aafd2a2..000000000 --- a/src/lib/combiner.cc +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include "combiner.h" -#include "image.h" - -using boost::shared_ptr; - -Combiner::Combiner (Log* log) - : VideoProcessor (log) -{ - -} - -/** Process video for the left half of the frame. - * Subtitle parameter will be ignored. - * @param image Frame image. - */ -void -Combiner::process_video (shared_ptr<Image> image, bool, shared_ptr<Subtitle>) -{ - _image = image; -} - -/** Process video for the right half of the frame. - * @param image Frame image. - * @param sub Subtitle (which will be put onto the whole frame) - */ -void -Combiner::process_video_b (shared_ptr<Image> image, bool, shared_ptr<Subtitle> sub) -{ - /* Copy the right half of this image into our _image */ - /* XXX: this should probably be in the Image class */ - for (int i = 0; i < image->components(); ++i) { - int const line_size = image->line_size()[i]; - int const half_line_size = line_size / 2; - int const stride = image->stride()[i]; - - uint8_t* p = _image->data()[i]; - uint8_t* q = image->data()[i]; - - for (int j = 0; j < image->lines (i); ++j) { - memcpy (p + half_line_size, q + half_line_size, half_line_size); - p += stride; - q += stride; - } - } - - Video (_image, false, sub); - _image.reset (); -} diff --git a/src/lib/config.cc b/src/lib/config.cc index a74c36f73..5b96d108c 100644 --- a/src/lib/config.cc +++ b/src/lib/config.cc @@ -22,36 +22,120 @@ #include <fstream> #include <glib.h> #include <boost/filesystem.hpp> +#include <libdcp/colour_matrix.h> +#include <libcxml/cxml.h> #include "config.h" #include "server.h" #include "scaler.h" #include "filter.h" +#include "ratio.h" +#include "dcp_content_type.h" #include "sound_processor.h" -#include "cinema.h" +#include "colour_conversion.h" + +#include "i18n.h" using std::vector; using std::ifstream; using std::string; using std::ofstream; using std::list; +using std::max; using boost::shared_ptr; +using boost::lexical_cast; +using boost::optional; Config* Config::_instance = 0; /** Construct default configuration */ Config::Config () - : _num_local_encoding_threads (2) + : _num_local_encoding_threads (max (2U, boost::thread::hardware_concurrency())) , _server_port (6192) - , _reference_scaler (Scaler::from_id ("bicubic")) - , _tms_path (".") - , _sound_processor (SoundProcessor::from_id ("dolby_cp750")) + , _tms_path (N_(".")) + , _sound_processor (SoundProcessor::from_id (N_("dolby_cp750"))) + , _default_still_length (10) + , _default_container (Ratio::from_id ("185")) + , _default_dcp_content_type (DCPContentType::from_dci_name ("TST")) + , _default_j2k_bandwidth (200000000) { - ifstream f (read_file().c_str ()); - string line; + _allowed_dcp_frame_rates.push_back (24); + _allowed_dcp_frame_rates.push_back (25); + _allowed_dcp_frame_rates.push_back (30); + _allowed_dcp_frame_rates.push_back (48); + _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)); +} + +void +Config::read () +{ + if (!boost::filesystem::exists (file (false))) { + read_old_metadata (); + return; + } - shared_ptr<Cinema> cinema; - shared_ptr<Screen> screen; + cxml::Document f ("Config"); + f.read_file (file (false)); + optional<string> c; + + _num_local_encoding_threads = f.number_child<int> ("NumLocalEncodingThreads"); + _default_directory = f.string_child ("DefaultDirectory"); + _server_port = f.number_child<int> ("ServerPort"); + list<shared_ptr<cxml::Node> > servers = f.node_children ("Server"); + for (list<shared_ptr<cxml::Node> >::iterator i = servers.begin(); i != servers.end(); ++i) { + _servers.push_back (ServerDescription (*i)); + } + + _tms_ip = f.string_child ("TMSIP"); + _tms_path = f.string_child ("TMSPath"); + _tms_user = f.string_child ("TMSUser"); + _tms_password = f.string_child ("TMSPassword"); + + c = f.optional_string_child ("SoundProcessor"); + if (c) { + _sound_processor = SoundProcessor::from_id (c.get ()); + } + + _language = f.optional_string_child ("Language"); + + c = f.optional_string_child ("DefaultContainer"); + if (c) { + _default_container = Ratio::from_id (c.get ()); + } + + c = f.optional_string_child ("DefaultDCPContentType"); + if (c) { + _default_dcp_content_type = DCPContentType::from_dci_name (c.get ()); + } + + _dcp_metadata.issuer = f.optional_string_child ("DCPMetadataIssuer").get_value_or (""); + _dcp_metadata.creator = f.optional_string_child ("DCPMetadataCreator").get_value_or (""); + + _default_dci_metadata = DCIMetadata (f.node_child ("DCIMetadata")); + _default_still_length = f.optional_number_child<int>("DefaultStillLength").get_value_or (10); + _default_j2k_bandwidth = f.optional_number_child<int>("DefaultJ2KBandwidth").get_value_or (200000000); + + list<shared_ptr<cxml::Node> > cc = f.node_children ("ColourConversion"); + + if (!cc.empty ()) { + _colour_conversions.clear (); + } + + for (list<shared_ptr<cxml::Node> >::iterator i = cc.begin(); i != cc.end(); ++i) { + _colour_conversions.push_back (PresetColourConversion (*i)); + } +} + +void +Config::read_old_metadata () +{ + ifstream f (file(true).c_str ()); + string line; + while (getline (f, line)) { if (line.empty ()) { continue; @@ -69,76 +153,58 @@ Config::Config () string const k = line.substr (0, s); string const v = line.substr (s + 1); - if (k == "num_local_encoding_threads") { + if (k == N_("num_local_encoding_threads")) { _num_local_encoding_threads = atoi (v.c_str ()); - } else if (k == "default_directory") { + } else if (k == N_("default_directory")) { _default_directory = v; - } else if (k == "server_port") { + } else if (k == N_("server_port")) { _server_port = atoi (v.c_str ()); - } else if (k == "reference_scaler") { - _reference_scaler = Scaler::from_id (v); - } else if (k == "reference_filter") { - _reference_filters.push_back (Filter::from_id (v)); - } else if (k == "server") { - _servers.push_back (ServerDescription::create_from_metadata (v)); - } else if (k == "tms_ip") { + } else if (k == N_("server")) { + optional<ServerDescription> server = ServerDescription::create_from_metadata (v); + if (server) { + _servers.push_back (server.get ()); + } + } else if (k == N_("tms_ip")) { _tms_ip = v; - } else if (k == "tms_path") { + } else if (k == N_("tms_path")) { _tms_path = v; - } else if (k == "tms_user") { + } else if (k == N_("tms_user")) { _tms_user = v; - } else if (k == "tms_password") { + } else if (k == N_("tms_password")) { _tms_password = v; - } else if (k == "sound_processor") { + } else if (k == N_("sound_processor")) { _sound_processor = SoundProcessor::from_id (v); - } else if (k == "cinema") { - if (cinema) { - _cinemas.push_back (cinema); - } - cinema.reset (new Cinema (v, "")); - } else if (k == "cinema_email") { - assert (cinema); - cinema->email = v; - } else if (k == "screen") { - assert (cinema); - if (screen) { - cinema->screens.push_back (screen); - } - screen.reset (new Screen (v, shared_ptr<libdcp::Certificate> ())); - } else if (k == "screen_certificate") { - assert (screen); - shared_ptr<Certificate> c (new libdcp::Certificate); - c->set_from_string (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_dci_name (v); + } else if (k == "dcp_metadata_issuer") { + _dcp_metadata.issuer = v; + } else if (k == "dcp_metadata_creator") { + _dcp_metadata.creator = v; + } else if (k == "dcp_metadata_issue_date") { + _dcp_metadata.issue_date = v; } - } - if (cinema) { - _cinemas.push_back (cinema); + _default_dci_metadata.read_old_metadata (k, v); } } /** @return Filename to write configuration to */ string -Config::write_file () const +Config::file (bool old) const { boost::filesystem::path p; p /= g_get_user_config_dir (); - p /= "dvdomatic"; - boost::filesystem::create_directory (p); - p /= "config"; - return p.string (); -} - -string -Config::read_file () const -{ - if (boost::filesystem::exists (write_file ())) { - return write_file (); + boost::system::error_code ec; + boost::filesystem::create_directory (p, ec); + if (old) { + p /= ".dvdomatic"; + } else { + p /= "dcpomatic.xml"; } - - boost::filesystem::path p; - p /= g_get_user_config_dir (); - p /= ".dvdomatic"; return p.string (); } @@ -159,6 +225,13 @@ Config::instance () { if (_instance == 0) { _instance = new Config; + try { + _instance->read (); + } catch (...) { + /* configuration load failed; never mind, just + stick with the default. + */ + } } return _instance; @@ -168,33 +241,46 @@ Config::instance () void Config::write () const { - ofstream f (write_file().c_str ()); - f << "num_local_encoding_threads " << _num_local_encoding_threads << "\n" - << "default_directory " << _default_directory << "\n" - << "server_port " << _server_port << "\n" - << "reference_scaler " << _reference_scaler->id () << "\n"; + xmlpp::Document doc; + xmlpp::Element* root = doc.create_root_node ("Config"); - for (vector<Filter const *>::const_iterator i = _reference_filters.begin(); i != _reference_filters.end(); ++i) { - f << "reference_filter " << (*i)->id () << "\n"; - } + root->add_child("NumLocalEncodingThreads")->add_child_text (lexical_cast<string> (_num_local_encoding_threads)); + root->add_child("DefaultDirectory")->add_child_text (_default_directory); + root->add_child("ServerPort")->add_child_text (lexical_cast<string> (_server_port)); - for (vector<ServerDescription*>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) { - f << "server " << (*i)->as_metadata () << "\n"; + for (vector<ServerDescription>::const_iterator i = _servers.begin(); i != _servers.end(); ++i) { + i->as_xml (root->add_child ("Server")); } - f << "tms_ip " << _tms_ip << "\n"; - f << "tms_path " << _tms_path << "\n"; - f << "tms_user " << _tms_user << "\n"; - f << "tms_password " << _tms_password << "\n"; - f << "sound_processor " << _sound_processor->id () << "\n"; + root->add_child("TMSIP")->add_child_text (_tms_ip); + 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 (_language) { + root->add_child("Language")->add_child_text (_language.get()); + } + if (_default_container) { + root->add_child("DefaultContainer")->add_child_text (_default_container->id ()); + } + if (_default_dcp_content_type) { + root->add_child("DefaultDCPContentType")->add_child_text (_default_dcp_content_type->dci_name ()); + } + root->add_child("DCPMetadataIssuer")->add_child_text (_dcp_metadata.issuer); + root->add_child("DCPMetadataCreator")->add_child_text (_dcp_metadata.creator); - for (list<shared_ptr<Cinema> >::const_iterator i = _cinemas.begin(); i != _cinemas.end(); ++i) { - f << "cinema " << (*i)->name << "\n"; - f << "cinema_email " << (*i)->email << "\n"; - for (list<shared_ptr<Screen> >::iterator j = (*i)->screens.begin(); j != (*i)->screens.end(); ++j) { - f << "screen " << (*j)->name << "\n"; - } + _default_dci_metadata.as_xml (root->add_child ("DCIMetadata")); + + root->add_child("DefaultStillLength")->add_child_text (lexical_cast<string> (_default_still_length)); + root->add_child("DefaultJ2KBandwidth")->add_child_text (lexical_cast<string> (_default_j2k_bandwidth)); + + for (vector<PresetColourConversion>::const_iterator i = _colour_conversions.begin(); i != _colour_conversions.end(); ++i) { + i->as_xml (root->add_child ("ColourConversion")); } + + doc.write_to_file_formatted (file (false)); } string @@ -206,3 +292,10 @@ Config::default_directory_or (string a) const return _default_directory; } + +void +Config::drop () +{ + delete _instance; + _instance = 0; +} diff --git a/src/lib/config.h b/src/lib/config.h index ee4e4eaec..48eabd54c 100644 --- a/src/lib/config.h +++ b/src/lib/config.h @@ -21,23 +21,29 @@ * @brief Class holding configuration. */ -#ifndef DVDOMATIC_CONFIG_H -#define DVDOMATIC_CONFIG_H +#ifndef DCPOMATIC_CONFIG_H +#define DCPOMATIC_CONFIG_H #include <vector> #include <boost/shared_ptr.hpp> #include <boost/signals2.hpp> +#include <libdcp/metadata.h> +#include "dci_metadata.h" +#include "colour_conversion.h" +#include "server.h" class ServerDescription; class Scaler; class Filter; class SoundProcessor; +class DCPContentType; +class Ratio; class Cinema; /** @class Config * @brief A singleton class holding configuration. */ -class Config +class Config : public boost::noncopyable { public: @@ -58,18 +64,10 @@ public: } /** @return J2K encoding servers to use */ - std::vector<ServerDescription*> servers () const { + std::vector<ServerDescription> servers () const { return _servers; } - Scaler const * reference_scaler () const { - return _reference_scaler; - } - - std::vector<Filter const *> reference_filters () const { - return _reference_filters; - } - /** @return The IP address of a TMS that we can copy DCPs to */ std::string tms_ip () const { return _tms_ip; @@ -98,6 +96,42 @@ public: std::list<boost::shared_ptr<Cinema> > cinemas () const { return _cinemas; } + + std::list<int> allowed_dcp_frame_rates () const { + return _allowed_dcp_frame_rates; + } + + DCIMetadata default_dci_metadata () const { + return _default_dci_metadata; + } + + boost::optional<std::string> language () const { + return _language; + } + + int default_still_length () const { + return _default_still_length; + } + + Ratio const * default_container () const { + return _default_container; + } + + DCPContentType const * default_dcp_content_type () const { + return _default_dcp_content_type; + } + + libdcp::XMLMetadata dcp_metadata () const { + return _dcp_metadata; + } + + int default_j2k_bandwidth () const { + return _default_j2k_bandwidth; + } + + std::vector<PresetColourConversion> colour_conversions () const { + return _colour_conversions; + } /** @param n New number of local encoding threads */ void set_num_local_encoding_threads (int n) { @@ -114,7 +148,7 @@ public: } /** @param s New list of servers */ - void set_servers (std::vector<ServerDescription*> s) { + void set_servers (std::vector<ServerDescription> s) { _servers = s; } @@ -153,17 +187,59 @@ public: void remove_cinema (boost::shared_ptr<Cinema> c) { _cinemas.remove (c); } + + void set_allowed_dcp_frame_rates (std::list<int> const & r) { + _allowed_dcp_frame_rates = r; + } + + void set_default_dci_metadata (DCIMetadata d) { + _default_dci_metadata = d; + } + + void set_language (std::string l) { + _language = l; + } + + void unset_language () { + _language = boost::none; + } + + void set_default_still_length (int s) { + _default_still_length = s; + } + + void set_default_container (Ratio const * c) { + _default_container = c; + } + + void set_default_dcp_content_type (DCPContentType const * t) { + _default_dcp_content_type = t; + } + + void set_dcp_metadata (libdcp::XMLMetadata m) { + _dcp_metadata = m; + } + + void set_default_j2k_bandwidth (int b) { + _default_j2k_bandwidth = b; + } + + void set_colour_conversions (std::vector<PresetColourConversion> const & c) { + _colour_conversions = c; + } void write () const; std::string crypt_chain_directory () const; static Config* instance (); + static void drop (); private: Config (); - std::string read_file () const; - std::string write_file () const; + std::string file (bool) const; + void read (); + void read_old_metadata (); /** number of threads to use for J2K encoding on the local machine */ int _num_local_encoding_threads; @@ -173,7 +249,7 @@ private: int _server_port; /** J2K encoding servers to use */ - std::vector<ServerDescription *> _servers; + std::vector<ServerDescription> _servers; /** Scaler to use for the "A" part of A/B comparisons */ Scaler const * _reference_scaler; /** Filters to use for the "A" part of A/B comparisons */ @@ -188,6 +264,16 @@ private: std::string _tms_password; /** Our sound processor */ SoundProcessor const * _sound_processor; + std::list<int> _allowed_dcp_frame_rates; + /** Default DCI metadata for newly-created Films */ + DCIMetadata _default_dci_metadata; + boost::optional<std::string> _language; + int _default_still_length; + Ratio const * _default_container; + DCPContentType const * _default_dcp_content_type; + libdcp::XMLMetadata _dcp_metadata; + int _default_j2k_bandwidth; + std::vector<PresetColourConversion> _colour_conversions; std::list<boost::shared_ptr<Cinema> > _cinemas; diff --git a/src/lib/content.cc b/src/lib/content.cc new file mode 100644 index 000000000..d2a07f795 --- /dev/null +++ b/src/lib/content.cc @@ -0,0 +1,176 @@ +/* + 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 <boost/thread/mutex.hpp> +#include <libxml++/libxml++.h> +#include <libcxml/cxml.h> +#include "content.h" +#include "util.h" +#include "content_factory.h" +#include "ui_signaller.h" + +using std::string; +using std::set; +using boost::shared_ptr; +using boost::lexical_cast; + +int const ContentProperty::POSITION = 400; +int const ContentProperty::LENGTH = 401; +int const ContentProperty::TRIM_START = 402; +int const ContentProperty::TRIM_END = 403; + +Content::Content (shared_ptr<const Film> f, Time p) + : _film (f) + , _position (p) + , _trim_start (0) + , _trim_end (0) + , _change_signals_frequent (false) +{ + +} + +Content::Content (shared_ptr<const Film> f, boost::filesystem::path p) + : _film (f) + , _path (p) + , _position (0) + , _trim_start (0) + , _trim_end (0) + , _change_signals_frequent (false) +{ + +} + +Content::Content (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : _film (f) + , _change_signals_frequent (false) +{ + _path = node->string_child ("Path"); + _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"); +} + +void +Content::as_xml (xmlpp::Node* node) const +{ + boost::mutex::scoped_lock lm (_mutex); + + node->add_child("Path")->add_child_text (_path.string()); + node->add_child("Digest")->add_child_text (_digest); + node->add_child("Position")->add_child_text (lexical_cast<string> (_position)); + node->add_child("TrimStart")->add_child_text (lexical_cast<string> (_trim_start)); + node->add_child("TrimEnd")->add_child_text (lexical_cast<string> (_trim_end)); +} + +void +Content::examine (shared_ptr<Job> job) +{ + boost::mutex::scoped_lock lm (_mutex); + boost::filesystem::path p = _path; + lm.unlock (); + + string d; + if (boost::filesystem::is_regular_file (p)) { + d = md5_digest (p); + } else { + d = md5_digest_directory (p, job); + } + + lm.lock (); + _digest = d; +} + +void +Content::signal_changed (int p) +{ + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (Changed), shared_from_this (), p, _change_signals_frequent)); + } +} + +void +Content::set_position (Time p) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _position = p; + } + + signal_changed (ContentProperty::POSITION); +} + +void +Content::set_trim_start (Time t) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _trim_start = t; + } + + signal_changed (ContentProperty::TRIM_START); +} + +void +Content::set_trim_end (Time t) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _trim_end = t; + } + + signal_changed (ContentProperty::TRIM_END); +} + + +shared_ptr<Content> +Content::clone () const +{ + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return shared_ptr<Content> (); + } + + /* This is a bit naughty, but I can't think of a compelling reason not to do it ... */ + xmlpp::Document doc; + xmlpp::Node* node = doc.create_root_node ("Content"); + as_xml (node); + return content_factory (film, shared_ptr<cxml::Node> (new cxml::Node (node))); +} + +string +Content::technical_summary () const +{ + return String::compose ("%1 %2 %3", path(), digest(), position()); +} + +Time +Content::length_after_trim () const +{ + return 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 ())); +} diff --git a/src/lib/content.h b/src/lib/content.h new file mode 100644 index 000000000..3c57dddbe --- /dev/null +++ b/src/lib/content.h @@ -0,0 +1,133 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_CONTENT_H +#define DCPOMATIC_CONTENT_H + +#include <set> +#include <boost/filesystem.hpp> +#include <boost/signals2.hpp> +#include <boost/thread/mutex.hpp> +#include <boost/enable_shared_from_this.hpp> +#include <libxml++/libxml++.h> +#include "types.h" + +namespace cxml { + class Node; +} + +class Job; +class Film; + +class ContentProperty +{ +public: + static int const POSITION; + static int const LENGTH; + static int const TRIM_START; + static int const TRIM_END; +}; + +class Content : public boost::enable_shared_from_this<Content>, public boost::noncopyable +{ +public: + Content (boost::shared_ptr<const Film>, Time); + Content (boost::shared_ptr<const Film>, boost::filesystem::path); + Content (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + virtual ~Content () {} + + virtual void examine (boost::shared_ptr<Job>); + virtual std::string summary () const = 0; + 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; + + boost::shared_ptr<Content> clone () const; + + boost::filesystem::path path () const { + boost::mutex::scoped_lock lm (_mutex); + return _path; + } + + /** @return MD5 digest of the content's file(s) */ + std::string digest () const { + boost::mutex::scoped_lock lm (_mutex); + return _digest; + } + + void set_position (Time); + + /** Time that this content starts; i.e. the time that the first + * bit of the content (trimmed or not) will happen. + */ + Time position () const { + boost::mutex::scoped_lock lm (_mutex); + return _position; + } + + void set_trim_start (Time); + + Time trim_start () const { + boost::mutex::scoped_lock lm (_mutex); + return _trim_start; + } + + void set_trim_end (Time); + + Time trim_end () const { + boost::mutex::scoped_lock lm (_mutex); + return _trim_end; + } + + Time end () const { + return position() + length_after_trim(); + } + + Time length_after_trim () const; + + void set_change_signals_frequent (bool f) { + _change_signals_frequent = f; + } + + bool trimmed (Time) const; + + boost::signals2::signal<void (boost::weak_ptr<Content>, int, bool)> Changed; + +protected: + void signal_changed (int); + + 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. + */ + mutable boost::mutex _mutex; + +private: + /** Path of a file or a directory containing files */ + boost::filesystem::path _path; + std::string _digest; + Time _position; + Time _trim_start; + Time _trim_end; + bool _change_signals_frequent; +}; + +#endif diff --git a/src/lib/content_factory.cc b/src/lib/content_factory.cc new file mode 100644 index 000000000..6ed01f174 --- /dev/null +++ b/src/lib/content_factory.cc @@ -0,0 +1,47 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "ffmpeg_content.h" +#include "still_image_content.h" +#include "moving_image_content.h" +#include "sndfile_content.h" + +using std::string; +using boost::shared_ptr; + +shared_ptr<Content> +content_factory (shared_ptr<const Film> film, shared_ptr<cxml::Node> node) +{ + string const type = node->string_child ("Type"); + + boost::shared_ptr<Content> content; + + if (type == "FFmpeg") { + content.reset (new FFmpegContent (film, node)); + } else if (type == "StillImage") { + content.reset (new StillImageContent (film, node)); + } else if (type == "MovingImage") { + content.reset (new MovingImageContent (film, node)); + } else if (type == "Sndfile") { + content.reset (new SndfileContent (film, node)); + } + + return content; +} diff --git a/src/lib/audio_source.cc b/src/lib/content_factory.h index 53b0dda15..27cd36024 100644 --- a/src/lib/audio_source.cc +++ b/src/lib/content_factory.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,14 +17,6 @@ */ -#include "audio_source.h" -#include "audio_sink.h" +class Film; -using boost::shared_ptr; -using boost::bind; - -void -AudioSource::connect_audio (shared_ptr<AudioSink> s) -{ - Audio.connect (bind (&AudioSink::process_audio, s, _1)); -} +extern boost::shared_ptr<Content> content_factory (boost::shared_ptr<const Film>, boost::shared_ptr<cxml::Node>); diff --git a/src/lib/cross.cc b/src/lib/cross.cc index 2c66ab53a..61ec8de5e 100644 --- a/src/lib/cross.cc +++ b/src/lib/cross.cc @@ -17,21 +17,217 @@ */ +#include <fstream> +#include <boost/algorithm/string.hpp> #include "cross.h" -#ifdef DVDOMATIC_POSIX +#include "compose.hpp" +#include "log.h" +#ifdef DCPOMATIC_LINUX #include <unistd.h> +#include <mntent.h> #endif -#ifdef DVDOMATIC_WINDOWS -#include "windows.h" +#ifdef DCPOMATIC_WINDOWS +#include <windows.h> +#undef DATADIR +#include <shlwapi.h> #endif +#ifdef DCPOMATIC_OSX +#include <sys/sysctl.h> +#include <mach-o/dyld.h> +#endif + +using std::pair; +using std::list; +using std::ifstream; +using std::string; +using std::wstring; +using std::make_pair; +using boost::shared_ptr; void -dvdomatic_sleep (int s) +dcpomatic_sleep (int s) { -#ifdef DVDOMATIC_POSIX +#ifdef DCPOMATIC_POSIX sleep (s); #endif -#ifdef DVDOMATIC_WINDOWS +#ifdef DCPOMATIC_WINDOWS Sleep (s * 1000); #endif } + +/** @return A string of CPU information (model name etc.) */ +string +cpu_info () +{ + string info; + +#ifdef DCPOMATIC_LINUX + ifstream f ("/proc/cpuinfo"); + while (f.good ()) { + string l; + getline (f, l); + if (boost::algorithm::starts_with (l, "model name")) { + string::size_type const c = l.find (':'); + if (c != string::npos) { + info = l.substr (c + 2); + } + } + } +#endif + +#ifdef DCPOMATIC_OSX + char buffer[64]; + size_t N = sizeof (buffer); + if (sysctlbyname ("machdep.cpu.brand_string", buffer, &N, 0, 0) == 0) { + info = buffer; + } +#endif + +#ifdef DCPOMATIC_WINDOWS + HKEY key; + if (RegOpenKeyEx (HKEY_LOCAL_MACHINE, L"HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", 0, KEY_READ, &key) != ERROR_SUCCESS) { + return info; + } + + DWORD type; + DWORD data; + if (RegQueryValueEx (key, L"ProcessorNameString", 0, &type, 0, &data) != ERROR_SUCCESS) { + return info; + } + + if (type != REG_SZ) { + return info; + } + + wstring value (data / sizeof (wchar_t), L'\0'); + if (RegQueryValueEx (key, L"ProcessorNameString", 0, 0, reinterpret_cast<LPBYTE> (&value[0]), &data) != ERROR_SUCCESS) { + RegCloseKey (key); + return info; + } + + info = string (value.begin(), value.end()); + + RegCloseKey (key); + +#endif + + return info; +} + +void +run_ffprobe (boost::filesystem::path content, boost::filesystem::path out, shared_ptr<Log> log) +{ +#ifdef DCPOMATIC_WINDOWS + SECURITY_ATTRIBUTES security; + security.nLength = sizeof (security); + security.bInheritHandle = TRUE; + security.lpSecurityDescriptor = 0; + + HANDLE child_stderr_read; + HANDLE child_stderr_write; + if (!CreatePipe (&child_stderr_read, &child_stderr_write, &security, 0)) { + log->log ("ffprobe call failed (could not CreatePipe)"); + return; + } + + wchar_t dir[512]; + GetModuleFileName (GetModuleHandle (0), dir, sizeof (dir)); + PathRemoveFileSpec (dir); + SetCurrentDirectory (dir); + + STARTUPINFO startup_info; + ZeroMemory (&startup_info, sizeof (startup_info)); + startup_info.cb = sizeof (startup_info); + startup_info.hStdError = child_stderr_write; + startup_info.dwFlags |= STARTF_USESTDHANDLES; + + wchar_t command[512]; + wcscpy (command, L"ffprobe.exe \""); + + wchar_t file[512]; + MultiByteToWideChar (CP_UTF8, 0, content.string().c_str(), -1, file, sizeof(file)); + wcscat (command, file); + + wcscat (command, L"\""); + + PROCESS_INFORMATION process_info; + ZeroMemory (&process_info, sizeof (process_info)); + if (!CreateProcess (0, command, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &startup_info, &process_info)) { + log->log ("ffprobe call failed (could not CreateProcess)"); + return; + } + + FILE* o = fopen (out.string().c_str(), "w"); + if (!o) { + log->log ("ffprobe call failed (could not create output file)"); + return; + } + + CloseHandle (child_stderr_write); + + while (1) { + char buffer[512]; + DWORD read; + if (!ReadFile(child_stderr_read, buffer, sizeof(buffer), &read, 0) || read == 0) { + break; + } + fwrite (buffer, read, 1, o); + } + + fclose (o); + + WaitForSingleObject (process_info.hProcess, INFINITE); + CloseHandle (process_info.hProcess); + CloseHandle (process_info.hThread); + CloseHandle (child_stderr_read); +#endif + +#ifdef DCPOMATIC_LINUX + string ffprobe = "ffprobe \"" + content.string() + "\" 2> \"" + out.string() + "\""; + log->log (String::compose ("Probing with %1", ffprobe)); + system (ffprobe.c_str ()); +#endif + +#ifdef DCPOMATIC_OSX + uint32_t size = 1024; + char buffer[size]; + if (_NSGetExecutablePath (buffer, &size)) { + log->log ("_NSGetExecutablePath failed"); + return; + } + + boost::filesystem::path path (buffer); + path.remove_filename (); + path /= "ffprobe"; + + string ffprobe = path.string() + " \"" + content.string() + "\" 2> \"" + out.string() + "\""; + log->log (String::compose ("Probing with %1", ffprobe)); + system (ffprobe.c_str ()); +#endif +} + +list<pair<string, string> > +mount_info () +{ + list<pair<string, string> > m; + +#ifdef DCPOMATIC_LINUX + FILE* f = setmntent ("/etc/mtab", "r"); + if (!f) { + return m; + } + + while (1) { + struct mntent* mnt = getmntent (f); + if (!mnt) { + break; + } + + m.push_back (make_pair (mnt->mnt_dir, mnt->mnt_type)); + } + + endmntent (f); +#endif + + return m; +} diff --git a/src/lib/cross.h b/src/lib/cross.h index 110660b16..58fa821c7 100644 --- a/src/lib/cross.h +++ b/src/lib/cross.h @@ -17,8 +17,15 @@ */ -#ifdef DVDOMATIC_WINDOWS +#include <boost/filesystem.hpp> + +#ifdef DCPOMATIC_WINDOWS #define WEXITSTATUS(w) (w) #endif -void dvdomatic_sleep (int); +class Log; + +void dcpomatic_sleep (int); +extern std::string cpu_info (); +extern void run_ffprobe (boost::filesystem::path, boost::filesystem::path, boost::shared_ptr<Log>); +extern std::list<std::pair<std::string, std::string> > mount_info (); diff --git a/src/lib/dci_metadata.cc b/src/lib/dci_metadata.cc new file mode 100644 index 000000000..27306a15e --- /dev/null +++ b/src/lib/dci_metadata.cc @@ -0,0 +1,73 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <iostream> +#include <libcxml/cxml.h> +#include "dci_metadata.h" + +#include "i18n.h" + +using std::string; +using boost::lexical_cast; +using boost::shared_ptr; + +DCIMetadata::DCIMetadata (shared_ptr<const cxml::Node> node) +{ + content_version = node->number_child<int> ("ContentVersion"); + audio_language = node->string_child ("AudioLanguage"); + subtitle_language = node->string_child ("SubtitleLanguage"); + territory = node->string_child ("Territory"); + rating = node->string_child ("Rating"); + studio = node->string_child ("Studio"); + facility = node->string_child ("Facility"); + package_type = node->string_child ("PackageType"); +} + +void +DCIMetadata::as_xml (xmlpp::Node* root) const +{ + root->add_child("ContentVersion")->add_child_text (lexical_cast<string> (content_version)); + root->add_child("AudioLanguage")->add_child_text (audio_language); + root->add_child("SubtitleLanguage")->add_child_text (subtitle_language); + root->add_child("Territory")->add_child_text (territory); + root->add_child("Rating")->add_child_text (rating); + root->add_child("Studio")->add_child_text (studio); + root->add_child("Facility")->add_child_text (facility); + root->add_child("PackageType")->add_child_text (package_type); +} + +void +DCIMetadata::read_old_metadata (string k, string v) +{ + if (k == N_("audio_language")) { + audio_language = v; + } else if (k == N_("subtitle_language")) { + subtitle_language = v; + } else if (k == N_("territory")) { + territory = v; + } else if (k == N_("rating")) { + rating = v; + } else if (k == N_("studio")) { + studio = v; + } else if (k == N_("facility")) { + facility = v; + } else if (k == N_("package_type")) { + package_type = v; + } +} diff --git a/src/lib/dci_metadata.h b/src/lib/dci_metadata.h new file mode 100644 index 000000000..738e439de --- /dev/null +++ b/src/lib/dci_metadata.h @@ -0,0 +1,52 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_DCI_METADATA_H +#define DCPOMATIC_DCI_METADATA_H + +#include <string> +#include <libxml++/libxml++.h> + +namespace cxml { + class Node; +} + +class DCIMetadata +{ +public: + DCIMetadata () + : content_version (1) + {} + + DCIMetadata (boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + void read_old_metadata (std::string, std::string); + + int content_version; + std::string audio_language; + std::string subtitle_language; + std::string territory; + std::string rating; + std::string studio; + std::string facility; + std::string package_type; +}; + +#endif diff --git a/src/lib/dcp_content_type.cc b/src/lib/dcp_content_type.cc index aae805308..82bd5fa01 100644 --- a/src/lib/dcp_content_type.cc +++ b/src/lib/dcp_content_type.cc @@ -24,6 +24,8 @@ #include <cassert> #include "dcp_content_type.h" +#include "i18n.h" + using namespace std; vector<DCPContentType const *> DCPContentType::_dcp_content_types; @@ -39,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, "FTR")); - _dcp_content_types.push_back (new DCPContentType ("Short", libdcp::SHORT, "SHR")); - _dcp_content_types.push_back (new DCPContentType ("Trailer", libdcp::TRAILER, "TLR")); - _dcp_content_types.push_back (new DCPContentType ("Test", libdcp::TEST, "TST")); - _dcp_content_types.push_back (new DCPContentType ("Transitional", libdcp::TRANSITIONAL, "XSN")); - _dcp_content_types.push_back (new DCPContentType ("Rating", libdcp::RATING, "RTG")); - _dcp_content_types.push_back (new DCPContentType ("Teaser", libdcp::TEASER, "TSR")); - _dcp_content_types.push_back (new DCPContentType ("Policy", libdcp::POLICY, "POL")); - _dcp_content_types.push_back (new DCPContentType ("Public Service Announcement", libdcp::PUBLIC_SERVICE_ANNOUNCEMENT, "PSA")); - _dcp_content_types.push_back (new DCPContentType ("Advertisement", libdcp::ADVERTISEMENT, "ADV")); + _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"))); } DCPContentType const * @@ -64,6 +66,18 @@ DCPContentType::from_pretty_name (string n) } DCPContentType const * +DCPContentType::from_dci_name (string n) +{ + for (vector<DCPContentType const *>::const_iterator i = _dcp_content_types.begin(); i != _dcp_content_types.end(); ++i) { + if ((*i)->dci_name() == n) { + return *i; + } + } + + return 0; +} + +DCPContentType const * DCPContentType::from_index (int n) { assert (n >= 0 && n < int (_dcp_content_types.size ())); diff --git a/src/lib/dcp_content_type.h b/src/lib/dcp_content_type.h index 2b6e60a19..965c16347 100644 --- a/src/lib/dcp_content_type.h +++ b/src/lib/dcp_content_type.h @@ -17,8 +17,8 @@ */ -#ifndef DVDOMATIC_DCP_CONTENT_TYPE_H -#define DVDOMATIC_DCP_CONTENT_TYPE_H +#ifndef DCPOMATIC_DCP_CONTENT_TYPE_H +#define DCPOMATIC_DCP_CONTENT_TYPE_H /** @file src/content_type.h * @brief A description of the type of content for a DCP (e.g. feature, trailer etc.) @@ -31,7 +31,7 @@ /** @class DCPContentType * @brief A description of the type of content for a DCP (e.g. feature, trailer etc.) */ -class DCPContentType +class DCPContentType : public boost::noncopyable { public: DCPContentType (std::string, libdcp::ContentKind, std::string); @@ -50,6 +50,7 @@ public: } static DCPContentType const * from_pretty_name (std::string); + static DCPContentType const * from_dci_name (std::string); static DCPContentType const * from_index (int); static int as_index (DCPContentType const *); static std::vector<DCPContentType const *> all (); diff --git a/src/lib/dcp_video_frame.cc b/src/lib/dcp_video_frame.cc index c6b29ba41..1c812cbf2 100644 --- a/src/lib/dcp_video_frame.cc +++ b/src/lib/dcp_video_frame.cc @@ -43,108 +43,73 @@ #include <boost/asio.hpp> #include <boost/filesystem.hpp> #include <boost/lexical_cast.hpp> +#include <libdcp/rec709_linearised_gamma_lut.h> +#include <libdcp/srgb_linearised_gamma_lut.h> +#include <libdcp/gamma_lut.h> +#include <libdcp/xyz_frame.h> +#include <libdcp/rgb_xyz.h> +#include <libdcp/colour_matrix.h> +#include <libcxml/cxml.h> #include "film.h" #include "dcp_video_frame.h" -#include "lut.h" #include "config.h" -#include "options.h" #include "exceptions.h" #include "server.h" #include "util.h" #include "scaler.h" #include "image.h" #include "log.h" -#include "subtitle.h" + +#include "i18n.h" using std::string; using std::stringstream; using std::ofstream; +using std::cout; using boost::shared_ptr; +using boost::lexical_cast; +using libdcp::Size; + +#define DCI_COEFFICENT (48.0 / 52.37) /** Construct a DCP video frame. * @param input Input image. - * @param out Required size of output, in pixels (including any padding). - * @param s Scaler to use. - * @param p Number of pixels of padding either side of the image. - * @param f Index of the frame within the Film. - * @param fps Frames per second of the Film. - * @param pp FFmpeg post-processing string to use. - * @param clut Colour look-up table to use (see Config::colour_lut_index ()) + * @param f Index of the frame within the DCP. * @param bw J2K bandwidth to use (see Config::j2k_bandwidth ()) * @param l Log to write to. */ DCPVideoFrame::DCPVideoFrame ( - shared_ptr<const Image> yuv, shared_ptr<Subtitle> sub, - Size out, int p, int subtitle_offset, float subtitle_scale, - Scaler const * s, SourceFrame f, float fps, string pp, int clut, int bw, Log* l + shared_ptr<const Image> image, int f, Eyes eyes, ColourConversion c, int dcp_fps, int bw, shared_ptr<Log> l ) - : _input (yuv) - , _subtitle (sub) - , _out_size (out) - , _padding (p) - , _subtitle_offset (subtitle_offset) - , _subtitle_scale (subtitle_scale) - , _scaler (s) + : _image (image) , _frame (f) - , _frames_per_second (dcp_frame_rate(fps).frames_per_second) - , _post_process (pp) - , _colour_lut (clut) + , _eyes (eyes) + , _conversion (c) + , _frames_per_second (dcp_fps) , _j2k_bandwidth (bw) , _log (l) - , _image (0) - , _parameters (0) - , _cinfo (0) - , _cio (0) { } -/** Create a libopenjpeg container suitable for our output image */ -void -DCPVideoFrame::create_openjpeg_container () -{ - for (int i = 0; i < 3; ++i) { - _cmptparm[i].dx = 1; - _cmptparm[i].dy = 1; - _cmptparm[i].w = _out_size.width; - _cmptparm[i].h = _out_size.height; - _cmptparm[i].x0 = 0; - _cmptparm[i].y0 = 0; - _cmptparm[i].prec = 12; - _cmptparm[i].bpp = 12; - _cmptparm[i].sgnd = 0; - } - - _image = opj_image_create (3, &_cmptparm[0], CLRSPC_SRGB); - if (_image == 0) { - throw EncodeError ("could not create libopenjpeg image"); - } - - _image->x0 = 0; - _image->y0 = 0; - _image->x1 = _out_size.width; - _image->y1 = _out_size.height; -} - -DCPVideoFrame::~DCPVideoFrame () +DCPVideoFrame::DCPVideoFrame (shared_ptr<const Image> image, shared_ptr<const cxml::Node> node, shared_ptr<Log> log) + : _image (image) + , _log (log) { - if (_image) { - opj_image_destroy (_image); - } - - if (_cio) { - opj_cio_close (_cio); - } - - if (_cinfo) { - opj_destroy_compress (_cinfo); + _frame = node->number_child<int> ("Frame"); + string const eyes = node->string_child ("Eyes"); + if (eyes == "Both") { + _eyes = EYES_BOTH; + } else if (eyes == "Left") { + _eyes = EYES_LEFT; + } else if (eyes == "Right") { + _eyes = EYES_RIGHT; + } else { + assert (false); } - - if (_parameters) { - free (_parameters->cp_comment); - } - - delete _parameters; + _conversion = ColourConversion (node->node_child ("ColourConversion")); + _frames_per_second = node->number_child<int> ("FramesPerSecond"); + _j2k_bandwidth = node->number_child<int> ("J2KBandwidth"); } /** J2K-encode this frame on the local host. @@ -153,151 +118,133 @@ DCPVideoFrame::~DCPVideoFrame () shared_ptr<EncodedData> DCPVideoFrame::encode_locally () { - if (!_post_process.empty ()) { - _input = _input->post_process (_post_process, true); - } - - shared_ptr<Image> prepared = _input->scale_and_convert_to_rgb (_out_size, _padding, _scaler, true); - - if (_subtitle) { - Rect tx = subtitle_transformed_area ( - float (_out_size.width) / _input->size().width, - float (_out_size.height) / _input->size().height, - _subtitle->area(), _subtitle_offset, _subtitle_scale - ); - - shared_ptr<Image> im = _subtitle->image()->scale (tx.size(), _scaler, true); - prepared->alpha_blend (im, tx.position()); + shared_ptr<libdcp::LUT> in_lut; + if (_conversion.input_gamma_linearised) { + in_lut = libdcp::SRGBLinearisedGammaLUT::cache.get (12, _conversion.input_gamma); + } else { + in_lut = libdcp::GammaLUT::cache.get (12, _conversion.input_gamma); } - create_openjpeg_container (); - - struct { - double r, g, b; - } s; - - struct { - double x, y, z; - } d; - - /* Copy our RGB into the openjpeg container, converting to XYZ in the process */ - - int jn = 0; - for (int y = 0; y < _out_size.height; ++y) { - uint8_t* p = prepared->data()[0] + y * prepared->stride()[0]; - for (int x = 0; x < _out_size.width; ++x) { - - /* In gamma LUT (converting 8-bit input to 12-bit) */ - s.r = lut_in[_colour_lut][*p++ << 4]; - s.g = lut_in[_colour_lut][*p++ << 4]; - s.b = lut_in[_colour_lut][*p++ << 4]; - - /* RGB to XYZ Matrix */ - d.x = ((s.r * color_matrix[_colour_lut][0][0]) + - (s.g * color_matrix[_colour_lut][0][1]) + - (s.b * color_matrix[_colour_lut][0][2])); - - d.y = ((s.r * color_matrix[_colour_lut][1][0]) + - (s.g * color_matrix[_colour_lut][1][1]) + - (s.b * color_matrix[_colour_lut][1][2])); - - d.z = ((s.r * color_matrix[_colour_lut][2][0]) + - (s.g * color_matrix[_colour_lut][2][1]) + - (s.b * color_matrix[_colour_lut][2][2])); - - /* DCI companding */ - d.x = d.x * DCI_COEFFICENT * (DCI_LUT_SIZE - 1); - d.y = d.y * DCI_COEFFICENT * (DCI_LUT_SIZE - 1); - d.z = d.z * DCI_COEFFICENT * (DCI_LUT_SIZE - 1); - - /* Out gamma LUT */ - _image->comps[0].data[jn] = lut_out[LO_DCI][(int) d.x]; - _image->comps[1].data[jn] = lut_out[LO_DCI][(int) d.y]; - _image->comps[2].data[jn] = lut_out[LO_DCI][(int) d.z]; - - ++jn; + /* XXX: libdcp should probably use boost */ + + double matrix[3][3]; + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + matrix[i][j] = _conversion.matrix (i, j); } } - + + shared_ptr<libdcp::XYZFrame> xyz = libdcp::rgb_to_xyz ( + _image, + in_lut, + libdcp::GammaLUT::cache.get (16, 1 / _conversion.output_gamma), + matrix + ); + /* Set the max image and component sizes based on frame_rate */ - int const max_cs_len = ((float) _j2k_bandwidth) / 8 / _frames_per_second; + int max_cs_len = ((float) _j2k_bandwidth) / 8 / _frames_per_second; + if (_eyes == EYES_LEFT || _eyes == EYES_RIGHT) { + /* In 3D we have only half the normal bandwidth per eye */ + max_cs_len /= 2; + } int const max_comp_size = max_cs_len / 1.25; + /* get a J2K compressor handle */ + opj_cinfo_t* cinfo = opj_create_compress (CODEC_J2K); + if (cinfo == 0) { + throw EncodeError (N_("could not create JPEG2000 encoder")); + } + /* Set encoding parameters to default values */ - _parameters = new opj_cparameters_t; - opj_set_default_encoder_parameters (_parameters); + opj_cparameters_t parameters; + opj_set_default_encoder_parameters (¶meters); /* Set default cinema parameters */ - _parameters->tile_size_on = false; - _parameters->cp_tdx = 1; - _parameters->cp_tdy = 1; + parameters.tile_size_on = false; + parameters.cp_tdx = 1; + parameters.cp_tdy = 1; /* Tile part */ - _parameters->tp_flag = 'C'; - _parameters->tp_on = 1; + parameters.tp_flag = 'C'; + parameters.tp_on = 1; /* Tile and Image shall be at (0,0) */ - _parameters->cp_tx0 = 0; - _parameters->cp_ty0 = 0; - _parameters->image_offset_x0 = 0; - _parameters->image_offset_y0 = 0; + parameters.cp_tx0 = 0; + parameters.cp_ty0 = 0; + parameters.image_offset_x0 = 0; + parameters.image_offset_y0 = 0; /* Codeblock size = 32x32 */ - _parameters->cblockw_init = 32; - _parameters->cblockh_init = 32; - _parameters->csty |= 0x01; + parameters.cblockw_init = 32; + parameters.cblockh_init = 32; + parameters.csty |= 0x01; /* The progression order shall be CPRL */ - _parameters->prog_order = CPRL; + parameters.prog_order = CPRL; /* No ROI */ - _parameters->roi_compno = -1; + parameters.roi_compno = -1; - _parameters->subsampling_dx = 1; - _parameters->subsampling_dy = 1; + parameters.subsampling_dx = 1; + parameters.subsampling_dy = 1; /* 9-7 transform */ - _parameters->irreversible = 1; + parameters.irreversible = 1; - _parameters->tcp_rates[0] = 0; - _parameters->tcp_numlayers++; - _parameters->cp_disto_alloc = 1; - _parameters->cp_rsiz = CINEMA2K; - _parameters->cp_comment = strdup ("DVD-o-matic"); - _parameters->cp_cinema = CINEMA2K_24; + parameters.tcp_rates[0] = 0; + parameters.tcp_numlayers++; + parameters.cp_disto_alloc = 1; + parameters.cp_rsiz = CINEMA2K; + parameters.cp_comment = strdup (N_("DCP-o-matic")); + parameters.cp_cinema = CINEMA2K_24; /* 3 components, so use MCT */ - _parameters->tcp_mct = 1; + parameters.tcp_mct = 1; /* set max image */ - _parameters->max_comp_size = max_comp_size; - _parameters->tcp_rates[0] = ((float) (3 * _image->comps[0].w * _image->comps[0].h * _image->comps[0].prec)) / (max_cs_len * 8); - - /* get a J2K compressor handle */ - _cinfo = opj_create_compress (CODEC_J2K); - if (_cinfo == 0) { - throw EncodeError ("could not create JPEG2000 encoder"); - } + parameters.max_comp_size = max_comp_size; + parameters.tcp_rates[0] = ((float) (3 * xyz->size().width * xyz->size().height * 12)) / (max_cs_len * 8); /* Set event manager to null (openjpeg 1.3 bug) */ - _cinfo->event_mgr = 0; + cinfo->event_mgr = 0; /* Setup the encoder parameters using the current image and user parameters */ - opj_setup_encoder (_cinfo, _parameters, _image); + opj_setup_encoder (cinfo, ¶meters, xyz->opj_image ()); - _cio = opj_cio_open ((opj_common_ptr) _cinfo, 0, 0); - if (_cio == 0) { - throw EncodeError ("could not open JPEG2000 stream"); + opj_cio_t* cio = opj_cio_open ((opj_common_ptr) cinfo, 0, 0); + if (cio == 0) { + opj_destroy_compress (cinfo); + throw EncodeError (N_("could not open JPEG2000 stream")); } - int const r = opj_encode (_cinfo, _cio, _image, 0); + int const r = opj_encode (cinfo, cio, xyz->opj_image(), 0); if (r == 0) { - throw EncodeError ("JPEG2000 encoding failed"); + opj_cio_close (cio); + opj_destroy_compress (cinfo); + throw EncodeError (N_("JPEG2000 encoding failed")); } - _log->log (String::compose ("Finished locally-encoded frame %1", _frame)); - - return shared_ptr<EncodedData> (new LocallyEncodedData (_cio->buffer, cio_tell (_cio))); + switch (_eyes) { + case EYES_BOTH: + _log->log (String::compose (N_("Finished locally-encoded frame %1 for mono"), _frame)); + break; + case EYES_LEFT: + _log->log (String::compose (N_("Finished locally-encoded frame %1 for L"), _frame)); + break; + case EYES_RIGHT: + _log->log (String::compose (N_("Finished locally-encoded frame %1 for R"), _frame)); + break; + default: + break; + } + + shared_ptr<EncodedData> enc (new LocallyEncodedData (cio->buffer, cio_tell (cio))); + + opj_cio_close (cio); + free (parameters.cp_comment); + opj_destroy_compress (cinfo); + + return enc; } /** Send this frame to a remote server for J2K encoding, then read the result. @@ -305,100 +252,135 @@ DCPVideoFrame::encode_locally () * @return Encoded data. */ shared_ptr<EncodedData> -DCPVideoFrame::encode_remotely (ServerDescription const * serv) +DCPVideoFrame::encode_remotely (ServerDescription serv) { boost::asio::io_service io_service; boost::asio::ip::tcp::resolver resolver (io_service); - boost::asio::ip::tcp::resolver::query query (serv->host_name(), boost::lexical_cast<string> (Config::instance()->server_port ())); + boost::asio::ip::tcp::resolver::query query (serv.host_name(), boost::lexical_cast<string> (Config::instance()->server_port ())); boost::asio::ip::tcp::resolver::iterator endpoint_iterator = resolver.resolve (query); shared_ptr<Socket> socket (new Socket); - socket->connect (*endpoint_iterator, 30); - - stringstream s; - s << "encode please\n" - << "input_width " << _input->size().width << "\n" - << "input_height " << _input->size().height << "\n" - << "input_pixel_format " << _input->pixel_format() << "\n" - << "output_width " << _out_size.width << "\n" - << "output_height " << _out_size.height << "\n" - << "padding " << _padding << "\n" - << "subtitle_offset " << _subtitle_offset << "\n" - << "subtitle_scale " << _subtitle_scale << "\n" - << "scaler " << _scaler->id () << "\n" - << "frame " << _frame << "\n" - << "frames_per_second " << _frames_per_second << "\n"; - - if (!_post_process.empty()) { - s << "post_process " << _post_process << "\n"; - } - - s << "colour_lut " << _colour_lut << "\n" - << "j2k_bandwidth " << _j2k_bandwidth << "\n"; - - if (_subtitle) { - s << "subtitle_x " << _subtitle->position().x << "\n" - << "subtitle_y " << _subtitle->position().y << "\n" - << "subtitle_width " << _subtitle->image()->size().width << "\n" - << "subtitle_height " << _subtitle->image()->size().height << "\n"; - } + socket->connect (*endpoint_iterator); + + xmlpp::Document doc; + xmlpp::Element* root = doc.create_root_node ("EncodingRequest"); + + root->add_child("Version")->add_child_text (lexical_cast<string> (SERVER_LINK_VERSION)); + root->add_child("Width")->add_child_text (lexical_cast<string> (_image->size().width)); + root->add_child("Height")->add_child_text (lexical_cast<string> (_image->size().height)); + add_metadata (root); + + stringstream xml; + doc.write_to_stream (xml, "UTF-8"); _log->log (String::compose ( - "Sending to remote; pixel format %1, components %2, lines (%3,%4,%5), line sizes (%6,%7,%8)", - _input->pixel_format(), _input->components(), - _input->lines(0), _input->lines(1), _input->lines(2), - _input->line_size()[0], _input->line_size()[1], _input->line_size()[2] + N_("Sending to remote; pixel format %1, components %2, lines (%3,%4,%5), line sizes (%6,%7,%8)"), + _image->pixel_format(), _image->components(), + _image->lines(0), _image->lines(1), _image->lines(2), + _image->line_size()[0], _image->line_size()[1], _image->line_size()[2] )); + + socket->write (xml.str().length() + 1); + socket->write ((uint8_t *) xml.str().c_str(), xml.str().length() + 1); + + _image->write_to_socket (socket); + + shared_ptr<EncodedData> e (new RemotelyEncodedData (socket->read_uint32 ())); + socket->read (e->data(), e->size()); + + _log->log (String::compose (N_("Finished remotely-encoded frame %1"), _frame)); - socket->write ((uint8_t *) s.str().c_str(), s.str().length() + 1, 30); + return e; +} - _input->write_to_socket (socket); - if (_subtitle) { - _subtitle->image()->write_to_socket (socket); +void +DCPVideoFrame::add_metadata (xmlpp::Element* el) const +{ + el->add_child("Frame")->add_child_text (lexical_cast<string> (_frame)); + + switch (_eyes) { + case EYES_BOTH: + el->add_child("Eyes")->add_child_text ("Both"); + break; + case EYES_LEFT: + el->add_child("Eyes")->add_child_text ("Left"); + break; + case EYES_RIGHT: + el->add_child("Eyes")->add_child_text ("Right"); + break; + default: + assert (false); } + + _conversion.as_xml (el->add_child("ColourConversion")); + + el->add_child("FramesPerSecond")->add_child_text (lexical_cast<string> (_frames_per_second)); + el->add_child("J2KBandwidth")->add_child_text (lexical_cast<string> (_j2k_bandwidth)); +} - char buffer[32]; - socket->read_indefinite ((uint8_t *) buffer, sizeof (buffer), 30); - socket->consume (strlen (buffer) + 1); - shared_ptr<EncodedData> e (new RemotelyEncodedData (atoi (buffer))); +EncodedData::EncodedData (int s) + : _data (new uint8_t[s]) + , _size (s) +{ - /* now read the rest */ - socket->read_definite_and_consume (e->data(), e->size(), 30); +} - _log->log (String::compose ("Finished remotely-encoded frame %1", _frame)); +EncodedData::EncodedData (string file) +{ + _size = boost::filesystem::file_size (file); + _data = new uint8_t[_size]; + + FILE* f = fopen (file.c_str(), N_("rb")); + if (!f) { + throw FileError (_("could not open file for reading"), file); + } - return e; + 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 opt Options. - * @param frame Frame index. + * @param Film Film. + * @param frame DCP frame index. */ void -EncodedData::write (shared_ptr<const EncodeOptions> opt, SourceFrame frame) +EncodedData::write (shared_ptr<const Film> film, int frame, Eyes eyes) const { - string const tmp_j2k = opt->frame_out_path (frame, true); + string const tmp_j2c = film->j2c_path (frame, eyes, true); - FILE* f = fopen (tmp_j2k.c_str (), "wb"); + FILE* f = fopen (tmp_j2c.c_str (), N_("wb")); if (!f) { - throw WriteFileError (tmp_j2k, errno); + throw WriteFileError (tmp_j2c, errno); } fwrite (_data, 1, _size, f); fclose (f); - string const real_j2k = opt->frame_out_path (frame, false); + string 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_j2k, real_j2k); + boost::filesystem::rename (tmp_j2c, real_j2c); +} - /* Write a file containing the hash */ - string const hash = opt->hash_out_path (frame, false); - ofstream h (hash.c_str()); - h << md5_digest (_data, _size) << "\n"; - h.close (); +void +EncodedData::write_info (shared_ptr<const Film> film, int frame, Eyes eyes, libdcp::FrameInfo fin) const +{ + string const info = film->info_path (frame, eyes); + ofstream h (info.c_str()); + fin.write (h); } /** Send this data to a socket. @@ -407,20 +389,19 @@ EncodedData::write (shared_ptr<const EncodeOptions> opt, SourceFrame frame) void EncodedData::send (shared_ptr<Socket> socket) { - stringstream s; - s << _size; - socket->write ((uint8_t *) s.str().c_str(), s.str().length() + 1, 30); - socket->write (_data, _size, 30); + socket->write (_size); + socket->write (_data, _size); } -/** @param s Size of data in bytes */ -RemotelyEncodedData::RemotelyEncodedData (int s) - : EncodedData (new uint8_t[s], s) +LocallyEncodedData::LocallyEncodedData (uint8_t* d, int s) + : EncodedData (s) { - + memcpy (_data, d, s); } -RemotelyEncodedData::~RemotelyEncodedData () +/** @param s Size of data in bytes */ +RemotelyEncodedData::RemotelyEncodedData (int s) + : EncodedData (s) { - delete[] _data; + } diff --git a/src/lib/dcp_video_frame.h b/src/lib/dcp_video_frame.h index 134720da8..bf0b7f8c4 100644 --- a/src/lib/dcp_video_frame.h +++ b/src/lib/dcp_video_frame.h @@ -19,14 +19,15 @@ */ #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. */ -class FilmState; -class EncodeOptions; +class Film; class ServerDescription; class Scaler; class Image; @@ -36,21 +37,19 @@ class Subtitle; /** @class EncodedData * @brief Container for J2K-encoded data. */ -class EncodedData +class EncodedData : public boost::noncopyable { public: - /** @param d Data (will not be freed by this class, but may be by subclasses) - * @param s Size of data, in bytes. - */ - EncodedData (uint8_t* d, int s) - : _data (d) - , _size (s) - {} + /** @param s Size of data, in bytes */ + EncodedData (int s); - virtual ~EncodedData () {} + EncodedData (std::string f); + + virtual ~EncodedData (); void send (boost::shared_ptr<Socket> socket); - void write (boost::shared_ptr<const EncodeOptions>, SourceFrame); + void write (boost::shared_ptr<const Film>, int, Eyes) const; + void write_info (boost::shared_ptr<const Film>, int, Eyes, libdcp::FrameInfo) const; /** @return data */ uint8_t* data () const { @@ -64,7 +63,7 @@ public: protected: uint8_t* _data; ///< data - int _size; ///< data size in bytes + int _size; ///< data size in bytes }; /** @class LocallyEncodedData @@ -75,12 +74,10 @@ protected: class LocallyEncodedData : public EncodedData { public: - /** @param d Data (which will not be freed by this class) + /** @param d Data (which will be copied by this class) * @param s Size of data, in bytes. */ - LocallyEncodedData (uint8_t* d, int s) - : EncodedData (d, s) - {} + LocallyEncodedData (uint8_t* d, int s); }; /** @class RemotelyEncodedData @@ -91,7 +88,6 @@ class RemotelyEncodedData : public EncodedData { public: RemotelyEncodedData (int s); - ~RemotelyEncodedData (); }; /** @class DCPVideoFrame @@ -103,44 +99,33 @@ public: * Objects of this class are used for the queue that we keep * of images that require encoding. */ -class DCPVideoFrame +class DCPVideoFrame : public boost::noncopyable { public: - DCPVideoFrame ( - boost::shared_ptr<const Image>, boost::shared_ptr<Subtitle>, Size, - int, int, float, Scaler const *, SourceFrame, float, std::string, int, int, Log * - ); - - virtual ~DCPVideoFrame (); + DCPVideoFrame (boost::shared_ptr<const Image>, int, Eyes, ColourConversion, int, int, boost::shared_ptr<Log>); + DCPVideoFrame (boost::shared_ptr<const Image>, boost::shared_ptr<const cxml::Node>, boost::shared_ptr<Log>); boost::shared_ptr<EncodedData> encode_locally (); - boost::shared_ptr<EncodedData> encode_remotely (ServerDescription const *); + boost::shared_ptr<EncodedData> encode_remotely (ServerDescription); - SourceFrame frame () const { + Eyes eyes () const { + return _eyes; + } + + int frame () const { return _frame; } private: - void create_openjpeg_container (); - - boost::shared_ptr<const Image> _input; ///< the input image - boost::shared_ptr<Subtitle> _subtitle; ///< any subtitle that should be on the image - Size _out_size; ///< the required size of the output, in pixels - int _padding; - int _subtitle_offset; - float _subtitle_scale; - Scaler const * _scaler; ///< scaler to use - SourceFrame _frame; ///< frame index within the Film's source - int _frames_per_second; ///< Frames per second that we will use for the DCP (rounded) - std::string _post_process; ///< FFmpeg post-processing string to use - int _colour_lut; ///< Colour look-up table to use - int _j2k_bandwidth; ///< J2K bandwidth to use - - Log* _log; ///< log - - opj_image_cmptparm_t _cmptparm[3]; ///< libopenjpeg's opj_image_cmptparm_t - opj_image* _image; ///< libopenjpeg's image container - opj_cparameters_t* _parameters; ///< libopenjpeg's parameters - opj_cinfo_t* _cinfo; ///< libopenjpeg's opj_cinfo_t - opj_cio_t* _cio; ///< libopenjpeg's opj_cio_t + + void add_metadata (xmlpp::Element *) const; + + boost::shared_ptr<const Image> _image; + int _frame; ///< frame index within the DCP's intrinsic duration + Eyes _eyes; + ColourConversion _conversion; + int _frames_per_second; ///< Frames per second that we will use for the DCP + int _j2k_bandwidth; ///< J2K bandwidth to use + + boost::shared_ptr<Log> _log; ///< log }; diff --git a/src/lib/decoder.cc b/src/lib/decoder.cc index 7066b488e..3f4cda6eb 100644 --- a/src/lib/decoder.cc +++ b/src/lib/decoder.cc @@ -21,56 +21,18 @@ * @brief Parent class for decoders of content. */ -#include <iostream> -#include <stdint.h> -#include <boost/lexical_cast.hpp> #include "film.h" -#include "format.h" -#include "job.h" -#include "options.h" -#include "exceptions.h" -#include "image.h" -#include "util.h" -#include "log.h" #include "decoder.h" -#include "delay_line.h" -#include "subtitle.h" -#include "filter_graph.h" -using std::string; -using std::stringstream; -using std::min; -using std::pair; -using std::list; +#include "i18n.h" + using boost::shared_ptr; -using boost::optional; /** @param f Film. - * @param o Options. - * @param j Job that we are running within, or 0 + * @param o Decode options. */ -Decoder::Decoder (boost::shared_ptr<Film> f, boost::shared_ptr<const DecodeOptions> o, Job* j) +Decoder::Decoder (shared_ptr<const Film> f) : _film (f) - , _opt (o) - , _job (j) -{ - _film_connection = f->Changed.connect (bind (&Decoder::film_changed, this, _1)); -} - -/** Seek to a position as a source timestamp in seconds. - * @return true on error. - */ -bool -Decoder::seek (double) { - throw DecodeError ("decoder does not support seek"); -} -/** Seek so that the next frame we will produce is the same as the last one. - * @return true on error. - */ -bool -Decoder::seek_to_last () -{ - throw DecodeError ("decoder does not support seek"); } diff --git a/src/lib/decoder.h b/src/lib/decoder.h index 3908afa2f..d67592ed8 100644 --- a/src/lib/decoder.h +++ b/src/lib/decoder.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-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 @@ -21,60 +21,36 @@ * @brief Parent class for decoders of content. */ -#ifndef DVDOMATIC_DECODER_H -#define DVDOMATIC_DECODER_H +#ifndef DCPOMATIC_DECODER_H +#define DCPOMATIC_DECODER_H -#include <vector> -#include <string> -#include <stdint.h> #include <boost/shared_ptr.hpp> -#include <boost/signals2.hpp> -#include "util.h" -#include "stream.h" -#include "video_source.h" -#include "audio_source.h" -#include "film.h" +#include <boost/weak_ptr.hpp> +#include <boost/utility.hpp> -class Job; -class DecodeOptions; -class Image; -class Log; -class DelayLine; -class TimedSubtitle; -class Subtitle; -class FilterGraph; +class Film; /** @class Decoder. * @brief Parent class for decoders of content. - * - * These classes can be instructed run through their content (by - * calling ::go), and they emit signals when video or audio data is - * ready for something else to process. */ -class Decoder +class Decoder : public boost::noncopyable { public: - Decoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); + Decoder (boost::shared_ptr<const Film>); virtual ~Decoder () {} - virtual bool pass () = 0; - virtual bool seek (double); - virtual bool seek_to_last (); - - boost::signals2::signal<void()> OutputChanged; + /** Perform one decode pass of the content, which may or may not + * cause the object to emit some data. + */ + virtual void pass () = 0; + virtual bool done () const = 0; protected: - /** our Film */ - boost::shared_ptr<Film> _film; - /** our options */ - boost::shared_ptr<const DecodeOptions> _opt; - /** associated Job, or 0 */ - Job* _job; -private: - virtual void film_changed (Film::Property) {} + virtual void flush () {}; - boost::signals2::scoped_connection _film_connection; + /** The Film that we are decoding in */ + boost::weak_ptr<const Film> _film; }; #endif diff --git a/src/lib/decoder_factory.cc b/src/lib/decoder_factory.cc deleted file mode 100644 index 2a0d828e2..000000000 --- a/src/lib/decoder_factory.cc +++ /dev/null @@ -1,56 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/decoder_factory.cc - * @brief A method to create an appropriate decoder for some content. - */ - -#include <boost/filesystem.hpp> -#include "ffmpeg_decoder.h" -#include "imagemagick_decoder.h" -#include "film.h" -#include "external_audio_decoder.h" -#include "decoder_factory.h" - -using std::string; -using std::pair; -using std::make_pair; -using boost::shared_ptr; -using boost::dynamic_pointer_cast; - -Decoders -decoder_factory ( - shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j - ) -{ - if (boost::filesystem::is_directory (f->content_path()) || f->content_type() == STILL) { - /* A single image file, or a directory of them */ - return Decoders ( - shared_ptr<VideoDecoder> (new ImageMagickDecoder (f, o, j)), - shared_ptr<AudioDecoder> (new ExternalAudioDecoder (f, o, j)) - ); - } - - shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (f, o, j)); - if (f->use_content_audio()) { - return Decoders (fd, fd); - } - - return Decoders (fd, shared_ptr<AudioDecoder> (new ExternalAudioDecoder (f, o, j))); -} diff --git a/src/lib/decoder_factory.h b/src/lib/decoder_factory.h deleted file mode 100644 index 47d977ce7..000000000 --- a/src/lib/decoder_factory.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#ifndef DVDOMATIC_DECODER_FACTORY_H -#define DVDOMATIC_DECODER_FACTORY_H - -/** @file src/decoder_factory.h - * @brief A method to create appropriate decoders for some content. - */ - -class Film; -class DecodeOptions; -class Job; -class VideoDecoder; -class AudioDecoder; - -struct Decoders { - Decoders () {} - - Decoders (boost::shared_ptr<VideoDecoder> v, boost::shared_ptr<AudioDecoder> a) - : video (v) - , audio (a) - {} - - boost::shared_ptr<VideoDecoder> video; - boost::shared_ptr<AudioDecoder> audio; -}; - -extern Decoders decoder_factory ( - boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job * - ); - -#endif diff --git a/src/lib/delay_line.cc b/src/lib/delay_line.cc deleted file mode 100644 index 45d8e9d9d..000000000 --- a/src/lib/delay_line.cc +++ /dev/null @@ -1,102 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <stdint.h> -#include <cstring> -#include <algorithm> -#include <iostream> -#include "delay_line.h" -#include "util.h" - -using std::min; -using boost::shared_ptr; - -/** @param channels Number of channels of audio. - * @param frames Delay in frames, +ve to move audio later. - */ -DelayLine::DelayLine (Log* log, int channels, int frames) - : AudioProcessor (log) - , _negative_delay_remaining (0) - , _frames (frames) -{ - if (_frames > 0) { - /* We need a buffer to keep some data in */ - _buffers.reset (new AudioBuffers (channels, _frames)); - _buffers->make_silent (); - } else if (_frames < 0) { - /* We can do -ve delays just by chopping off - the start, so no buffer needed. - */ - _negative_delay_remaining = -_frames; - } -} - -void -DelayLine::process_audio (shared_ptr<AudioBuffers> data) -{ - if (_buffers) { - /* We have some buffers, so we are moving the audio later */ - - /* Copy the input data */ - AudioBuffers input (*data.get ()); - - int to_do = data->frames (); - - /* Write some of our buffer to the output */ - int const from_buffer = min (to_do, _buffers->frames()); - data->copy_from (_buffers.get(), from_buffer, 0, 0); - to_do -= from_buffer; - - /* Write some of the input to the output */ - int const from_input = to_do; - data->copy_from (&input, from_input, 0, from_buffer); - - int const left_in_buffer = _buffers->frames() - from_buffer; - - /* Shuffle our buffer down */ - _buffers->move (from_buffer, 0, left_in_buffer); - - /* Copy remaining input data to our buffer */ - _buffers->copy_from (&input, input.frames() - from_input, from_input, left_in_buffer); - - } else { - - /* Chop the initial data off until _negative_delay_remaining - is zero, then just pass data. - */ - - int const to_do = min (data->frames(), _negative_delay_remaining); - if (to_do) { - data->move (to_do, 0, data->frames() - to_do); - data->set_frames (data->frames() - to_do); - _negative_delay_remaining -= to_do; - } - } - - Audio (data); -} - -void -DelayLine::process_end () -{ - if (_frames < 0) { - _buffers->make_silent (); - Audio (_buffers); - } -} diff --git a/src/lib/dolby_cp750.cc b/src/lib/dolby_cp750.cc index 262e57bc7..162626ff0 100644 --- a/src/lib/dolby_cp750.cc +++ b/src/lib/dolby_cp750.cc @@ -19,10 +19,12 @@ #include "dolby_cp750.h" +#include "i18n.h" + using namespace std; DolbyCP750::DolbyCP750 () - : SoundProcessor ("dolby_cp750", "Dolby CP750") + : SoundProcessor ("dolby_cp750", _("Dolby CP750")) { } diff --git a/src/lib/encoder.cc b/src/lib/encoder.cc index efedfcfef..35ebfb52e 100644 --- a/src/lib/encoder.cc +++ b/src/lib/encoder.cc @@ -22,19 +22,17 @@ */ #include <iostream> -#include <boost/filesystem.hpp> -#include <boost/lexical_cast.hpp> #include "encoder.h" #include "util.h" -#include "options.h" #include "film.h" #include "log.h" -#include "exceptions.h" -#include "filter.h" #include "config.h" #include "dcp_video_frame.h" #include "server.h" #include "cross.h" +#include "writer.h" + +#include "i18n.h" using std::pair; using std::string; @@ -42,187 +40,108 @@ using std::stringstream; using std::vector; using std::list; using std::cout; +using std::min; using std::make_pair; -using namespace boost; +using boost::shared_ptr; +using boost::optional; int const Encoder::_history_size = 25; -/** @param f Film that we are encoding. - * @param o Options. - */ -Encoder::Encoder (shared_ptr<const Film> f, shared_ptr<const EncodeOptions> o) +/** @param f Film that we are encoding */ +Encoder::Encoder (shared_ptr<const Film> f, shared_ptr<Job> j) : _film (f) - , _opt (o) - , _just_skipped (false) - , _video_frame (0) - , _audio_frame (0) -#ifdef HAVE_SWRESAMPLE - , _swr_context (0) -#endif - , _audio_frames_written (0) - , _process_end (false) + , _job (j) + , _video_frames_out (0) + , _state (TRANSCODING) + , _terminate (false) { - if (_film->audio_stream()) { - /* Create sound output files with .tmp suffixes; we will rename - them if and when we complete. - */ - for (int i = 0; i < dcp_audio_channels (_film->audio_channels()); ++i) { - SF_INFO sf_info; - sf_info.samplerate = dcp_audio_sample_rate (_film->audio_stream()->sample_rate()); - /* We write mono files */ - sf_info.channels = 1; - sf_info.format = SF_FORMAT_WAV | SF_FORMAT_PCM_24; - SNDFILE* f = sf_open (_opt->multichannel_audio_out_path (i, true).c_str (), SFM_WRITE, &sf_info); - if (f == 0) { - throw CreateFileError (_opt->multichannel_audio_out_path (i, true)); - } - _sound_files.push_back (f); - } - } + _have_a_real_frame[EYES_BOTH] = false; + _have_a_real_frame[EYES_LEFT] = false; + _have_a_real_frame[EYES_RIGHT] = false; } Encoder::~Encoder () { - close_sound_files (); - terminate_worker_threads (); + terminate_threads (); + if (_writer) { + _writer->finish (); + } } void Encoder::process_begin () { - if (_film->audio_stream() && _film->audio_stream()->sample_rate() != _film->target_audio_sample_rate()) { -#ifdef HAVE_SWRESAMPLE - - stringstream s; - s << "Will resample audio from " << _film->audio_stream()->sample_rate() << " to " << _film->target_audio_sample_rate(); - _film->log()->log (s.str ()); - - /* We will be using planar float data when we call the resampler */ - _swr_context = swr_alloc_set_opts ( - 0, - _film->audio_stream()->channel_layout(), - AV_SAMPLE_FMT_FLTP, - _film->target_audio_sample_rate(), - _film->audio_stream()->channel_layout(), - AV_SAMPLE_FMT_FLTP, - _film->audio_stream()->sample_rate(), - 0, 0 - ); - - swr_init (_swr_context); -#else - throw EncodeError ("Cannot resample audio as libswresample is not present"); -#endif - } else { -#ifdef HAVE_SWRESAMPLE - _swr_context = 0; -#endif - } - for (int i = 0; i < Config::instance()->num_local_encoding_threads (); ++i) { - _worker_threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, (ServerDescription *) 0))); + _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, optional<ServerDescription> ()))); } - vector<ServerDescription*> servers = Config::instance()->servers (); + vector<ServerDescription> servers = Config::instance()->servers (); - for (vector<ServerDescription*>::iterator i = servers.begin(); i != servers.end(); ++i) { - for (int j = 0; j < (*i)->threads (); ++j) { - _worker_threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, *i))); + for (vector<ServerDescription>::iterator i = servers.begin(); i != servers.end(); ++i) { + for (int j = 0; j < i->threads (); ++j) { + _threads.push_back (new boost::thread (boost::bind (&Encoder::encoder_thread, this, *i))); } } + + _writer.reset (new Writer (_film, _job)); } void Encoder::process_end () { -#if HAVE_SWRESAMPLE - if (_film->audio_stream() && _film->audio_stream()->channels() && _swr_context) { - - shared_ptr<AudioBuffers> out (new AudioBuffers (_film->audio_stream()->channels(), 256)); - - while (1) { - int const frames = swr_convert (_swr_context, (uint8_t **) out->data(), 256, 0, 0); - - if (frames < 0) { - throw EncodeError ("could not run sample-rate converter"); - } - - if (frames == 0) { - break; - } - - out->set_frames (frames); - write_audio (out); - } - - swr_free (&_swr_context); - } -#endif + boost::mutex::scoped_lock lock (_mutex); - if (_film->audio_stream()) { - close_sound_files (); - - /* Rename .wav.tmp files to .wav */ - for (int i = 0; i < dcp_audio_channels (_film->audio_channels()); ++i) { - if (boost::filesystem::exists (_opt->multichannel_audio_out_path (i, false))) { - boost::filesystem::remove (_opt->multichannel_audio_out_path (i, false)); - } - boost::filesystem::rename (_opt->multichannel_audio_out_path (i, true), _opt->multichannel_audio_out_path (i, false)); - } - } - - boost::mutex::scoped_lock lock (_worker_mutex); - - _film->log()->log ("Clearing queue of " + lexical_cast<string> (_queue.size ())); + _film->log()->log (String::compose (N_("Clearing queue of %1"), _queue.size ())); /* Keep waking workers until the queue is empty */ while (!_queue.empty ()) { - _film->log()->log ("Waking with " + lexical_cast<string> (_queue.size ()), Log::VERBOSE); - _worker_condition.notify_all (); - _worker_condition.wait (lock); + _film->log()->log (String::compose (N_("Waking with %1"), _queue.size ()), Log::VERBOSE); + _condition.notify_all (); + _condition.wait (lock); } lock.unlock (); - terminate_worker_threads (); + terminate_threads (); - _film->log()->log ("Mopping up " + lexical_cast<string> (_queue.size())); + _film->log()->log (String::compose (N_("Mopping up %1"), _queue.size())); /* The following sequence of events can occur in the above code: 1. a remote worker takes the last image off the queue 2. the loop above terminates 3. the remote worker fails to encode the image and puts it back on the queue - 4. the remote worker is then terminated by terminate_worker_threads + 4. the remote worker is then terminated by terminate_threads So just mop up anything left in the queue here. */ for (list<shared_ptr<DCPVideoFrame> >::iterator i = _queue.begin(); i != _queue.end(); ++i) { - _film->log()->log (String::compose ("Encode left-over frame %1", (*i)->frame ())); + _film->log()->log (String::compose (N_("Encode left-over frame %1"), (*i)->frame ())); try { - shared_ptr<EncodedData> e = (*i)->encode_locally (); - e->write (_opt, (*i)->frame ()); + _writer->write ((*i)->encode_locally(), (*i)->frame (), (*i)->eyes ()); frame_done (); } catch (std::exception& e) { - _film->log()->log (String::compose ("Local encode failed (%1)", e.what ())); + _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ())); } } - /* Now do links (or copies on windows) to duplicate frames */ - for (list<pair<int, int> >::iterator i = _links_required.begin(); i != _links_required.end(); ++i) { - link (_opt->frame_out_path (i->first, false), _opt->frame_out_path (i->second, false)); - link (_opt->hash_out_path (i->first, false), _opt->hash_out_path (i->second, false)); + { + boost::mutex::scoped_lock lm (_state_mutex); + _state = HASHING; } + + _writer->finish (); + _writer.reset (); } /** @return an estimate of the current number of frames we are encoding per second, * or 0 if not known. */ float -Encoder::current_frames_per_second () const +Encoder::current_encoding_rate () const { - boost::mutex::scoped_lock lock (_history_mutex); + boost::mutex::scoped_lock lock (_state_mutex); if (int (_time_history.size()) < _history_size) { return 0; } @@ -233,20 +152,12 @@ Encoder::current_frames_per_second () const return _history_size / (seconds (now) - seconds (_time_history.back ())); } -/** @return true if the last frame to be processed was skipped as it already existed */ -bool -Encoder::skipping () const -{ - boost::mutex::scoped_lock (_history_mutex); - return _just_skipped; -} - -/** @return Number of video frames that have been received */ -SourceFrame -Encoder::video_frame () const +/** @return Number of video frames that have been sent out */ +int +Encoder::video_frames_out () const { - boost::mutex::scoped_lock (_history_mutex); - return _video_frame; + boost::mutex::scoped_lock (_state_mutex); + return _video_frames_out; } /** Should be called when a frame has been encoded successfully. @@ -255,8 +166,7 @@ Encoder::video_frame () const void Encoder::frame_done () { - boost::mutex::scoped_lock lock (_history_mutex); - _just_skipped = false; + boost::mutex::scoped_lock lock (_state_mutex); struct timeval tv; gettimeofday (&tv, 0); @@ -266,186 +176,81 @@ Encoder::frame_done () } } -/** Called by a subclass when it has just skipped the processing - of a frame because it has already been done. -*/ -void -Encoder::frame_skipped () -{ - boost::mutex::scoped_lock lock (_history_mutex); - _just_skipped = true; -} - void -Encoder::process_video (shared_ptr<Image> image, bool same, boost::shared_ptr<Subtitle> sub) +Encoder::process_video (shared_ptr<const Image> image, Eyes eyes, ColourConversion conversion, bool same) { - if (_opt->video_skip != 0 && (_video_frame % _opt->video_skip) != 0) { - ++_video_frame; - return; - } + boost::mutex::scoped_lock lock (_mutex); - if (_opt->video_range) { - pair<SourceFrame, SourceFrame> const r = _opt->video_range.get(); - if (_video_frame < r.first || _video_frame >= r.second) { - ++_video_frame; - return; - } - } - - boost::mutex::scoped_lock lock (_worker_mutex); + /* XXX: discard 3D here if required */ /* Wait until the queue has gone down a bit */ - while (_queue.size() >= _worker_threads.size() * 2 && !_process_end) { + while (_queue.size() >= _threads.size() * 2 && !_terminate) { TIMING ("decoder sleeps with queue of %1", _queue.size()); - _worker_condition.wait (lock); + _condition.wait (lock); TIMING ("decoder wakes with queue of %1", _queue.size()); } - if (_process_end) { + if (_terminate) { return; } - /* Only do the processing if we don't already have a file for this frame */ - if (boost::filesystem::exists (_opt->frame_out_path (_video_frame, false))) { - frame_skipped (); - return; + if (_writer->thrown ()) { + _writer->rethrow (); } - if (same && _last_real_frame) { - /* Use the last frame that we encoded. We need to postpone doing the actual link, - as on windows the link is really a copy and the reference frame might not have - finished encoding yet. - */ - _links_required.push_back (make_pair (_last_real_frame.get(), _video_frame)); + if (_writer->can_fake_write (_video_frames_out)) { + _writer->fake_write (_video_frames_out, eyes); + _have_a_real_frame[eyes] = false; + frame_done (); + } else if (same && _have_a_real_frame[eyes]) { + /* Use the last frame that we encoded. */ + _writer->repeat (_video_frames_out, eyes); + frame_done (); } else { /* Queue this new frame for encoding */ - pair<string, string> const s = Filter::ffmpeg_strings (_film->filters()); TIMING ("adding to queue of %1", _queue.size ()); - _queue.push_back (boost::shared_ptr<DCPVideoFrame> ( + _queue.push_back (shared_ptr<DCPVideoFrame> ( new DCPVideoFrame ( - image, sub, _opt->out_size, _opt->padding, _film->subtitle_offset(), _film->subtitle_scale(), - _film->scaler(), _video_frame, _film->frames_per_second(), s.second, - _film->colour_lut(), _film->j2k_bandwidth(), - _film->log() + image, _video_frames_out, eyes, conversion, _film->video_frame_rate(), + _film->j2k_bandwidth(), _film->log() ) )); - _worker_condition.notify_all (); - _last_real_frame = _video_frame; - } - - ++_video_frame; -} - -void -Encoder::process_audio (shared_ptr<AudioBuffers> data) -{ - if (_opt->audio_range) { - shared_ptr<AudioBuffers> trimmed (new AudioBuffers (*data.get ())); - - /* Range that we are encoding */ - pair<int64_t, int64_t> required_range = _opt->audio_range.get(); - /* Range of this block of data */ - pair<int64_t, int64_t> this_range (_audio_frame, _audio_frame + trimmed->frames()); - - if (this_range.second < required_range.first || required_range.second < this_range.first) { - /* No part of this audio is within the required range */ - return; - } else if (required_range.first >= this_range.first && required_range.first < this_range.second) { - /* Trim start */ - int64_t const shift = required_range.first - this_range.first; - trimmed->move (shift, 0, trimmed->frames() - shift); - trimmed->set_frames (trimmed->frames() - shift); - } else if (required_range.second >= this_range.first && required_range.second < this_range.second) { - /* Trim end */ - trimmed->set_frames (required_range.second - this_range.first); - } - - data = trimmed; + _condition.notify_all (); + _have_a_real_frame[eyes] = true; } -#if HAVE_SWRESAMPLE - /* Maybe sample-rate convert */ - if (_swr_context) { - - /* Compute the resampled frames count and add 32 for luck */ - int const max_resampled_frames = ceil ((int64_t) data->frames() * _film->target_audio_sample_rate() / _film->audio_stream()->sample_rate()) + 32; - - shared_ptr<AudioBuffers> resampled (new AudioBuffers (_film->audio_stream()->channels(), max_resampled_frames)); - - /* Resample audio */ - int const resampled_frames = swr_convert ( - _swr_context, (uint8_t **) resampled->data(), max_resampled_frames, (uint8_t const **) data->data(), data->frames() - ); - - if (resampled_frames < 0) { - throw EncodeError ("could not run sample-rate converter"); - } - - resampled->set_frames (resampled_frames); - - /* And point our variables at the resampled audio */ - data = resampled; + if (eyes != EYES_LEFT) { + ++_video_frames_out; } -#endif - - if (_film->audio_channels() == 1) { - /* We need to switch things around so that the mono channel is on - the centre channel of a 5.1 set (with other channels silent). - */ - - shared_ptr<AudioBuffers> b (new AudioBuffers (6, data->frames ())); - b->make_silent (libdcp::LEFT); - b->make_silent (libdcp::RIGHT); - memcpy (b->data()[libdcp::CENTRE], data->data()[0], data->frames() * sizeof(float)); - b->make_silent (libdcp::LFE); - b->make_silent (libdcp::LS); - b->make_silent (libdcp::RS); - - data = b; - } - - write_audio (data); - - _audio_frame += data->frames (); } void -Encoder::write_audio (shared_ptr<const AudioBuffers> audio) +Encoder::process_audio (shared_ptr<const AudioBuffers> data) { - for (int i = 0; i < audio->channels(); ++i) { - sf_write_float (_sound_files[i], audio->data(i), audio->frames()); - } - - _audio_frames_written += audio->frames (); + _writer->write (data); } void -Encoder::close_sound_files () +Encoder::terminate_threads () { - for (vector<SNDFILE*>::iterator i = _sound_files.begin(); i != _sound_files.end(); ++i) { - sf_close (*i); - } - - _sound_files.clear (); -} - -void -Encoder::terminate_worker_threads () -{ - boost::mutex::scoped_lock lock (_worker_mutex); - _process_end = true; - _worker_condition.notify_all (); + boost::mutex::scoped_lock lock (_mutex); + _terminate = true; + _condition.notify_all (); lock.unlock (); - for (list<boost::thread *>::iterator i = _worker_threads.begin(); i != _worker_threads.end(); ++i) { - (*i)->join (); + for (list<boost::thread *>::iterator i = _threads.begin(); i != _threads.end(); ++i) { + if ((*i)->joinable ()) { + (*i)->join (); + } delete *i; } + + _threads.clear (); } void -Encoder::encoder_thread (ServerDescription* server) +Encoder::encoder_thread (optional<ServerDescription> server) { /* Number of seconds that we currently wait between attempts to connect to the server; not relevant for localhost @@ -456,18 +261,18 @@ Encoder::encoder_thread (ServerDescription* server) while (1) { TIMING ("encoder thread %1 sleeps", boost::this_thread::get_id()); - boost::mutex::scoped_lock lock (_worker_mutex); - while (_queue.empty () && !_process_end) { - _worker_condition.wait (lock); + boost::mutex::scoped_lock lock (_mutex); + while (_queue.empty () && !_terminate) { + _condition.wait (lock); } - if (_process_end) { + if (_terminate) { return; } TIMING ("encoder thread %1 wakes with queue of %2", boost::this_thread::get_id(), _queue.size()); - boost::shared_ptr<DCPVideoFrame> vf = _queue.front (); - _film->log()->log (String::compose ("Encoder thread %1 pops frame %2 from queue", boost::this_thread::get_id(), vf->frame()), Log::VERBOSE); + shared_ptr<DCPVideoFrame> vf = _queue.front (); + _film->log()->log (String::compose (N_("Encoder thread %1 pops frame %2 (%3) from queue"), boost::this_thread::get_id(), vf->frame(), vf->eyes ())); _queue.pop_front (); lock.unlock (); @@ -476,10 +281,10 @@ Encoder::encoder_thread (ServerDescription* server) if (server) { try { - encoded = vf->encode_remotely (server); + encoded = vf->encode_remotely (server.get ()); if (remote_backoff > 0) { - _film->log()->log (String::compose ("%1 was lost, but now she is found; removing backoff", server->host_name ())); + _film->log()->log (String::compose (N_("%1 was lost, but now she is found; removing backoff"), server->host_name ())); } /* This job succeeded, so remove any backoff */ @@ -492,7 +297,7 @@ Encoder::encoder_thread (ServerDescription* server) } _film->log()->log ( String::compose ( - "Remote encode of %1 on %2 failed (%3); thread sleeping for %4s", + N_("Remote encode of %1 on %2 failed (%3); thread sleeping for %4s"), vf->frame(), server->host_name(), e.what(), remote_backoff) ); } @@ -503,42 +308,27 @@ Encoder::encoder_thread (ServerDescription* server) encoded = vf->encode_locally (); TIMING ("encoder thread %1 finishes local encode of %2", boost::this_thread::get_id(), vf->frame()); } catch (std::exception& e) { - _film->log()->log (String::compose ("Local encode failed (%1)", e.what ())); + _film->log()->log (String::compose (N_("Local encode failed (%1)"), e.what ())); } } if (encoded) { - encoded->write (_opt, vf->frame ()); + _writer->write (encoded, vf->frame (), vf->eyes ()); frame_done (); } else { lock.lock (); _film->log()->log ( - String::compose ("Encoder thread %1 pushes frame %2 back onto queue after failure", boost::this_thread::get_id(), vf->frame()) + String::compose (N_("Encoder thread %1 pushes frame %2 back onto queue after failure"), boost::this_thread::get_id(), vf->frame()) ); _queue.push_front (vf); lock.unlock (); } if (remote_backoff > 0) { - dvdomatic_sleep (remote_backoff); + dcpomatic_sleep (remote_backoff); } lock.lock (); - _worker_condition.notify_all (); + _condition.notify_all (); } } - -void -Encoder::link (string a, string b) const -{ -#ifdef DVDOMATIC_POSIX - int const r = symlink (a.c_str(), b.c_str()); - if (r) { - throw EncodeError (String::compose ("could not create symlink from %1 to %2", a, b)); - } -#endif - -#ifdef DVDOMATIC_WINDOWS - boost::filesystem::copy_file (a, b); -#endif -} diff --git a/src/lib/encoder.h b/src/lib/encoder.h index 52ccfc166..e9b30df9e 100644 --- a/src/lib/encoder.h +++ b/src/lib/encoder.h @@ -17,11 +17,11 @@ */ -#ifndef DVDOMATIC_ENCODER_H -#define DVDOMATIC_ENCODER_H +#ifndef DCPOMATIC_ENCODER_H +#define DCPOMATIC_ENCODER_H /** @file src/encoder.h - * @brief Parent class for classes which can encode video and audio frames. + * @brief Encoder to J2K and WAV for DCP. */ #include <boost/shared_ptr.hpp> @@ -33,111 +33,92 @@ #include <stdint.h> extern "C" { #include <libavutil/samplefmt.h> -} -#ifdef HAVE_SWRESAMPLE -extern "C" { #include <libswresample/swresample.h> } -#endif -#include <sndfile.h> #include "util.h" -#include "video_sink.h" -#include "audio_sink.h" -class EncodeOptions; class Image; -class Subtitle; class AudioBuffers; class Film; class ServerDescription; class DCPVideoFrame; +class EncodedData; +class Writer; +class Job; /** @class Encoder * @brief Encoder to J2K and WAV for DCP. * - * Video is supplied to process_video as YUV frames, and audio + * Video is supplied to process_video as RGB frames, and audio * is supplied as uncompressed PCM in blocks of various sizes. */ -class Encoder : public VideoSink, public AudioSink +class Encoder : public boost::noncopyable { public: - Encoder (boost::shared_ptr<const Film> f, boost::shared_ptr<const EncodeOptions> o); + Encoder (boost::shared_ptr<const Film> f, boost::shared_ptr<Job>); virtual ~Encoder (); /** Called to indicate that a processing run is about to begin */ - virtual void process_begin (); + void process_begin (); /** Call with a frame of video. * @param i Video frame image. * @param same true if i is the same as the last time we were called. - * @param s A subtitle that should be on this frame, or 0. */ - void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s); + void process_video (boost::shared_ptr<const Image> i, Eyes eyes, ColourConversion, bool same); /** Call with some audio data */ - void process_audio (boost::shared_ptr<AudioBuffers>); + void process_audio (boost::shared_ptr<const AudioBuffers>); /** Called when a processing run has finished */ - virtual void process_end (); + void process_end (); + + float current_encoding_rate () const; + int video_frames_out () const; - float current_frames_per_second () const; - bool skipping () const; - SourceFrame video_frame () const; + enum State { + TRANSCODING, + HASHING + }; -protected: + State state () const { + boost::mutex::scoped_lock lm (_state_mutex); + return _state; + } + +private: void frame_done (); - void frame_skipped (); + void encoder_thread (boost::optional<ServerDescription>); + void terminate_threads (); + /** Film that we are encoding */ boost::shared_ptr<const Film> _film; - /** Options */ - boost::shared_ptr<const EncodeOptions> _opt; + boost::shared_ptr<Job> _job; - /** Mutex for _time_history, _just_skipped and _last_frame */ - mutable boost::mutex _history_mutex; + /** Mutex for _time_history, _last_frame and _state */ + mutable boost::mutex _state_mutex; /** List of the times of completion of the last _history_size frames; first is the most recently completed. */ std::list<struct timeval> _time_history; /** Number of frames that we should keep history for */ static int const _history_size; - /** true if the last frame we processed was skipped (because it was already done) */ - bool _just_skipped; - /** Number of video frames received so far */ - SourceFrame _video_frame; - /** Number of audio frames received so far */ - int64_t _audio_frame; + /** Number of video frames written for the DCP so far */ + int _video_frames_out; + State _state; -private: - void close_sound_files (); - void write_audio (boost::shared_ptr<const AudioBuffers> audio); - - void encoder_thread (ServerDescription *); - void terminate_worker_threads (); - void link (std::string, std::string) const; - -#if HAVE_SWRESAMPLE - SwrContext* _swr_context; -#endif - - /** List of links that we need to create when all frames have been processed; - * such that we need to call link (first, second) for each member of this list. - * In other words, `first' is a `real' frame and `second' should be a link to `first'. - */ - std::list<std::pair<int, int> > _links_required; - - std::vector<SNDFILE*> _sound_files; - int64_t _audio_frames_written; - - boost::optional<int> _last_real_frame; - bool _process_end; + bool _have_a_real_frame[EYES_COUNT]; + bool _terminate; std::list<boost::shared_ptr<DCPVideoFrame> > _queue; - std::list<boost::thread *> _worker_threads; - mutable boost::mutex _worker_mutex; - boost::condition _worker_condition; + std::list<boost::thread *> _threads; + mutable boost::mutex _mutex; + boost::condition _condition; + + boost::shared_ptr<Writer> _writer; }; #endif diff --git a/src/lib/examine_content_job.cc b/src/lib/examine_content_job.cc index a783cde33..cbf180ffc 100644 --- a/src/lib/examine_content_job.cc +++ b/src/lib/examine_content_job.cc @@ -17,27 +17,21 @@ */ -/** @file src/examine_content_job.cc - * @brief A class to run through content at high speed to find its length. - */ - #include <boost/filesystem.hpp> #include "examine_content_job.h" -#include "options.h" -#include "decoder_factory.h" -#include "decoder.h" -#include "transcoder.h" #include "log.h" +#include "content.h" #include "film.h" -#include "video_decoder.h" + +#include "i18n.h" using std::string; -using std::vector; -using std::pair; +using std::cout; using boost::shared_ptr; -ExamineContentJob::ExamineContentJob (shared_ptr<Film> f, shared_ptr<Job> req) - : Job (f, req) +ExamineContentJob::ExamineContentJob (shared_ptr<const Film> f, shared_ptr<Content> c) + : Job (f) + , _content (c) { } @@ -49,61 +43,13 @@ ExamineContentJob::~ExamineContentJob () string ExamineContentJob::name () const { - if (_film->name().empty ()) { - return "Examine content"; - } - - return String::compose ("Examine content of %1", _film->name()); + return _("Examine content"); } void ExamineContentJob::run () { - descend (0.5); - _film->set_content_digest (md5_digest (_film->content_path ())); - ascend (); - - descend (0.5); - - /* Set the film's length to either - a) a length judged by running through the content or - b) the length from a decoder's header. - */ - if (!_film->trust_content_header()) { - /* Decode the content to get an accurate length */ - - /* We don't want to use any existing length here, as progress - will be messed up. - */ - _film->unset_length (); - _film->set_crop (Crop ()); - - shared_ptr<DecodeOptions> o (new DecodeOptions); - o->decode_audio = false; - - Decoders decoders = decoder_factory (_film, o, this); - - set_progress_unknown (); - while (!decoders.video->pass()) { - /* keep going */ - } - - _film->set_length (decoders.video->video_frame()); - - _film->log()->log (String::compose ("Video length examined as %1 frames", _film->length().get())); - - } else { - - /* Get a quick decoder to get the content's length from its header */ - - shared_ptr<DecodeOptions> o (new DecodeOptions); - Decoders d = decoder_factory (_film, o, 0); - _film->set_length (d.video->length()); - - _film->log()->log (String::compose ("Video length obtained from header as %1 frames", _film->length().get())); - } - - ascend (); + _content->examine (shared_from_this ()); set_progress (1); set_state (FINISHED_OK); } diff --git a/src/lib/examine_content_job.h b/src/lib/examine_content_job.h index 729c287b5..b6903b86b 100644 --- a/src/lib/examine_content_job.h +++ b/src/lib/examine_content_job.h @@ -17,22 +17,22 @@ */ -/** @file src/examine_content_job.h - * @brief A class to obtain the length and MD5 digest of a content file. - */ - +#include <boost/shared_ptr.hpp> #include "job.h" -/** @class ExamineContentJob - * @brief A class to obtain the length and MD5 digest of a content file. - */ +class Content; +class Log; + class ExamineContentJob : public Job { public: - ExamineContentJob (boost::shared_ptr<Film>, boost::shared_ptr<Job> req); + ExamineContentJob (boost::shared_ptr<const Film>, boost::shared_ptr<Content>); ~ExamineContentJob (); std::string name () const; void run (); + +private: + boost::shared_ptr<Content> _content; }; diff --git a/src/lib/exceptions.cc b/src/lib/exceptions.cc new file mode 100644 index 000000000..8144f41b9 --- /dev/null +++ b/src/lib/exceptions.cc @@ -0,0 +1,63 @@ +/* + Copyright (C) 2012-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 "exceptions.h" +#include "compose.hpp" + +#include "i18n.h" + +using std::string; + +/** @param f File that we were trying to open */ +OpenFileError::OpenFileError (boost::filesystem::path f) + : FileError (String::compose (_("could not open file %1"), f.string()), f) +{ + +} + +/** @param f File that we were trying to create */ +CreateFileError::CreateFileError (boost::filesystem::path f) + : FileError (String::compose (_("could not create file %1"), f.string()), f) +{ + +} + +ReadFileError::ReadFileError (boost::filesystem::path f, int e) + : FileError (String::compose (_("could not read from file %1 (%2)"), f.string(), strerror (e)), f) +{ + +} + +WriteFileError::WriteFileError (boost::filesystem::path f, int e) + : FileError (String::compose (_("could not write to file %1 (%2)"), f.string(), strerror (e)), f) +{ + +} + +MissingSettingError::MissingSettingError (string s) + : SettingError (s, String::compose (_("missing required setting %1"), s)) +{ + +} + +PixelFormatError::PixelFormatError (std::string o, AVPixelFormat f) + : StringError (String::compose (_("Cannot handle pixel format %1 during %2"), f, o)) +{ + +} diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 06177863a..b04d973dc 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -17,13 +17,21 @@ */ +#ifndef DCPOMATIC_EXCEPTIONS_H +#define DCPOMATIC_EXCEPTIONS_H + /** @file src/exceptions.h * @brief Our exceptions. */ #include <stdexcept> -#include <sstream> #include <cstring> +#include <boost/exception/all.hpp> +#include <boost/filesystem.hpp> +#include <boost/thread.hpp> +extern "C" { +#include <libavutil/pixfmt.h> +} /** @class StringError * @brief A parent class for exceptions using messages held in a std::string @@ -80,7 +88,7 @@ public: /** @param m Error message. * @param f Name of the file that this exception concerns. */ - FileError (std::string m, std::string f) + FileError (std::string m, boost::filesystem::path f) : StringError (m) , _file (f) {} @@ -88,13 +96,13 @@ public: virtual ~FileError () throw () {} /** @return name of the file that this exception concerns */ - std::string file () const { + boost::filesystem::path file () const { return _file; } private: /** name of the file that this exception concerns */ - std::string _file; + boost::filesystem::path _file; }; @@ -105,9 +113,7 @@ class OpenFileError : public FileError { public: /** @param f File that we were trying to open */ - OpenFileError (std::string f) - : FileError ("could not open file " + f, f) - {} + OpenFileError (boost::filesystem::path f); }; /** @class CreateFileError. @@ -117,9 +123,7 @@ class CreateFileError : public FileError { public: /** @param f File that we were trying to create */ - CreateFileError (std::string f) - : FileError ("could not create file " + f, f) - {} + CreateFileError (boost::filesystem::path f); }; @@ -132,16 +136,7 @@ public: /** @param f File that we were trying to read from. * @param e errno value, or 0. */ - ReadFileError (std::string f, int e = 0) - : FileError ("", f) - { - std::stringstream s; - s << "could not read from file " << f; - if (e) { - s << " (" << strerror (e) << ")"; - } - _what = s.str (); - } + ReadFileError (boost::filesystem::path f, int e = 0); }; /** @class WriteFileError. @@ -153,16 +148,7 @@ public: /** @param f File that we were trying to write to. * @param e errno value, or 0. */ - WriteFileError (std::string f, int e) - : FileError ("", f) - { - std::stringstream s; - s << "could not write to file " << f; - if (e) { - s << " (" << strerror (e) << ")"; - } - _what = s.str (); - } + WriteFileError (boost::filesystem::path f, int e); }; /** @class SettingError. @@ -197,9 +183,7 @@ class MissingSettingError : public SettingError { public: /** @param s Name of setting that was required */ - MissingSettingError (std::string s) - : SettingError (s, "missing required setting " + s) - {} + MissingSettingError (std::string s); }; /** @class BadSettingError @@ -232,3 +216,38 @@ public: : StringError (s) {} }; + +class PixelFormatError : public StringError +{ +public: + PixelFormatError (std::string o, AVPixelFormat f); +}; + +class ExceptionStore +{ +public: + bool thrown () const { + boost::mutex::scoped_lock lm (_mutex); + return _exception; + } + + void rethrow () { + boost::mutex::scoped_lock lm (_mutex); + boost::rethrow_exception (_exception); + } + +protected: + + void store_current () { + boost::mutex::scoped_lock lm (_mutex); + _exception = boost::current_exception (); + } + +private: + boost::exception_ptr _exception; + mutable boost::mutex _mutex; +}; + + + +#endif diff --git a/src/lib/external_audio_decoder.cc b/src/lib/external_audio_decoder.cc deleted file mode 100644 index 25c8068b6..000000000 --- a/src/lib/external_audio_decoder.cc +++ /dev/null @@ -1,186 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <iostream> -#include <sndfile.h> -#include "external_audio_decoder.h" -#include "film.h" -#include "exceptions.h" - -using std::vector; -using std::string; -using std::stringstream; -using std::min; -using std::cout; -using boost::shared_ptr; -using boost::optional; - -ExternalAudioDecoder::ExternalAudioDecoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j) - : Decoder (f, o, j) - , AudioDecoder (f, o, j) -{ - sf_count_t frames; - vector<SNDFILE*> sf = open_files (frames); - close_files (sf); -} - -vector<SNDFILE*> -ExternalAudioDecoder::open_files (sf_count_t & frames) -{ - vector<string> const files = _film->external_audio (); - - int N = 0; - for (size_t i = 0; i < files.size(); ++i) { - if (!files[i].empty()) { - N = i + 1; - } - } - - if (N == 0) { - return vector<SNDFILE*> (); - } - - bool first = true; - frames = 0; - - vector<SNDFILE*> sndfiles; - for (size_t i = 0; i < (size_t) N; ++i) { - if (files[i].empty ()) { - sndfiles.push_back (0); - } else { - SF_INFO info; - SNDFILE* s = sf_open (files[i].c_str(), SFM_READ, &info); - if (!s) { - throw DecodeError ("could not open external audio file for reading"); - } - - if (info.channels != 1) { - throw DecodeError ("external audio files must be mono"); - } - - sndfiles.push_back (s); - - if (first) { - shared_ptr<ExternalAudioStream> st ( - new ExternalAudioStream ( - info.samplerate, av_get_default_channel_layout (N) - ) - ); - - _audio_streams.push_back (st); - _audio_stream = st; - frames = info.frames; - first = false; - } else { - if (info.frames != frames) { - throw DecodeError ("external audio files have differing lengths"); - } - } - } - } - - return sndfiles; -} - -bool -ExternalAudioDecoder::pass () -{ - sf_count_t frames; - vector<SNDFILE*> sndfiles = open_files (frames); - if (sndfiles.empty()) { - 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 = _audio_stream->sample_rate() / 2; - - shared_ptr<AudioBuffers> audio (new AudioBuffers (_audio_stream->channels(), block)); - while (frames > 0) { - sf_count_t const this_time = min (block, frames); - for (size_t i = 0; i < sndfiles.size(); ++i) { - if (!sndfiles[i]) { - audio->make_silent (i); - } else { - sf_read_float (sndfiles[i], audio->data(i), block); - } - } - - audio->set_frames (this_time); - Audio (audio); - frames -= this_time; - } - - close_files (sndfiles); - - return true; -} - -void -ExternalAudioDecoder::close_files (vector<SNDFILE*> const & sndfiles) -{ - for (size_t i = 0; i < sndfiles.size(); ++i) { - sf_close (sndfiles[i]); - } -} - -shared_ptr<ExternalAudioStream> -ExternalAudioStream::create () -{ - return shared_ptr<ExternalAudioStream> (new ExternalAudioStream); -} - -shared_ptr<ExternalAudioStream> -ExternalAudioStream::create (string t, optional<int> v) -{ - if (!v) { - /* version < 1; no type in the string, and there's only FFmpeg streams anyway */ - return shared_ptr<ExternalAudioStream> (); - } - - stringstream s (t); - string type; - s >> type; - if (type != "external") { - return shared_ptr<ExternalAudioStream> (); - } - - return shared_ptr<ExternalAudioStream> (new ExternalAudioStream (t, v)); -} - -ExternalAudioStream::ExternalAudioStream (string t, optional<int> v) -{ - assert (v); - - stringstream s (t); - string type; - s >> type >> _sample_rate >> _channel_layout; -} - -ExternalAudioStream::ExternalAudioStream () -{ - -} - -string -ExternalAudioStream::to_string () const -{ - return String::compose ("external %1 %2", _sample_rate, _channel_layout); -} diff --git a/src/lib/ffmpeg.cc b/src/lib/ffmpeg.cc new file mode 100644 index 000000000..c97b79a71 --- /dev/null +++ b/src/lib/ffmpeg.cc @@ -0,0 +1,148 @@ +/* + 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. + +*/ + +extern "C" { +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libswscale/swscale.h> +#include <libpostproc/postprocess.h> +} +#include "ffmpeg.h" +#include "ffmpeg_content.h" +#include "exceptions.h" + +#include "i18n.h" + +using std::string; +using std::cout; +using std::stringstream; +using boost::shared_ptr; + +boost::mutex FFmpeg::_mutex; + +FFmpeg::FFmpeg (boost::shared_ptr<const FFmpegContent> c) + : _ffmpeg_content (c) + , _format_context (0) + , _frame (0) + , _video_stream (-1) +{ + setup_general (); + setup_video (); + setup_audio (); +} + +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); + } + } + + av_free (_frame); + + avformat_close_input (&_format_context); +} + +void +FFmpeg::setup_general () +{ + av_register_all (); + + if (avformat_open_input (&_format_context, _ffmpeg_content->path().string().c_str(), 0, 0) < 0) { + throw OpenFileError (_ffmpeg_content->path().string ()); + } + + if (avformat_find_stream_info (_format_context, 0) < 0) { + throw DecodeError (_("could not find stream information")); + } + + /* Find video stream */ + + for (uint32_t i = 0; i < _format_context->nb_streams; ++i) { + AVStream* s = _format_context->streams[i]; + if (s->codec->codec_type == AVMEDIA_TYPE_VIDEO) { + _video_stream = i; + } + } + + if (_video_stream < 0) { + throw DecodeError (N_("could not find video stream")); + } + + _frame = avcodec_alloc_frame (); + if (_frame == 0) { + throw DecodeError (N_("could not allocate frame")); + } +} + +void +FFmpeg::setup_video () +{ + boost::mutex::scoped_lock lm (_mutex); + + 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 () +{ + 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")); + } + } +} + + +AVCodecContext * +FFmpeg::video_codec_context () const +{ + return _format_context->streams[_video_stream]->codec; +} + +AVCodecContext * +FFmpeg::audio_codec_context () const +{ + return _format_context->streams[_ffmpeg_content->audio_stream()->id]->codec; +} diff --git a/src/lib/ffmpeg.h b/src/lib/ffmpeg.h new file mode 100644 index 000000000..4d1a45da3 --- /dev/null +++ b/src/lib/ffmpeg.h @@ -0,0 +1,75 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_FFMPEG_H +#define DCPOMATIC_FFMPEG_H + +#include <vector> +#include <boost/shared_ptr.hpp> +#include <boost/thread/mutex.hpp> +extern "C" { +#include <libavcodec/avcodec.h> +} + +struct AVFilterGraph; +struct AVCodecContext; +struct AVFilterContext; +struct AVFormatContext; +struct AVFrame; +struct AVBufferContext; +struct AVCodec; +struct AVStream; + +class FFmpegContent; + +class FFmpeg +{ +public: + FFmpeg (boost::shared_ptr<const FFmpegContent>); + virtual ~FFmpeg (); + + boost::shared_ptr<const FFmpegContent> ffmpeg_content () const { + return _ffmpeg_content; + } + +protected: + AVCodecContext* video_codec_context () const; + AVCodecContext* audio_codec_context () const; + + boost::shared_ptr<const FFmpegContent> _ffmpeg_content; + + AVFormatContext* _format_context; + AVPacket _packet; + AVFrame* _frame; + + int _video_stream; + + /* It would appear (though not completely verified) that one must have + a mutex around calls to avcodec_open* and avcodec_close... and here + it is. + */ + static boost::mutex _mutex; + +private: + void setup_general (); + void setup_video (); + void setup_audio (); +}; + +#endif diff --git a/src/lib/ffmpeg_compatibility.cc b/src/lib/ffmpeg_compatibility.cc deleted file mode 100644 index 09f9276ac..000000000 --- a/src/lib/ffmpeg_compatibility.cc +++ /dev/null @@ -1,117 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -extern "C" { -#include <libavfilter/avfiltergraph.h> -} -#include "exceptions.h" - -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 - -typedef struct { - enum PixelFormat pix_fmt; -} AVSinkContext; - -static int -avsink_init (AVFilterContext* ctx, const char* args, void* opaque) -{ - AVSinkContext* priv = (AVSinkContext *) ctx->priv; - if (!opaque) { - return AVERROR (EINVAL); - } - - *priv = *(AVSinkContext *) opaque; - return 0; -} - -static void -null_end_frame (AVFilterLink *) -{ - -} - -static int -avsink_query_formats (AVFilterContext* ctx) -{ - AVSinkContext* priv = (AVSinkContext *) ctx->priv; - enum PixelFormat pix_fmts[] = { - priv->pix_fmt, - PIX_FMT_NONE - }; - - avfilter_set_common_formats (ctx, avfilter_make_format_list ((int *) pix_fmts)); - return 0; -} - -#endif - -AVFilter* -get_sink () -{ -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 - /* XXX does this leak stuff? */ - AVFilter* buffer_sink = new AVFilter; - buffer_sink->name = av_strdup ("avsink"); - buffer_sink->priv_size = sizeof (AVSinkContext); - buffer_sink->init = avsink_init; - buffer_sink->query_formats = avsink_query_formats; - buffer_sink->inputs = new AVFilterPad[2]; - AVFilterPad* i0 = const_cast<AVFilterPad*> (&buffer_sink->inputs[0]); - i0->name = "default"; - i0->type = AVMEDIA_TYPE_VIDEO; - i0->min_perms = AV_PERM_READ; - i0->rej_perms = 0; - i0->start_frame = 0; - i0->get_video_buffer = 0; - i0->get_audio_buffer = 0; - i0->end_frame = null_end_frame; - i0->draw_slice = 0; - i0->filter_samples = 0; - i0->poll_frame = 0; - i0->request_frame = 0; - i0->config_props = 0; - const_cast<AVFilterPad*> (&buffer_sink->inputs[1])->name = 0; - buffer_sink->outputs = new AVFilterPad[1]; - const_cast<AVFilterPad*> (&buffer_sink->outputs[0])->name = 0; - return buffer_sink; -#else - AVFilter* buffer_sink = avfilter_get_by_name("buffersink"); - if (buffer_sink == 0) { - throw DecodeError ("Could not create buffer sink filter"); - } - - return buffer_sink; -#endif -} - -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 -AVFilterInOut * -avfilter_inout_alloc () -{ - return (AVFilterInOut *) av_malloc (sizeof (AVFilterInOut)); -} -#endif - -#ifndef HAVE_AV_FRAME_GET_BEST_EFFORT_TIMESTAMP -int64_t av_frame_get_best_effort_timestamp (AVFrame const * f) -{ - return f->best_effort_timestamp; -} - -#endif diff --git a/src/lib/ffmpeg_content.cc b/src/lib/ffmpeg_content.cc new file mode 100644 index 000000000..de967c045 --- /dev/null +++ b/src/lib/ffmpeg_content.cc @@ -0,0 +1,407 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "ffmpeg_content.h" +#include "ffmpeg_examiner.h" +#include "compose.hpp" +#include "job.h" +#include "util.h" +#include "filter.h" +#include "film.h" +#include "log.h" + +#include "i18n.h" + +using std::string; +using std::stringstream; +using std::vector; +using std::list; +using std::cout; +using std::pair; +using boost::shared_ptr; +using boost::lexical_cast; + +int const FFmpegContentProperty::SUBTITLE_STREAMS = 100; +int const FFmpegContentProperty::SUBTITLE_STREAM = 101; +int const FFmpegContentProperty::AUDIO_STREAMS = 102; +int const FFmpegContentProperty::AUDIO_STREAM = 103; +int const FFmpegContentProperty::FILTERS = 104; + +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , VideoContent (f, p) + , AudioContent (f, p) + , SubtitleContent (f, p) +{ + +} + +FFmpegContent::FFmpegContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) + , VideoContent (f, node) + , AudioContent (f, node) + , SubtitleContent (f, node) +{ + list<shared_ptr<cxml::Node> > c = node->node_children ("SubtitleStream"); + for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) { + _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (*i))); + if ((*i)->optional_number_child<int> ("Selected")) { + _subtitle_stream = _subtitle_streams.back (); + } + } + + c = node->node_children ("AudioStream"); + for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) { + _audio_streams.push_back (shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (*i))); + if ((*i)->optional_number_child<int> ("Selected")) { + _audio_stream = _audio_streams.back (); + } + } + + c = node->node_children ("Filter"); + for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) { + _filters.push_back (Filter::from_id ((*i)->content ())); + } + + _first_video = node->optional_number_child<double> ("FirstVideo"); +} + +void +FFmpegContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("FFmpeg"); + Content::as_xml (node); + VideoContent::as_xml (node); + AudioContent::as_xml (node); + SubtitleContent::as_xml (node); + + boost::mutex::scoped_lock lm (_mutex); + + for (vector<shared_ptr<FFmpegSubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) { + xmlpp::Node* t = node->add_child("SubtitleStream"); + if (_subtitle_stream && *i == _subtitle_stream) { + t->add_child("Selected")->add_child_text("1"); + } + (*i)->as_xml (t); + } + + for (vector<shared_ptr<FFmpegAudioStream> >::const_iterator i = _audio_streams.begin(); i != _audio_streams.end(); ++i) { + xmlpp::Node* t = node->add_child("AudioStream"); + if (_audio_stream && *i == _audio_stream) { + t->add_child("Selected")->add_child_text("1"); + } + (*i)->as_xml (t); + } + + for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) { + node->add_child("Filter")->add_child_text ((*i)->id ()); + } + + if (_first_video) { + node->add_child("FirstVideo")->add_child_text (lexical_cast<string> (_first_video.get ())); + } +} + +void +FFmpegContent::examine (shared_ptr<Job> job) +{ + job->set_progress_unknown (); + + Content::examine (job); + + shared_ptr<const Film> film = _film.lock (); + assert (film); + + shared_ptr<FFmpegExaminer> examiner (new FFmpegExaminer (shared_from_this ())); + + VideoContent::Frame video_length = 0; + video_length = examiner->video_length (); + film->log()->log (String::compose ("Video length obtained from header as %1 frames", video_length)); + + { + boost::mutex::scoped_lock lm (_mutex); + + _video_length = video_length; + + _subtitle_streams = examiner->subtitle_streams (); + if (!_subtitle_streams.empty ()) { + _subtitle_stream = _subtitle_streams.front (); + } + + _audio_streams = examiner->audio_streams (); + if (!_audio_streams.empty ()) { + _audio_stream = _audio_streams.front (); + } + + _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); + signal_changed (FFmpegContentProperty::AUDIO_STREAM); + signal_changed (AudioContentProperty::AUDIO_CHANNELS); +} + +string +FFmpegContent::summary () const +{ + /* Get the string() here so that the name does not have quotes around it */ + return String::compose (_("%1 [movie]"), path().filename().string()); +} + +string +FFmpegContent::technical_summary () const +{ + string as = "none"; + if (_audio_stream) { + as = String::compose ("id %1", _audio_stream->id); + } + + string ss = "none"; + if (_subtitle_stream) { + ss = String::compose ("id %1", _subtitle_stream->id); + } + + pair<string, string> filt = Filter::ffmpeg_strings (_filters); + + return Content::technical_summary() + " - " + + VideoContent::technical_summary() + " - " + + AudioContent::technical_summary() + " - " + + String::compose ( + "ffmpeg: audio %1, subtitle %2, filters %3 %4", as, ss, filt.first, filt.second + ); +} + +string +FFmpegContent::information () const +{ + if (video_length() == 0 || video_frame_rate() == 0) { + return ""; + } + + stringstream s; + + s << String::compose (_("%1 frames; %2 frames per second"), video_length(), video_frame_rate()) << "\n"; + s << VideoContent::information (); + + return s.str (); +} + +void +FFmpegContent::set_subtitle_stream (shared_ptr<FFmpegSubtitleStream> s) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _subtitle_stream = s; + } + + signal_changed (FFmpegContentProperty::SUBTITLE_STREAM); +} + +void +FFmpegContent::set_audio_stream (shared_ptr<FFmpegAudioStream> s) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _audio_stream = s; + } + + signal_changed (FFmpegContentProperty::AUDIO_STREAM); +} + +AudioContent::Frame +FFmpegContent::audio_length () const +{ + int const cafr = content_audio_frame_rate (); + int const vfr = video_frame_rate (); + VideoContent::Frame const vl = video_length (); + + boost::mutex::scoped_lock lm (_mutex); + if (!_audio_stream) { + return 0; + } + + return video_frames_to_audio_frames (vl, cafr, vfr); +} + +int +FFmpegContent::audio_channels () const +{ + boost::mutex::scoped_lock lm (_mutex); + + if (!_audio_stream) { + return 0; + } + + return _audio_stream->channels; +} + +int +FFmpegContent::content_audio_frame_rate () const +{ + boost::mutex::scoped_lock lm (_mutex); + + if (!_audio_stream) { + return 0; + } + + return _audio_stream->frame_rate; +} + +int +FFmpegContent::output_audio_frame_rate () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + /* Resample to a DCI-approved sample rate */ + double t = dcp_audio_frame_rate (content_audio_frame_rate ()); + + FrameRateConversion frc (video_frame_rate(), film->video_frame_rate()); + + /* Compensate if the DCP is being run at a different frame rate + to the source; that is, if the video is run such that it will + look different in the DCP compared to the source (slower or faster). + skip/repeat doesn't come into effect here. + */ + + if (frc.change_speed) { + t *= video_frame_rate() * frc.factor() / film->video_frame_rate(); + } + + return rint (t); +} + +bool +operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b) +{ + return a.id == b.id; +} + +bool +operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b) +{ + return a.id == b.id; +} + +FFmpegAudioStream::FFmpegAudioStream (shared_ptr<const cxml::Node> node) + : mapping (node->node_child ("Mapping")) +{ + name = node->string_child ("Name"); + id = node->number_child<int> ("Id"); + frame_rate = node->number_child<int> ("FrameRate"); + channels = node->number_child<int64_t> ("Channels"); + first_audio = node->optional_number_child<double> ("FirstAudio"); +} + +void +FFmpegAudioStream::as_xml (xmlpp::Node* root) const +{ + root->add_child("Name")->add_child_text (name); + root->add_child("Id")->add_child_text (lexical_cast<string> (id)); + root->add_child("FrameRate")->add_child_text (lexical_cast<string> (frame_rate)); + root->add_child("Channels")->add_child_text (lexical_cast<string> (channels)); + if (first_audio) { + root->add_child("FirstAudio")->add_child_text (lexical_cast<string> (first_audio.get ())); + } + mapping.as_xml (root->add_child("Mapping")); +} + +/** Construct a SubtitleStream from a value returned from to_string(). + * @param t String returned from to_string(). + * @param v State file version. + */ +FFmpegSubtitleStream::FFmpegSubtitleStream (shared_ptr<const cxml::Node> node) +{ + name = node->string_child ("Name"); + id = node->number_child<int> ("Id"); +} + +void +FFmpegSubtitleStream::as_xml (xmlpp::Node* root) const +{ + root->add_child("Name")->add_child_text (name); + root->add_child("Id")->add_child_text (lexical_cast<string> (id)); +} + +Time +FFmpegContent::full_length () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + FrameRateConversion frc (video_frame_rate (), film->video_frame_rate ()); + return video_length() * frc.factor() * TIME_HZ / film->video_frame_rate (); +} + +AudioMapping +FFmpegContent::audio_mapping () const +{ + boost::mutex::scoped_lock lm (_mutex); + + if (!_audio_stream) { + return AudioMapping (); + } + + return _audio_stream->mapping; +} + +void +FFmpegContent::set_filters (vector<Filter const *> const & filters) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _filters = filters; + } + + signal_changed (FFmpegContentProperty::FILTERS); +} + +void +FFmpegContent::set_audio_mapping (AudioMapping m) +{ + audio_stream()->mapping = m; + signal_changed (AudioContentProperty::AUDIO_MAPPING); +} + +string +FFmpegContent::identifier () const +{ + stringstream s; + + s << VideoContent::identifier(); + + boost::mutex::scoped_lock lm (_mutex); + + if (_subtitle_stream) { + s << "_" << _subtitle_stream->id; + } + + for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) { + s << "_" << (*i)->id (); + } + + return s.str (); +} + diff --git a/src/lib/ffmpeg_content.h b/src/lib/ffmpeg_content.h new file mode 100644 index 000000000..775cb9220 --- /dev/null +++ b/src/lib/ffmpeg_content.h @@ -0,0 +1,169 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_FFMPEG_CONTENT_H +#define DCPOMATIC_FFMPEG_CONTENT_H + +#include <boost/enable_shared_from_this.hpp> +#include "video_content.h" +#include "audio_content.h" +#include "subtitle_content.h" +#include "audio_mapping.h" + +class Filter; +class ffmpeg_pts_offset_test; + +class FFmpegAudioStream +{ +public: + FFmpegAudioStream (std::string n, int i, int f, int c) + : name (n) + , id (i) + , frame_rate (f) + , channels (c) + , mapping (c) + { + mapping.make_default (); + } + + FFmpegAudioStream (boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + + std::string name; + int id; + int frame_rate; + int channels; + AudioMapping mapping; + boost::optional<double> first_audio; + +private: + friend class ffmpeg_pts_offset_test; + + /* Constructor for tests */ + FFmpegAudioStream () + : mapping (1) + {} +}; + +extern bool operator== (FFmpegAudioStream const & a, FFmpegAudioStream const & b); + +class FFmpegSubtitleStream +{ +public: + FFmpegSubtitleStream (std::string n, int i) + : name (n) + , id (i) + {} + + FFmpegSubtitleStream (boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + + std::string name; + int id; +}; + +extern bool operator== (FFmpegSubtitleStream const & a, FFmpegSubtitleStream const & b); + +class FFmpegContentProperty : public VideoContentProperty +{ +public: + static int const SUBTITLE_STREAMS; + static int const SUBTITLE_STREAM; + static int const AUDIO_STREAMS; + static int const AUDIO_STREAM; + static int const FILTERS; +}; + +class FFmpegContent : public VideoContent, public AudioContent, public SubtitleContent +{ +public: + FFmpegContent (boost::shared_ptr<const Film>, boost::filesystem::path); + FFmpegContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + boost::shared_ptr<FFmpegContent> shared_from_this () { + return boost::dynamic_pointer_cast<FFmpegContent> (Content::shared_from_this ()); + } + + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + std::string information () const; + void as_xml (xmlpp::Node *) const; + Time full_length () const; + + std::string identifier () const; + + /* AudioContent */ + int audio_channels () const; + AudioContent::Frame audio_length () const; + int content_audio_frame_rate () const; + int output_audio_frame_rate () const; + AudioMapping audio_mapping () const; + void set_audio_mapping (AudioMapping); + + void set_filters (std::vector<Filter const *> const &); + + std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const { + boost::mutex::scoped_lock lm (_mutex); + return _subtitle_streams; + } + + boost::shared_ptr<FFmpegSubtitleStream> subtitle_stream () const { + boost::mutex::scoped_lock lm (_mutex); + return _subtitle_stream; + } + + std::vector<boost::shared_ptr<FFmpegAudioStream> > audio_streams () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_streams; + } + + boost::shared_ptr<FFmpegAudioStream> audio_stream () const { + boost::mutex::scoped_lock lm (_mutex); + return _audio_stream; + } + + std::vector<Filter const *> filters () const { + boost::mutex::scoped_lock lm (_mutex); + return _filters; + } + + void set_subtitle_stream (boost::shared_ptr<FFmpegSubtitleStream>); + void set_audio_stream (boost::shared_ptr<FFmpegAudioStream>); + + boost::optional<double> first_video () const { + boost::mutex::scoped_lock lm (_mutex); + return _first_video; + } + +private: + friend class 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; + /** Video filters that should be used when generating DCPs */ + std::vector<Filter const *> _filters; +}; + +#endif diff --git a/src/lib/ffmpeg_decoder.cc b/src/lib/ffmpeg_decoder.cc index a19f26ad7..8da607e7e 100644 --- a/src/lib/ffmpeg_decoder.cc +++ b/src/lib/ffmpeg_decoder.cc @@ -28,355 +28,189 @@ #include <iostream> #include <stdint.h> #include <boost/lexical_cast.hpp> +#include <sndfile.h> extern "C" { -#include <tiffio.h> #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> -#include <libswscale/swscale.h> -#include <libpostproc/postprocess.h> } -#include <sndfile.h> #include "film.h" -#include "format.h" -#include "transcoder.h" -#include "job.h" #include "filter.h" -#include "options.h" #include "exceptions.h" #include "image.h" #include "util.h" #include "log.h" #include "ffmpeg_decoder.h" #include "filter_graph.h" -#include "subtitle.h" +#include "audio_buffers.h" +#include "ffmpeg_content.h" + +#include "i18n.h" using std::cout; using std::string; using std::vector; using std::stringstream; using std::list; +using std::min; +using std::pair; using boost::shared_ptr; using boost::optional; using boost::dynamic_pointer_cast; - -FFmpegDecoder::FFmpegDecoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j) - : Decoder (f, o, j) - , VideoDecoder (f, o, j) - , AudioDecoder (f, o, j) - , _format_context (0) - , _video_stream (-1) - , _frame (0) - , _video_codec_context (0) - , _video_codec (0) - , _audio_codec_context (0) - , _audio_codec (0) +using libdcp::Size; + +FFmpegDecoder::FFmpegDecoder (shared_ptr<const Film> f, shared_ptr<const FFmpegContent> c, bool video, bool audio) + : Decoder (f) + , VideoDecoder (f, c) + , AudioDecoder (f) + , SubtitleDecoder (f) + , FFmpeg (c) , _subtitle_codec_context (0) , _subtitle_codec (0) + , _decode_video (video) + , _decode_audio (audio) + , _video_pts_offset (0) + , _audio_pts_offset (0) + , _just_sought (false) { - setup_general (); - setup_video (); - setup_audio (); setup_subtitle (); - if (!o->video_sync) { - _first_video = 0; - } -} + /* Audio and video frame PTS values may not start with 0. We want + to fiddle them so that: -FFmpegDecoder::~FFmpegDecoder () -{ - if (_audio_codec_context) { - avcodec_close (_audio_codec_context); - } - - if (_video_codec_context) { - avcodec_close (_video_codec_context); - } + 1. One of them starts at time 0. + 2. The first video PTS value ends up on a frame boundary. - if (_subtitle_codec_context) { - avcodec_close (_subtitle_codec_context); - } + Then we remove big initial gaps in PTS and we allow our + insertion of black frames to work. - av_free (_frame); - - avformat_close_input (&_format_context); -} + We will do: + audio_pts_to_use = audio_pts_from_ffmpeg + audio_pts_offset; + video_pts_to_use = video_pts_from_ffmpeg + video_pts_offset; + */ -void -FFmpegDecoder::setup_general () -{ - av_register_all (); + bool const have_video = video && c->first_video(); + bool const have_audio = audio && c->audio_stream() && c->audio_stream()->first_audio; - if (avformat_open_input (&_format_context, _film->content_path().c_str(), 0, 0) < 0) { - throw OpenFileError (_film->content_path ()); - } + /* First, make one of them start at 0 */ - if (avformat_find_stream_info (_format_context, 0) < 0) { - throw DecodeError ("could not find stream information"); + if (have_audio && have_video) { + _video_pts_offset = _audio_pts_offset = - min (c->first_video().get(), c->audio_stream()->first_audio.get()); + } else if (have_video) { + _video_pts_offset = - c->first_video().get(); + } else if (have_audio) { + _audio_pts_offset = - c->audio_stream()->first_audio.get(); } - /* Find video, audio and subtitle streams and choose the first of each */ - - for (uint32_t i = 0; i < _format_context->nb_streams; ++i) { - AVStream* s = _format_context->streams[i]; - if (s->codec->codec_type == AVMEDIA_TYPE_VIDEO) { - _video_stream = i; - } else if (s->codec->codec_type == AVMEDIA_TYPE_AUDIO) { - - /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up, - so bodge it here. No idea why we should have to do this. - */ - - if (s->codec->channel_layout == 0) { - s->codec->channel_layout = av_get_default_channel_layout (s->codec->channels); - } - - _audio_streams.push_back ( - shared_ptr<AudioStream> ( - new FFmpegAudioStream (stream_name (s), i, s->codec->sample_rate, s->codec->channel_layout) - ) - ); - - } else if (s->codec->codec_type == AVMEDIA_TYPE_SUBTITLE) { - _subtitle_streams.push_back ( - shared_ptr<SubtitleStream> ( - new SubtitleStream (stream_name (s), i) - ) - ); + /* Now adjust both so that the video pts starts on a frame */ + if (have_video && have_audio) { + double first_video = c->first_video().get() + _video_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 (); } - } - - if (_video_stream < 0) { - throw DecodeError ("could not find video stream"); - } - _frame = avcodec_alloc_frame (); - if (_frame == 0) { - throw DecodeError ("could not allocate frame"); + _video_pts_offset += first_video - old_first_video; + _audio_pts_offset += first_video - old_first_video; } } -void -FFmpegDecoder::setup_video () +FFmpegDecoder::~FFmpegDecoder () { - _video_codec_context = _format_context->streams[_video_stream]->codec; - _video_codec = avcodec_find_decoder (_video_codec_context->codec_id); - - if (_video_codec == 0) { - throw DecodeError ("could not find video decoder"); - } + boost::mutex::scoped_lock lm (_mutex); - if (avcodec_open2 (_video_codec_context, _video_codec, 0) < 0) { - throw DecodeError ("could not open video decoder"); + if (_subtitle_codec_context) { + avcodec_close (_subtitle_codec_context); } } void -FFmpegDecoder::setup_audio () +FFmpegDecoder::flush () { - if (!_audio_stream) { - return; + /* Get any remaining frames */ + + _packet.data = 0; + _packet.size = 0; + + /* XXX: should we reset _packet.data and size after each *_decode_* call? */ + + if (_decode_video) { + while (decode_video_packet ()) {} } - - shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream); - assert (ffa); - _audio_codec_context = _format_context->streams[ffa->id()]->codec; - _audio_codec = avcodec_find_decoder (_audio_codec_context->codec_id); - - if (_audio_codec == 0) { - throw DecodeError ("could not find audio decoder"); + if (_ffmpeg_content->audio_stream() && _decode_audio) { + decode_audio_packet (); } - if (avcodec_open2 (_audio_codec_context, _audio_codec, 0) < 0) { - throw DecodeError ("could not open audio decoder"); - } + /* Stop us being asked for any more data */ + _video_position = _ffmpeg_content->video_length (); + _audio_position = _ffmpeg_content->audio_length (); } void -FFmpegDecoder::setup_subtitle () -{ - if (!_subtitle_stream) { - return; - } - - _subtitle_codec_context = _format_context->streams[_subtitle_stream->id()]->codec; - _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id); - - if (_subtitle_codec == 0) { - throw DecodeError ("could not find subtitle decoder"); - } - - if (avcodec_open2 (_subtitle_codec_context, _subtitle_codec, 0) < 0) { - throw DecodeError ("could not open subtitle decoder"); - } -} - - -bool FFmpegDecoder::pass () { int r = av_read_frame (_format_context, &_packet); - + if (r < 0) { if (r != AVERROR_EOF) { /* Maybe we should fail here, but for now we'll just finish off instead */ char buf[256]; av_strerror (r, buf, sizeof(buf)); - _film->log()->log (String::compose ("error on av_read_frame (%1) (%2)", buf, r)); - } - - /* Get any remaining frames */ - - _packet.data = 0; - _packet.size = 0; - - /* XXX: should we reset _packet.data and size after each *_decode_* call? */ - - int frame_finished; - - while (avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { - filter_and_emit_video (_frame); + shared_ptr<const Film> film = _film.lock (); + assert (film); + film->log()->log (String::compose (N_("error on av_read_frame (%1) (%2)"), buf, r)); } - if (_audio_stream && _opt->decode_audio) { - while (avcodec_decode_audio4 (_audio_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { - int const data_size = av_samples_get_buffer_size ( - 0, _audio_codec_context->channels, _frame->nb_samples, audio_sample_format (), 1 - ); - - assert (_audio_codec_context->channels == _film->audio_channels()); - Audio (deinterleave_audio (_frame->data[0], data_size)); - } - } - - return true; + flush (); + return; } avcodec_get_frame_defaults (_frame); - shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream); - - if (_packet.stream_index == _video_stream) { - - int frame_finished; - int const r = avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet); - if (r >= 0 && frame_finished) { - - if (r != _packet.size) { - _film->log()->log (String::compose ("Used only %1 bytes of %2 in packet", r, _packet.size)); - } - - if (_opt->video_sync) { - out_with_sync (); - } else { - filter_and_emit_video (_frame); - } - } - - } else if (ffa && _packet.stream_index == ffa->id() && _opt->decode_audio) { - - int frame_finished; - if (avcodec_decode_audio4 (_audio_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) { - - /* Where we are in the source, in seconds */ - double const source_pts_seconds = av_q2d (_format_context->streams[_packet.stream_index]->time_base) - * av_frame_get_best_effort_timestamp(_frame); - - /* We only decode audio if we've had our first video packet through, and if it - was before this packet. Until then audio is thrown away. - */ - - if (_first_video && _first_video.get() <= source_pts_seconds) { - - if (!_first_audio) { - _first_audio = source_pts_seconds; - - /* This is our first audio frame, and if we've arrived here we must have had our - first video frame. Push some silence to make up any gap between our first - video frame and our first audio. - */ - - /* frames of silence that we must push */ - int const s = rint ((_first_audio.get() - _first_video.get()) * ffa->sample_rate ()); - - _film->log()->log ( - String::compose ( - "First video at %1, first audio at %2, pushing %3 audio frames of silence for %4 channels (%5 bytes per sample)", - _first_video.get(), _first_audio.get(), s, ffa->channels(), bytes_per_audio_sample() - ) - ); - - if (s) { - shared_ptr<AudioBuffers> audio (new AudioBuffers (ffa->channels(), s)); - audio->make_silent (); - Audio (audio); - } - } - - int const data_size = av_samples_get_buffer_size ( - 0, _audio_codec_context->channels, _frame->nb_samples, audio_sample_format (), 1 - ); - - assert (_audio_codec_context->channels == _film->audio_channels()); - Audio (deinterleave_audio (_frame->data[0], data_size)); - } - } - - } else if (_subtitle_stream && _packet.stream_index == _subtitle_stream->id() && _opt->decode_subtitles && _first_video) { - - int got_subtitle; - AVSubtitle sub; - if (avcodec_decode_subtitle2 (_subtitle_codec_context, &sub, &got_subtitle, &_packet) && got_subtitle) { - /* Sometimes we get an empty AVSubtitle, which is used by some codecs to - indicate that the previous subtitle should stop. - */ - if (sub.num_rects > 0) { - shared_ptr<TimedSubtitle> ts; - try { - emit_subtitle (shared_ptr<TimedSubtitle> (new TimedSubtitle (sub))); - } catch (...) { - /* some problem with the subtitle; we probably didn't understand it */ - } - } else { - emit_subtitle (shared_ptr<TimedSubtitle> ()); - } - avsubtitle_free (&sub); - } - } + shared_ptr<const Film> film = _film.lock (); + assert (film); + if (_packet.stream_index == _video_stream && _decode_video) { + decode_video_packet (); + } else if (_ffmpeg_content->audio_stream() && _packet.stream_index == _ffmpeg_content->audio_stream()->id && _decode_audio) { + decode_audio_packet (); + } else if (_ffmpeg_content->subtitle_stream() && _packet.stream_index == _ffmpeg_content->subtitle_stream()->id && film->with_subtitles ()) { + decode_subtitle_packet (); + } + av_free_packet (&_packet); - return false; } +/** @param data pointer to array of pointers to buffers. + * Only the first buffer will be used for non-planar data, otherwise there will be one per channel. + */ shared_ptr<AudioBuffers> -FFmpegDecoder::deinterleave_audio (uint8_t* data, int size) +FFmpegDecoder::deinterleave_audio (uint8_t** data, int size) { - assert (_film->audio_channels()); + assert (_ffmpeg_content->audio_channels()); assert (bytes_per_audio_sample()); - shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream); - assert (ffa); - /* Deinterleave and convert to float */ - assert ((size % (bytes_per_audio_sample() * ffa->channels())) == 0); + assert ((size % (bytes_per_audio_sample() * _ffmpeg_content->audio_channels())) == 0); int const total_samples = size / bytes_per_audio_sample(); - int const frames = total_samples / _film->audio_channels(); - shared_ptr<AudioBuffers> audio (new AudioBuffers (ffa->channels(), frames)); + int const frames = total_samples / _ffmpeg_content->audio_channels(); + shared_ptr<AudioBuffers> audio (new AudioBuffers (_ffmpeg_content->audio_channels(), frames)); switch (audio_sample_format()) { case AV_SAMPLE_FMT_S16: { - int16_t* p = reinterpret_cast<int16_t *> (data); + int16_t* p = reinterpret_cast<int16_t *> (data[0]); int sample = 0; int channel = 0; for (int i = 0; i < total_samples; ++i) { audio->data(channel)[sample] = float(*p++) / (1 << 15); ++channel; - if (channel == _film->audio_channels()) { + if (channel == _ffmpeg_content->audio_channels()) { channel = 0; ++sample; } @@ -386,10 +220,10 @@ FFmpegDecoder::deinterleave_audio (uint8_t* data, int size) case AV_SAMPLE_FMT_S16P: { - int16_t* p = reinterpret_cast<int16_t *> (data); - for (int i = 0; i < _film->audio_channels(); ++i) { + int16_t** p = reinterpret_cast<int16_t **> (data); + for (int i = 0; i < _ffmpeg_content->audio_channels(); ++i) { for (int j = 0; j < frames; ++j) { - audio->data(i)[j] = static_cast<float>(*p++) / (1 << 15); + audio->data(i)[j] = static_cast<float>(p[i][j]) / (1 << 15); } } } @@ -397,14 +231,14 @@ FFmpegDecoder::deinterleave_audio (uint8_t* data, int size) case AV_SAMPLE_FMT_S32: { - int32_t* p = reinterpret_cast<int32_t *> (data); + int32_t* p = reinterpret_cast<int32_t *> (data[0]); int sample = 0; int channel = 0; for (int i = 0; i < total_samples; ++i) { audio->data(channel)[sample] = static_cast<float>(*p++) / (1 << 31); ++channel; - if (channel == _film->audio_channels()) { + if (channel == _ffmpeg_content->audio_channels()) { channel = 0; ++sample; } @@ -414,14 +248,14 @@ FFmpegDecoder::deinterleave_audio (uint8_t* data, int size) case AV_SAMPLE_FMT_FLT: { - float* p = reinterpret_cast<float*> (data); + float* p = reinterpret_cast<float*> (data[0]); int sample = 0; int channel = 0; for (int i = 0; i < total_samples; ++i) { audio->data(channel)[sample] = *p++; ++channel; - if (channel == _film->audio_channels()) { + if (channel == _ffmpeg_content->audio_channels()) { channel = 0; ++sample; } @@ -431,309 +265,342 @@ FFmpegDecoder::deinterleave_audio (uint8_t* data, int size) case AV_SAMPLE_FMT_FLTP: { - float* p = reinterpret_cast<float*> (data); - for (int i = 0; i < _film->audio_channels(); ++i) { - memcpy (audio->data(i), p, frames * sizeof(float)); - p += frames; + float** p = reinterpret_cast<float**> (data); + for (int i = 0; i < _ffmpeg_content->audio_channels(); ++i) { + memcpy (audio->data(i), p[i], frames * sizeof(float)); } } break; default: - throw DecodeError (String::compose ("Unrecognised audio sample format (%1)", static_cast<int> (audio_sample_format()))); + throw DecodeError (String::compose (_("Unrecognised audio sample format (%1)"), static_cast<int> (audio_sample_format()))); } return audio; } -float -FFmpegDecoder::frames_per_second () 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); -} - AVSampleFormat FFmpegDecoder::audio_sample_format () const { - if (_audio_codec_context == 0) { + if (!_ffmpeg_content->audio_stream()) { return (AVSampleFormat) 0; } - return _audio_codec_context->sample_fmt; + return audio_codec_context()->sample_fmt; } -Size -FFmpegDecoder::native_size () const +int +FFmpegDecoder::bytes_per_audio_sample () const { - return Size (_video_codec_context->width, _video_codec_context->height); + return av_get_bytes_per_sample (audio_sample_format ()); } -PixelFormat -FFmpegDecoder::pixel_format () const +void +FFmpegDecoder::seek (VideoContent::Frame frame, bool accurate) { - return _video_codec_context->pix_fmt; -} + double const time_base = av_q2d (_format_context->streams[_video_stream]->time_base); -int -FFmpegDecoder::time_base_numerator () const -{ - return _video_codec_context->time_base.num; -} + /* 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. + */ + int initial = frame; -int -FFmpegDecoder::time_base_denominator () const -{ - return _video_codec_context->time_base.den; -} + if (accurate) { + initial -= 5; + } -int -FFmpegDecoder::sample_aspect_ratio_numerator () const -{ - return _video_codec_context->sample_aspect_ratio.num; -} + if (initial < 0) { + initial = 0; + } -int -FFmpegDecoder::sample_aspect_ratio_denominator () const -{ - return _video_codec_context->sample_aspect_ratio.den; -} + /* Initial seek time in the stream's timebase */ + int64_t const initial_vt = ((initial / _ffmpeg_content->video_frame_rate()) - _video_pts_offset) / time_base; -string -FFmpegDecoder::stream_name (AVStream* s) const -{ - stringstream n; - - AVDictionaryEntry const * lang = av_dict_get (s->metadata, "language", 0, 0); - if (lang) { - n << lang->value; + 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); } + + _just_sought = true; + _video_position = frame; - AVDictionaryEntry const * title = av_dict_get (s->metadata, "title", 0, 0); - if (title) { - if (!n.str().empty()) { - n << " "; - } - n << title->value; + if (frame == 0 || !accurate) { + /* We're already there, or we're as close as we need to be */ + return; } - if (n.str().empty()) { - n << "unknown"; - } + while (1) { + int r = av_read_frame (_format_context, &_packet); + if (r < 0) { + return; + } - return n.str (); -} + if (_packet.stream_index != _video_stream) { + continue; + } + + avcodec_get_frame_defaults (_frame); + + 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 + _video_pts_offset) * _ffmpeg_content->video_frame_rate() + ); -int -FFmpegDecoder::bytes_per_audio_sample () const -{ - return av_get_bytes_per_sample (audio_sample_format ()); + if (_video_position >= (frame - 1)) { + av_free_packet (&_packet); + break; + } + } + + av_free_packet (&_packet); + } } void -FFmpegDecoder::set_audio_stream (shared_ptr<AudioStream> s) +FFmpegDecoder::decode_audio_packet () { - AudioDecoder::set_audio_stream (s); - setup_audio (); -} + /* Audio packets can contain multiple frames, so we may have to call avcodec_decode_audio4 + several times. + */ + + AVPacket copy_packet = _packet; + + while (copy_packet.size > 0) { -void -FFmpegDecoder::set_subtitle_stream (shared_ptr<SubtitleStream> s) -{ - VideoDecoder::set_subtitle_stream (s); - setup_subtitle (); - OutputChanged (); + 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); + film->log()->log (String::compose ("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) + _audio_pts_offset; + + if (pts > 0) { + /* Emit some silence */ + shared_ptr<AudioBuffers> silence ( + new AudioBuffers ( + _ffmpeg_content->audio_channels(), + pts * _ffmpeg_content->content_audio_frame_rate() + ) + ); + + silence->make_silent (); + audio (silence, _audio_position); + } + } + + 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); + } + + copy_packet.data += decode_result; + copy_packet.size -= decode_result; + } } -void -FFmpegDecoder::filter_and_emit_video (AVFrame* frame) +bool +FFmpegDecoder::decode_video_packet () { + int frame_finished; + if (avcodec_decode_video2 (video_codec_context(), _frame, &frame_finished, &_packet) < 0 || !frame_finished) { + return false; + } + boost::mutex::scoped_lock lm (_filter_graphs_mutex); - - shared_ptr<FilterGraph> graph; + shared_ptr<FilterGraph> graph; + list<shared_ptr<FilterGraph> >::iterator i = _filter_graphs.begin(); - while (i != _filter_graphs.end() && !(*i)->can_process (Size (frame->width, frame->height), (AVPixelFormat) frame->format)) { + while (i != _filter_graphs.end() && !(*i)->can_process (libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)) { ++i; } if (i == _filter_graphs.end ()) { - graph.reset (new FilterGraph (_film, this, Size (frame->width, frame->height), (AVPixelFormat) frame->format)); + shared_ptr<const Film> film = _film.lock (); + assert (film); + + graph.reset (new FilterGraph (_ffmpeg_content, libdcp::Size (_frame->width, _frame->height), (AVPixelFormat) _frame->format)); _filter_graphs.push_back (graph); - _film->log()->log (String::compose ("New graph for %1x%2, pixel format %3", frame->width, frame->height, frame->format)); + + film->log()->log (String::compose (N_("New graph for %1x%2, pixel format %3"), _frame->width, _frame->height, _frame->format)); } else { graph = *i; } - list<shared_ptr<Image> > images = graph->process (frame); - - for (list<shared_ptr<Image> >::iterator i = images.begin(); i != images.end(); ++i) { - emit_video (*i, frame_time ()); - } -} - -bool -FFmpegDecoder::seek (double p) -{ - return do_seek (p, false); -} - -bool -FFmpegDecoder::seek_to_last () -{ - /* This AVSEEK_FLAG_BACKWARD in do_seek is a bit of a hack; without it, if we ask for a seek to the same place as last time - (used when we change decoder parameters and want to re-fetch the frame) we end up going forwards rather than - staying in the same place. - */ - return do_seek (last_source_time(), true); -} + list<pair<shared_ptr<Image>, int64_t> > images = graph->process (_frame); -bool -FFmpegDecoder::do_seek (double p, bool backwards) -{ - int64_t const vt = p / av_q2d (_format_context->streams[_video_stream]->time_base); - - int const r = av_seek_frame (_format_context, _video_stream, vt, backwards ? AVSEEK_FLAG_BACKWARD : 0); + string post_process = Filter::ffmpeg_strings (_ffmpeg_content->filters()).second; - avcodec_flush_buffers (_video_codec_context); - if (_subtitle_codec_context) { - avcodec_flush_buffers (_subtitle_codec_context); - } - - return r < 0; -} + for (list<pair<shared_ptr<Image>, int64_t> >::iterator i = images.begin(); i != images.end(); ++i) { -shared_ptr<FFmpegAudioStream> -FFmpegAudioStream::create (string t, optional<int> v) -{ - if (!v) { - /* version < 1; no type in the string, and there's only FFmpeg streams anyway */ - return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v)); - } + shared_ptr<Image> image = i->first; + if (!post_process.empty ()) { + image = image->post_process (post_process, true); + } + + if (i->second != AV_NOPTS_VALUE) { - stringstream s (t); - string type; - s >> type; - if (type != "ffmpeg") { - return shared_ptr<FFmpegAudioStream> (); - } + double const pts = i->second * av_q2d (_format_context->streams[_video_stream]->time_base) + _video_pts_offset; - return shared_ptr<FFmpegAudioStream> (new FFmpegAudioStream (t, v)); -} + 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->video_frame_rate ()); + _just_sought = false; + } -FFmpegAudioStream::FFmpegAudioStream (string t, optional<int> version) -{ - stringstream n (t); - - int name_index = 4; - if (!version) { - name_index = 2; - int channels; - n >> _id >> channels; - _channel_layout = av_get_default_channel_layout (channels); - _sample_rate = 0; - } else { - string type; - /* Current (marked version 1) */ - n >> type >> _id >> _sample_rate >> _channel_layout; - assert (type == "ffmpeg"); - } + double const next = _video_position / _ffmpeg_content->video_frame_rate(); + double const one_frame = 1 / _ffmpeg_content->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 + ) + ); + + black->make_black (); + video (image, false, _video_position); + delta -= one_frame; + } - for (int i = 0; i < name_index; ++i) { - size_t const s = t.find (' '); - if (s != string::npos) { - t = t.substr (s + 1); + if (delta > -one_frame) { + /* This PTS is within a frame of being right; emit this (otherwise it will be dropped) */ + video (image, false, _video_position); + } + + } else { + shared_ptr<const Film> film = _film.lock (); + assert (film); + film->log()->log ("Dropping frame without PTS"); } } - _name = t; -} - -string -FFmpegAudioStream::to_string () const -{ - return String::compose ("ffmpeg %1 %2 %3 %4", _id, _sample_rate, _channel_layout, _name); + return true; } + void -FFmpegDecoder::out_with_sync () +FFmpegDecoder::setup_subtitle () { - /* Where we are in the output, in seconds */ - double const out_pts_seconds = video_frame() / frames_per_second(); - - /* Where we are in the source, in seconds */ - double const source_pts_seconds = av_q2d (_format_context->streams[_packet.stream_index]->time_base) - * av_frame_get_best_effort_timestamp(_frame); + boost::mutex::scoped_lock lm (_mutex); - _film->log()->log ( - String::compose ("Source video frame ready; source at %1, output at %2", source_pts_seconds, out_pts_seconds), - Log::VERBOSE - ); - - if (!_first_video) { - _first_video = source_pts_seconds; + if (!_ffmpeg_content->subtitle_stream() || _ffmpeg_content->subtitle_stream()->id >= int (_format_context->nb_streams)) { + return; } - - /* Difference between where we are and where we should be */ - double const delta = source_pts_seconds - _first_video.get() - out_pts_seconds; - double const one_frame = 1 / frames_per_second(); - - /* Insert frames if required to get out_pts_seconds up to pts_seconds */ - if (delta > one_frame) { - int const extra = rint (delta / one_frame); - for (int i = 0; i < extra; ++i) { - repeat_last_video (); - _film->log()->log ( - String::compose ( - "Extra video frame inserted at %1s; source frame %2, source PTS %3 (at %4 fps)", - out_pts_seconds, video_frame(), source_pts_seconds, frames_per_second() - ) - ); - } + + _subtitle_codec_context = _format_context->streams[_ffmpeg_content->subtitle_stream()->id]->codec; + _subtitle_codec = avcodec_find_decoder (_subtitle_codec_context->codec_id); + + if (_subtitle_codec == 0) { + throw DecodeError (_("could not find subtitle decoder")); } - if (delta > -one_frame) { - /* Process this frame */ - filter_and_emit_video (_frame); - } else { - /* Otherwise we are omitting a frame to keep things right */ - _film->log()->log (String::compose ("Frame removed at %1s", out_pts_seconds)); + 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::film_changed (Film::Property p) +FFmpegDecoder::decode_subtitle_packet () { - switch (p) { - case Film::CROP: - case Film::FILTERS: - { - boost::mutex::scoped_lock lm (_filter_graphs_mutex); - _filter_graphs.clear (); + int got_subtitle; + AVSubtitle sub; + if (avcodec_decode_subtitle2 (_subtitle_codec_context, &sub, &got_subtitle, &_packet) < 0 || !got_subtitle) { + return; } - OutputChanged (); - break; - default: - break; + /* Sometimes we get an empty AVSubtitle, which is used by some codecs to + indicate that the previous subtitle should stop. + */ + if (sub.num_rects <= 0) { + subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0); + 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 + source that we may have chopped off for the DCP) + */ + double const packet_time = static_cast<double> (sub.pts) / AV_TIME_BASE; + + /* 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; -/** @return Length (in video frames) according to our content's header */ -SourceFrame -FFmpegDecoder::length () const -{ - return (double(_format_context->duration) / AV_TIME_BASE) * frames_per_second(); -} + AVSubtitleRect const * rect = sub.rects[0]; -double -FFmpegDecoder::frame_time () const -{ - return av_frame_get_best_effort_timestamp(_frame) * av_q2d (_format_context->streams[_video_stream]->time_base); + if (rect->type != SUBTITLE_BITMAP) { + throw DecodeError (_("non-bitmap subtitles not yet supported")); + } + + shared_ptr<Image> image (new Image (PIX_FMT_RGBA, libdcp::Size (rect->w, rect->h), true)); + + /* Start of the first line in the subtitle */ + uint8_t* sub_p = rect->pict.data[0]; + /* sub_p looks up into a RGB palette which is here */ + uint32_t const * palette = (uint32_t *) rect->pict.data[1]; + /* Start of the output data */ + uint32_t* out_p = (uint32_t *) image->data()[0]; + + for (int y = 0; y < rect->h; ++y) { + uint8_t* sub_line_p = sub_p; + uint32_t* out_line_p = out_p; + for (int x = 0; x < rect->w; ++x) { + *out_line_p++ = palette[*sub_line_p++]; + } + sub_p += rect->pict.linesize[0]; + out_p += image->stride()[0] / sizeof (uint32_t); + } + + libdcp::Size const vs = _ffmpeg_content->video_size (); + + subtitle ( + 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); } - diff --git a/src/lib/ffmpeg_decoder.h b/src/lib/ffmpeg_decoder.h index 2fb8675f9..11f83ed97 100644 --- a/src/lib/ffmpeg_decoder.h +++ b/src/lib/ffmpeg_decoder.h @@ -35,115 +35,55 @@ extern "C" { #include "decoder.h" #include "video_decoder.h" #include "audio_decoder.h" -#include "film.h" - -struct AVFilterGraph; -struct AVCodecContext; -struct AVFilterContext; -struct AVFormatContext; -struct AVFrame; -struct AVBufferContext; -struct AVCodec; -struct AVStream; -class Job; -class Options; -class Image; -class Log; - -class FFmpegAudioStream : public AudioStream -{ -public: - FFmpegAudioStream (std::string n, int i, int s, int64_t c) - : AudioStream (s, c) - , _name (n) - , _id (i) - {} - - std::string to_string () const; - - std::string name () const { - return _name; - } +#include "subtitle_decoder.h" +#include "ffmpeg.h" - int id () const { - return _id; - } - - static boost::shared_ptr<FFmpegAudioStream> create (std::string t, boost::optional<int> v); - -private: - friend class stream_test; - - FFmpegAudioStream (std::string t, boost::optional<int> v); - - std::string _name; - int _id; -}; +class Film; +class FilterGraph; +class ffmpeg_pts_offset_test; /** @class FFmpegDecoder * @brief A decoder using FFmpeg to decode content. */ -class FFmpegDecoder : public VideoDecoder, public AudioDecoder +class FFmpegDecoder : public VideoDecoder, public AudioDecoder, public SubtitleDecoder, public FFmpeg { public: - FFmpegDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); + FFmpegDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const FFmpegContent>, bool video, bool audio); ~FFmpegDecoder (); - float frames_per_second () const; - Size native_size () const; - SourceFrame length () const; - int time_base_numerator () const; - int time_base_denominator () const; - int sample_aspect_ratio_numerator () const; - int sample_aspect_ratio_denominator () const; - - void set_audio_stream (boost::shared_ptr<AudioStream>); - void set_subtitle_stream (boost::shared_ptr<SubtitleStream>); - - bool seek (double); - bool seek_to_last (); + void pass (); + void seek (VideoContent::Frame, bool); + bool done () const; private: + friend class ::ffmpeg_pts_offset_test; - bool pass (); - bool do_seek (double p, bool); - PixelFormat pixel_format () const; - AVSampleFormat audio_sample_format () const; - int bytes_per_audio_sample () const; + static double compute_pts_offset (double, double, float); - void out_with_sync (); - void filter_and_emit_video (AVFrame *); - double frame_time () const; + void flush (); - void setup_general (); - void setup_video (); - void setup_audio (); void setup_subtitle (); - void maybe_add_subtitle (); - boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t* data, int size); - - void film_changed (Film::Property); + AVSampleFormat audio_sample_format () const; + int bytes_per_audio_sample () const; - std::string stream_name (AVStream* s) const; + bool decode_video_packet (); + void decode_audio_packet (); + void decode_subtitle_packet (); - AVFormatContext* _format_context; - int _video_stream; - - AVFrame* _frame; + void maybe_add_subtitle (); + boost::shared_ptr<AudioBuffers> deinterleave_audio (uint8_t** data, int size); - AVCodecContext* _video_codec_context; - AVCodec* _video_codec; - AVCodecContext* _audio_codec_context; ///< may be 0 if there is no audio - AVCodec* _audio_codec; ///< may be 0 if there is no audio AVCodecContext* _subtitle_codec_context; ///< may be 0 if there is no subtitle - AVCodec* _subtitle_codec; ///< may be 0 if there is no subtitle - - AVPacket _packet; - - boost::optional<double> _first_video; - boost::optional<double> _first_audio; - + AVCodec* _subtitle_codec; ///< may be 0 if there is no subtitle + std::list<boost::shared_ptr<FilterGraph> > _filter_graphs; boost::mutex _filter_graphs_mutex; + + bool _decode_video; + bool _decode_audio; + + double _video_pts_offset; + double _audio_pts_offset; + bool _just_sought; }; diff --git a/src/lib/ffmpeg_examiner.cc b/src/lib/ffmpeg_examiner.cc new file mode 100644 index 000000000..ceeaee112 --- /dev/null +++ b/src/lib/ffmpeg_examiner.cc @@ -0,0 +1,166 @@ +/* + 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. + +*/ + +extern "C" { +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +} +#include "ffmpeg_examiner.h" +#include "ffmpeg_content.h" + +using std::string; +using std::cout; +using std::stringstream; +using boost::shared_ptr; +using boost::optional; + +FFmpegExaminer::FFmpegExaminer (shared_ptr<const FFmpegContent> c) + : FFmpeg (c) +{ + /* Find audio and subtitle streams */ + + for (uint32_t i = 0; i < _format_context->nb_streams; ++i) { + AVStream* s = _format_context->streams[i]; + if (s->codec->codec_type == AVMEDIA_TYPE_AUDIO) { + + /* This is a hack; sometimes it seems that _audio_codec_context->channel_layout isn't set up, + so bodge it here. No idea why we should have to do this. + */ + + if (s->codec->channel_layout == 0) { + s->codec->channel_layout = av_get_default_channel_layout (s->codec->channels); + } + + _audio_streams.push_back ( + shared_ptr<FFmpegAudioStream> ( + new FFmpegAudioStream (stream_name (s), i, s->codec->sample_rate, s->codec->channels) + ) + ); + + } else if (s->codec->codec_type == AVMEDIA_TYPE_SUBTITLE) { + _subtitle_streams.push_back (shared_ptr<FFmpegSubtitleStream> (new FFmpegSubtitleStream (stream_name (s), i))); + } + } + + /* Run through until we find the first audio (for each stream) and video */ + + while (1) { + int r = av_read_frame (_format_context, &_packet); + if (r < 0) { + break; + } + + int frame_finished; + avcodec_get_frame_defaults (_frame); + + 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 (_video_stream); + } + } else { + for (size_t i = 0; i < _audio_streams.size(); ++i) { + if (_packet.stream_index == _audio_streams[i]->id && !_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]->id); + } + } + } + } + + 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; + } + + av_free_packet (&_packet); + + if (_first_video && have_all_audio) { + break; + } + } +} + +optional<double> +FFmpegExaminer::frame_time (int stream) const +{ + optional<double> t; + + int64_t const bet = av_frame_get_best_effort_timestamp (_frame); + if (bet != AV_NOPTS_VALUE) { + t = bet * av_q2d (_format_context->streams[stream]->time_base); + } + + return t; +} + +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); +} + +libdcp::Size +FFmpegExaminer::video_size () const +{ + return libdcp::Size (video_codec_context()->width, video_codec_context()->height); +} + +/** @return Length (in video frames) according to our content's header */ +VideoContent::Frame +FFmpegExaminer::video_length () const +{ + return (double (_format_context->duration) / AV_TIME_BASE) * video_frame_rate(); +} + +string +FFmpegExaminer::stream_name (AVStream* s) const +{ + stringstream n; + + if (s->metadata) { + AVDictionaryEntry const * lang = av_dict_get (s->metadata, "language", 0, 0); + if (lang) { + n << lang->value; + } + + AVDictionaryEntry const * title = av_dict_get (s->metadata, "title", 0, 0); + if (title) { + if (!n.str().empty()) { + n << " "; + } + n << title->value; + } + } + + if (n.str().empty()) { + n << "unknown"; + } + + return n.str (); +} diff --git a/src/lib/ffmpeg_examiner.h b/src/lib/ffmpeg_examiner.h new file mode 100644 index 000000000..4912d899a --- /dev/null +++ b/src/lib/ffmpeg_examiner.h @@ -0,0 +1,55 @@ +/* + 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 <boost/optional.hpp> +#include "ffmpeg.h" +#include "video_examiner.h" + +class FFmpegAudioStream; +class FFmpegSubtitleStream; + +class FFmpegExaminer : public FFmpeg, public VideoExaminer +{ +public: + FFmpegExaminer (boost::shared_ptr<const FFmpegContent>); + + float video_frame_rate () const; + libdcp::Size video_size () const; + VideoContent::Frame video_length () const; + + std::vector<boost::shared_ptr<FFmpegSubtitleStream> > subtitle_streams () const { + return _subtitle_streams; + } + + std::vector<boost::shared_ptr<FFmpegAudioStream> > audio_streams () const { + return _audio_streams; + } + + boost::optional<double> first_video () const { + return _first_video; + } + +private: + std::string stream_name (AVStream* s) const; + boost::optional<double> frame_time (int) const; + + std::vector<boost::shared_ptr<FFmpegSubtitleStream> > _subtitle_streams; + std::vector<boost::shared_ptr<FFmpegAudioStream> > _audio_streams; + boost::optional<double> _first_video; +}; diff --git a/src/lib/film.cc b/src/lib/film.cc index fb3423bb4..e885fe5fd 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-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 @@ -30,33 +30,30 @@ #include <boost/lexical_cast.hpp> #include <boost/date_time.hpp> #include <libxml++/libxml++.h> +#include <libcxml/cxml.h> #include <libdcp/crypt_chain.h> -#include <libdcp/certificates.h> -#include "cinema.h" +#include <libdcp/cpl.h> #include "film.h" -#include "format.h" #include "job.h" -#include "filter.h" -#include "transcoder.h" #include "util.h" #include "job_manager.h" -#include "ab_transcode_job.h" #include "transcode_job.h" #include "scp_dcp_job.h" -#include "make_dcp_job.h" #include "log.h" -#include "options.h" #include "exceptions.h" #include "examine_content_job.h" #include "scaler.h" -#include "decoder_factory.h" #include "config.h" -#include "check_hashes_job.h" #include "version.h" #include "ui_signaller.h" -#include "video_decoder.h" -#include "audio_decoder.h" -#include "external_audio_decoder.h" +#include "playlist.h" +#include "player.h" +#include "dcp_content_type.h" +#include "ratio.h" +#include "cross.h" +#include "cinema.h" + +#include "i18n.h" using std::string; using std::stringstream; @@ -69,52 +66,54 @@ using std::ofstream; using std::setfill; using std::min; using std::make_pair; -using std::list; +using std::endl; using std::cout; +using std::list; using boost::shared_ptr; +using boost::weak_ptr; using boost::lexical_cast; +using boost::dynamic_pointer_cast; using boost::to_upper_copy; using boost::ends_with; using boost::starts_with; using boost::optional; +using libdcp::Size; -int const Film::state_version = 1; +int const Film::state_version = 4; -/** Construct a Film object in a given directory, reading any metadata - * file that exists in that directory. An exception will be thrown if - * must_exist is true and the specified directory does not exist. +/** Construct a Film object in a given directory. * - * @param d Film directory. - * @param must_exist true to throw an exception if does not exist. + * @param dir Film directory. */ -Film::Film (string d, bool must_exist) - : _use_dci_name (true) - , _trust_content_header (true) - , _dcp_content_type (0) - , _format (0) +Film::Film (boost::filesystem::path dir) + : _playlist (new Playlist) + , _use_dci_name (true) + , _dcp_content_type (Config::instance()->default_dcp_content_type ()) + , _container (Config::instance()->default_container ()) + , _resolution (RESOLUTION_2K) , _scaler (Scaler::from_id ("bicubic")) - , _dcp_trim_start (0) - , _dcp_trim_end (0) - , _dcp_ab (false) - , _use_content_audio (true) - , _audio_gain (0) - , _audio_delay (0) - , _still_duration (10) , _with_subtitles (false) - , _subtitle_offset (0) - , _subtitle_scale (1) , _encrypted (false) - , _colour_lut (0) - , _j2k_bandwidth (200000000) - , _frames_per_second (0) + , _j2k_bandwidth (Config::instance()->default_j2k_bandwidth ()) + , _dci_metadata (Config::instance()->default_dci_metadata ()) + , _video_frame_rate (24) + , _audio_channels (MAX_AUDIO_CHANNELS) + , _three_d (false) + , _sequence_video (true) + , _interop (false) , _dirty (false) { + set_dci_date_today (); + + _playlist->Changed.connect (bind (&Film::playlist_changed, this)); + _playlist->ContentChanged.connect (bind (&Film::playlist_content_changed, this, _1, _2)); + /* Make state.directory a complete path without ..s (where possible) (Code swiped from Adam Bowen on stackoverflow) */ - boost::filesystem::path p (boost::filesystem::system_complete (d)); + boost::filesystem::path p (boost::filesystem::system_complete (dir)); boost::filesystem::path result; for (boost::filesystem::path::iterator i = p.begin(); i != p.end(); ++i) { if (*i == "..") { @@ -129,239 +128,161 @@ Film::Film (string d, bool must_exist) } set_directory (result.string ()); - - if (!boost::filesystem::exists (directory())) { - if (must_exist) { - throw OpenFileError (directory()); - } else { - boost::filesystem::create_directory (directory()); - } - } + _log.reset (new FileLog (file ("log"))); - _external_audio_stream = ExternalAudioStream::create (); - - if (must_exist) { - read_metadata (); - } - - _log = new FileLog (file ("log")); - set_dci_date_today (); + _playlist->set_sequence_video (_sequence_video); } -Film::Film (Film const & o) - : boost::enable_shared_from_this<Film> (o) - , _log (0) - , _directory (o._directory) - , _name (o._name) - , _use_dci_name (o._use_dci_name) - , _content (o._content) - , _trust_content_header (o._trust_content_header) - , _dcp_content_type (o._dcp_content_type) - , _format (o._format) - , _crop (o._crop) - , _filters (o._filters) - , _scaler (o._scaler) - , _dcp_trim_start (o._dcp_trim_start) - , _dcp_trim_end (o._dcp_trim_end) - , _reel_size (o._reel_size) - , _dcp_ab (o._dcp_ab) - , _content_audio_stream (o._content_audio_stream) - , _external_audio (o._external_audio) - , _use_content_audio (o._use_content_audio) - , _audio_gain (o._audio_gain) - , _audio_delay (o._audio_delay) - , _still_duration (o._still_duration) - , _subtitle_stream (o._subtitle_stream) - , _with_subtitles (o._with_subtitles) - , _subtitle_offset (o._subtitle_offset) - , _subtitle_scale (o._subtitle_scale) - , _encrypted (o._encrypted) - , _colour_lut (o._colour_lut) - , _j2k_bandwidth (o._j2k_bandwidth) - , _audio_language (o._audio_language) - , _subtitle_language (o._subtitle_language) - , _territory (o._territory) - , _rating (o._rating) - , _studio (o._studio) - , _facility (o._facility) - , _package_type (o._package_type) - , _size (o._size) - , _length (o._length) - , _content_digest (o._content_digest) - , _content_audio_streams (o._content_audio_streams) - , _external_audio_stream (o._external_audio_stream) - , _subtitle_streams (o._subtitle_streams) - , _frames_per_second (o._frames_per_second) - , _dirty (o._dirty) +string +Film::video_identifier () const { + assert (container ()); + LocaleGuard lg; -} + stringstream s; + s << container()->id() + << "_" << resolution_to_string (_resolution) + << "_" << _playlist->video_identifier() + << "_" << _video_frame_rate + << "_" << scaler()->id() + << "_" << j2k_bandwidth(); -Film::~Film () -{ - delete _log; + if (_interop) { + s << "_I"; + } else { + s << "_S"; + } + + if (_three_d) { + s << "_3D"; + } + + return s.str (); } -/** @return The path to the directory to write JPEG2000 files to */ +/** @return The path to the directory to write video frame info files to */ string -Film::j2k_dir () const +Film::info_dir () const { - assert (format()); - boost::filesystem::path p; + p /= "info"; + p /= video_identifier (); + return dir (p.string()); +} - /* Start with j2c */ - p /= "j2c"; +string +Film::internal_video_mxf_dir () const +{ + return dir ("video"); +} - pair<string, string> f = Filter::ffmpeg_strings (filters()); +string +Film::internal_video_mxf_filename () const +{ + return video_identifier() + ".mxf"; +} - /* Write stuff to specify the filter / post-processing settings that are in use, - so that we don't get confused about J2K files generated using different - settings. - */ - stringstream s; - s << format()->id() - << "_" << content_digest() - << "_" << crop().left << "_" << crop().right << "_" << crop().top << "_" << crop().bottom - << "_" << f.first << "_" << f.second - << "_" << scaler()->id() - << "_" << j2k_bandwidth() - << "_" << boost::lexical_cast<int> (colour_lut()); +string +Film::video_mxf_filename () const +{ + return filename_safe_name() + "_video.mxf"; +} - p /= s.str (); +string +Film::audio_mxf_filename () const +{ + return filename_safe_name() + "_audio.mxf"; +} - /* Similarly for the A/B case */ - if (dcp_ab()) { - stringstream s; - pair<string, string> fa = Filter::ffmpeg_strings (Config::instance()->reference_filters()); - s << "ab_" << Config::instance()->reference_scaler()->id() << "_" << fa.first << "_" << fa.second; - p /= s.str (); +string +Film::filename_safe_name () const +{ + string const n = name (); + string o; + for (size_t i = 0; i < n.length(); ++i) { + if (isalnum (n[i])) { + o += n[i]; + } else { + o += "_"; + } } - - return dir (p.string()); + + return o; } -/** Add suitable Jobs to the JobManager to create a DCP for this Film. - * @param true to transcode, false to use the WAV and J2K files that are already there. - */ +boost::filesystem::path +Film::audio_analysis_path (shared_ptr<const AudioContent> c) const +{ + boost::filesystem::path p = dir ("analysis"); + p /= c->digest(); + return p; +} + +/** Add suitable Jobs to the JobManager to create a DCP for this Film */ void -Film::make_dcp (bool transcode) +Film::make_dcp () { set_dci_date_today (); if (dcp_name().find ("/") != string::npos) { - throw BadSettingError ("name", "cannot contain slashes"); + throw BadSettingError (_("name"), _("cannot contain slashes")); } - log()->log (String::compose ("DVD-o-matic %1 git %2 using %3", dvdomatic_version, dvdomatic_git_commit, dependency_version_summary())); + log()->log (String::compose ("DCP-o-matic %1 git %2 using %3", dcpomatic_version, dcpomatic_git_commit, dependency_version_summary())); { char buffer[128]; gethostname (buffer, sizeof (buffer)); log()->log (String::compose ("Starting to make DCP on %1", buffer)); } - - log()->log (String::compose ("Content is %1; type %2", content_path(), (content_type() == STILL ? "still" : "video"))); - log()->log (String::compose ("Content length %1", length().get())); - log()->log (String::compose ("Content digest %1", content_digest())); + + ContentList cl = content (); + for (ContentList::const_iterator i = cl.begin(); i != cl.end(); ++i) { + log()->log (String::compose ("Content: %1", (*i)->technical_summary())); + } + log()->log (String::compose ("DCP video rate %1 fps", video_frame_rate())); log()->log (String::compose ("%1 threads", Config::instance()->num_local_encoding_threads())); log()->log (String::compose ("J2K bandwidth %1", j2k_bandwidth())); -#ifdef DVDOMATIC_DEBUG - log()->log ("DVD-o-matic built in debug mode."); +#ifdef DCPOMATIC_DEBUG + log()->log ("DCP-o-matic built in debug mode."); #else - log()->log ("DVD-o-matic built in optimised mode."); + log()->log ("DCP-o-matic built in optimised mode."); #endif #ifdef LIBDCP_DEBUG log()->log ("libdcp built in debug mode."); #else log()->log ("libdcp built in optimised mode."); #endif - pair<string, int> const c = cpu_info (); - log()->log (String::compose ("CPU: %1, %2 processors", c.first, c.second)); + log()->log (String::compose ("CPU: %1, %2 processors", cpu_info(), boost::thread::hardware_concurrency ())); + list<pair<string, string> > const m = mount_info (); + for (list<pair<string, string> >::const_iterator i = m.begin(); i != m.end(); ++i) { + log()->log (String::compose ("Mount: %1 %2", i->first, i->second)); + } - if (format() == 0) { - throw MissingSettingError ("format"); + if (container() == 0) { + throw MissingSettingError (_("container")); } - if (content().empty ()) { - throw MissingSettingError ("content"); + if (content().empty()) { + throw StringError (_("You must add some content to the DCP before creating it")); } if (dcp_content_type() == 0) { - throw MissingSettingError ("content type"); + throw MissingSettingError (_("content type")); } if (name().empty()) { - throw MissingSettingError ("name"); - } - - shared_ptr<EncodeOptions> oe (new EncodeOptions (j2k_dir(), ".j2c", dir ("wavs"))); - oe->out_size = format()->dcp_size (); - oe->padding = format()->dcp_padding (shared_from_this ()); - if (dcp_length ()) { - oe->video_range = make_pair (dcp_trim_start(), dcp_trim_start() + dcp_length().get()); - if (audio_stream()) { - oe->audio_range = make_pair ( - - video_frames_to_audio_frames ( - oe->video_range.get().first, - dcp_audio_sample_rate (audio_stream()->sample_rate()), - dcp_frame_rate (frames_per_second()).frames_per_second - ), - - video_frames_to_audio_frames ( - oe->video_range.get().second, - dcp_audio_sample_rate (audio_stream()->sample_rate()), - dcp_frame_rate (frames_per_second()).frames_per_second - ) - ); - } - - } - - oe->video_skip = dcp_frame_rate (frames_per_second()).skip; - - shared_ptr<DecodeOptions> od (new DecodeOptions); - od->decode_subtitles = with_subtitles (); - - shared_ptr<Job> r; - - if (transcode) { - if (dcp_ab()) { - r = JobManager::instance()->add (shared_ptr<Job> (new ABTranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ()))); - } else { - r = JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this(), od, oe, shared_ptr<Job> ()))); - } + throw MissingSettingError (_("name")); } - r = JobManager::instance()->add (shared_ptr<Job> (new CheckHashesJob (shared_from_this(), od, oe, r))); - JobManager::instance()->add (shared_ptr<Job> (new MakeDCPJob (shared_from_this(), oe, r))); -} - -/** Start a job to examine our content file */ -void -Film::examine_content () -{ - if (_examine_content_job) { - return; - } - - _examine_content_job.reset (new ExamineContentJob (shared_from_this(), shared_ptr<Job> ())); - _examine_content_job->Finished.connect (bind (&Film::examine_content_finished, this)); - JobManager::instance()->add (_examine_content_job); -} - -void -Film::examine_content_finished () -{ - _examine_content_job.reset (); + JobManager::instance()->add (shared_ptr<Job> (new TranscodeJob (shared_from_this()))); } /** Start a job to send our DCP to the configured TMS */ void Film::send_dcp_to_tms () { - shared_ptr<Job> j (new SCPDCPJob (shared_from_this(), shared_ptr<Job> ())); + shared_ptr<Job> j (new SCPDCPJob (shared_from_this())); JobManager::instance()->add (j); } @@ -371,12 +292,12 @@ Film::send_dcp_to_tms () int Film::encoded_frames () const { - if (format() == 0) { + if (container() == 0) { return 0; } int N = 0; - for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (j2k_dir ()); i != boost::filesystem::directory_iterator(); ++i) { + for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (info_dir ()); i != boost::filesystem::directory_iterator(); ++i) { ++N; boost::this_thread::interruption_point (); } @@ -388,86 +309,44 @@ Film::encoded_frames () const void Film::write_metadata () const { - boost::mutex::scoped_lock lm (_state_mutex); + if (!boost::filesystem::exists (directory())) { + boost::filesystem::create_directory (directory()); + } + + LocaleGuard lg; boost::filesystem::create_directories (directory()); - string const m = file ("metadata"); - ofstream f (m.c_str ()); - if (!f.good ()) { - throw CreateFileError (m); - } + xmlpp::Document doc; + xmlpp::Element* root = doc.create_root_node ("Metadata"); - f << "version " << state_version << "\n"; + root->add_child("Version")->add_child_text (lexical_cast<string> (state_version)); + root->add_child("Name")->add_child_text (_name); + root->add_child("UseDCIName")->add_child_text (_use_dci_name ? "1" : "0"); - /* User stuff */ - f << "name " << _name << "\n"; - f << "use_dci_name " << _use_dci_name << "\n"; - f << "content " << _content << "\n"; - f << "trust_content_header " << (_trust_content_header ? "1" : "0") << "\n"; if (_dcp_content_type) { - f << "dcp_content_type " << _dcp_content_type->pretty_name () << "\n"; - } - if (_format) { - f << "format " << _format->as_metadata () << "\n"; - } - f << "left_crop " << _crop.left << "\n"; - f << "right_crop " << _crop.right << "\n"; - f << "top_crop " << _crop.top << "\n"; - f << "bottom_crop " << _crop.bottom << "\n"; - for (vector<Filter const *>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) { - f << "filter " << (*i)->id () << "\n"; - } - f << "scaler " << _scaler->id () << "\n"; - f << "dcp_trim_start " << _dcp_trim_start << "\n"; - f << "dcp_trim_end " << _dcp_trim_end << "\n"; - if (_reel_size) { - f << "reel_size " << _reel_size.get() << "\n"; - } - f << "dcp_ab " << (_dcp_ab ? "1" : "0") << "\n"; - if (_content_audio_stream) { - f << "selected_content_audio_stream " << _content_audio_stream->to_string() << "\n"; - } - for (vector<string>::const_iterator i = _external_audio.begin(); i != _external_audio.end(); ++i) { - f << "external_audio " << *i << "\n"; - } - f << "use_content_audio " << (_use_content_audio ? "1" : "0") << "\n"; - f << "audio_gain " << _audio_gain << "\n"; - f << "audio_delay " << _audio_delay << "\n"; - f << "still_duration " << _still_duration << "\n"; - if (_subtitle_stream) { - f << "selected_subtitle_stream " << _subtitle_stream->to_string() << "\n"; + root->add_child("DCPContentType")->add_child_text (_dcp_content_type->dci_name ()); } - f << "with_subtitles " << _with_subtitles << "\n"; - f << "subtitle_offset " << _subtitle_offset << "\n"; - f << "subtitle_scale " << _subtitle_scale << "\n"; - f << "encrypted " << _encrypted << "\n"; - f << "colour_lut " << _colour_lut << "\n"; - f << "j2k_bandwidth " << _j2k_bandwidth << "\n"; - f << "audio_language " << _audio_language << "\n"; - f << "subtitle_language " << _subtitle_language << "\n"; - f << "territory " << _territory << "\n"; - f << "rating " << _rating << "\n"; - f << "studio " << _studio << "\n"; - f << "facility " << _facility << "\n"; - f << "package_type " << _package_type << "\n"; - - f << "width " << _size.width << "\n"; - f << "height " << _size.height << "\n"; - f << "length " << _length.get_value_or(0) << "\n"; - f << "content_digest " << _content_digest << "\n"; - - for (vector<shared_ptr<AudioStream> >::const_iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) { - f << "content_audio_stream " << (*i)->to_string () << "\n"; - } - - f << "external_audio_stream " << _external_audio_stream->to_string() << "\n"; - for (vector<shared_ptr<SubtitleStream> >::const_iterator i = _subtitle_streams.begin(); i != _subtitle_streams.end(); ++i) { - f << "subtitle_stream " << (*i)->to_string () << "\n"; + if (_container) { + root->add_child("Container")->add_child_text (_container->id ()); } - f << "frames_per_second " << _frames_per_second << "\n"; + 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 (lexical_cast<string> (_j2k_bandwidth)); + _dci_metadata.as_xml (root->add_child ("DCIMetadata")); + root->add_child("VideoFrameRate")->add_child_text (lexical_cast<string> (_video_frame_rate)); + root->add_child("DCIDate")->add_child_text (boost::gregorian::to_iso_string (_dci_date)); + root->add_child("AudioChannels")->add_child_text (lexical_cast<string> (_audio_channels)); + 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("Encrypted")->add_child_text (_encrypted ? "1" : "0"); + _playlist->as_xml (root->add_child ("Playlist")); + + doc.write_to_file_formatted (file ("metadata.xml")); _dirty = false; } @@ -476,273 +355,82 @@ Film::write_metadata () const void Film::read_metadata () { - boost::mutex::scoped_lock lm (_state_mutex); - - _external_audio.clear (); - _content_audio_streams.clear (); - _subtitle_streams.clear (); - - boost::optional<int> version; - - /* Backward compatibility things */ - boost::optional<int> audio_sample_rate; - boost::optional<int> audio_stream_index; - boost::optional<int> subtitle_stream_index; + LocaleGuard lg; - ifstream f (file ("metadata").c_str()); - if (!f.good()) { - throw OpenFileError (file("metadata")); + if (boost::filesystem::exists (file ("metadata")) && !boost::filesystem::exists (file ("metadata.xml"))) { + throw StringError (_("This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version. You will need to create a new Film, re-add your content and set it up again. Sorry!")); } - - multimap<string, string> kv = read_key_value (f); - /* We need version before anything else */ - multimap<string, string>::iterator v = kv.find ("version"); - if (v != kv.end ()) { - version = atoi (v->second.c_str()); - } + cxml::Document f ("Metadata"); + f.read_file (file ("metadata.xml")); - for (multimap<string, string>::const_iterator i = kv.begin(); i != kv.end(); ++i) { - string const k = i->first; - string const v = i->second; + _name = f.string_child ("Name"); + _use_dci_name = f.bool_child ("UseDCIName"); - if (k == "audio_sample_rate") { - audio_sample_rate = atoi (v.c_str()); + { + optional<string> c = f.optional_string_child ("DCPContentType"); + if (c) { + _dcp_content_type = DCPContentType::from_dci_name (c.get ()); } + } - /* User-specified stuff */ - if (k == "name") { - _name = v; - } else if (k == "use_dci_name") { - _use_dci_name = (v == "1"); - } else if (k == "content") { - _content = v; - } else if (k == "trust_content_header") { - _trust_content_header = (v == "1"); - } else if (k == "dcp_content_type") { - _dcp_content_type = DCPContentType::from_pretty_name (v); - } else if (k == "format") { - _format = Format::from_metadata (v); - } else if (k == "left_crop") { - _crop.left = atoi (v.c_str ()); - } else if (k == "right_crop") { - _crop.right = atoi (v.c_str ()); - } else if (k == "top_crop") { - _crop.top = atoi (v.c_str ()); - } else if (k == "bottom_crop") { - _crop.bottom = atoi (v.c_str ()); - } else if (k == "filter") { - _filters.push_back (Filter::from_id (v)); - } else if (k == "scaler") { - _scaler = Scaler::from_id (v); - } else if (k == "dcp_trim_start") { - _dcp_trim_start = atoi (v.c_str ()); - } else if (k == "dcp_trim_end") { - _dcp_trim_end = atoi (v.c_str ()); - } else if (k == "reel_size") { - _reel_size = boost::lexical_cast<uint64_t> (v); - } else if (k == "dcp_ab") { - _dcp_ab = (v == "1"); - } else if (k == "selected_content_audio_stream" || (!version && k == "selected_audio_stream")) { - if (!version) { - audio_stream_index = atoi (v.c_str ()); - } else { - _content_audio_stream = audio_stream_factory (v, version); - } - } else if (k == "external_audio") { - _external_audio.push_back (v); - } else if (k == "use_content_audio") { - _use_content_audio = (v == "1"); - } else if (k == "audio_gain") { - _audio_gain = atof (v.c_str ()); - } else if (k == "audio_delay") { - _audio_delay = atoi (v.c_str ()); - } else if (k == "still_duration") { - _still_duration = atoi (v.c_str ()); - } else if (k == "selected_subtitle_stream") { - if (!version) { - subtitle_stream_index = atoi (v.c_str ()); - } else { - _subtitle_stream = subtitle_stream_factory (v, version); - } - } else if (k == "with_subtitles") { - _with_subtitles = (v == "1"); - } else if (k == "subtitle_offset") { - _subtitle_offset = atoi (v.c_str ()); - } else if (k == "subtitle_scale") { - _subtitle_scale = atof (v.c_str ()); - } else if (k == "encrypted") { - _encrypted = (v == "1"); - } else if (k == "colour_lut") { - _colour_lut = atoi (v.c_str ()); - } else if (k == "j2k_bandwidth") { - _j2k_bandwidth = atoi (v.c_str ()); - } else if (k == "audio_language") { - _audio_language = v; - } else if (k == "subtitle_language") { - _subtitle_language = v; - } else if (k == "territory") { - _territory = v; - } else if (k == "rating") { - _rating = v; - } else if (k == "studio") { - _studio = v; - } else if (k == "facility") { - _facility = v; - } else if (k == "package_type") { - _package_type = v; - } - - /* Cached stuff */ - if (k == "width") { - _size.width = atoi (v.c_str ()); - } else if (k == "height") { - _size.height = atoi (v.c_str ()); - } else if (k == "length") { - int const vv = atoi (v.c_str ()); - if (vv) { - _length = vv; - } - } else if (k == "content_digest") { - _content_digest = v; - } else if (k == "content_audio_stream" || (!version && k == "audio_stream")) { - _content_audio_streams.push_back (audio_stream_factory (v, version)); - } else if (k == "external_audio_stream") { - _external_audio_stream = audio_stream_factory (v, version); - } else if (k == "subtitle_stream") { - _subtitle_streams.push_back (subtitle_stream_factory (v, version)); - } else if (k == "frames_per_second") { - _frames_per_second = atof (v.c_str ()); + { + optional<string> c = f.optional_string_child ("Container"); + if (c) { + _container = Ratio::from_id (c.get ()); } } - if (!version) { - if (audio_sample_rate) { - /* version < 1 didn't specify sample rate in the audio streams, so fill it in here */ - for (vector<shared_ptr<AudioStream> >::iterator i = _content_audio_streams.begin(); i != _content_audio_streams.end(); ++i) { - (*i)->set_sample_rate (audio_sample_rate.get()); - } - } + _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"); + _dci_metadata = DCIMetadata (f.node_child ("DCIMetadata")); + _video_frame_rate = f.number_child<int> ("VideoFrameRate"); + _dci_date = boost::gregorian::from_undelimited_string (f.string_child ("DCIDate")); + _audio_channels = f.number_child<int> ("AudioChannels"); + _sequence_video = f.bool_child ("SequenceVideo"); + _three_d = f.bool_child ("ThreeD"); + _interop = f.bool_child ("Interop"); - /* also the selected stream was specified as an index */ - if (audio_stream_index && audio_stream_index.get() >= 0 && audio_stream_index.get() < (int) _content_audio_streams.size()) { - _content_audio_stream = _content_audio_streams[audio_stream_index.get()]; - } + _playlist->set_from_xml (shared_from_this(), f.node_child ("Playlist")); - /* similarly the subtitle */ - if (subtitle_stream_index && subtitle_stream_index.get() >= 0 && subtitle_stream_index.get() < (int) _subtitle_streams.size()) { - _subtitle_stream = _subtitle_streams[subtitle_stream_index.get()]; - } - } - _dirty = false; } -Size -Film::cropped_size (Size s) const -{ - boost::mutex::scoped_lock lm (_state_mutex); - s.width -= _crop.left + _crop.right; - s.height -= _crop.top + _crop.bottom; - return s; -} - /** Given a directory name, return its full path within the Film's directory. * The directory (and its parents) will be created if they do not exist. */ string Film::dir (string d) const { - boost::mutex::scoped_lock lm (_directory_mutex); boost::filesystem::path p; p /= _directory; p /= d; + boost::filesystem::create_directories (p); + return p.string (); } /** Given a file or directory name, return its full path within the Film's directory. - * _directory_mutex must not be locked on entry. + * Any required parent directories will be created. */ string Film::file (string f) const { - boost::mutex::scoped_lock lm (_directory_mutex); boost::filesystem::path p; p /= _directory; p /= f; - return p.string (); -} - -/** @return full path of the content (actual video) file - * of the Film. - */ -string -Film::content_path () const -{ - boost::mutex::scoped_lock lm (_state_mutex); - if (boost::filesystem::path(_content).has_root_directory ()) { - return _content; - } - - return file (_content); -} -ContentType -Film::content_type () const -{ - if (boost::filesystem::is_directory (_content)) { - /* Directory of images, we assume */ - return VIDEO; - } - - if (still_image_file (_content)) { - return STILL; - } - - return VIDEO; -} - -/** @return The sampling rate that we will resample the audio to */ -int -Film::target_audio_sample_rate () const -{ - if (!audio_stream()) { - return 0; - } + boost::filesystem::create_directories (p.parent_path ()); - /* Resample to a DCI-approved sample rate */ - double t = dcp_audio_sample_rate (audio_stream()->sample_rate()); - - DCPFrameRate dfr = dcp_frame_rate (frames_per_second ()); - - /* Compensate for the fact that video will be rounded to the - nearest integer number of frames per second. - */ - if (dfr.run_fast) { - t *= _frames_per_second * dfr.skip / dfr.frames_per_second; - } - - return rint (t); -} - -boost::optional<int> -Film::dcp_length () const -{ - if (content_type() == STILL) { - return _still_duration * frames_per_second(); - } - - if (!length()) { - return boost::optional<int> (); - } - - return length().get() - dcp_trim_start() - dcp_trim_end(); + return p.string (); } /** @return a DCI-compliant name for a DCP of this film */ string -Film::dci_name () const +Film::dci_name (bool if_created_now) const { stringstream d; @@ -758,64 +446,82 @@ Film::dci_name () const fixed_name = fixed_name.substr (0, 14); } - d << fixed_name << "_"; + d << fixed_name; if (dcp_content_type()) { - d << dcp_content_type()->dci_name() << "_"; + d << "_" << dcp_content_type()->dci_name(); + d << "-" << dci_metadata().content_version; + } + + if (three_d ()) { + d << "-3D"; + } + + if (video_frame_rate() != 24) { + d << "-" << video_frame_rate(); } - if (format()) { - d << format()->dci_name() << "_"; + if (container()) { + d << "_" << container()->dci_name(); } - if (!audio_language().empty ()) { - d << audio_language(); - if (!subtitle_language().empty() && with_subtitles()) { - d << "-" << subtitle_language(); + DCIMetadata const dm = dci_metadata (); + + if (!dm.audio_language.empty ()) { + d << "_" << dm.audio_language; + if (!dm.subtitle_language.empty() && with_subtitles()) { + d << "-" << dm.subtitle_language; } else { d << "-XX"; } - - d << "_"; } - if (!territory().empty ()) { - d << territory(); - if (!rating().empty ()) { - d << "-" << rating(); + if (!dm.territory.empty ()) { + d << "_" << dm.territory; + if (!dm.rating.empty ()) { + d << "-" << dm.rating; } - d << "_"; } - switch (audio_channels()) { + switch (audio_channels ()) { case 1: - d << "10_"; + d << "_10"; break; case 2: - d << "20_"; + d << "_20"; break; - case 6: - d << "51_"; + case 3: + d << "_30"; + break; + case 4: + d << "_40"; break; - case 8: - d << "71_"; + case 5: + d << "_50"; + break; + case 6: + d << "_51"; break; } - d << "2K_"; + d << "_" << resolution_to_string (_resolution); - if (!studio().empty ()) { - d << studio() << "_"; + if (!dm.studio.empty ()) { + d << "_" << dm.studio; } - d << boost::gregorian::to_iso_string (_dci_date) << "_"; + if (if_created_now) { + d << "_" << boost::gregorian::to_iso_string (boost::gregorian::day_clock::local_day ()); + } else { + d << "_" << boost::gregorian::to_iso_string (_dci_date); + } - if (!facility().empty ()) { - d << facility() << "_"; + if (!dm.facility.empty ()) { + d << "_" << dm.facility; } - if (!package_type().empty ()) { - d << package_type(); + if (!dm.package_type.empty ()) { + d << "_" << dm.package_type; } return d.str (); @@ -823,10 +529,10 @@ Film::dci_name () const /** @return name to give the DCP */ string -Film::dcp_name () const +Film::dcp_name (bool if_created_now) const { if (use_dci_name()) { - return dci_name (); + return dci_name (if_created_now); } return name(); @@ -836,7 +542,6 @@ Film::dcp_name () const void Film::set_directory (string d) { - boost::mutex::scoped_lock lm (_state_mutex); _directory = d; _dirty = true; } @@ -844,612 +549,344 @@ Film::set_directory (string d) void Film::set_name (string n) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _name = n; - } + _name = n; signal_changed (NAME); } void Film::set_use_dci_name (bool u) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _use_dci_name = u; - } + _use_dci_name = u; signal_changed (USE_DCI_NAME); } void -Film::set_content (string c) -{ - string check = directory (); - -#if BOOST_FILESYSTEM_VERSION == 3 - boost::filesystem::path slash ("/"); - string platform_slash = slash.make_preferred().string (); -#else -#ifdef DVDOMATIC_WINDOWS - string platform_slash = "\\"; -#else - string platform_slash = "/"; -#endif -#endif - - if (!ends_with (check, platform_slash)) { - check += platform_slash; - } - - if (boost::filesystem::path(c).has_root_directory () && starts_with (c, check)) { - c = c.substr (_directory.length() + 1); - } - - string old_content; - - { - boost::mutex::scoped_lock lm (_state_mutex); - if (c == _content) { - return; - } - - old_content = _content; - _content = c; - } - - /* Reset streams here in case the new content doesn't have one or the other */ - _content_audio_stream = shared_ptr<AudioStream> (); - _subtitle_stream = shared_ptr<SubtitleStream> (); - - /* Start off using content audio */ - set_use_content_audio (true); - - /* Create a temporary decoder so that we can get information - about the content. - */ - - try { - shared_ptr<DecodeOptions> o (new DecodeOptions); - Decoders d = decoder_factory (shared_from_this(), o, 0); - - set_size (d.video->native_size ()); - set_frames_per_second (d.video->frames_per_second ()); - set_subtitle_streams (d.video->subtitle_streams ()); - if (d.audio) { - set_content_audio_streams (d.audio->audio_streams ()); - } - - /* Start off with the first audio and subtitle streams */ - if (d.audio && !d.audio->audio_streams().empty()) { - set_content_audio_stream (d.audio->audio_streams().front()); - } - - if (!d.video->subtitle_streams().empty()) { - set_subtitle_stream (d.video->subtitle_streams().front()); - } - - { - boost::mutex::scoped_lock lm (_state_mutex); - _content = c; - } - - signal_changed (CONTENT); - - examine_content (); - - } catch (...) { - - boost::mutex::scoped_lock lm (_state_mutex); - _content = old_content; - throw; - - } - - /* Default format */ - switch (content_type()) { - case STILL: - set_format (Format::from_id ("var-185")); - break; - case VIDEO: - set_format (Format::from_id ("185")); - break; - } - - /* Still image DCPs must use external audio */ - if (content_type() == STILL) { - set_use_content_audio (false); - } -} - -void -Film::set_trust_content_header (bool t) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _trust_content_header = t; - } - - signal_changed (TRUST_CONTENT_HEADER); - - if (!_trust_content_header && !content().empty()) { - /* We just said that we don't trust the content's header */ - examine_content (); - } -} - -void Film::set_dcp_content_type (DCPContentType const * t) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _dcp_content_type = t; - } + _dcp_content_type = t; signal_changed (DCP_CONTENT_TYPE); } void -Film::set_format (Format const * f) +Film::set_container (Ratio const * c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _format = f; - } - signal_changed (FORMAT); + _container = c; + signal_changed (CONTAINER); } void -Film::set_crop (Crop c) +Film::set_resolution (Resolution r) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _crop = c; - } - signal_changed (CROP); + _resolution = r; + signal_changed (RESOLUTION); } void -Film::set_left_crop (int c) +Film::set_scaler (Scaler const * s) { - { - boost::mutex::scoped_lock lm (_state_mutex); - - if (_crop.left == c) { - return; - } - - _crop.left = c; - } - signal_changed (CROP); + _scaler = s; + signal_changed (SCALER); } void -Film::set_right_crop (int c) +Film::set_with_subtitles (bool w) { - { - boost::mutex::scoped_lock lm (_state_mutex); - if (_crop.right == c) { - return; - } - - _crop.right = c; - } - signal_changed (CROP); + _with_subtitles = w; + signal_changed (WITH_SUBTITLES); } void -Film::set_top_crop (int c) +Film::set_j2k_bandwidth (int b) { - { - boost::mutex::scoped_lock lm (_state_mutex); - if (_crop.top == c) { - return; - } - - _crop.top = c; - } - signal_changed (CROP); + _j2k_bandwidth = b; + signal_changed (J2K_BANDWIDTH); } void -Film::set_bottom_crop (int c) +Film::set_dci_metadata (DCIMetadata m) { - { - boost::mutex::scoped_lock lm (_state_mutex); - if (_crop.bottom == c) { - return; - } - - _crop.bottom = c; - } - signal_changed (CROP); + _dci_metadata = m; + signal_changed (DCI_METADATA); } void -Film::set_filters (vector<Filter const *> f) +Film::set_video_frame_rate (int f) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _filters = f; - } - signal_changed (FILTERS); + _video_frame_rate = f; + signal_changed (VIDEO_FRAME_RATE); } void -Film::set_scaler (Scaler const * s) +Film::set_audio_channels (int c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _scaler = s; - } - signal_changed (SCALER); + _audio_channels = c; + signal_changed (AUDIO_CHANNELS); } void -Film::set_dcp_trim_start (int t) +Film::set_three_d (bool t) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _dcp_trim_start = t; - } - signal_changed (DCP_TRIM_START); + _three_d = t; + signal_changed (THREE_D); } void -Film::set_dcp_trim_end (int t) +Film::set_interop (bool i) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _dcp_trim_end = t; - } - signal_changed (DCP_TRIM_END); + _interop = i; + signal_changed (INTEROP); } void -Film::set_reel_size (uint64_t s) +Film::signal_changed (Property p) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _reel_size = s; - } - signal_changed (REEL_SIZE); -} + _dirty = true; -void -Film::unset_reel_size () -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _reel_size = boost::optional<uint64_t> (); + switch (p) { + case Film::CONTENT: + set_video_frame_rate (_playlist->best_dcp_frame_rate ()); + break; + case Film::VIDEO_FRAME_RATE: + case Film::SEQUENCE_VIDEO: + _playlist->maybe_sequence_video (); + break; + default: + break; } - signal_changed (REEL_SIZE); -} -void -Film::set_dcp_ab (bool a) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _dcp_ab = a; + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (Changed), p)); } - signal_changed (DCP_AB); } void -Film::set_content_audio_stream (shared_ptr<AudioStream> s) +Film::set_dci_date_today () { - { - boost::mutex::scoped_lock lm (_state_mutex); - _content_audio_stream = s; - } - signal_changed (CONTENT_AUDIO_STREAM); + _dci_date = boost::gregorian::day_clock::local_day (); } -void -Film::set_external_audio (vector<string> a) +string +Film::info_path (int f, Eyes e) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _external_audio = a; - } + boost::filesystem::path p; + p /= info_dir (); - shared_ptr<DecodeOptions> o (new DecodeOptions); - shared_ptr<ExternalAudioDecoder> decoder (new ExternalAudioDecoder (shared_from_this(), o, 0)); - if (decoder->audio_stream()) { - _external_audio_stream = decoder->audio_stream (); + stringstream s; + s.width (8); + s << setfill('0') << f; + + if (e == EYES_LEFT) { + s << ".L"; + } else if (e == EYES_RIGHT) { + s << ".R"; } + + s << ".md5"; - signal_changed (EXTERNAL_AUDIO); + p /= s.str(); + + /* info_dir() will already have added any initial bit of the path, + so don't call file() on this. + */ + return p.string (); } -void -Film::set_use_content_audio (bool e) +string +Film::j2c_path (int f, Eyes e, bool t) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _use_content_audio = e; - } + boost::filesystem::path p; + p /= "j2c"; + p /= video_identifier (); - signal_changed (USE_CONTENT_AUDIO); -} + stringstream s; + s.width (8); + s << setfill('0') << f; -void -Film::set_audio_gain (float g) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _audio_gain = g; + if (e == EYES_LEFT) { + s << ".L"; + } else if (e == EYES_RIGHT) { + s << ".R"; } - signal_changed (AUDIO_GAIN); -} + + s << ".j2c"; -void -Film::set_audio_delay (int d) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _audio_delay = d; + if (t) { + s << ".tmp"; } - signal_changed (AUDIO_DELAY); -} -void -Film::set_still_duration (int d) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _still_duration = d; - } - signal_changed (STILL_DURATION); + p /= s.str(); + return file (p.string ()); } -void -Film::set_subtitle_stream (shared_ptr<SubtitleStream> s) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _subtitle_stream = s; - } - signal_changed (SUBTITLE_STREAM); -} +/** Make an educated guess as to whether we have a complete DCP + * or not. + * @return true if we do. + */ -void -Film::set_with_subtitles (bool w) +bool +Film::have_dcp () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _with_subtitles = w; + try { + libdcp::DCP dcp (dir (dcp_name())); + dcp.read (); + } catch (...) { + return false; } - signal_changed (WITH_SUBTITLES); -} -void -Film::set_subtitle_offset (int o) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _subtitle_offset = o; - } - signal_changed (SUBTITLE_OFFSET); + return true; } -void -Film::set_subtitle_scale (float s) +shared_ptr<Player> +Film::make_player () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _subtitle_scale = s; - } - signal_changed (SUBTITLE_SCALE); + return shared_ptr<Player> (new Player (shared_from_this (), _playlist)); } void Film::set_encrypted (bool e) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _encrypted = e; - } + _encrypted = e; signal_changed (ENCRYPTED); } -void -Film::set_colour_lut (int i) +shared_ptr<Playlist> +Film::playlist () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _colour_lut = i; - } - signal_changed (COLOUR_LUT); + return _playlist; } -void -Film::set_j2k_bandwidth (int b) +ContentList +Film::content () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _j2k_bandwidth = b; - } - signal_changed (J2K_BANDWIDTH); + return _playlist->content (); } void -Film::set_audio_language (string l) +Film::examine_and_add_content (shared_ptr<Content> c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _audio_language = l; - } - signal_changed (DCI_METADATA); + shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c)); + j->Finished.connect (bind (&Film::maybe_add_content, this, boost::weak_ptr<Job> (j), boost::weak_ptr<Content> (c))); + JobManager::instance()->add (j); } void -Film::set_subtitle_language (string l) +Film::maybe_add_content (weak_ptr<Job> j, weak_ptr<Content> c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _subtitle_language = l; + shared_ptr<Job> job = j.lock (); + if (!job || !job->finished_ok ()) { + return; } - signal_changed (DCI_METADATA); -} - -void -Film::set_territory (string t) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _territory = t; + + shared_ptr<Content> content = c.lock (); + if (content) { + add_content (content); } - signal_changed (DCI_METADATA); } void -Film::set_rating (string r) +Film::add_content (shared_ptr<Content> c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _rating = r; + /* Add video content after any existing content */ + if (dynamic_pointer_cast<VideoContent> (c)) { + c->set_position (_playlist->video_end ()); } - signal_changed (DCI_METADATA); + + _playlist->add (c); } void -Film::set_studio (string s) +Film::remove_content (shared_ptr<Content> c) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _studio = s; - } - signal_changed (DCI_METADATA); + _playlist->remove (c); } -void -Film::set_facility (string f) +Time +Film::length () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _facility = f; - } - signal_changed (DCI_METADATA); + return _playlist->length (); } -void -Film::set_package_type (string p) +bool +Film::has_subtitles () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _package_type = p; - } - signal_changed (DCI_METADATA); + return _playlist->has_subtitles (); } -void -Film::set_size (Size s) +OutputVideoFrame +Film::best_video_frame_rate () const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _size = s; - } - signal_changed (SIZE); + return _playlist->best_dcp_frame_rate (); } void -Film::set_length (SourceFrame l) +Film::playlist_content_changed (boost::weak_ptr<Content> c, int p) { - { - boost::mutex::scoped_lock lm (_state_mutex); - _length = l; + if (p == VideoContentProperty::VIDEO_FRAME_RATE) { + set_video_frame_rate (_playlist->best_dcp_frame_rate ()); + } + + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (ContentChanged), c, p)); } - signal_changed (LENGTH); } void -Film::unset_length () +Film::playlist_changed () { - { - boost::mutex::scoped_lock lm (_state_mutex); - _length = boost::none; - } - signal_changed (LENGTH); + signal_changed (CONTENT); } -void -Film::set_content_digest (string d) +OutputAudioFrame +Film::time_to_audio_frames (Time t) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _content_digest = d; - } - _dirty = true; + return t * audio_frame_rate () / TIME_HZ; } -void -Film::set_content_audio_streams (vector<shared_ptr<AudioStream> > s) +OutputVideoFrame +Film::time_to_video_frames (Time t) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _content_audio_streams = s; - } - signal_changed (CONTENT_AUDIO_STREAMS); + return t * video_frame_rate () / TIME_HZ; } -void -Film::set_subtitle_streams (vector<shared_ptr<SubtitleStream> > s) +Time +Film::audio_frames_to_time (OutputAudioFrame f) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _subtitle_streams = s; - } - signal_changed (SUBTITLE_STREAMS); + return f * TIME_HZ / audio_frame_rate (); } -void -Film::set_frames_per_second (float f) +Time +Film::video_frames_to_time (OutputVideoFrame f) const { - { - boost::mutex::scoped_lock lm (_state_mutex); - _frames_per_second = f; - } - signal_changed (FRAMES_PER_SECOND); -} - -void -Film::signal_changed (Property p) -{ - { - boost::mutex::scoped_lock lm (_state_mutex); - _dirty = true; - } - - if (ui_signaller) { - ui_signaller->emit (boost::bind (boost::ref (Changed), p)); - } + return f * TIME_HZ / video_frame_rate (); } -int -Film::audio_channels () const +OutputAudioFrame +Film::audio_frame_rate () const { - shared_ptr<AudioStream> s = audio_stream (); - if (!s) { - return 0; - } - - return s->channels (); + /* XXX */ + return 48000; } void -Film::set_dci_date_today () +Film::set_sequence_video (bool s) { - _dci_date = boost::gregorian::day_clock::local_day (); + _sequence_video = s; + _playlist->set_sequence_video (s); + signal_changed (SEQUENCE_VIDEO); } -boost::shared_ptr<AudioStream> -Film::audio_stream () const +libdcp::Size +Film::full_frame () const { - if (use_content_audio()) { - return _content_audio_stream; + switch (_resolution) { + case RESOLUTION_2K: + return libdcp::Size (2048, 1080); + case RESOLUTION_4K: + return libdcp::Size (4096, 2160); } - return _external_audio_stream; + assert (false); + return libdcp::Size (); } void @@ -1509,7 +946,9 @@ Film::make_kdms ( dcp.read (); /* XXX: single CPL only */ - shared_ptr<xmlpp::Document> kdm = dcp.cpls().front()->make_kdm (chain, signer_key.string(), (*i)->certificate, from, until); + shared_ptr<xmlpp::Document> kdm = dcp.cpls().front()->make_kdm ( + chain, signer_key.string(), (*i)->certificate, from, until, _interop, libdcp::MXFMetadata (), Config::instance()->dcp_metadata () + ); boost::filesystem::path out = directory; out /= "kdm.xml"; diff --git a/src/lib/film.h b/src/lib/film.h index 1a78e9d34..809eabdaa 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -18,60 +18,61 @@ */ /** @file src/film.h - * @brief A representation of a piece of video (with sound), including naming, - * the source content file, and how it should be presented in a DCP. + * @brief A representation of some audio and video content, and details of + * how they should be presented in a DCP. */ -#ifndef DVDOMATIC_FILM_H -#define DVDOMATIC_FILM_H +#ifndef DCPOMATIC_FILM_H +#define DCPOMATIC_FILM_H #include <string> #include <vector> #include <inttypes.h> -#include <boost/thread/mutex.hpp> -#include <boost/thread.hpp> #include <boost/signals2.hpp> #include <boost/enable_shared_from_this.hpp> -#include <boost/date_time/posix_time/posix_time.hpp> -extern "C" { -#include <libavcodec/avcodec.h> -} -#include "dcp_content_type.h" +#include <boost/filesystem.hpp> #include "util.h" -#include "stream.h" +#include "types.h" +#include "dci_metadata.h" -class Format; -class Job; -class Filter; +class DCPContentType; class Log; -class ExamineContentJob; -class ExternalAudioStream; +class Content; +class Player; +class Playlist; +class AudioContent; +class Scaler; class Screen; /** @class Film - * @brief A representation of a video, maybe with sound. * - * A representation of a piece of video (maybe with sound), including naming, - * the source content file, and how it should be presented in a DCP. + * @brief A representation of some audio and video content, and details of + * how they should be presented in a DCP. + * + * The content of a Film is held in a Playlist (created and managed by the Film). */ -class Film : public boost::enable_shared_from_this<Film> +class Film : public boost::enable_shared_from_this<Film>, public boost::noncopyable { public: - Film (std::string d, bool must_exist = true); - Film (Film const &); - ~Film (); + Film (boost::filesystem::path); - std::string j2k_dir () const; + std::string info_dir () const; + std::string j2c_path (int, Eyes, bool) const; + std::string info_path (int, Eyes) const; + std::string internal_video_mxf_dir () const; + std::string internal_video_mxf_filename () const; + boost::filesystem::path audio_analysis_path (boost::shared_ptr<const AudioContent>) const; - void examine_content (); - void send_dcp_to_tms (); + std::string video_mxf_filename () const; + std::string audio_mxf_filename () const; - void make_dcp (bool); + void send_dcp_to_tms (); + void make_dcp (); /** @return Logger. * It is safe to call this from any thread. */ - Log* log () const { + boost::shared_ptr<Log> log () const { return _log; } @@ -80,27 +81,38 @@ public: std::string file (std::string f) const; std::string dir (std::string d) const; - std::string content_path () const; - ContentType content_type () const; - - int target_audio_sample_rate () const; - - void write_metadata () const; void read_metadata (); + void write_metadata () const; - Size cropped_size (Size) const; - boost::optional<int> dcp_length () const; - std::string dci_name () const; - std::string dcp_name () const; + std::string dci_name (bool if_created_now) const; + std::string dcp_name (bool if_created_now = false) const; /** @return true if our state has changed since we last saved it */ bool dirty () const { return _dirty; } - int audio_channels () const; + libdcp::Size full_frame () const; - void set_dci_date_today (); + bool have_dcp () 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; + + /* Proxies for some Playlist methods */ + + ContentList content () const; + + Time length () const; + bool has_subtitles () const; + OutputVideoFrame best_video_frame_rate () const; void make_kdms ( std::list<boost::shared_ptr<Screen> >, @@ -116,424 +128,180 @@ public: NONE, NAME, USE_DCI_NAME, + /** The playlist's content list has changed (i.e. content has been added, moved around or removed) */ CONTENT, - TRUST_CONTENT_HEADER, DCP_CONTENT_TYPE, - FORMAT, - CROP, - FILTERS, + CONTAINER, + RESOLUTION, SCALER, - DCP_TRIM_START, - DCP_TRIM_END, - REEL_SIZE, - DCP_AB, - CONTENT_AUDIO_STREAM, - EXTERNAL_AUDIO, - USE_CONTENT_AUDIO, - AUDIO_GAIN, - AUDIO_DELAY, - STILL_DURATION, - SUBTITLE_STREAM, WITH_SUBTITLES, - SUBTITLE_OFFSET, - SUBTITLE_SCALE, ENCRYPTED, - COLOUR_LUT, J2K_BANDWIDTH, DCI_METADATA, - SIZE, - LENGTH, - CONTENT_AUDIO_STREAMS, - SUBTITLE_STREAMS, - FRAMES_PER_SECOND, + VIDEO_FRAME_RATE, + AUDIO_CHANNELS, + /** The setting of _three_d has been changed */ + THREE_D, + SEQUENCE_VIDEO, + INTEROP, }; /* GET */ std::string directory () const { - boost::mutex::scoped_lock lm (_directory_mutex); return _directory; } std::string name () const { - boost::mutex::scoped_lock lm (_state_mutex); return _name; } bool use_dci_name () const { - boost::mutex::scoped_lock lm (_state_mutex); return _use_dci_name; } - std::string content () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _content; - } - - bool trust_content_header () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _trust_content_header; - } - DCPContentType const * dcp_content_type () const { - boost::mutex::scoped_lock lm (_state_mutex); return _dcp_content_type; } - Format const * format () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _format; + Ratio const * container () const { + return _container; } - Crop crop () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _crop; - } - - std::vector<Filter const *> filters () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _filters; + Resolution resolution () const { + return _resolution; } Scaler const * scaler () const { - boost::mutex::scoped_lock lm (_state_mutex); return _scaler; } - SourceFrame dcp_trim_start () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _dcp_trim_start; - } - - SourceFrame dcp_trim_end () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _dcp_trim_end; - } - - boost::optional<uint64_t> reel_size () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _reel_size; - } - - bool dcp_ab () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _dcp_ab; - } - - boost::shared_ptr<AudioStream> content_audio_stream () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _content_audio_stream; - } - - std::vector<std::string> external_audio () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _external_audio; - } - - bool use_content_audio () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _use_content_audio; - } - - float audio_gain () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _audio_gain; - } - - int audio_delay () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _audio_delay; - } - - int still_duration () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _still_duration; - } - - boost::shared_ptr<SubtitleStream> subtitle_stream () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _subtitle_stream; - } - bool with_subtitles () const { - boost::mutex::scoped_lock lm (_state_mutex); return _with_subtitles; } - int subtitle_offset () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _subtitle_offset; - } - - float subtitle_scale () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _subtitle_scale; - } - bool encrypted () const { - boost::mutex::scoped_lock lm (_state_mutex); return _encrypted; } - int colour_lut () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _colour_lut; - } - int j2k_bandwidth () const { - boost::mutex::scoped_lock lm (_state_mutex); return _j2k_bandwidth; } - std::string audio_language () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _audio_language; - } - - std::string subtitle_language () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _subtitle_language; - } - - std::string territory () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _territory; - } - - std::string rating () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _rating; - } - - std::string studio () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _studio; - } - - std::string facility () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _facility; - } - - std::string package_type () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _package_type; + DCIMetadata dci_metadata () const { + return _dci_metadata; } - Size size () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _size; + /** @return The frame rate of the DCP */ + int video_frame_rate () const { + return _video_frame_rate; } - boost::optional<SourceFrame> length () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _length; + int audio_channels () const { + return _audio_channels; } - - std::string content_digest () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _content_digest; + + bool three_d () const { + return _three_d; } - - std::vector<boost::shared_ptr<AudioStream> > content_audio_streams () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _content_audio_streams; + + bool sequence_video () const { + return _sequence_video; } - std::vector<boost::shared_ptr<SubtitleStream> > subtitle_streams () const { - boost::mutex::scoped_lock lm (_state_mutex); - return _subtitle_streams; + bool interop () const { + return _interop; } - float frames_per_second () const { - boost::mutex::scoped_lock lm (_state_mutex); - if (content_type() == STILL) { - return 24; - } - - return _frames_per_second; - } - - boost::shared_ptr<AudioStream> audio_stream () const; - /* SET */ void set_directory (std::string); void set_name (std::string); void set_use_dci_name (bool); - void set_content (std::string); - void set_trust_content_header (bool); + void examine_and_add_content (boost::shared_ptr<Content>); + void add_content (boost::shared_ptr<Content>); + void remove_content (boost::shared_ptr<Content>); void set_dcp_content_type (DCPContentType const *); - void set_format (Format const *); - void set_crop (Crop); - void set_left_crop (int); - void set_right_crop (int); - void set_top_crop (int); - void set_bottom_crop (int); - void set_filters (std::vector<Filter const *>); + void set_container (Ratio const *); + void set_resolution (Resolution); void set_scaler (Scaler const *); - void set_dcp_trim_start (int); - void set_dcp_trim_end (int); - void set_reel_size (uint64_t); - void unset_reel_size (); - void set_dcp_ab (bool); - void set_content_audio_stream (boost::shared_ptr<AudioStream>); - void set_external_audio (std::vector<std::string>); - void set_use_content_audio (bool); - void set_audio_gain (float); - void set_audio_delay (int); - void set_still_duration (int); - void set_subtitle_stream (boost::shared_ptr<SubtitleStream>); void set_with_subtitles (bool); - void set_subtitle_offset (int); - void set_subtitle_scale (float); void set_encrypted (bool); - void set_colour_lut (int); void set_j2k_bandwidth (int); - void set_audio_language (std::string); - void set_subtitle_language (std::string); - void set_territory (std::string); - void set_rating (std::string); - void set_studio (std::string); - void set_facility (std::string); - void set_package_type (std::string); - void set_size (Size); - void set_length (SourceFrame); - void unset_length (); - void set_content_digest (std::string); - void set_content_audio_streams (std::vector<boost::shared_ptr<AudioStream> >); - void set_subtitle_streams (std::vector<boost::shared_ptr<SubtitleStream> >); - void set_frames_per_second (float); - - /** Emitted when some property has changed */ + void set_dci_metadata (DCIMetadata); + void set_video_frame_rate (int); + void set_audio_channels (int); + void set_three_d (bool); + void set_dci_date_today (); + void set_sequence_video (bool); + void set_interop (bool); + + /** Emitted when some property has of the Film has changed */ mutable boost::signals2::signal<void (Property)> Changed; + /** Emitted when some property of our content has changed */ + mutable boost::signals2::signal<void (boost::weak_ptr<Content>, int)> ContentChanged; + /** Current version number of the state file */ static int const state_version; private: - - /** Log to write to */ - Log* _log; - - /** Any running ExamineContentJob, or 0 */ - boost::shared_ptr<ExamineContentJob> _examine_content_job; - - /** The date that we should use in a DCI name */ - boost::gregorian::date _dci_date; void signal_changed (Property); - void examine_content_finished (); + std::string video_identifier () const; + void playlist_changed (); + void playlist_content_changed (boost::weak_ptr<Content>, int); + std::string filename_safe_name () const; + void maybe_add_content (boost::weak_ptr<Job>, boost::weak_ptr<Content>); + + /** Log to write to */ + boost::shared_ptr<Log> _log; + boost::shared_ptr<Playlist> _playlist; /** Complete path to directory containing the film metadata; * must not be relative. */ std::string _directory; - /** Mutex for _directory */ - mutable boost::mutex _directory_mutex; - /** Name for DVD-o-matic */ + /** Name for DCP-o-matic */ std::string _name; /** True if a auto-generated DCI-compliant name should be used for our DCP */ bool _use_dci_name; - /** File or directory containing content; may be relative to our directory - * or an absolute path. - */ - std::string _content; - /** If this is true, we will believe the length specified by the content - * file's header; if false, we will run through the whole content file - * the first time we see it in order to obtain the length. - */ - bool _trust_content_header; /** The type of content that this Film represents (feature, trailer etc.) */ DCPContentType const * _dcp_content_type; - /** The format to present this Film in (flat, scope, etc.) */ - Format const * _format; - /** The crop to apply to the source */ - Crop _crop; - /** Video filters that should be used when generating DCPs */ - std::vector<Filter const *> _filters; + /** The container to put this Film in (flat, scope, etc.) */ + Ratio const * _container; + /** DCP resolution (2K or 4K) */ + Resolution _resolution; /** Scaler algorithm to use */ Scaler const * _scaler; - /** Frames to trim off the start of the DCP */ - int _dcp_trim_start; - /** Frames to trim off the end of the DCP */ - int _dcp_trim_end; - /** Approximate target reel size in bytes; if not set, use a single reel */ - boost::optional<uint64_t> _reel_size; - /** true to create an A/B comparison DCP, where the left half of the image - is the video without any filters or post-processing, and the right half - has the specified filters and post-processing. - */ - bool _dcp_ab; - /** The audio stream to use from our content */ - boost::shared_ptr<AudioStream> _content_audio_stream; - /** List of filenames of external audio files, in channel order - (L, R, C, Lfe, Ls, Rs) - */ - std::vector<std::string> _external_audio; - /** true to use audio from our content file; false to use external audio */ - bool _use_content_audio; - /** Gain to apply to audio in dB */ - float _audio_gain; - /** Delay to apply to audio (positive moves audio later) in milliseconds */ - int _audio_delay; - /** Duration to make still-sourced films (in seconds) */ - int _still_duration; - boost::shared_ptr<SubtitleStream> _subtitle_stream; /** True if subtitles should be shown for this film */ bool _with_subtitles; - /** y offset for placing subtitles, in source pixels; +ve is further down - the frame, -ve is further up. - */ - int _subtitle_offset; - /** scale factor to apply to subtitles */ - float _subtitle_scale; bool _encrypted; - - /** index of colour LUT to use when converting RGB to XYZ. - * 0: sRGB - * 1: Rec 709 - */ - int _colour_lut; /** bandwidth for J2K files in bits per second */ int _j2k_bandwidth; - - /* DCI naming stuff */ - std::string _audio_language; - std::string _subtitle_language; - std::string _territory; - std::string _rating; - std::string _studio; - std::string _facility; - std::string _package_type; - - /* Data which are cached to speed things up */ - - /** Size, in pixels, of the source (ignoring cropping) */ - Size _size; - /** The length of the source, in video frames (as far as we know) */ - boost::optional<SourceFrame> _length; - /** MD5 digest of our content file */ - std::string _content_digest; - /** The audio streams in our content */ - std::vector<boost::shared_ptr<AudioStream> > _content_audio_streams; - /** A stream to represent possible external audio (will always exist) */ - boost::shared_ptr<AudioStream> _external_audio_stream; - /** the subtitle streams that we can use */ - std::vector<boost::shared_ptr<SubtitleStream> > _subtitle_streams; - /** Frames per second of the source */ - float _frames_per_second; + /** DCI naming stuff */ + DCIMetadata _dci_metadata; + /** Frames per second to run our DCP at */ + int _video_frame_rate; + /** The date that we should use in a DCI name */ + boost::gregorian::date _dci_date; + /** Number of audio channels to put in the DCP */ + int _audio_channels; + /** If true, the DCP will be written in 3D mode; otherwise in 2D. + This will be regardless of what content is on the playlist. + */ + bool _three_d; + bool _sequence_video; + bool _interop; /** true if our state has changed since we last saved it */ mutable bool _dirty; - /** Mutex for all state except _directory */ - mutable boost::mutex _state_mutex; - friend class paths_test; + friend class film_metadata_test; }; #endif diff --git a/src/lib/filter.cc b/src/lib/filter.cc index 446cc111d..640a531e8 100644 --- a/src/lib/filter.cc +++ b/src/lib/filter.cc @@ -22,6 +22,12 @@ */ #include "filter.h" +extern "C" { +#include <libavfilter/avfilter.h> +#include <libpostproc/postprocess.h> +} + +#include "i18n.h" using namespace std; @@ -29,12 +35,14 @@ vector<Filter const *> Filter::_filters; /** @param i Our id. * @param n User-visible name. + * @param c User-visible category. * @param v String for a FFmpeg video filter descriptor, or "". * @param p String for a FFmpeg post-processing descriptor, or "". */ -Filter::Filter (string i, string n, string v, string p) +Filter::Filter (string i, string n, string c, string v, string p) : _id (i) , _name (n) + , _category (c) , _vf (v) , _pp (p) { @@ -57,30 +65,46 @@ Filter::setup_filters () { /* Note: "none" is a magic id name, so don't use it here */ - _filters.push_back (new Filter ("pphb", "Horizontal deblocking filter", "", "hb")); - _filters.push_back (new Filter ("ppvb", "Vertical deblocking filter", "", "vb")); - _filters.push_back (new Filter ("ppha", "Horizontal deblocking filter A", "", "ha")); - _filters.push_back (new Filter ("ppva", "Vertical deblocking filter A", "", "va")); - _filters.push_back (new Filter ("pph1", "Experimental horizontal deblocking filter 1", "", "h1")); - _filters.push_back (new Filter ("pphv", "Experimental vertical deblocking filter 1", "", "v1")); - _filters.push_back (new Filter ("ppdr", "Deringing filter", "", "dr")); - _filters.push_back (new Filter ("pplb", "Linear blend deinterlacer", "", "lb")); - _filters.push_back (new Filter ("ppli", "Linear interpolating deinterlacer", "", "li")); - _filters.push_back (new Filter ("ppci", "Cubic interpolating deinterlacer", "", "ci")); - _filters.push_back (new Filter ("ppmd", "Median deinterlacer", "", "md")); - _filters.push_back (new Filter ("ppfd", "FFMPEG deinterlacer", "", "fd")); - _filters.push_back (new Filter ("ppl5", "FIR low-pass deinterlacer", "", "l5")); - _filters.push_back (new Filter ("mcdeint", "Motion compensating deinterlacer", "mcdeint", "")); - _filters.push_back (new Filter ("kerndeint", "Kernel deinterlacer", "kerndeint", "")); - _filters.push_back (new Filter ("yadif", "Yet Another Deinterlacing Filter", "yadif", "")); - _filters.push_back (new Filter ("pptn", "Temporal noise reducer", "", "tn")); - _filters.push_back (new Filter ("ppfq", "Force quantizer", "", "fq")); - _filters.push_back (new Filter ("gradfun", "Gradient debander", "gradfun", "")); - _filters.push_back (new Filter ("unsharp", "Unsharp mask and Gaussian blur", "unsharp", "")); - _filters.push_back (new Filter ("denoise3d", "3D denoiser", "denoise3d", "")); - _filters.push_back (new Filter ("hqdn3d", "High quality 3D denoiser", "hqdn3d", "")); - _filters.push_back (new Filter ("telecine", "Telecine filter", "telecine", "")); - _filters.push_back (new Filter ("ow", "Overcomplete wavelet denoiser", "mp=ow", "")); + maybe_add (N_("pphb"), _("Horizontal deblocking filter"), _("De-blocking"), N_(""), N_("hb")); + maybe_add (N_("ppvb"), _("Vertical deblocking filter"), _("De-blocking"), N_(""), N_("vb")); + maybe_add (N_("ppha"), _("Horizontal deblocking filter A"), _("De-blocking"), N_(""), N_("ha")); + maybe_add (N_("ppva"), _("Vertical deblocking filter A"), _("De-blocking"), N_(""), N_("va")); + maybe_add (N_("pph1"), _("Experimental horizontal deblocking filter 1"), _("De-blocking"), N_(""), N_("h1")); + maybe_add (N_("pphv"), _("Experimental vertical deblocking filter 1"), _("De-blocking"), N_(""), N_("v1")); + maybe_add (N_("ppdr"), _("Deringing filter"), _("Misc"), N_(""), N_("dr")); + maybe_add (N_("pplb"), _("Linear blend deinterlacer"), _("De-interlacing"), N_(""), N_("lb")); + maybe_add (N_("ppli"), _("Linear interpolating deinterlacer"), _("De-interlacing"), N_(""), N_("li")); + maybe_add (N_("ppci"), _("Cubic interpolating deinterlacer"), _("De-interlacing"), N_(""), N_("ci")); + maybe_add (N_("ppmd"), _("Median deinterlacer"), _("De-interlacing"), N_(""), N_("md")); + maybe_add (N_("ppfd"), _("FFMPEG deinterlacer"), _("De-interlacing"), N_(""), N_("fd")); + maybe_add (N_("ppl5"), _("FIR low-pass deinterlacer"), _("De-interlacing"), N_(""), N_("l5")); + maybe_add (N_("mcdeint"), _("Motion compensating deinterlacer"), _("De-interlacing"), N_("mcdeint"), N_("")); + maybe_add (N_("kerndeint"), _("Kernel deinterlacer"), _("De-interlacing"), N_("kerndeint"), N_("")); + maybe_add (N_("yadif"), _("Yet Another Deinterlacing Filter"), _("De-interlacing"), N_("yadif"), N_("")); + maybe_add (N_("pptn"), _("Temporal noise reducer"), _("Noise reduction"), N_(""), N_("tn")); + maybe_add (N_("ppfq"), _("Force quantizer"), _("Misc"), N_(""), N_("fq")); + maybe_add (N_("gradfun"), _("Gradient debander"), _("Misc"), N_("gradfun"), N_("")); + maybe_add (N_("unsharp"), _("Unsharp mask and Gaussian blur"), _("Misc"), N_("unsharp"), N_("")); + maybe_add (N_("denoise3d"), _("3D denoiser"), _("Noise reduction"), N_("denoise3d"), N_("")); + maybe_add (N_("hqdn3d"), _("High quality 3D denoiser"), _("Noise reduction"), N_("hqdn3d"), N_("")); + maybe_add (N_("telecine"), _("Telecine filter"), _("Misc"), N_("telecine"), N_("")); + maybe_add (N_("ow"), _("Overcomplete wavelet denoiser"), _("Noise reduction"), N_("mp=ow"), N_("")); +} + +void +Filter::maybe_add (string i, string n, string c, string v, string p) +{ + if (!v.empty ()) { + if (avfilter_get_by_name (i.c_str())) { + _filters.push_back (new Filter (i, n, c, v, p)); + } + } else if (!p.empty ()) { + pp_mode* m = pp_get_mode_by_name_and_quality (p.c_str(), PP_QUALITY_MAX); + if (m) { + _filters.push_back (new Filter (i, n, c, v, p)); + pp_free_mode (m); + } + } } /** @param filters Set of filters. @@ -96,14 +120,14 @@ Filter::ffmpeg_strings (vector<Filter const *> const & filters) for (vector<Filter const *>::const_iterator i = filters.begin(); i != filters.end(); ++i) { if (!(*i)->vf().empty ()) { if (!vf.empty ()) { - vf += ","; + vf += N_(","); } vf += (*i)->vf (); } if (!(*i)->pp().empty ()) { if (!pp.empty()) { - pp += ","; + pp += N_(","); } pp += (*i)->pp (); } diff --git a/src/lib/filter.h b/src/lib/filter.h index 20c55049c..5971cd5cf 100644 --- a/src/lib/filter.h +++ b/src/lib/filter.h @@ -21,19 +21,20 @@ * @brief A class to describe one of FFmpeg's video or post-processing filters. */ -#ifndef DVDOMATIC_FILTER_H -#define DVDOMATIC_FILTER_H +#ifndef DCPOMATIC_FILTER_H +#define DCPOMATIC_FILTER_H #include <string> #include <vector> +#include <boost/utility.hpp> /** @class Filter * @brief A class to describe one of FFmpeg's video or post-processing filters. */ -class Filter +class Filter : public boost::noncopyable { public: - Filter (std::string, std::string, std::string, std::string); + Filter (std::string, std::string, std::string, std::string, std::string); /** @return our id */ std::string id () const { @@ -54,6 +55,10 @@ public: std::string pp () const { return _pp; } + + std::string category () const { + return _category; + } static std::vector<Filter const *> all (); static Filter const * from_id (std::string); @@ -66,6 +71,7 @@ private: std::string _id; /** user-visible name */ std::string _name; + std::string _category; /** string for a FFmpeg video filter descriptor */ std::string _vf; /** string for a FFmpeg post-processing descriptor */ @@ -73,6 +79,7 @@ private: /** all available filters */ static std::vector<Filter const *> _filters; + static void maybe_add (std::string, std::string, std::string, std::string, std::string); }; #endif diff --git a/src/lib/filter_graph.cc b/src/lib/filter_graph.cc index 376ab404f..cd5d19807 100644 --- a/src/lib/filter_graph.cc +++ b/src/lib/filter_graph.cc @@ -23,77 +23,71 @@ extern "C" { #include <libavfilter/avfiltergraph.h> -#ifdef HAVE_BUFFERSRC_H #include <libavfilter/buffersrc.h> -#endif -#if (LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 53 && LIBAVFILTER_VERSION_MINOR <= 77) || LIBAVFILTER_VERSION_MAJOR == 3 #include <libavfilter/avcodec.h> #include <libavfilter/buffersink.h> -#elif LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 -#include <libavfilter/vsrc_buffer.h> -#endif #include <libavformat/avio.h> } #include "decoder.h" #include "filter_graph.h" -#include "ffmpeg_compatibility.h" #include "filter.h" #include "exceptions.h" #include "image.h" -#include "film.h" -#include "ffmpeg_decoder.h" +#include "ffmpeg_content.h" + +#include "i18n.h" using std::stringstream; using std::string; using std::list; +using std::pair; +using std::make_pair; +using std::cout; using boost::shared_ptr; +using boost::weak_ptr; +using libdcp::Size; -/** Construct a FilterGraph for the settings in a film. - * @param film Film. - * @param decoder Decoder that we are using. +/** Construct a FilterGraph for the settings in a piece of content. + * @param content Content. * @param s Size of the images to process. * @param p Pixel format of the images to process. */ -FilterGraph::FilterGraph (shared_ptr<Film> film, FFmpegDecoder* decoder, Size s, AVPixelFormat p) +FilterGraph::FilterGraph (shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p) : _buffer_src_context (0) , _buffer_sink_context (0) , _size (s) , _pixel_format (p) { - string filters = Filter::ffmpeg_strings (film->filters()).first; - if (!filters.empty ()) { - filters += ","; + _frame = av_frame_alloc (); + + string filters = Filter::ffmpeg_strings (content->filters()).first; + if (filters.empty ()) { + filters = "copy"; } - filters += crop_string (Position (film->crop().left, film->crop().top), film->cropped_size (decoder->native_size())); - - avfilter_register_all (); - AVFilterGraph* graph = avfilter_graph_alloc(); if (graph == 0) { - throw DecodeError ("Could not create filter graph."); + throw DecodeError (N_("could not create filter graph.")); } - AVFilter* buffer_src = avfilter_get_by_name("buffer"); + AVFilter* buffer_src = avfilter_get_by_name(N_("buffer")); if (buffer_src == 0) { - throw DecodeError ("Could not find buffer src filter"); + throw DecodeError (N_("could not find buffer src filter")); } - AVFilter* buffer_sink = get_sink (); + AVFilter* buffer_sink = avfilter_get_by_name(N_("buffersink")); + if (buffer_sink == 0) { + throw DecodeError (N_("Could not create buffer sink filter")); + } stringstream a; - a << _size.width << ":" - << _size.height << ":" - << _pixel_format << ":" - << decoder->time_base_numerator() << ":" - << decoder->time_base_denominator() << ":" - << decoder->sample_aspect_ratio_numerator() << ":" - << decoder->sample_aspect_ratio_denominator(); - - int r; - - if ((r = avfilter_graph_create_filter (&_buffer_src_context, buffer_src, "in", a.str().c_str(), 0, graph)) < 0) { - throw DecodeError ("could not create buffer source"); + a << "video_size=" << _size.width << "x" << _size.height << ":" + << "pix_fmt=" << _pixel_format << ":" + << "time_base=1/1:" + << "pixel_aspect=1/1"; + + if (avfilter_graph_create_filter (&_buffer_src_context, buffer_src, "in", a.str().c_str(), 0, graph) < 0) { + throw DecodeError (N_("could not create buffer source")); } AVBufferSinkParams* sink_params = av_buffersink_params_alloc (); @@ -102,99 +96,59 @@ FilterGraph::FilterGraph (shared_ptr<Film> film, FFmpegDecoder* decoder, Size s, pixel_fmts[1] = PIX_FMT_NONE; sink_params->pixel_fmts = pixel_fmts; - if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, "out", 0, sink_params, graph) < 0) { - throw DecodeError ("could not create buffer sink."); + if (avfilter_graph_create_filter (&_buffer_sink_context, buffer_sink, N_("out"), 0, sink_params, graph) < 0) { + throw DecodeError (N_("could not create buffer sink.")); } + av_free (sink_params); + AVFilterInOut* outputs = avfilter_inout_alloc (); - outputs->name = av_strdup("in"); + outputs->name = av_strdup(N_("in")); outputs->filter_ctx = _buffer_src_context; outputs->pad_idx = 0; outputs->next = 0; AVFilterInOut* inputs = avfilter_inout_alloc (); - inputs->name = av_strdup("out"); + inputs->name = av_strdup(N_("out")); inputs->filter_ctx = _buffer_sink_context; inputs->pad_idx = 0; inputs->next = 0; -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 - if (avfilter_graph_parse (graph, filters.c_str(), inputs, outputs, 0) < 0) { - throw DecodeError ("could not set up filter graph."); - } -#else if (avfilter_graph_parse (graph, filters.c_str(), &inputs, &outputs, 0) < 0) { - throw DecodeError ("could not set up filter graph."); + throw DecodeError (N_("could not set up filter graph.")); } -#endif if (avfilter_graph_config (graph, 0) < 0) { - throw DecodeError ("could not configure filter graph."); + throw DecodeError (N_("could not configure filter graph.")); } /* XXX: leaking `inputs' / `outputs' ? */ } +FilterGraph::~FilterGraph () +{ + av_frame_free (&_frame); +} + /** Take an AVFrame and process it using our configured filters, returning a - * set of Images. + * set of Images. Caller handles memory management of the input frame. */ -list<shared_ptr<Image> > -FilterGraph::process (AVFrame const * frame) +list<pair<shared_ptr<Image>, int64_t> > +FilterGraph::process (AVFrame* frame) { - list<shared_ptr<Image> > images; - -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 53 && LIBAVFILTER_VERSION_MINOR <= 61 - - if (av_vsrc_buffer_add_frame (_buffer_src_context, frame, 0) < 0) { - throw DecodeError ("could not push buffer into filter chain."); - } - -#elif LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR == 15 - - AVRational par; - par.num = sample_aspect_ratio_numerator (); - par.den = sample_aspect_ratio_denominator (); - - if (av_vsrc_buffer_add_frame (_buffer_src_context, frame, 0, par) < 0) { - throw DecodeError ("could not push buffer into filter chain."); - } - -#else + list<pair<shared_ptr<Image>, int64_t> > images; if (av_buffersrc_write_frame (_buffer_src_context, frame) < 0) { - throw DecodeError ("could not push buffer into filter chain."); + throw DecodeError (N_("could not push buffer into filter chain.")); } -#endif - -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 15 && LIBAVFILTER_VERSION_MINOR <= 61 - while (avfilter_poll_frame (_buffer_sink_context->inputs[0])) { -#else - while (av_buffersink_read (_buffer_sink_context, 0)) { -#endif - -#if LIBAVFILTER_VERSION_MAJOR == 2 && LIBAVFILTER_VERSION_MINOR >= 15 - - int r = avfilter_request_frame (_buffer_sink_context->inputs[0]); - if (r < 0) { - throw DecodeError ("could not request filtered frame"); - } - - AVFilterBufferRef* filter_buffer = _buffer_sink_context->inputs[0]->cur_buf; - -#else - - AVFilterBufferRef* filter_buffer; - if (av_buffersink_get_buffer_ref (_buffer_sink_context, &filter_buffer, 0) < 0) { - filter_buffer = 0; + while (1) { + if (av_buffersink_get_frame (_buffer_sink_context, _frame) < 0) { + break; } -#endif - - if (filter_buffer) { - /* This takes ownership of filter_buffer */ - images.push_back (shared_ptr<Image> (new FilterBufferImage ((PixelFormat) frame->format, filter_buffer))); - } + images.push_back (make_pair (shared_ptr<Image> (new Image (_frame)), av_frame_get_best_effort_timestamp (_frame))); + av_frame_unref (_frame); } return images; @@ -205,7 +159,7 @@ FilterGraph::process (AVFrame const * frame) * @return true if this chain can process images with `s' and `p', otherwise false. */ bool -FilterGraph::can_process (Size s, AVPixelFormat p) const +FilterGraph::can_process (libdcp::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 9e6ac6252..9b403c2bc 100644 --- a/src/lib/filter_graph.h +++ b/src/lib/filter_graph.h @@ -21,32 +21,33 @@ * @brief A graph of FFmpeg filters. */ -#ifndef DVDOMATIC_FILTER_GRAPH_H -#define DVDOMATIC_FILTER_GRAPH_H +#ifndef DCPOMATIC_FILTER_GRAPH_H +#define DCPOMATIC_FILTER_GRAPH_H #include "util.h" -#include "ffmpeg_compatibility.h" class Image; class VideoFilter; -class FFmpegDecoder; +class FFmpegContent; /** @class FilterGraph * @brief A graph of FFmpeg filters. */ -class FilterGraph +class FilterGraph : public boost::noncopyable { public: - FilterGraph (boost::shared_ptr<Film> film, FFmpegDecoder* decoder, Size s, AVPixelFormat p); + FilterGraph (boost::shared_ptr<const FFmpegContent> content, libdcp::Size s, AVPixelFormat p); + ~FilterGraph (); - bool can_process (Size s, AVPixelFormat p) const; - std::list<boost::shared_ptr<Image> > process (AVFrame const * frame); + bool can_process (libdcp::Size s, AVPixelFormat p) const; + std::list<std::pair<boost::shared_ptr<Image>, int64_t> > process (AVFrame * frame); private: AVFilterContext* _buffer_src_context; AVFilterContext* _buffer_sink_context; - Size _size; ///< size of the images that this chain can process + libdcp::Size _size; ///< size of the images that this chain can process AVPixelFormat _pixel_format; ///< pixel format of the images that this chain can process + AVFrame* _frame; }; #endif diff --git a/src/lib/format.cc b/src/lib/format.cc deleted file mode 100644 index 975862411..000000000 --- a/src/lib/format.cc +++ /dev/null @@ -1,186 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/format.cc - * @brief Class to describe a format (aspect ratio) that a Film should - * be shown in. - */ - -#include <sstream> -#include <cstdlib> -#include <cassert> -#include <iomanip> -#include <iostream> -#include "format.h" -#include "film.h" - -using std::string; -using std::setprecision; -using std::stringstream; -using std::vector; -using boost::shared_ptr; - -vector<Format const *> Format::_formats; - -/** @return A name to be presented to the user */ -string -FixedFormat::name () const -{ - stringstream s; - if (!_nickname.empty ()) { - s << _nickname << " ("; - } - - s << setprecision(3) << (_ratio / 100.0) << ":1"; - - if (!_nickname.empty ()) { - s << ")"; - } - - return s.str (); -} - -/** @return Identifier for this format as metadata for a Film's metadata file */ -string -Format::as_metadata () const -{ - return _id; -} - -/** Fill our _formats vector with all available formats */ -void -Format::setup_formats () -{ - _formats.push_back (new FixedFormat (119, Size (1285, 1080), "119", "1.19", "F")); - _formats.push_back (new FixedFormat (133, Size (1436, 1080), "133", "1.33", "F")); - _formats.push_back (new FixedFormat (138, Size (1485, 1080), "138", "1.375", "F")); - _formats.push_back (new FixedFormat (133, Size (1998, 1080), "133-in-flat", "4:3 within Flat", "F")); - _formats.push_back (new FixedFormat (137, Size (1480, 1080), "137", "Academy", "F")); - _formats.push_back (new FixedFormat (166, Size (1793, 1080), "166", "1.66", "F")); - _formats.push_back (new FixedFormat (166, Size (1998, 1080), "166-in-flat", "1.66 within Flat", "F")); - _formats.push_back (new FixedFormat (178, Size (1998, 1080), "178-in-flat", "16:9 within Flat", "F")); - _formats.push_back (new FixedFormat (178, Size (1920, 1080), "178", "16:9", "F")); - _formats.push_back (new FixedFormat (185, Size (1998, 1080), "185", "Flat", "F")); - _formats.push_back (new FixedFormat (239, Size (2048, 858), "239", "Scope", "S")); - _formats.push_back (new VariableFormat (Size (1998, 1080), "var-185", "Flat", "F")); - _formats.push_back (new VariableFormat (Size (2048, 858), "var-239", "Scope", "S")); -} - -/** @param n Nickname. - * @return Matching Format, or 0. - */ -Format const * -Format::from_nickname (string n) -{ - vector<Format const *>::iterator i = _formats.begin (); - while (i != _formats.end() && (*i)->nickname() != n) { - ++i; - } - - if (i == _formats.end ()) { - return 0; - } - - return *i; -} - -/** @param i Id. - * @return Matching Format, or 0. - */ -Format const * -Format::from_id (string i) -{ - vector<Format const *>::iterator j = _formats.begin (); - while (j != _formats.end() && (*j)->id() != i) { - ++j; - } - - if (j == _formats.end ()) { - return 0; - } - - return *j; -} - - -/** @param m Metadata, as returned from as_metadata(). - * @return Matching Format, or 0. - */ -Format const * -Format::from_metadata (string m) -{ - return from_id (m); -} - -/** @return All available formats */ -vector<Format const *> -Format::all () -{ - return _formats; -} - -/** @param r Ratio multiplied by 100 (e.g. 185) - * @param dcp Size (in pixels) of the images that we should put in a DCP. - * @param id ID (e.g. 185) - * @param n Nick name (e.g. Flat) - */ -FixedFormat::FixedFormat (int r, Size dcp, string id, string n, string d) - : Format (dcp, id, n, d) - , _ratio (r) -{ - -} - -int -Format::dcp_padding (shared_ptr<const Film> f) const -{ - int p = rint ((_dcp_size.width - (_dcp_size.height * ratio_as_integer(f) / 100.0)) / 2.0); - - /* This comes out -ve for Scope; bodge it */ - if (p < 0) { - p = 0; - } - - return p; -} - -VariableFormat::VariableFormat (Size dcp, string id, string n, string d) - : Format (dcp, id, n, d) -{ - -} - -int -VariableFormat::ratio_as_integer (shared_ptr<const Film> f) const -{ - return rint (ratio_as_float (f) * 100); -} - -float -VariableFormat::ratio_as_float (shared_ptr<const Film> f) const -{ - return float (f->size().width) / f->size().height; -} - -/** @return A name to be presented to the user */ -string -VariableFormat::name () const -{ - return _nickname; -} diff --git a/src/lib/format.h b/src/lib/format.h deleted file mode 100644 index 2118237a4..000000000 --- a/src/lib/format.h +++ /dev/null @@ -1,134 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/format.h - * @brief Classes to describe a format (aspect ratio) that a Film should - * be shown in. - */ - -#include <string> -#include <vector> -#include "util.h" - -class Film; - -class Format -{ -public: - Format (Size dcp, std::string id, std::string n, std::string d) - : _dcp_size (dcp) - , _id (id) - , _nickname (n) - , _dci_name (d) - {} - - /** @return the aspect ratio multiplied by 100 - * (e.g. 239 for Cinemascope 2.39:1) - */ - virtual int ratio_as_integer (boost::shared_ptr<const Film> f) const = 0; - - /** @return the ratio as a floating point number */ - virtual float ratio_as_float (boost::shared_ptr<const Film> f) const = 0; - - int dcp_padding (boost::shared_ptr<const Film> f) const; - - /** @return size in pixels of the images that we should - * put in a DCP for this ratio. This size will not correspond - * to the ratio when we are doing things like 16:9 in a Flat frame. - */ - Size dcp_size () const { - return _dcp_size; - } - - std::string id () const { - return _id; - } - - /** @return Full name to present to the user */ - virtual std::string name () const = 0; - - /** @return Nickname (e.g. Flat, Scope) */ - std::string nickname () const { - return _nickname; - } - - std::string dci_name () const { - return _dci_name; - } - - std::string as_metadata () const; - - static Format const * from_nickname (std::string n); - static Format const * from_metadata (std::string m); - static Format const * from_id (std::string i); - static std::vector<Format const *> all (); - static void setup_formats (); - -protected: - /** Size in pixels of the images that we should - * put in a DCP for this ratio. This size will not correspond - * to the ratio when we are doing things like 16:9 in a Flat frame. - */ - Size _dcp_size; - /** id for use in metadata */ - std::string _id; - /** nickname (e.g. Flat, Scope) */ - std::string _nickname; - std::string _dci_name; - -private: - /** all available formats */ - static std::vector<Format const *> _formats; -}; - -/** @class FixedFormat - * @brief Class to describe a format whose ratio is fixed regardless - * of source size. - */ -class FixedFormat : public Format -{ -public: - FixedFormat (int, Size, std::string, std::string, std::string); - - int ratio_as_integer (boost::shared_ptr<const Film>) const { - return _ratio; - } - - float ratio_as_float (boost::shared_ptr<const Film>) const { - return _ratio / 100.0; - } - - std::string name () const; - -private: - - /** Ratio expressed as the actual ratio multiplied by 100 */ - int _ratio; -}; - -class VariableFormat : public Format -{ -public: - VariableFormat (Size, std::string, std::string, std::string); - - int ratio_as_integer (boost::shared_ptr<const Film> f) const; - float ratio_as_float (boost::shared_ptr<const Film> f) const; - - std::string name () const; -}; diff --git a/src/lib/video_source.cc b/src/lib/i18n.h index 56742e2b4..890313bc6 100644 --- a/src/lib/video_source.cc +++ b/src/lib/i18n.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,14 +17,7 @@ */ -#include "video_source.h" -#include "video_sink.h" +#include <libintl.h> -using boost::shared_ptr; -using boost::bind; - -void -VideoSource::connect_video (shared_ptr<VideoSink> s) -{ - Video.connect (bind (&VideoSink::process_video, s, _1, _2, _3)); -} +#define _(x) dgettext ("libdcpomatic", x) +#define N_(x) x diff --git a/src/lib/image.cc b/src/lib/image.cc index f774f476f..dbea62d26 100644 --- a/src/lib/image.cc +++ b/src/lib/image.cc @@ -18,35 +18,39 @@ */ /** @file src/image.cc - * @brief A set of classes to describe video images. + * @brief A class to describe a video image. */ -#include <sstream> -#include <iomanip> #include <iostream> -#include <sys/time.h> -#include <boost/algorithm/string.hpp> -#include <boost/bind.hpp> -#include <openjpeg.h> extern "C" { -#include <libavcodec/avcodec.h> -#include <libavformat/avformat.h> #include <libswscale/swscale.h> -#include <libavfilter/avfiltergraph.h> -#include <libpostproc/postprocess.h> #include <libavutil/pixfmt.h> +#include <libavutil/pixdesc.h> +#include <libpostproc/postprocess.h> } #include "image.h" #include "exceptions.h" #include "scaler.h" -using namespace std; -using namespace boost; +using std::string; +using std::min; +using std::cout; +using boost::shared_ptr; +using libdcp::Size; -void -Image::swap (Image& other) +int +Image::line_factor (int n) const { - std::swap (_pixel_format, other._pixel_format); + if (n == 0) { + return 1; + } + + AVPixFmtDescriptor const * d = av_pix_fmt_desc_get(_pixel_format); + if (!d) { + throw PixelFormatError ("lines()", _pixel_format); + } + + return pow (2.0f, d->log2_chroma_h); } /** @param n Component index. @@ -55,55 +59,39 @@ Image::swap (Image& other) int Image::lines (int n) const { - switch (_pixel_format) { - case PIX_FMT_YUV420P: - if (n == 0) { - return size().height; - } else { - return size().height / 2; - } - break; - case PIX_FMT_RGB24: - case PIX_FMT_RGBA: - case PIX_FMT_YUV422P10LE: - case PIX_FMT_YUV422P: - return size().height; - default: - assert (false); - } - - return 0; + return rint (ceil (static_cast<double>(size().height) / line_factor (n))); } /** @return Number of components */ int Image::components () const { - switch (_pixel_format) { - case PIX_FMT_YUV420P: - case PIX_FMT_YUV422P10LE: - case PIX_FMT_YUV422P: - return 3; - case PIX_FMT_RGB24: - case PIX_FMT_RGBA: - return 1; - default: - assert (false); + AVPixFmtDescriptor const * d = av_pix_fmt_desc_get(_pixel_format); + if (!d) { + throw PixelFormatError ("components()", _pixel_format); } - return 0; + if ((d->flags & PIX_FMT_PLANAR) == 0) { + return 1; + } + + return d->nb_components; } shared_ptr<Image> -Image::scale (Size out_size, Scaler const * scaler, bool aligned) const +Image::scale (libdcp::Size out_size, Scaler const * scaler, AVPixelFormat result_format, bool result_aligned) const { assert (scaler); + /* Empirical testing suggests that sws_scale() will crash if + the input image is not aligned. + */ + assert (aligned ()); - shared_ptr<Image> scaled (new SimpleImage (pixel_format(), out_size, aligned)); + shared_ptr<Image> scaled (new Image (result_format, out_size, result_aligned)); struct SwsContext* scale_context = sws_getContext ( size().width, size().height, pixel_format(), - out_size.width, out_size.height, pixel_format(), + out_size.width, out_size.height, result_format, scaler->ffmpeg_id (), 0, 0, 0 ); @@ -119,61 +107,6 @@ Image::scale (Size out_size, Scaler const * scaler, bool aligned) const return scaled; } -/** Scale this image to a given size and convert it to RGB. - * @param out_size Output image size in pixels. - * @param scaler Scaler to use. - */ -shared_ptr<Image> -Image::scale_and_convert_to_rgb (Size out_size, int padding, Scaler const * scaler, bool aligned) const -{ - assert (scaler); - - Size content_size = out_size; - content_size.width -= (padding * 2); - - shared_ptr<Image> rgb (new SimpleImage (PIX_FMT_RGB24, content_size, aligned)); - - struct SwsContext* scale_context = sws_getContext ( - size().width, size().height, pixel_format(), - content_size.width, content_size.height, PIX_FMT_RGB24, - scaler->ffmpeg_id (), 0, 0, 0 - ); - - /* Scale and convert to RGB from whatever its currently in (which may be RGB) */ - sws_scale ( - scale_context, - data(), stride(), - 0, size().height, - rgb->data(), rgb->stride() - ); - - /* Put the image in the right place in a black frame if are padding; this is - a bit grubby and expensive, but probably inconsequential in the great - scheme of things. - */ - if (padding > 0) { - shared_ptr<Image> padded_rgb (new SimpleImage (PIX_FMT_RGB24, out_size, aligned)); - padded_rgb->make_black (); - - /* XXX: we are cheating a bit here; we know the frame is RGB so we can - make assumptions about its composition. - */ - uint8_t* p = padded_rgb->data()[0] + padding * 3; - uint8_t* q = rgb->data()[0]; - for (int j = 0; j < rgb->lines(0); ++j) { - memcpy (p, q, rgb->line_size()[0]); - p += padded_rgb->stride()[0]; - q += rgb->stride()[0]; - } - - rgb = padded_rgb; - } - - sws_freeContext (scale_context); - - return rgb; -} - /** Run a FFmpeg post-process on this image and return the processed version. * @param pp Flags for the required set of post processes. * @return Post-processed image. @@ -181,7 +114,7 @@ Image::scale_and_convert_to_rgb (Size out_size, int padding, Scaler const * scal shared_ptr<Image> Image::post_process (string pp, bool aligned) const { - shared_ptr<Image> out (new SimpleImage (pixel_format(), size (), aligned)); + shared_ptr<Image> out (new Image (pixel_format(), size (), aligned)); int pp_format = 0; switch (pixel_format()) { @@ -190,10 +123,17 @@ Image::post_process (string pp, bool aligned) const break; case PIX_FMT_YUV422P10LE: case PIX_FMT_YUV422P: + case PIX_FMT_UYVY422: pp_format = PP_FORMAT_422; break; + case PIX_FMT_YUV444P: + case PIX_FMT_YUV444P9BE: + case PIX_FMT_YUV444P9LE: + case PIX_FMT_YUV444P10BE: + case PIX_FMT_YUV444P10LE: + pp_format = PP_FORMAT_444; default: - assert (false); + throw PixelFormatError ("post_process", pixel_format()); } pp_mode* mode = pp_get_mode_by_name_and_quality (pp.c_str (), PP_QUALITY_MAX); @@ -215,53 +155,137 @@ Image::post_process (string pp, bool aligned) const shared_ptr<Image> Image::crop (Crop crop, bool aligned) const { - Size cropped_size = size (); + libdcp::Size cropped_size = size (); cropped_size.width -= crop.left + crop.right; cropped_size.height -= crop.top + crop.bottom; - shared_ptr<Image> out (new SimpleImage (pixel_format(), cropped_size, aligned)); + shared_ptr<Image> out (new Image (pixel_format(), cropped_size, aligned)); for (int c = 0; c < components(); ++c) { int const crop_left_in_bytes = bytes_per_pixel(c) * crop.left; - int const cropped_width_in_bytes = bytes_per_pixel(c) * cropped_size.width; - + /* bytes_per_pixel() could be a fraction; in this case the stride will be rounded + up, and we need to make sure that we copy over the width (up to the stride) + rather than short of the width; hence the ceil() here. + */ + int const cropped_width_in_bytes = ceil (bytes_per_pixel(c) * cropped_size.width); + /* Start of the source line, cropped from the top but not the left */ - uint8_t* in_p = data()[c] + crop.top * stride()[c]; + uint8_t* in_p = data()[c] + (crop.top / out->line_factor(c)) * stride()[c]; uint8_t* out_p = out->data()[c]; - - for (int y = 0; y < cropped_size.height; ++y) { + + for (int y = 0; y < out->lines(c); ++y) { memcpy (out_p, in_p + crop_left_in_bytes, cropped_width_in_bytes); - in_p += line_size()[c]; - out_p += out->line_size()[c]; + in_p += stride()[c]; + out_p += out->stride()[c]; } } return out; } +/** Blacken a YUV image whose bits per pixel is rounded up to 16 */ +void +Image::yuv_16_black (uint16_t v) +{ + memset (data()[0], 0, lines(0) * stride()[0]); + for (int i = 1; i < 3; ++i) { + int16_t* p = reinterpret_cast<int16_t*> (data()[i]); + for (int y = 0; y < size().height; ++y) { + for (int x = 0; x < line_size()[i] / 2; ++x) { + p[x] = v; + } + p += stride()[i] / 2; + } + } +} + +uint16_t +Image::swap_16 (uint16_t v) +{ + return ((v >> 8) & 0xff) | ((v & 0xff) << 8); +} + void Image::make_black () { + /* U/V black value for 8-bit colour */ + static uint8_t const eight_bit_uv = (1 << 7) - 1; + /* U/V black value for 9-bit colour */ + static uint16_t const nine_bit_uv = (1 << 8) - 1; + /* U/V black value for 10-bit colour */ + static uint16_t const ten_bit_uv = (1 << 9) - 1; + /* U/V black value for 16-bit colour */ + static uint16_t const sixteen_bit_uv = (1 << 15) - 1; + switch (_pixel_format) { case PIX_FMT_YUV420P: - case PIX_FMT_YUV422P10LE: case PIX_FMT_YUV422P: + case PIX_FMT_YUV444P: memset (data()[0], 0, lines(0) * stride()[0]); - memset (data()[1], 0x80, lines(1) * stride()[1]); - memset (data()[2], 0x80, lines(2) * stride()[2]); + memset (data()[1], eight_bit_uv, lines(1) * stride()[1]); + memset (data()[2], eight_bit_uv, lines(2) * stride()[2]); + break; + + case PIX_FMT_YUVJ420P: + case PIX_FMT_YUVJ422P: + case PIX_FMT_YUVJ444P: + memset (data()[0], 0, lines(0) * stride()[0]); + memset (data()[1], eight_bit_uv + 1, lines(1) * stride()[1]); + memset (data()[2], eight_bit_uv + 1, lines(2) * stride()[2]); + break; + + case PIX_FMT_YUV422P9LE: + case PIX_FMT_YUV444P9LE: + yuv_16_black (nine_bit_uv); + break; + + case PIX_FMT_YUV422P9BE: + case PIX_FMT_YUV444P9BE: + yuv_16_black (swap_16 (nine_bit_uv)); + break; + + case PIX_FMT_YUV422P10LE: + case PIX_FMT_YUV444P10LE: + yuv_16_black (ten_bit_uv); + break; + + case PIX_FMT_YUV422P16LE: + case PIX_FMT_YUV444P16LE: + yuv_16_black (sixteen_bit_uv); + break; + + case PIX_FMT_YUV444P10BE: + case PIX_FMT_YUV422P10BE: + yuv_16_black (swap_16 (ten_bit_uv)); break; case PIX_FMT_RGB24: memset (data()[0], 0, lines(0) * stride()[0]); 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 / 4; ++x) { + *p++ = eight_bit_uv; // Cb + *p++ = 0; // Y0 + *p++ = eight_bit_uv; // Cr + *p++ = 0; // Y1 + } + } + break; + } + default: - assert (false); + throw PixelFormatError ("make_black()", _pixel_format); } } void -Image::alpha_blend (shared_ptr<const Image> other, Position position) +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); @@ -297,12 +321,27 @@ Image::alpha_blend (shared_ptr<const Image> other, Position position) } void +Image::copy (shared_ptr<const Image> other, Position<int> position) +{ + /* Only implemented for RGB24 onto RGB24 so far */ + assert (_pixel_format == PIX_FMT_RGB24 && other->pixel_format() == PIX_FMT_RGB24); + assert (position.x >= 0 && position.y >= 0); + + int const N = min (position.x + other->size().width, size().width) - position.x; + for (int ty = position.y, oy = 0; ty < size().height && oy < other->size().height; ++ty, ++oy) { + uint8_t * const tp = data()[0] + ty * stride()[0] + position.x * 3; + uint8_t * const op = other->data()[0] + oy * other->stride()[0]; + memcpy (tp, op, N * 3); + } +} + +void Image::read_from_socket (shared_ptr<Socket> socket) { for (int i = 0; i < components(); ++i) { uint8_t* p = data()[i]; for (int y = 0; y < lines(i); ++y) { - socket->read_definite_and_consume (p, line_size()[i], 30); + socket->read (p, line_size()[i]); p += stride()[i]; } } @@ -314,7 +353,7 @@ Image::write_to_socket (shared_ptr<Socket> socket) const for (int i = 0; i < components(); ++i) { uint8_t* p = data()[i]; for (int y = 0; y < lines(i); ++y) { - socket->write (p, line_size()[i], 30); + socket->write (p, line_size()[i]); p += stride()[i]; } } @@ -324,60 +363,52 @@ Image::write_to_socket (shared_ptr<Socket> socket) const float Image::bytes_per_pixel (int c) const { - if (c == 3) { + AVPixFmtDescriptor const * d = av_pix_fmt_desc_get(_pixel_format); + if (!d) { + throw PixelFormatError ("lines()", _pixel_format); + } + + if (c >= components()) { return 0; } + + float bpp[4] = { 0, 0, 0, 0 }; + + bpp[0] = floor ((d->comp[0].depth_minus1 + 1 + 7) / 8); + if (d->nb_components > 1) { + bpp[1] = floor ((d->comp[1].depth_minus1 + 1 + 7) / 8) / pow (2.0f, d->log2_chroma_w); + } + if (d->nb_components > 2) { + bpp[2] = floor ((d->comp[2].depth_minus1 + 1 + 7) / 8) / pow (2.0f, d->log2_chroma_w); + } + if (d->nb_components > 3) { + bpp[3] = floor ((d->comp[3].depth_minus1 + 1 + 7) / 8) / pow (2.0f, d->log2_chroma_w); + } - switch (_pixel_format) { - case PIX_FMT_RGB24: - if (c == 0) { - return 3; - } else { - return 0; - } - case PIX_FMT_RGBA: - if (c == 0) { - return 4; - } else { - return 0; - } - case PIX_FMT_YUV420P: - case PIX_FMT_YUV422P: - if (c == 0) { - return 1; - } else { - return 0.5; - } - case PIX_FMT_YUV422P10LE: - if (c == 1) { - return 2; - } else { - return 1; - } - default: - assert (false); + if ((d->flags & PIX_FMT_PLANAR) == 0) { + /* Not planar; sum them up */ + return bpp[0] + bpp[1] + bpp[2] + bpp[3]; } - return 0; + return bpp[c]; } - -/** Construct a SimpleImage of a given size and format, allocating memory +/** Construct a Image of a given size and format, allocating memory * as required. * * @param p Pixel format. * @param s Size in pixels. */ -SimpleImage::SimpleImage (AVPixelFormat p, Size s, bool aligned) - : Image (p) - , _size (s) +Image::Image (AVPixelFormat p, libdcp::Size s, bool aligned) + : libdcp::Image (s) + , _pixel_format (p) , _aligned (aligned) { allocate (); } void -SimpleImage::allocate () +Image::allocate () { _data = (uint8_t **) av_malloc (4 * sizeof (uint8_t *)); _data[0] = _data[1] = _data[2] = _data[3] = 0; @@ -389,43 +420,96 @@ SimpleImage::allocate () _stride[0] = _stride[1] = _stride[2] = _stride[3] = 0; for (int i = 0; i < components(); ++i) { - _line_size[i] = _size.width * bytes_per_pixel(i); + _line_size[i] = ceil (_size.width * bytes_per_pixel(i)); _stride[i] = stride_round_up (i, _line_size, _aligned ? 32 : 1); - _data[i] = (uint8_t *) av_malloc (_stride[i] * lines (i)); + + /* The assembler function ff_rgb24ToY_avx (in libswscale/x86/input.asm) + uses a 16-byte fetch to read three bytes (R/G/B) of image data. + Hence on the last pixel of the last line it reads over the end of + the actual data by 1 byte. If the width of an image is a multiple + of the stride alignment there will be no padding at the end of image lines. + OS X crashes on this illegal read, though other operating systems don't + seem to mind. The nasty + 1 in this malloc makes sure there is always a byte + for that instruction to read safely. + */ + _data[i] = (uint8_t *) av_malloc (_stride[i] * lines (i) + 1); } } -SimpleImage::SimpleImage (SimpleImage const & other) - : Image (other) +Image::Image (Image const & other) + : libdcp::Image (other) + , _pixel_format (other._pixel_format) + , _aligned (other._aligned) +{ + allocate (); + + for (int i = 0; i < components(); ++i) { + uint8_t* p = _data[i]; + uint8_t* q = other._data[i]; + for (int j = 0; j < lines(i); ++j) { + memcpy (p, q, _line_size[i]); + p += stride()[i]; + q += other.stride()[i]; + } + } +} + +Image::Image (AVFrame* frame) + : libdcp::Image (libdcp::Size (frame->width, frame->height)) + , _pixel_format (static_cast<AVPixelFormat> (frame->format)) + , _aligned (true) { - _size = other._size; - _aligned = other._aligned; - allocate (); for (int i = 0; i < components(); ++i) { - memcpy (_data[i], other._data[i], _line_size[i] * lines(i)); + uint8_t* p = _data[i]; + uint8_t* q = frame->data[i]; + for (int j = 0; j < lines(i); ++j) { + memcpy (p, q, _line_size[i]); + p += stride()[i]; + /* AVFrame's linesize is what we call `stride' */ + q += frame->linesize[i]; + } } } -SimpleImage& -SimpleImage::operator= (SimpleImage const & other) +Image::Image (shared_ptr<const Image> other, bool aligned) + : libdcp::Image (other) + , _pixel_format (other->_pixel_format) + , _aligned (aligned) +{ + allocate (); + + for (int i = 0; i < components(); ++i) { + assert(line_size()[i] == other->line_size()[i]); + uint8_t* p = _data[i]; + uint8_t* q = other->data()[i]; + for (int j = 0; j < lines(i); ++j) { + memcpy (p, q, line_size()[i]); + p += stride()[i]; + q += other->stride()[i]; + } + } +} + +Image& +Image::operator= (Image const & other) { if (this == &other) { return *this; } - SimpleImage tmp (other); + Image tmp (other); swap (tmp); return *this; } void -SimpleImage::swap (SimpleImage & other) +Image::swap (Image & other) { - Image::swap (other); + libdcp::Image::swap (other); - std::swap (_size, other._size); + std::swap (_pixel_format, other._pixel_format); for (int i = 0; i < 4; ++i) { std::swap (_data[i], other._data[i]); @@ -436,8 +520,8 @@ SimpleImage::swap (SimpleImage & other) std::swap (_aligned, other._aligned); } -/** Destroy a SimpleImage */ -SimpleImage::~SimpleImage () +/** Destroy a Image */ +Image::~Image () { for (int i = 0; i < components(); ++i) { av_free (_data[i]); @@ -449,91 +533,32 @@ SimpleImage::~SimpleImage () } uint8_t ** -SimpleImage::data () const +Image::data () const { return _data; } int * -SimpleImage::line_size () const +Image::line_size () const { return _line_size; } int * -SimpleImage::stride () const +Image::stride () const { return _stride; } -Size -SimpleImage::size () const +libdcp::Size +Image::size () const { return _size; } -FilterBufferImage::FilterBufferImage (AVPixelFormat p, AVFilterBufferRef* b) - : Image (p) - , _buffer (b) -{ - -} - -FilterBufferImage::~FilterBufferImage () -{ - avfilter_unref_buffer (_buffer); -} - -uint8_t ** -FilterBufferImage::data () const -{ - return _buffer->data; -} - -int * -FilterBufferImage::line_size () const -{ - return _buffer->linesize; -} - -int * -FilterBufferImage::stride () const -{ - /* XXX? */ - return _buffer->linesize; -} - -Size -FilterBufferImage::size () const -{ - return Size (_buffer->video->w, _buffer->video->h); -} - -RGBPlusAlphaImage::RGBPlusAlphaImage (shared_ptr<const Image> im) - : SimpleImage (im->pixel_format(), im->size(), false) -{ - assert (im->pixel_format() == PIX_FMT_RGBA); - - _alpha = (uint8_t *) av_malloc (im->size().width * im->size().height); - - uint8_t* in = im->data()[0]; - uint8_t* out = data()[0]; - uint8_t* out_alpha = _alpha; - for (int y = 0; y < im->size().height; ++y) { - uint8_t* in_r = in; - for (int x = 0; x < im->size().width; ++x) { - *out++ = *in_r++; - *out++ = *in_r++; - *out++ = *in_r++; - *out_alpha++ = *in_r++; - } - - in += im->stride()[0]; - } -} - -RGBPlusAlphaImage::~RGBPlusAlphaImage () +bool +Image::aligned () const { - av_free (_alpha); + return _aligned; } diff --git a/src/lib/image.h b/src/lib/image.h index e19c6f54b..6af74a8c5 100644 --- a/src/lib/image.h +++ b/src/lib/image.h @@ -21,8 +21,8 @@ * @brief A set of classes to describe video images. */ -#ifndef DVDOMATIC_IMAGE_H -#define DVDOMATIC_IMAGE_H +#ifndef DCPOMATIC_IMAGE_H +#define DCPOMATIC_IMAGE_H #include <string> #include <boost/shared_ptr.hpp> @@ -31,50 +31,36 @@ extern "C" { #include <libavcodec/avcodec.h> #include <libavfilter/avfilter.h> } +#include <libdcp/image.h> #include "util.h" -#include "ffmpeg_compatibility.h" +#include "position.h" class Scaler; -class RGBFrameImage; -class SimpleImage; -/** @class Image - * @brief Parent class for wrappers of some image, in some format, that - * can present a set of components and a size in pixels. - * - * This class also has some conversion / processing methods. - * - * The main point of this class (and its subclasses) is to abstract - * details of FFmpeg's memory management and varying data formats. - */ -class Image +class Image : public libdcp::Image { public: - Image (AVPixelFormat p) - : _pixel_format (p) - {} + Image (AVPixelFormat, libdcp::Size, bool); + Image (AVFrame *); + Image (Image const &); + Image (boost::shared_ptr<const Image>, bool); + Image& operator= (Image const &); + ~Image (); - virtual ~Image () {} - - /** @return Array of pointers to arrays of the component data */ - virtual uint8_t ** data () const = 0; - - /** @return Array of sizes of the data in each line, in bytes (without any alignment padding bytes) */ - virtual int * line_size () const = 0; - - /** @return Array of strides for each line (including any alignment padding bytes) */ - virtual int * stride () const = 0; - - /** @return Size of the image, in pixels */ - virtual Size size () const = 0; + uint8_t ** data () const; + int * line_size () const; + int * stride () const; + libdcp::Size size () const; + bool aligned () const; int components () const; + int line_factor (int) const; int lines (int) const; - boost::shared_ptr<Image> scale_and_convert_to_rgb (Size out_size, int padding, Scaler const * scaler, bool aligned) const; - boost::shared_ptr<Image> scale (Size, Scaler const *, bool aligned) const; + boost::shared_ptr<Image> scale (libdcp::Size, Scaler const *, AVPixelFormat, bool aligned) const; boost::shared_ptr<Image> post_process (std::string, bool aligned) const; - void alpha_blend (boost::shared_ptr<const Image> image, Position pos); + void alpha_blend (boost::shared_ptr<const Image> image, Position<int> pos); + void copy (boost::shared_ptr<const Image> image, Position<int> pos); boost::shared_ptr<Image> crop (Crop c, bool aligned) const; void make_black (); @@ -86,76 +72,20 @@ public: return _pixel_format; } -protected: - virtual void swap (Image &); - float bytes_per_pixel (int) const; - -private: - AVPixelFormat _pixel_format; ///< FFmpeg's way of describing the pixel format of this Image -}; - -/** @class FilterBufferImage - * @brief An Image that is held in an AVFilterBufferRef. - */ -class FilterBufferImage : public Image -{ -public: - FilterBufferImage (AVPixelFormat, AVFilterBufferRef *); - ~FilterBufferImage (); - - uint8_t ** data () const; - int * line_size () const; - int * stride () const; - Size size () const; - private: - /* Not allowed */ - FilterBufferImage (FilterBufferImage const &); - FilterBufferImage& operator= (FilterBufferImage const &); + friend class pixel_formats_test; - AVFilterBufferRef* _buffer; -}; - -/** @class SimpleImage - * @brief An Image for which memory is allocated using a `simple' av_malloc(). - */ -class SimpleImage : public Image -{ -public: - SimpleImage (AVPixelFormat, Size, bool); - SimpleImage (SimpleImage const &); - SimpleImage& operator= (SimpleImage const &); - ~SimpleImage (); - - uint8_t ** data () const; - int * line_size () const; - int * stride () const; - Size size () const; - -protected: void allocate (); - void swap (SimpleImage &); + void swap (Image &); + float bytes_per_pixel (int) const; + void yuv_16_black (uint16_t); + static uint16_t swap_16 (uint16_t); -private: - Size _size; ///< size in pixels + AVPixelFormat _pixel_format; ///< FFmpeg's way of describing the pixel format of this Image uint8_t** _data; ///< array of pointers to components int* _line_size; ///< array of sizes of the data in each line, in pixels (without any alignment padding bytes) int* _stride; ///< array of strides for each line (including any alignment padding bytes) bool _aligned; }; -class RGBPlusAlphaImage : public SimpleImage -{ -public: - RGBPlusAlphaImage (boost::shared_ptr<const Image>); - ~RGBPlusAlphaImage (); - - uint8_t* alpha () const { - return _alpha; - } - -private: - uint8_t* _alpha; -}; - #endif diff --git a/src/lib/imagemagick_decoder.cc b/src/lib/imagemagick_decoder.cc deleted file mode 100644 index bad1fb813..000000000 --- a/src/lib/imagemagick_decoder.cc +++ /dev/null @@ -1,149 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <iostream> -#include <boost/filesystem.hpp> -#include <Magick++.h> -#include "imagemagick_decoder.h" -#include "image.h" -#include "film.h" -#include "exceptions.h" - -using std::cout; -using boost::shared_ptr; - -ImageMagickDecoder::ImageMagickDecoder ( - boost::shared_ptr<Film> f, boost::shared_ptr<const DecodeOptions> o, Job* j) - : Decoder (f, o, j) - , VideoDecoder (f, o, j) -{ - if (boost::filesystem::is_directory (_film->content_path())) { - for ( - boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (_film->content_path()); - i != boost::filesystem::directory_iterator(); - ++i) { - - if (still_image_file (i->path().string())) { - _files.push_back (i->path().string()); - } - } - } else { - _files.push_back (_film->content_path ()); - } - - _iter = _files.begin (); -} - -Size -ImageMagickDecoder::native_size () const -{ - if (_files.empty ()) { - throw DecodeError ("no still image files found"); - } - - /* Look at the first file and assume its size holds for all */ - using namespace MagickCore; - Magick::Image* image = new Magick::Image (_film->content_path ()); - Size const s = Size (image->columns(), image->rows()); - delete image; - - return s; -} - -bool -ImageMagickDecoder::pass () -{ - if (_iter == _files.end()) { - if (!_film->dcp_length() || video_frame() >= _film->dcp_length().get()) { - return true; - } - - repeat_last_video (); - return false; - } - - Magick::Image* magick_image = new Magick::Image (_film->content_path ()); - - Size size = native_size (); - shared_ptr<Image> image (new SimpleImage (PIX_FMT_RGB24, size, false)); - - using namespace MagickCore; - - uint8_t* p = image->data()[0]; - for (int y = 0; y < size.height; ++y) { - for (int x = 0; x < size.width; ++x) { - Magick::Color c = magick_image->pixelColor (x, y); - *p++ = c.redQuantum() * 255 / QuantumRange; - *p++ = c.greenQuantum() * 255 / QuantumRange; - *p++ = c.blueQuantum() * 255 / QuantumRange; - } - } - - delete magick_image; - - image = image->crop (_film->crop(), false); - - emit_video (image, 0); - - ++_iter; - return false; -} - -PixelFormat -ImageMagickDecoder::pixel_format () const -{ - /* XXX: always true? */ - return PIX_FMT_RGB24; -} - -bool -ImageMagickDecoder::seek_to_last () -{ - if (_iter == _files.end()) { - _iter = _files.begin(); - } else { - --_iter; - } - - return false; -} - -bool -ImageMagickDecoder::seek (double t) -{ - int const f = t * frames_per_second(); - - _iter = _files.begin (); - for (int i = 0; i < f; ++i) { - if (_iter == _files.end()) { - return true; - } - ++_iter; - } - - return false; -} - -void -ImageMagickDecoder::film_changed (Film::Property p) -{ - if (p == Film::CROP) { - OutputChanged (); - } -} diff --git a/src/lib/imagemagick_decoder.h b/src/lib/imagemagick_decoder.h deleted file mode 100644 index 6f426f308..000000000 --- a/src/lib/imagemagick_decoder.h +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include "video_decoder.h" - -namespace Magick { - class Image; -} - -class ImageMagickDecoder : public VideoDecoder -{ -public: - ImageMagickDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); - - float frames_per_second () const { - /* We don't know */ - return 0; - } - - Size native_size () const; - - SourceFrame length () const { - /* We don't know */ - return 0; - } - - int audio_channels () const { - return 0; - } - - int audio_sample_rate () const { - return 0; - } - - int64_t audio_channel_layout () const { - return 0; - } - - bool has_subtitles () const { - return false; - } - - bool seek (double); - bool seek_to_last (); - -protected: - bool pass (); - PixelFormat pixel_format () const; - - int time_base_numerator () const { - return 0; - } - - int time_base_denominator () const { - return 0; - } - - int sample_aspect_ratio_numerator () const { - /* XXX */ - return 1; - } - - int sample_aspect_ratio_denominator () const { - /* XXX */ - return 1; - } - -private: - void film_changed (Film::Property); - - std::list<std::string> _files; - std::list<std::string>::iterator _iter; -}; diff --git a/src/lib/job.cc b/src/lib/job.cc index 896862d14..8924fa09c 100644 --- a/src/lib/job.cc +++ b/src/lib/job.cc @@ -26,21 +26,25 @@ #include <libdcp/exceptions.h> #include "job.h" #include "util.h" +#include "cross.h" +#include "ui_signaller.h" +#include "exceptions.h" + +#include "i18n.h" using std::string; using std::list; +using std::cout; using std::stringstream; using boost::shared_ptr; -/** @param s Film that we are operating on. - * @param req Job that must be completed before this job is run. - */ -Job::Job (shared_ptr<Film> f, shared_ptr<Job> req) +Job::Job (shared_ptr<const Film> f) : _film (f) - , _required (req) + , _thread (0) , _state (NEW) , _start_time (0) , _progress_unknown (false) + , _last_set (0) , _ran_for (0) { descend (1); @@ -52,7 +56,7 @@ Job::start () { set_state (RUNNING); _start_time = time (0); - boost::thread (boost::bind (&Job::run_wrapper, this)); + _thread = new boost::thread (boost::bind (&Job::run_wrapper, this)); } /** A wrapper for the ::run() method to catch exceptions */ @@ -67,19 +71,52 @@ Job::run_wrapper () set_progress (1); set_state (FINISHED_ERROR); - set_error (String::compose ("%1 (%2)", e.what(), boost::filesystem::path (e.filename()).leaf())); + + string m = String::compose (_("An error occurred whilst handling the file %1."), boost::filesystem::path (e.filename()).leaf()); + + try { + boost::filesystem::space_info const s = boost::filesystem::space (e.filename()); + if (s.available < pow (1024, 3)) { + m += N_("\n\n"); + m += _("The drive that the film is stored on is low in disc space. Free some more space and try again."); + } + } catch (...) { + + } + + set_error (e.what(), m); + + } catch (OpenFileError& e) { + + set_progress (1); + set_state (FINISHED_ERROR); + + set_error ( + String::compose (_("Could not open %1"), e.file().string()), + String::compose (_("DCP-o-matic could not open the file %1. Perhaps it does not exist or is in an unexpected format."), e.file().string()) + ); + + } catch (boost::thread_interrupted &) { + + set_state (FINISHED_CANCELLED); } catch (std::exception& e) { set_progress (1); set_state (FINISHED_ERROR); - set_error (e.what ()); + set_error ( + e.what (), + _("It is not known what caused this error. The best idea is to report the problem to the DCP-o-matic mailing list (carl@dcpomatic.com)") + ); } catch (...) { set_progress (1); set_state (FINISHED_ERROR); - set_error ("unknown exception"); + set_error ( + _("Unknown error"), + _("It is not known what caused this error. The best idea is to report the problem to the DCP-o-matic mailing list (carl@dcpomatic.com)") + ); } } @@ -105,7 +142,7 @@ bool Job::finished () const { boost::mutex::scoped_lock lm (_state_mutex); - return _state == FINISHED_OK || _state == FINISHED_ERROR; + return _state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED; } /** @return true if the job has finished successfully */ @@ -124,18 +161,40 @@ Job::finished_in_error () const return _state == FINISHED_ERROR; } +bool +Job::finished_cancelled () const +{ + boost::mutex::scoped_lock lm (_state_mutex); + return _state == FINISHED_CANCELLED; +} + +bool +Job::paused () const +{ + boost::mutex::scoped_lock lm (_state_mutex); + return _state == PAUSED; +} + /** Set the state of this job. * @param s New state. */ void Job::set_state (State s) { - boost::mutex::scoped_lock lm (_state_mutex); - _state = s; + bool finished = false; + + { + boost::mutex::scoped_lock lm (_state_mutex); + _state = s; + + if (_state == FINISHED_OK || _state == FINISHED_ERROR || _state == FINISHED_CANCELLED) { + _ran_for = elapsed_time (); + finished = true; + } + } - if (_state == FINISHED_OK || _state == FINISHED_ERROR) { - _ran_for = elapsed_time (); - Finished (); + if (finished && ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (Finished))); } } @@ -156,9 +215,25 @@ Job::elapsed_time () const void Job::set_progress (float p) { + if (fabs (p - _last_set) < 0.01) { + /* Calm excessive progress reporting */ + return; + } + + _last_set = p; + boost::mutex::scoped_lock lm (_progress_mutex); _progress_unknown = false; _stack.back().normalised = p; + boost::this_thread::interruption_point (); + + if (paused ()) { + dcpomatic_sleep (1); + } + + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (Progress))); + } } /** @return fractional overall progress, or -1 if not known */ @@ -211,22 +286,30 @@ Job::descend (float a) _stack.push_back (Level (a)); } -/** @return Any error string that the job has generated */ string -Job::error () const +Job::error_details () const +{ + boost::mutex::scoped_lock lm (_state_mutex); + return _error_details; +} + +/** @return A summary of any error that the job has generated */ +string +Job::error_summary () const { boost::mutex::scoped_lock lm (_state_mutex); - return _error; + return _error_summary; } /** Set the current error string. * @param e New error string. */ void -Job::set_error (string e) +Job::set_error (string s, string d) { boost::mutex::scoped_lock lm (_state_mutex); - _error = e; + _error_summary = s; + _error_details = d; } /** Say that this job's progress will be unknown until further notice */ @@ -245,15 +328,26 @@ Job::status () const int const t = elapsed_time (); int const r = remaining_time (); + int pc = rint (p * 100); + if (pc == 100) { + /* 100% makes it sound like we've finished when we haven't */ + pc = 99; + } + stringstream s; - if (!finished () && p >= 0 && t > 10 && r > 0) { - s << rint (p * 100) << "%; " << seconds_to_approximate_hms (r) << " remaining"; - } else if (!finished () && (t <= 10 || r == 0)) { - s << rint (p * 100) << "%"; + if (!finished ()) { + s << pc << N_("%"); + if (p >= 0 && t > 10 && r > 0) { + /// TRANSLATORS: remaining here follows an amount of time that is remaining + /// on an operation. + s << "; " << seconds_to_approximate_hms (r) << " " << _("remaining"); + } } else if (finished_ok ()) { - s << "OK (ran for " << seconds_to_hms (_ran_for) << ")"; + s << String::compose (_("OK (ran for %1)"), seconds_to_hms (_ran_for)); } else if (finished_in_error ()) { - s << "Error (" << error() << ")"; + s << String::compose (_("Error (%1)"), error_summary()); + } else if (finished_cancelled ()) { + s << _("Cancelled"); } return s.str (); @@ -265,3 +359,30 @@ Job::remaining_time () const { return elapsed_time() / overall_progress() - elapsed_time(); } + +void +Job::cancel () +{ + if (!_thread) { + return; + } + + _thread->interrupt (); + _thread->join (); +} + +void +Job::pause () +{ + if (running ()) { + set_state (PAUSED); + } +} + +void +Job::resume () +{ + if (paused ()) { + set_state (RUNNING); + } +} diff --git a/src/lib/job.h b/src/lib/job.h index f32cfa811..9b8b14a93 100644 --- a/src/lib/job.h +++ b/src/lib/job.h @@ -21,23 +21,24 @@ * @brief A parent class to represent long-running tasks which are run in their own thread. */ -#ifndef DVDOMATIC_JOB_H -#define DVDOMATIC_JOB_H +#ifndef DCPOMATIC_JOB_H +#define DCPOMATIC_JOB_H #include <string> #include <boost/thread/mutex.hpp> #include <boost/enable_shared_from_this.hpp> #include <boost/signals2.hpp> +#include <boost/thread.hpp> class Film; /** @class Job * @brief A parent class to represent long-running tasks which are run in their own thread. */ -class Job : public boost::enable_shared_from_this<Job> +class Job : public boost::enable_shared_from_this<Job>, public boost::noncopyable { public: - Job (boost::shared_ptr<Film> s, boost::shared_ptr<Job> req); + Job (boost::shared_ptr<const Film>); virtual ~Job() {} /** @return user-readable name of this job */ @@ -46,14 +47,20 @@ public: virtual void run () = 0; void start (); + void pause (); + void resume (); + void cancel (); bool is_new () const; bool running () const; bool finished () const; bool finished_ok () const; bool finished_in_error () const; + bool finished_cancelled () const; + bool paused () const; - std::string error () const; + std::string error_summary () const; + std::string error_details () const; int elapsed_time () const; virtual std::string status () const; @@ -63,11 +70,12 @@ public: void ascend (); void descend (float); float overall_progress () const; - - boost::shared_ptr<Job> required () const { - return _required; + bool progress_unknown () const { + return _progress_unknown; } + boost::signals2::signal<void()> Progress; + /** Emitted from the UI thread when the job is finished */ boost::signals2::signal<void()> Finished; protected: @@ -76,30 +84,32 @@ protected: /** Description of a job's state */ enum State { - NEW, ///< the job hasn't been started yet - RUNNING, ///< the job is running - FINISHED_OK, ///< the job has finished successfully - FINISHED_ERROR ///< the job has finished in error + NEW, ///< the job hasn't been started yet + RUNNING, ///< the job is running + PAUSED, ///< the job has been paused + FINISHED_OK, ///< the job has finished successfully + FINISHED_ERROR, ///< the job has finished in error + FINISHED_CANCELLED ///< the job was cancelled }; void set_state (State); - void set_error (std::string e); + void set_error (std::string s, std::string d); - /** Film for this job */ - boost::shared_ptr<Film> _film; + boost::shared_ptr<const Film> _film; private: void run_wrapper (); - boost::shared_ptr<Job> _required; + boost::thread* _thread; /** mutex for _state and _error */ mutable boost::mutex _state_mutex; /** current state of the job */ State _state; - /** message for an error that has occurred (when state == FINISHED_ERROR) */ - std::string _error; + /** summary of an error that has occurred (when state == FINISHED_ERROR) */ + std::string _error_summary; + std::string _error_details; /** time that this job was started */ time_t _start_time; @@ -119,6 +129,8 @@ private: /** true if this job's progress will always be unknown */ bool _progress_unknown; + float _last_set; + int _ran_for; }; diff --git a/src/lib/job_manager.cc b/src/lib/job_manager.cc index fa02fd370..a841fa60b 100644 --- a/src/lib/job_manager.cc +++ b/src/lib/job_manager.cc @@ -30,7 +30,9 @@ using std::string; using std::list; +using std::cout; using boost::shared_ptr; +using boost::weak_ptr; JobManager* JobManager::_instance = 0; @@ -43,19 +45,16 @@ JobManager::JobManager () shared_ptr<Job> JobManager::add (shared_ptr<Job> j) { - boost::mutex::scoped_lock lm (_mutex); - _jobs.push_back (j); - return j; -} + { + boost::mutex::scoped_lock lm (_mutex); + _jobs.push_back (j); + } -void -JobManager::add_after (shared_ptr<Job> after, shared_ptr<Job> j) -{ - boost::mutex::scoped_lock lm (_mutex); - list<shared_ptr<Job> >::iterator i = find (_jobs.begin(), _jobs.end(), after); - assert (i != _jobs.end ()); - ++i; - _jobs.insert (i, j); + if (ui_signaller) { + ui_signaller->emit (boost::bind (boost::ref (JobAdded), weak_ptr<Job> (j))); + } + + return j; } list<shared_ptr<Job> > @@ -111,13 +110,10 @@ JobManager::scheduler () } if ((*i)->is_new()) { - shared_ptr<Job> r = (*i)->required (); - if (!r || r->finished_ok ()) { - (*i)->start (); - - /* Only start one job at once */ - break; - } + (*i)->start (); + + /* Only start one job at once */ + break; } } } @@ -129,7 +125,7 @@ JobManager::scheduler () } } - dvdomatic_sleep (1); + dcpomatic_sleep (1); } } diff --git a/src/lib/job_manager.h b/src/lib/job_manager.h index cc1c1d28f..0040568c6 100644 --- a/src/lib/job_manager.h +++ b/src/lib/job_manager.h @@ -30,21 +30,24 @@ class Job; /** @class JobManager * @brief A simple scheduler for jobs. */ -class JobManager +class JobManager : public boost::noncopyable { public: boost::shared_ptr<Job> add (boost::shared_ptr<Job>); - void add_after (boost::shared_ptr<Job> after, boost::shared_ptr<Job> j); std::list<boost::shared_ptr<Job> > get () const; bool work_to_do () const; bool errors () const; + boost::signals2::signal<void (boost::weak_ptr<Job>)> JobAdded; boost::signals2::signal<void (bool)> ActiveJobsChanged; static JobManager* instance (); private: + /* This function is part of the test suite */ + friend void ::wait_for_jobs (); + JobManager (); void scheduler (); diff --git a/src/lib/log.cc b/src/lib/log.cc index 06cff0495..ef36a902c 100644 --- a/src/lib/log.cc +++ b/src/lib/log.cc @@ -25,10 +25,12 @@ #include <time.h> #include "log.h" +#include "i18n.h" + using namespace std; Log::Log () - : _level (VERBOSE) + : _level (STANDARD) { } @@ -48,7 +50,7 @@ Log::log (string m, Level l) string a = ctime (&t); stringstream s; - s << a.substr (0, a.length() - 1) << ": " << m; + s << a.substr (0, a.length() - 1) << N_(": ") << m; do_log (s.str ()); } @@ -65,7 +67,7 @@ Log::microsecond_log (string m, Level l) gettimeofday (&tv, 0); stringstream s; - s << tv.tv_sec << ":" << tv.tv_usec << " " << m; + s << tv.tv_sec << N_(":") << tv.tv_usec << N_(" ") << m; do_log (s.str ()); } @@ -79,10 +81,10 @@ Log::set_level (Level l) void Log::set_level (string l) { - if (l == "verbose") { + if (l == N_("verbose")) { set_level (VERBOSE); return; - } else if (l == "timing") { + } else if (l == N_("timing")) { set_level (TIMING); return; } @@ -101,6 +103,6 @@ void FileLog::do_log (string m) { ofstream f (_file.c_str(), fstream::app); - f << m << "\n"; + f << m << N_("\n"); } diff --git a/src/lib/log.h b/src/lib/log.h index 3a2cfcbfd..bd0066a83 100644 --- a/src/lib/log.h +++ b/src/lib/log.h @@ -17,8 +17,8 @@ */ -#ifndef DVDOMATIC_LOG_H -#define DVDOMATIC_LOG_H +#ifndef DCPOMATIC_LOG_H +#define DCPOMATIC_LOG_H /** @file src/log.h * @brief A very simple logging class. @@ -30,7 +30,7 @@ /** @class Log * @brief A very simple logging class. */ -class Log +class Log : public boost::noncopyable { public: Log (); diff --git a/src/lib/lut.h b/src/lib/lut.h deleted file mode 100644 index 26888a24a..000000000 --- a/src/lib/lut.h +++ /dev/null @@ -1,51 +0,0 @@ -/* - Taken from OpenDCP: Builds Digital Cinema Packages - Copyright (c) 2010-2011 Terrence Meiczinger, All Rights Reserved - - 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 3 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, see <http://www.gnu.org/licenses/>. -*/ - -/** @file src/lut.h - * @brief Look-up tables for colour conversions (from OpenDCP) - */ - -#define BIT_DEPTH 12 -#define BIT_PRECISION 16 -#define COLOR_DEPTH (4095) -#define DCI_LUT_SIZE ((COLOR_DEPTH + 1) * BIT_PRECISION) -#define DCI_GAMMA (2.6) -#define DCI_DEGAMMA (1/DCI_GAMMA) -#define DCI_COEFFICENT (48.0/52.37) - -enum COLOR_PROFILE_ENUM { - CP_SRGB = 0, - CP_REC709, - CP_DC28, - CP_MAX -}; - -enum LUT_IN_ENUM { - LI_SRGB = 0, - LI_REC709, - LI_MAX -}; - -enum LUT_OUT_ENUM { - LO_DCI = 0, - LO_MAX -}; - -extern float color_matrix[3][3][3]; -extern float lut_in[LI_MAX][4095+1]; -extern int lut_out[1][DCI_LUT_SIZE]; diff --git a/src/lib/matcher.cc b/src/lib/matcher.cc deleted file mode 100644 index 2b7a080fc..000000000 --- a/src/lib/matcher.cc +++ /dev/null @@ -1,113 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include "matcher.h" -#include "image.h" -#include "log.h" - -using std::min; -using boost::shared_ptr; - -Matcher::Matcher (Log* log, int sample_rate, float frames_per_second) - : AudioVideoProcessor (log) - , _sample_rate (sample_rate) - , _frames_per_second (frames_per_second) - , _video_frames (0) - , _audio_frames (0) -{ - -} - -void -Matcher::process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s) -{ - Video (i, same, s); - _video_frames++; - - _pixel_format = i->pixel_format (); - _size = i->size (); -} - -void -Matcher::process_audio (boost::shared_ptr<AudioBuffers> b) -{ - Audio (b); - _audio_frames += b->frames (); - - _channels = b->channels (); -} - -void -Matcher::process_end () -{ - if (_audio_frames == 0 || !_pixel_format || !_size || !_channels) { - /* We won't do anything */ - return; - } - - int64_t audio_short_by_frames = video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second) - _audio_frames; - - _log->log ( - String::compose ( - "Matching processor has seen %1 video frames (which equals %2 audio frames) and %3 audio frames", - _video_frames, - video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second), - _audio_frames - ) - ); - - if (audio_short_by_frames < 0) { - - _log->log (String::compose ("%1 too many audio frames", -audio_short_by_frames)); - - /* We have seen more audio than video. Emit enough black video frames so that we reverse this */ - int const black_video_frames = ceil (-audio_short_by_frames * _frames_per_second / _sample_rate); - - _log->log (String::compose ("Emitting %1 frames of black video", black_video_frames)); - - shared_ptr<Image> black (new SimpleImage (_pixel_format.get(), _size.get(), false)); - black->make_black (); - for (int i = 0; i < black_video_frames; ++i) { - Video (black, i != 0, shared_ptr<Subtitle>()); - } - - /* Now recompute our check value */ - audio_short_by_frames = video_frames_to_audio_frames (_video_frames, _sample_rate, _frames_per_second) - _audio_frames; - } - - if (audio_short_by_frames > 0) { - _log->log (String::compose ("Emitted %1 too few audio frames", audio_short_by_frames)); - - /* Do things in half second blocks as I think there may be limits - to what FFmpeg (and in particular the resampler) can cope with. - */ - int64_t const block = _sample_rate / 2; - shared_ptr<AudioBuffers> b (new AudioBuffers (_channels.get(), block)); - b->make_silent (); - - int64_t to_do = audio_short_by_frames; - while (to_do > 0) { - int64_t const this_time = min (to_do, block); - b->set_frames (this_time); - Audio (b); - _audio_frames += b->frames (); - to_do -= this_time; - } - } -} diff --git a/src/lib/matcher.h b/src/lib/matcher.h deleted file mode 100644 index b94c28446..000000000 --- a/src/lib/matcher.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <boost/optional.hpp> -#include "processor.h" -#include "ffmpeg_compatibility.h" - -class Matcher : public AudioVideoProcessor -{ -public: - Matcher (Log* log, int sample_rate, float frames_per_second); - void process_video (boost::shared_ptr<Image> i, bool, boost::shared_ptr<Subtitle> s); - void process_audio (boost::shared_ptr<AudioBuffers>); - void process_end (); - -private: - int _sample_rate; - float _frames_per_second; - int _video_frames; - int64_t _audio_frames; - boost::optional<AVPixelFormat> _pixel_format; - boost::optional<Size> _size; - boost::optional<int> _channels; -}; diff --git a/src/wx/film_list.h b/src/lib/moving_image.h index 5a4ac3cc1..a81403dbd 100644 --- a/src/wx/film_list.h +++ b/src/lib/moving_image.h @@ -17,25 +17,19 @@ */ -#include <string> -#include <vector> -#include <gtkmm.h> +class MovingImageContent; -class Film; - -class FilmList +class MovingImage { public: - FilmList (std::string); - - Gtk::Widget& widget (); - - sigc::signal<void, Film const *> SelectionChanged; + MovingImage (boost::shared_ptr<const MovingImageContent> c) + : _moving_image_content (c) + {} -private: - void selection_changed (); + boost::shared_ptr<const MovingImageContent> content () const { + return _moving_image_content; + } - std::string _directory; - std::vector<Film const *> _films; - Gtk::ListViewText _list; +protected: + boost::shared_ptr<const MovingImageContent> _moving_image_content; }; diff --git a/src/lib/moving_image_content.cc b/src/lib/moving_image_content.cc new file mode 100644 index 000000000..a72ad6e8e --- /dev/null +++ b/src/lib/moving_image_content.cc @@ -0,0 +1,118 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "moving_image_content.h" +#include "moving_image_examiner.h" +#include "config.h" +#include "compose.hpp" +#include "film.h" +#include "job.h" + +#include "i18n.h" + +using std::string; +using std::cout; +using std::list; +using std::stringstream; +using std::vector; +using boost::shared_ptr; + +MovingImageContent::MovingImageContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , VideoContent (f, p) +{ + +} + +MovingImageContent::MovingImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) + , VideoContent (f, node) +{ + list<shared_ptr<cxml::Node> > c = node->node_children ("File"); + for (list<shared_ptr<cxml::Node> >::const_iterator i = c.begin(); i != c.end(); ++i) { + _files.push_back ((*i)->content ()); + } +} + +string +MovingImageContent::summary () const +{ + /* Get the string() here so that the name does not have quotes around it */ + return String::compose (_("%1 [moving images]"), path().filename().string()); +} + +string +MovingImageContent::technical_summary () const +{ + return Content::technical_summary() + " - " + + VideoContent::technical_summary() + " - " + + "moving"; +} + +void +MovingImageContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("MovingImage"); + Content::as_xml (node); + VideoContent::as_xml (node); + + for (vector<boost::filesystem::path>::const_iterator i = _files.begin(); i != _files.end(); ++i) { + node->add_child("File")->add_child_text (i->filename().string()); + } +} + +void +MovingImageContent::examine (shared_ptr<Job> job) +{ + job->descend (0.5); + Content::examine (job); + job->ascend (); + + shared_ptr<const Film> film = _film.lock (); + assert (film); + + job->descend (0.5); + shared_ptr<MovingImageExaminer> examiner (new MovingImageExaminer (film, shared_from_this(), job)); + job->ascend (); + + take_from_video_examiner (examiner); + + _video_length = examiner->files().size (); + _files = examiner->files (); +} + +Time +MovingImageContent::full_length () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + FrameRateConversion frc (video_frame_rate(), film->video_frame_rate ()); + return video_length() * frc.factor() * TIME_HZ / video_frame_rate(); +} + +string +MovingImageContent::identifier () const +{ + stringstream s; + s << VideoContent::identifier (); + s << "_" << video_length(); + return s.str (); +} diff --git a/src/lib/moving_image_content.h b/src/lib/moving_image_content.h new file mode 100644 index 000000000..1a64750fe --- /dev/null +++ b/src/lib/moving_image_content.h @@ -0,0 +1,57 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_MOVING_IMAGE_CONTENT_H +#define DCPOMATIC_MOVING_IMAGE_CONTENT_H + +#include <boost/enable_shared_from_this.hpp> +#include "video_content.h" + +namespace cxml { + class Node; +} + +/** A directory of image files which are to be presented as a movie */ +class MovingImageContent : public VideoContent +{ +public: + MovingImageContent (boost::shared_ptr<const Film>, boost::filesystem::path); + MovingImageContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + boost::shared_ptr<MovingImageContent> shared_from_this () { + return boost::dynamic_pointer_cast<MovingImageContent> (Content::shared_from_this ()); + }; + + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + void as_xml (xmlpp::Node *) const; + Time full_length () const; + + std::string identifier () const; + + std::vector<boost::filesystem::path> const & files () const { + return _files; + } + +private: + std::vector<boost::filesystem::path> _files; +}; + +#endif diff --git a/src/lib/moving_image_decoder.cc b/src/lib/moving_image_decoder.cc new file mode 100644 index 000000000..096096063 --- /dev/null +++ b/src/lib/moving_image_decoder.cc @@ -0,0 +1,85 @@ +/* + Copyright (C) 2012-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 <iostream> +#include <boost/filesystem.hpp> +#include <Magick++.h> +#include "moving_image_content.h" +#include "moving_image_decoder.h" +#include "image.h" +#include "film.h" +#include "exceptions.h" + +#include "i18n.h" + +using std::cout; +using boost::shared_ptr; +using libdcp::Size; + +MovingImageDecoder::MovingImageDecoder (shared_ptr<const Film> f, shared_ptr<const MovingImageContent> c) + : Decoder (f) + , VideoDecoder (f, c) + , MovingImage (c) +{ + +} + +void +MovingImageDecoder::pass () +{ + if (_video_position >= _moving_image_content->video_length ()) { + return; + } + + boost::filesystem::path path = _moving_image_content->path (); + path /= _moving_image_content->files()[_video_position]; + + Magick::Image* magick_image = new Magick::Image (path.string()); + libdcp::Size size (magick_image->columns(), magick_image->rows()); + + shared_ptr<Image> image (new Image (PIX_FMT_RGB24, size, false)); + + using namespace MagickCore; + + uint8_t* p = image->data()[0]; + for (int y = 0; y < size.height; ++y) { + for (int x = 0; x < size.width; ++x) { + Magick::Color c = magick_image->pixelColor (x, y); + *p++ = c.redQuantum() * 255 / QuantumRange; + *p++ = c.greenQuantum() * 255 / QuantumRange; + *p++ = c.blueQuantum() * 255 / QuantumRange; + } + } + + delete magick_image; + + video (image, false, _video_position); +} + +void +MovingImageDecoder::seek (VideoContent::Frame frame, bool) +{ + _video_position = frame; +} + +bool +MovingImageDecoder::done () const +{ + return _video_position >= _moving_image_content->video_length (); +} diff --git a/src/lib/moving_image_decoder.h b/src/lib/moving_image_decoder.h new file mode 100644 index 000000000..5cc8b32b9 --- /dev/null +++ b/src/lib/moving_image_decoder.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2012-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 "video_decoder.h" +#include "moving_image.h" + +namespace Magick { + class Image; +} + +class MovingImageContent; + +class MovingImageDecoder : public VideoDecoder, public MovingImage +{ +public: + MovingImageDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const MovingImageContent>); + + /* Decoder */ + + void pass (); + void seek (VideoContent::Frame, bool); + bool done () const; +}; + diff --git a/src/lib/moving_image_examiner.cc b/src/lib/moving_image_examiner.cc new file mode 100644 index 000000000..077545aed --- /dev/null +++ b/src/lib/moving_image_examiner.cc @@ -0,0 +1,110 @@ +/* + 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 <iostream> +#include <boost/lexical_cast.hpp> +#include <Magick++.h> +#include "moving_image_content.h" +#include "moving_image_examiner.h" +#include "film.h" +#include "job.h" +#include "exceptions.h" + +#include "i18n.h" + +using std::cout; +using std::list; +using std::sort; +using boost::shared_ptr; +using boost::lexical_cast; + +MovingImageExaminer::MovingImageExaminer (shared_ptr<const Film> film, shared_ptr<const MovingImageContent> content, shared_ptr<Job> job) + : MovingImage (content) + , _film (film) + , _video_length (0) +{ + list<unsigned int> frames; + unsigned int files = 0; + + for (boost::filesystem::directory_iterator i(content->path()); i != boost::filesystem::directory_iterator(); ++i) { + if (boost::filesystem::is_regular_file (i->path ())) { + ++files; + } + } + + int j = 0; + for (boost::filesystem::directory_iterator i(content->path()); i != boost::filesystem::directory_iterator(); ++i) { + if (!boost::filesystem::is_regular_file (i->path ())) { + continue; + } + + if (valid_image_file (i->path ())) { + int n = lexical_cast<int> (i->path().stem().string()); + frames.push_back (n); + _files.push_back (i->path().filename ()); + + if (!_video_size) { + using namespace MagickCore; + Magick::Image* image = new Magick::Image (i->path().string()); + _video_size = libdcp::Size (image->columns(), image->rows()); + delete image; + } + } + + job->set_progress (float (j) / files); + ++j; + } + + frames.sort (); + sort (_files.begin(), _files.end ()); + + if (frames.size() < 2) { + throw StringError (String::compose (_("only %1 file(s) found in moving image directory"), frames.size ())); + } + + if (frames.front() != 0 && frames.front() != 1) { + throw StringError (String::compose (_("first frame in moving image directory is number %1"), frames.front ())); + } + + if (frames.back() != frames.size() && frames.back() != (frames.size() - 1)) { + throw StringError (String::compose (_("there are %1 images in the directory but the last one is number %2"), frames.size(), frames.back ())); + } + + _video_length = frames.size (); +} + +libdcp::Size +MovingImageExaminer::video_size () const +{ + return _video_size.get (); +} + +int +MovingImageExaminer::video_length () const +{ + cout << "ex video length is " << _video_length << "\n"; + return _video_length; +} + +float +MovingImageExaminer::video_frame_rate () const +{ + return 24; +} + diff --git a/src/lib/combiner.h b/src/lib/moving_image_examiner.h index 7fad1aeae..db6845ee5 100644 --- a/src/lib/combiner.h +++ b/src/lib/moving_image_examiner.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,26 +17,31 @@ */ -/** @file src/lib/combiner.h - * @brief Class for combining two video streams. - */ +#include "moving_image.h" +#include "video_examiner.h" -#include "processor.h" +namespace Magick { + class Image; +} -/** @class Combiner - * @brief A class which can combine two video streams into one, with - * one image used for the left half of the screen and the other for - * the right. - */ -class Combiner : public VideoProcessor +class MovingImageContent; + +class MovingImageExaminer : public MovingImage, public VideoExaminer { public: - Combiner (Log* log); + MovingImageExaminer (boost::shared_ptr<const Film>, boost::shared_ptr<const MovingImageContent>, boost::shared_ptr<Job>); + + float video_frame_rate () const; + libdcp::Size video_size () const; + VideoContent::Frame video_length () const; - void process_video (boost::shared_ptr<Image> i, bool, boost::shared_ptr<Subtitle> s); - void process_video_b (boost::shared_ptr<Image> i, bool, boost::shared_ptr<Subtitle> s); + std::vector<boost::filesystem::path> const & files () const { + return _files; + } private: - /** The image that we are currently working on */ - boost::shared_ptr<Image> _image; + boost::weak_ptr<const Film> _film; + boost::optional<libdcp::Size> _video_size; + VideoContent::Frame _video_length; + std::vector<boost::filesystem::path> _files; }; diff --git a/src/lib/options.h b/src/lib/options.h deleted file mode 100644 index 55b066a2d..000000000 --- a/src/lib/options.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/options.h - * @brief Options for a transcoding operation. - */ - -#include <string> -#include <iomanip> -#include <sstream> -#include <boost/optional.hpp> -#include "util.h" - -/** @class EncodeOptions - * @brief EncodeOptions for an encoding operation. - * - * These are settings which may be different, in different circumstances, for - * the same film; ie they are options for a particular operation. - */ -class EncodeOptions -{ -public: - - EncodeOptions (std::string f, std::string e, std::string m) - : padding (0) - , video_skip (0) - , _frame_out_path (f) - , _frame_out_extension (e) - , _multichannel_audio_out_path (m) - {} - - /** @return The path to write video frames to */ - std::string frame_out_path () const { - return _frame_out_path; - } - - /** @param f Source frame index. - * @param t true to return a temporary file path, otherwise a permanent one. - * @return The path to write this video frame to. - */ - std::string frame_out_path (SourceFrame f, bool t) const { - std::stringstream s; - s << _frame_out_path << "/"; - s.width (8); - s << std::setfill('0') << f << _frame_out_extension; - - if (t) { - s << ".tmp"; - } - - return s.str (); - } - - std::string hash_out_path (SourceFrame f, bool t) const { - return frame_out_path (f, t) + ".md5"; - } - - /** @return Path to write multichannel audio data to */ - std::string multichannel_audio_out_path () const { - return _multichannel_audio_out_path; - } - - /** @param c Audio channel index. - * @param t true to return a temporary file path, otherwise a permanent one. - * @return The path to write this audio file to. - */ - std::string multichannel_audio_out_path (int c, bool t) const { - std::stringstream s; - s << _multichannel_audio_out_path << "/" << (c + 1) << ".wav"; - if (t) { - s << ".tmp"; - } - - return s.str (); - } - - Size out_size; ///< size of output images - int padding; ///< number of pixels of padding (in terms of the output size) each side of the image - - /** Range of video frames to encode (in DCP frames) */ - boost::optional<std::pair<int, int> > video_range; - /** Range of audio frames to decode (in the DCP's sampling rate) */ - boost::optional<std::pair<int64_t, int64_t> > audio_range; - - /** Skip frames such that we don't decode any frame where (index % decode_video_skip) != 0; e.g. - * 1 for every frame, 2 for every other frame, etc. - */ - SourceFrame video_skip; - -private: - /** Path of the directory to write video frames to */ - std::string _frame_out_path; - /** Extension to use for video frame files (including the leading .) */ - std::string _frame_out_extension; - /** Path of the directory to write audio files to */ - std::string _multichannel_audio_out_path; -}; - - -class DecodeOptions -{ -public: - DecodeOptions () - : decode_audio (true) - , decode_subtitles (false) - , video_sync (true) - {} - - bool decode_audio; - bool decode_subtitles; - bool video_sync; -}; diff --git a/src/lib/player.cc b/src/lib/player.cc new file mode 100644 index 000000000..70d6fa877 --- /dev/null +++ b/src/lib/player.cc @@ -0,0 +1,654 @@ +/* + 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 <stdint.h> +#include "player.h" +#include "film.h" +#include "ffmpeg_decoder.h" +#include "ffmpeg_content.h" +#include "still_image_decoder.h" +#include "still_image_content.h" +#include "moving_image_decoder.h" +#include "moving_image_content.h" +#include "sndfile_decoder.h" +#include "sndfile_content.h" +#include "subtitle_content.h" +#include "playlist.h" +#include "job.h" +#include "image.h" +#include "ratio.h" +#include "resampler.h" +#include "log.h" +#include "scaler.h" + +using std::list; +using std::cout; +using std::min; +using std::max; +using std::vector; +using std::pair; +using std::map; +using boost::shared_ptr; +using boost::weak_ptr; +using boost::dynamic_pointer_cast; + +//#define DEBUG_PLAYER 1 + +class Piece +{ +public: + Piece (shared_ptr<Content> c) + : content (c) + , video_position (c->position ()) + , audio_position (c->position ()) + {} + + Piece (shared_ptr<Content> c, shared_ptr<Decoder> d) + : content (c) + , decoder (d) + , video_position (c->position ()) + , audio_position (c->position ()) + {} + + shared_ptr<Content> content; + shared_ptr<Decoder> decoder; + Time video_position; + Time audio_position; +}; + +#ifdef DEBUG_PLAYER +std::ostream& operator<<(std::ostream& s, Piece const & p) +{ + if (dynamic_pointer_cast<FFmpegContent> (p.content)) { + s << "\tffmpeg "; + } else if (dynamic_pointer_cast<StillImageContent> (p.content)) { + s << "\tstill image"; + } else if (dynamic_pointer_cast<SndfileContent> (p.content)) { + s << "\tsndfile "; + } + + s << " at " << p.content->position() << " until " << p.content->end(); + + return s; +} +#endif + +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) +{ + _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)); + _film_changed_connection = _film->Changed.connect (bind (&Player::film_changed, this, _1)); + set_video_container_size (_film->container()->size (_film->full_frame ())); +} + +void +Player::disable_video () +{ + _video = false; +} + +void +Player::disable_audio () +{ + _audio = false; +} + +bool +Player::pass () +{ + if (!_have_valid_pieces) { + setup_pieces (); + _have_valid_pieces = true; + } + +#ifdef DEBUG_PLAYER + cout << "= PASS\n"; +#endif + + Time earliest_t = TIME_MAX; + shared_ptr<Piece> earliest; + enum { + VIDEO, + AUDIO + } type = VIDEO; + + for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { + if ((*i)->decoder->done ()) { + continue; + } + + if (_video && dynamic_pointer_cast<VideoDecoder> ((*i)->decoder)) { + if ((*i)->video_position < earliest_t) { + earliest_t = (*i)->video_position; + earliest = *i; + type = VIDEO; + } + } + + if (_audio && dynamic_pointer_cast<AudioDecoder> ((*i)->decoder)) { + if ((*i)->audio_position < earliest_t) { + earliest_t = (*i)->audio_position; + earliest = *i; + type = AUDIO; + } + } + } + + if (!earliest) { +#ifdef DEBUG_PLAYER + cout << "no earliest piece.\n"; +#endif + + flush (); + return true; + } + + switch (type) { + case VIDEO: + if (earliest_t > _video_position) { +#ifdef DEBUG_PLAYER + cout << "no video here; emitting black frame (earliest=" << earliest_t << ", video_position=" << _video_position << ").\n"; +#endif + emit_black (); + } else { +#ifdef DEBUG_PLAYER + cout << "Pass video " << *earliest << "\n"; +#endif + earliest->decoder->pass (); + } + break; + + case AUDIO: + if (earliest_t > _audio_position) { +#ifdef DEBUG_PLAYER + cout << "no audio here (none until " << earliest_t << "); emitting silence.\n"; +#endif + emit_silence (_film->time_to_audio_frames (earliest_t - _audio_position)); + } else { +#ifdef DEBUG_PLAYER + cout << "Pass audio " << *earliest << "\n"; +#endif + 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 ()); + } + } + } + } + break; + } + + if (_audio) { + Time audio_done_up_to = TIME_MAX; + for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { + if (dynamic_pointer_cast<AudioDecoder> ((*i)->decoder)) { + audio_done_up_to = min (audio_done_up_to, (*i)->audio_position); + } + } + + TimedAudioBuffers<Time> tb = _audio_merger.pull (audio_done_up_to); + Audio (tb.audio, tb.time); + _audio_position += _film->audio_frames_to_time (tb.audio->frames ()); + } + +#ifdef DEBUG_PLAYER + cout << "\tpost pass _video_position=" << _video_position << " _audio_position=" << _audio_position << "\n"; +#endif + + return false; +} + +void +Player::process_video (weak_ptr<Piece> weak_piece, shared_ptr<const Image> image, Eyes eyes, bool same, VideoContent::Frame frame) +{ + shared_ptr<Piece> piece = weak_piece.lock (); + if (!piece) { + return; + } + + shared_ptr<VideoContent> content = dynamic_pointer_cast<VideoContent> (piece->content); + assert (content); + + FrameRateConversion frc (content->video_frame_rate(), _film->video_frame_rate()); + if (frc.skip && (frame % 2) == 1) { + return; + } + + Time const relative_time = (frame * frc.factor() * TIME_HZ / _film->video_frame_rate()); + if (content->trimmed (relative_time)) { + return; + } + + /* Convert to RGB first, as FFmpeg doesn't seem to like handling YUV images with odd widths */ + shared_ptr<Image> work_image = image->scale (image->size (), _film->scaler(), PIX_FMT_RGB24, true); + + work_image = work_image->crop (content->crop(), true); + + libdcp::Size const image_size = content->ratio()->size (_video_container_size); + + work_image = work_image->scale (image_size, _film->scaler(), PIX_FMT_RGB24, true); + + Time time = content->position() + relative_time - content->trim_start (); + + if (_film->with_subtitles () && _out_subtitle.image && time >= _out_subtitle.from && time <= _out_subtitle.to) { + work_image->alpha_blend (_out_subtitle.image, _out_subtitle.position); + } + + if (image_size != _video_container_size) { + assert (image_size.width <= _video_container_size.width); + assert (image_size.height <= _video_container_size.height); + shared_ptr<Image> im (new Image (PIX_FMT_RGB24, _video_container_size, true)); + im->make_black (); + im->copy (work_image, Position<int> ((_video_container_size.width - image_size.width) / 2, (_video_container_size.height - image_size.height) / 2)); + work_image = im; + } + +#ifdef DCPOMATIC_DEBUG + _last_video = piece->content; +#endif + + Video (work_image, eyes, content->colour_conversion(), same, time); + time += TIME_HZ / _film->video_frame_rate(); + + if (frc.repeat) { + Video (work_image, eyes, content->colour_conversion(), true, time); + time += TIME_HZ / _film->video_frame_rate(); + } + + _last_emit_was_black = false; + + _video_position = piece->video_position = time; +} + +void +Player::process_audio (weak_ptr<Piece> weak_piece, shared_ptr<const AudioBuffers> audio, AudioContent::Frame frame) +{ + shared_ptr<Piece> piece = weak_piece.lock (); + if (!piece) { + return; + } + + shared_ptr<AudioContent> content = dynamic_pointer_cast<AudioContent> (piece->content); + assert (content); + + /* 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); + + if (content->trimmed (relative_time)) { + return; + } + + Time time = content->position() + (content->audio_delay() * TIME_HZ / 1000) + relative_time; + + /* Remap channels */ + shared_ptr<AudioBuffers> dcp_mapped (new AudioBuffers (_film->audio_channels(), audio->frames())); + dcp_mapped->make_silent (); + list<pair<int, libdcp::Channel> > map = content->audio_mapping().content_to_dcp (); + for (list<pair<int, libdcp::Channel> >::iterator i = map.begin(); i != map.end(); ++i) { + if (i->first < audio->channels() && i->second < dcp_mapped->channels()) { + dcp_mapped->accumulate_channel (audio.get(), i->first, i->second); + } + } + + 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; + } + + shared_ptr<AudioBuffers> trimmed (new AudioBuffers (audio->channels(), audio->frames() - frames)); + trimmed->copy_from (audio.get(), audio->frames() - frames, frames, 0); + + audio = trimmed; + time = 0; + } + + _audio_merger.push (audio, time); + piece->audio_position += _film->audio_frames_to_time (audio->frames ()); +} + +void +Player::flush () +{ + TimedAudioBuffers<Time> tb = _audio_merger.flush (); + if (tb.audio) { + Audio (tb.audio, tb.time); + _audio_position += _film->audio_frames_to_time (tb.audio->frames ()); + } + + while (_video_position < _audio_position) { + emit_black (); + } + + while (_audio_position < _video_position) { + emit_silence (_film->time_to_audio_frames (_video_position - _audio_position)); + } + +} + +/** 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) +{ + if (!_have_valid_pieces) { + setup_pieces (); + _have_valid_pieces = true; + } + + if (_pieces.empty ()) { + return; + } + + 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; + } + + Time s = t - vc->position (); + s = max (static_cast<Time> (0), s); + s = min (vc->length_after_trim(), s); + + (*i)->video_position = (*i)->audio_position = vc->position() + s; + + FrameRateConversion frc (vc->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. + */ + VideoContent::Frame f = (s + vc->trim_start ()) * _film->video_frame_rate() / (frc.factor() * TIME_HZ); + dynamic_pointer_cast<VideoDecoder>((*i)->decoder)->seek (f, accurate); + } + + _video_position = _audio_position = t; + + /* XXX: don't seek audio because we don't need to... */ +} + +void +Player::setup_pieces () +{ + list<shared_ptr<Piece> > old_pieces = _pieces; + + _pieces.clear (); + + ContentList content = _playlist->content (); + sort (content.begin(), content.end(), ContentSorter ()); + + for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { + + shared_ptr<Piece> piece (new Piece (*i)); + + /* XXX: into content? */ + + shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i); + if (fc) { + shared_ptr<FFmpegDecoder> fd (new FFmpegDecoder (_film, fc, _video, _audio)); + + fd->Video.connect (bind (&Player::process_video, this, piece, _1, _2, _3, _4)); + fd->Audio.connect (bind (&Player::process_audio, this, piece, _1, _2)); + fd->Subtitle.connect (bind (&Player::process_subtitle, this, piece, _1, _2, _3, _4)); + + piece->decoder = fd; + } + + shared_ptr<const StillImageContent> ic = dynamic_pointer_cast<const StillImageContent> (*i); + if (ic) { + shared_ptr<StillImageDecoder> id; + + /* See if we can re-use an old StillImageDecoder */ + for (list<shared_ptr<Piece> >::const_iterator j = old_pieces.begin(); j != old_pieces.end(); ++j) { + shared_ptr<StillImageDecoder> imd = dynamic_pointer_cast<StillImageDecoder> ((*j)->decoder); + if (imd && imd->content() == ic) { + id = imd; + } + } + + if (!id) { + id.reset (new StillImageDecoder (_film, ic)); + id->Video.connect (bind (&Player::process_video, this, piece, _1, _2, _3, _4)); + } + + piece->decoder = id; + } + + shared_ptr<const MovingImageContent> mc = dynamic_pointer_cast<const MovingImageContent> (*i); + if (mc) { + shared_ptr<MovingImageDecoder> md; + + if (!md) { + md.reset (new MovingImageDecoder (_film, mc)); + md->Video.connect (bind (&Player::process_video, this, piece, _1, _2, _3, _4)); + } + + piece->decoder = md; + } + + 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, piece, _1, _2)); + + piece->decoder = sd; + } + + _pieces.push_back (piece); + } + +#ifdef DEBUG_PLAYER + cout << "=== Player setup:\n"; + for (list<shared_ptr<Piece> >::iterator i = _pieces.begin(); i != _pieces.end(); ++i) { + cout << *(i->get()) << "\n"; + } +#endif +} + +void +Player::content_changed (weak_ptr<Content> w, int property, bool frequent) +{ + shared_ptr<Content> c = w.lock (); + if (!c) { + return; + } + + if ( + property == ContentProperty::POSITION || property == ContentProperty::LENGTH || + property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END || + property == VideoContentProperty::VIDEO_CROP || property == VideoContentProperty::VIDEO_RATIO + ) { + + _have_valid_pieces = false; + Changed (frequent); + + } else if (property == SubtitleContentProperty::SUBTITLE_OFFSET || property == SubtitleContentProperty::SUBTITLE_SCALE) { + update_subtitle (); + Changed (frequent); + } else if (property == VideoContentProperty::VIDEO_FRAME_TYPE) { + Changed (frequent); + } +} + +void +Player::playlist_changed () +{ + _have_valid_pieces = false; + Changed (false); +} + +void +Player::set_video_container_size (libdcp::Size s) +{ + _video_container_size = s; + _black_frame.reset (new Image (PIX_FMT_RGB24, _video_container_size, true)); + _black_frame->make_black (); +} + +shared_ptr<Resampler> +Player::resampler (shared_ptr<AudioContent> c, bool create) +{ + map<shared_ptr<AudioContent>, shared_ptr<Resampler> >::iterator i = _resamplers.find (c); + if (i != _resamplers.end ()) { + return i->second; + } + + if (!create) { + return shared_ptr<Resampler> (); + } + + shared_ptr<Resampler> r (new Resampler (c->content_audio_frame_rate(), c->output_audio_frame_rate(), c->audio_channels())); + _resamplers[c] = r; + return r; +} + +void +Player::emit_black () +{ +#ifdef DCPOMATIC_DEBUG + _last_video.reset (); +#endif + + Video (_black_frame, EYES_BOTH, ColourConversion(), _last_emit_was_black, _video_position); + _video_position += _film->video_frames_to_time (1); + _last_emit_was_black = true; +} + +void +Player::emit_silence (OutputAudioFrame most) +{ + if (most == 0) { + return; + } + + OutputAudioFrame N = min (most, _film->audio_frame_rate() / 2); + shared_ptr<AudioBuffers> silence (new AudioBuffers (_film->audio_channels(), N)); + silence->make_silent (); + Audio (silence, _audio_position); + _audio_position += _film->audio_frames_to_time (N); +} + +void +Player::film_changed (Film::Property p) +{ + /* Here we should notice Film properties that affect our output, and + alert listeners that our output now would be different to how it was + last time we were run. + */ + + if (p == Film::SCALER || p == Film::WITH_SUBTITLES || p == Film::CONTAINER) { + Changed (false); + } +} + +void +Player::process_subtitle (weak_ptr<Piece> weak_piece, shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to) +{ + _in_subtitle.piece = weak_piece; + _in_subtitle.image = image; + _in_subtitle.rect = rect; + _in_subtitle.from = from; + _in_subtitle.to = to; + + update_subtitle (); +} + +void +Player::update_subtitle () +{ + shared_ptr<Piece> piece = _in_subtitle.piece.lock (); + if (!piece) { + return; + } + + if (!_in_subtitle.image) { + _out_subtitle.image.reset (); + return; + } + + shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content); + assert (sc); + + dcpomatic::Rect<double> in_rect = _in_subtitle.rect; + libdcp::Size scaled_size; + + in_rect.y += sc->subtitle_offset (); + + /* We will scale the subtitle up to fit _video_container_size, and also by the additional subtitle_scale */ + scaled_size.width = in_rect.width * _video_container_size.width * sc->subtitle_scale (); + scaled_size.height = in_rect.height * _video_container_size.height * sc->subtitle_scale (); + + /* Then we need a corrective translation, consisting of two parts: + * + * 1. that which is the result of the scaling of the subtitle by _video_container_size; this will be + * rect.x * _video_container_size.width and rect.y * _video_container_size.height. + * + * 2. that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be + * (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and + * (height_before_subtitle_scale * (1 - subtitle_scale) / 2). + * + * Combining these two translations gives these expressions. + */ + + _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2))); + _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2))); + + _out_subtitle.image = _in_subtitle.image->scale ( + scaled_size, + Scaler::from_id ("bicubic"), + _in_subtitle.image->pixel_format (), + true + ); + _out_subtitle.from = _in_subtitle.from + piece->content->position (); + _out_subtitle.to = _in_subtitle.to + piece->content->position (); +} diff --git a/src/lib/player.h b/src/lib/player.h new file mode 100644 index 000000000..2261f66ea --- /dev/null +++ b/src/lib/player.h @@ -0,0 +1,146 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_PLAYER_H +#define DCPOMATIC_PLAYER_H + +#include <list> +#include <boost/shared_ptr.hpp> +#include <boost/enable_shared_from_this.hpp> +#include "playlist.h" +#include "content.h" +#include "film.h" +#include "rect.h" +#include "audio_merger.h" +#include "audio_content.h" + +class Job; +class Film; +class Playlist; +class AudioContent; +class Piece; +class Image; +class Resampler; + +/** @class Player + * @brief A class which can `play' a Playlist; emitting its audio and video. + */ + +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); + + Time video_position () const { + return _video_position; + } + + void set_video_container_size (libdcp::Size); + + /** Emitted when a video frame is ready. + * First parameter is the video image. + * Second parameter is the eye(s) that should see this image. + * Third parameter is the colour conversion that should be used for this image. + * Fourth parameter is true if the image is the same as the last one that was emitted. + * Fifth parameter is the time. + */ + boost::signals2::signal<void (boost::shared_ptr<const Image>, Eyes, ColourConversion, bool, Time)> Video; + + /** 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. + * + * The parameter is true if these signals are currently likely to be frequent. + */ + boost::signals2::signal<void (bool)> Changed; + +private: + friend class PlayerWrapper; + + void process_video (boost::weak_ptr<Piece>, boost::shared_ptr<const Image>, Eyes, bool, VideoContent::Frame); + void process_audio (boost::weak_ptr<Piece>, boost::shared_ptr<const AudioBuffers>, AudioContent::Frame); + 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 (); + + 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<Image> _black_frame; + std::map<boost::shared_ptr<AudioContent>, boost::shared_ptr<Resampler> > _resamplers; + + struct { + boost::weak_ptr<Piece> piece; + boost::shared_ptr<Image> image; + dcpomatic::Rect<double> rect; + Time from; + Time to; + } _in_subtitle; + + struct { + boost::shared_ptr<Image> image; + Position<int> position; + Time from; + Time to; + } _out_subtitle; + +#ifdef DCPOMATIC_DEBUG + boost::shared_ptr<Content> _last_video; +#endif + + bool _last_emit_was_black; + + boost::signals2::scoped_connection _playlist_changed_connection; + boost::signals2::scoped_connection _playlist_content_changed_connection; + boost::signals2::scoped_connection _film_changed_connection; +}; + +#endif diff --git a/src/lib/playlist.cc b/src/lib/playlist.cc new file mode 100644 index 000000000..de48ff5f5 --- /dev/null +++ b/src/lib/playlist.cc @@ -0,0 +1,330 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include <boost/shared_ptr.hpp> +#include <boost/lexical_cast.hpp> +#include "playlist.h" +#include "sndfile_content.h" +#include "sndfile_decoder.h" +#include "video_content.h" +#include "ffmpeg_decoder.h" +#include "ffmpeg_content.h" +#include "still_image_decoder.h" +#include "still_image_content.h" +#include "content_factory.h" +#include "job.h" +#include "config.h" +#include "util.h" + +#include "i18n.h" + +using std::list; +using std::cout; +using std::vector; +using std::min; +using std::max; +using std::string; +using std::stringstream; +using std::pair; +using boost::optional; +using boost::shared_ptr; +using boost::weak_ptr; +using boost::dynamic_pointer_cast; +using boost::lexical_cast; + +Playlist::Playlist () + : _sequence_video (true) + , _sequencing_video (false) +{ + +} + +Playlist::~Playlist () +{ + _content.clear (); + reconnect (); +} + +void +Playlist::content_changed (weak_ptr<Content> content, int property, bool frequent) +{ + if (property == ContentProperty::LENGTH) { + maybe_sequence_video (); + } + + ContentChanged (content, property, frequent); +} + +void +Playlist::maybe_sequence_video () +{ + if (!_sequence_video || _sequencing_video) { + return; + } + + _sequencing_video = true; + + ContentList cl = _content; + sort (cl.begin(), cl.end(), ContentSorter ()); + Time last = 0; + for (ContentList::iterator i = cl.begin(); i != cl.end(); ++i) { + if (!dynamic_pointer_cast<VideoContent> (*i)) { + continue; + } + + (*i)->set_position (last); + last = (*i)->end (); + } + + _sequencing_video = false; +} + +string +Playlist::video_identifier () const +{ + string t; + + for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { + shared_ptr<const VideoContent> vc = dynamic_pointer_cast<const VideoContent> (*i); + if (vc) { + t += vc->identifier (); + } + } + + return md5_digest (t.c_str(), t.length()); +} + +/** @param node <Playlist> node */ +void +Playlist::set_from_xml (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node) +{ + list<shared_ptr<cxml::Node> > c = node->node_children ("Content"); + for (list<shared_ptr<cxml::Node> >::iterator i = c.begin(); i != c.end(); ++i) { + _content.push_back (content_factory (film, *i)); + } + + reconnect (); +} + +/** @param node <Playlist> node */ +void +Playlist::as_xml (xmlpp::Node* node) +{ + for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) { + (*i)->as_xml (node->add_child ("Content")); + } +} + +void +Playlist::add (shared_ptr<Content> c) +{ + _content.push_back (c); + reconnect (); + Changed (); +} + +void +Playlist::remove (shared_ptr<Content> c) +{ + ContentList::iterator i = _content.begin (); + while (i != _content.end() && *i != c) { + ++i; + } + + if (i != _content.end ()) { + _content.erase (i); + Changed (); + } +} + +void +Playlist::remove (ContentList c) +{ + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + ContentList::iterator j = _content.begin (); + while (j != _content.end() && *j != *i) { + ++j; + } + + if (j != _content.end ()) { + _content.erase (j); + } + } + + 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: + FrameRateCandidate (float source_, int dcp_) + : source (source_) + , dcp (dcp_) + {} + + float source; + int dcp; +}; + +int +Playlist::best_dcp_frame_rate () const +{ + list<int> const allowed_dcp_frame_rates = Config::instance()->allowed_dcp_frame_rates (); + + /* Work out what rates we could manage, including those achieved by using skip / repeat. */ + list<FrameRateCandidate> candidates; + + /* Start with the ones without skip / repeat so they will get matched in preference to skipped/repeated ones */ + for (list<int>::const_iterator i = allowed_dcp_frame_rates.begin(); i != allowed_dcp_frame_rates.end(); ++i) { + candidates.push_back (FrameRateCandidate (*i, *i)); + } + + /* Then the skip/repeat ones */ + for (list<int>::const_iterator i = allowed_dcp_frame_rates.begin(); i != allowed_dcp_frame_rates.end(); ++i) { + candidates.push_back (FrameRateCandidate (float (*i) / 2, *i)); + candidates.push_back (FrameRateCandidate (float (*i) * 2, *i)); + } + + /* Pick the best one */ + float error = std::numeric_limits<float>::max (); + optional<FrameRateCandidate> best; + list<FrameRateCandidate>::iterator i = candidates.begin(); + while (i != candidates.end()) { + + float this_error = 0; + for (ContentList::const_iterator j = _content.begin(); j != _content.end(); ++j) { + shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j); + if (!vc) { + continue; + } + + /* Use the largest difference between DCP and source as the "error" */ + this_error = max (this_error, float (fabs (i->source - vc->video_frame_rate ()))); + } + + if (this_error < error) { + error = this_error; + best = *i; + } + + ++i; + } + + if (!best) { + return 24; + } + + return best->dcp; +} + +Time +Playlist::length () const +{ + Time len = 0; + for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { + len = max (len, (*i)->end ()); + } + + return len; +} + +void +Playlist::reconnect () +{ + for (list<boost::signals2::connection>::iterator i = _content_connections.begin(); i != _content_connections.end(); ++i) { + i->disconnect (); + } + + _content_connections.clear (); + + for (ContentList::iterator i = _content.begin(); i != _content.end(); ++i) { + _content_connections.push_back ((*i)->Changed.connect (bind (&Playlist::content_changed, this, _1, _2, _3))); + } +} + +Time +Playlist::video_end () const +{ + Time end = 0; + for (ContentList::const_iterator i = _content.begin(); i != _content.end(); ++i) { + if (dynamic_pointer_cast<const VideoContent> (*i)) { + end = max (end, (*i)->end ()); + } + } + + return end; +} + +void +Playlist::set_sequence_video (bool s) +{ + _sequence_video = s; +} + +bool +ContentSorter::operator() (shared_ptr<Content> a, shared_ptr<Content> b) +{ + return a->position() < b->position(); +} + +/** @return content in an undefined order */ +ContentList +Playlist::content () const +{ + return _content; +} + +void +Playlist::repeat (ContentList c, int n) +{ + pair<Time, Time> range (TIME_MAX, 0); + for (ContentList::iterator i = c.begin(); i != c.end(); ++i) { + range.first = min (range.first, (*i)->position ()); + range.second = max (range.second, (*i)->position ()); + range.first = min (range.first, (*i)->end ()); + range.second = max (range.second, (*i)->end ()); + } + + Time 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 (); + copy->set_position (pos + copy->position() - range.first); + _content.push_back (copy); + } + pos += range.second - range.first; + } + + reconnect (); + Changed (); +} diff --git a/src/lib/playlist.h b/src/lib/playlist.h new file mode 100644 index 000000000..7dbf41604 --- /dev/null +++ b/src/lib/playlist.h @@ -0,0 +1,95 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_PLAYLIST_H +#define DCPOMATIC_PLAYLIST_H + +#include <list> +#include <boost/shared_ptr.hpp> +#include <boost/enable_shared_from_this.hpp> +#include "ffmpeg_content.h" +#include "audio_mapping.h" + +class Content; +class FFmpegContent; +class FFmpegDecoder; +class StillImageMagickContent; +class StillImageMagickDecoder; +class SndfileContent; +class SndfileDecoder; +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 : public boost::noncopyable +{ +public: + Playlist (); + ~Playlist (); + + void as_xml (xmlpp::Node *); + void set_from_xml (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + void add (boost::shared_ptr<Content>); + void remove (boost::shared_ptr<Content>); + void remove (ContentList); + + bool has_subtitles () const; + + ContentList content () const; + + std::string video_identifier () const; + + Time length () const; + + int best_dcp_frame_rate () const; + Time video_end () const; + + void set_sequence_video (bool); + void maybe_sequence_video (); + + void repeat (ContentList, int); + + mutable boost::signals2::signal<void ()> Changed; + /** Third parameter is true if signals are currently being emitted frequently */ + mutable boost::signals2::signal<void (boost::weak_ptr<Content>, int, bool)> ContentChanged; + +private: + void content_changed (boost::weak_ptr<Content>, int, bool); + void reconnect (); + + ContentList _content; + bool _sequence_video; + bool _sequencing_video; + std::list<boost::signals2::connection> _content_connections; +}; + +#endif diff --git a/src/lib/po/es_ES.po b/src/lib/po/es_ES.po new file mode 100644 index 000000000..0e1776ac6 --- /dev/null +++ b/src/lib/po/es_ES.po @@ -0,0 +1,658 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: LIBDCPOMATIC\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-02 19:10-0500\n" +"Last-Translator: Manuel AC <manuel.acevedo@civantos.>\n" +"Language-Team: Manuel AC <manuel.acevedo@civantos.com>\n" +"Language: es-ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/lib/sndfile_content.cc:70 +msgid "%1 channels, %2kHz, %3 samples" +msgstr "" + +#: src/lib/ffmpeg_content.cc:193 +#, fuzzy +msgid "%1 frames; %2 frames per second" +msgstr "fotogramas por segundo" + +#: src/lib/video_content.cc:131 +msgid "%1x%2 pixels (%3:1)" +msgstr "" + +#: src/lib/transcode_job.cc:81 +msgid "0%" +msgstr "0%" + +#: src/lib/ratio.cc:47 +msgid "1.19" +msgstr "1.19" + +#: src/lib/ratio.cc:50 +msgid "1.375" +msgstr "1.375" + +#: src/lib/ratio.cc:51 +msgid "1.66" +msgstr "1.66" + +#: src/lib/ratio.cc:52 +msgid "16:9" +msgstr "16:9" + +#: src/lib/filter.cc:88 +msgid "3D denoiser" +msgstr "reducción de ruido 3D" + +#: src/lib/ratio.cc:48 +msgid "4:3" +msgstr "" + +#: src/lib/ratio.cc:49 +msgid "Academy" +msgstr "Academy" + +#: src/lib/dcp_content_type.cc:53 +msgid "Advertisement" +msgstr "Publicidad" + +#: src/lib/job.cc:72 +msgid "An error occurred whilst handling the file %1." +msgstr "Ha ocurrido un error con el fichero %1." + +#: src/lib/analyse_audio_job.cc:53 +msgid "Analyse audio of %1" +msgstr "Analizar audio de %1" + +#: src/lib/scaler.cc:64 +msgid "Area" +msgstr "Área" + +#: src/lib/scaler.cc:62 +msgid "Bicubic" +msgstr "Bicúbico" + +#: src/lib/scaler.cc:69 +msgid "Bilinear" +msgstr "Bilineal" + +#: src/lib/job.cc:320 +msgid "Cancelled" +msgstr "" + +#: src/lib/exceptions.cc:60 +msgid "Cannot handle pixel format %1 during %2" +msgstr "" + +#: src/lib/util.cc:700 +msgid "Centre" +msgstr "" + +#: src/lib/scp_dcp_job.cc:109 +msgid "Copy DCP to TMS" +msgstr "Copiar DCP al TMS" + +#: src/lib/scp_dcp_job.cc:128 +msgid "Could not connect to server %1 (%2)" +msgstr "No se pudo conectar al servidor %1 (%2)" + +#: src/lib/scp_dcp_job.cc:150 +msgid "Could not create remote directory %1 (%2)" +msgstr "No se pudo crear la carpeta remota %1 (%2)" + +#: src/lib/scp_dcp_job.cc:175 +msgid "Could not open %1 to send" +msgstr "No se pudo abrir %1 para enviar" + +#: src/lib/scp_dcp_job.cc:145 +msgid "Could not start SCP session (%1)" +msgstr "No se pudo iniciar la sesión SCP (%1)" + +#: src/lib/scp_dcp_job.cc:187 +msgid "Could not write to remote file (%1)" +msgstr "No se pudo escribir el fichero remoto (%1)" + +#: src/lib/filter.cc:77 +msgid "Cubic interpolating deinterlacer" +msgstr "Desentrelazado por interpolación cúbica" + +#: src/lib/util.cc:723 +msgid "DCP and source have the same rate.\n" +msgstr "La fuente y el DCP tienen la misma velocidad.\n" + +#: src/lib/util.cc:733 +#, fuzzy +msgid "DCP will run at %1%% of the source speed.\n" +msgstr "El DCP se reproducirá al %1%% de la velocidad de la fuente.\n" + +#: src/lib/util.cc:726 +msgid "DCP will use every other frame of the source.\n" +msgstr "El DCP usará fotogramas alternos de la fuente.\n" + +#: src/lib/filter.cc:68 src/lib/filter.cc:69 src/lib/filter.cc:70 +#: src/lib/filter.cc:71 src/lib/filter.cc:72 src/lib/filter.cc:73 +msgid "De-blocking" +msgstr "De-blocking" + +#: src/lib/filter.cc:75 src/lib/filter.cc:76 src/lib/filter.cc:77 +#: src/lib/filter.cc:78 src/lib/filter.cc:79 src/lib/filter.cc:80 +#: src/lib/filter.cc:81 src/lib/filter.cc:82 src/lib/filter.cc:83 +msgid "De-interlacing" +msgstr "Desentrelazado" + +#: src/lib/filter.cc:74 +msgid "Deringing filter" +msgstr "Deringing filter" + +#: src/lib/dolby_cp750.cc:27 +msgid "Dolby CP750" +msgstr "Dolby CP750" + +#: src/lib/util.cc:728 +msgid "Each source frame will be doubled in the DCP.\n" +msgstr "Se doblará cada fotograma de la fuente en el DCP.\n" + +#: src/lib/job.cc:318 +msgid "Error (%1)" +msgstr "Error (%1)" + +#: src/lib/examine_content_job.cc:45 +msgid "Examine content" +msgstr "Examinar contenido" + +#: src/lib/filter.cc:72 +msgid "Experimental horizontal deblocking filter 1" +msgstr "Experimental horizontal deblocking filter 1" + +#: src/lib/filter.cc:73 +msgid "Experimental vertical deblocking filter 1" +msgstr "Experimental vertical deblocking filter 1" + +#: src/lib/filter.cc:79 +msgid "FFMPEG deinterlacer" +msgstr "Desentrelazado FFMPEG" + +#: src/lib/filter.cc:80 +msgid "FIR low-pass deinterlacer" +msgstr "Desentrelazado paso bajo FIR" + +#: src/lib/scp_dcp_job.cc:138 +msgid "Failed to authenticate with server (%1)" +msgstr "Fallo al identificarse con el servidor (%1)" + +#: src/lib/scaler.cc:70 +msgid "Fast Bilinear" +msgstr "Bilineal rápido" + +#: src/lib/dcp_content_type.cc:44 +msgid "Feature" +msgstr "Película" + +#: src/lib/ratio.cc:53 +msgid "Flat" +msgstr "Flat" + +#: src/lib/filter.cc:85 +msgid "Force quantizer" +msgstr "Force quantizer" + +#: src/lib/ratio.cc:55 +msgid "Full frame" +msgstr "" + +#: src/lib/scaler.cc:65 +msgid "Gaussian" +msgstr "Gaussiano" + +#: src/lib/filter.cc:86 +msgid "Gradient debander" +msgstr "Gradient debander" + +#: src/lib/filter.cc:89 +msgid "High quality 3D denoiser" +msgstr "Reductor de ruido 3D de alta calidad" + +#: src/lib/filter.cc:68 +msgid "Horizontal deblocking filter" +msgstr "Horizontal deblocking filter" + +#: src/lib/filter.cc:70 +msgid "Horizontal deblocking filter A" +msgstr "Horizontal deblocking filter A" + +#: src/lib/imagemagick_content.cc:50 +msgid "Image: %1" +msgstr "" + +#: src/lib/job.cc:96 src/lib/job.cc:105 +msgid "" +"It is not known what caused this error. The best idea is to report the " +"problem to the DCP-o-matic mailing list (carl@dcpomatic.com)" +msgstr "" +"Error desconocido. La mejor idea es informar del problema a la lista de " +"correo de DCP-o-matic (carl@dcpomatic.com)" + +#: src/lib/filter.cc:82 +msgid "Kernel deinterlacer" +msgstr "Kernel deinterlacer" + +#: src/lib/scaler.cc:66 +msgid "Lanczos" +msgstr "Lanczos" + +#: src/lib/util.cc:698 +msgid "Left" +msgstr "" + +#: src/lib/util.cc:702 +msgid "Left surround" +msgstr "" + +#: src/lib/util.cc:701 +msgid "Lfe (sub)" +msgstr "" + +#: src/lib/filter.cc:75 +msgid "Linear blend deinterlacer" +msgstr "Linear blend deinterlacer" + +#: src/lib/filter.cc:76 +msgid "Linear interpolating deinterlacer" +msgstr "Linear interpolating deinterlacer" + +#: src/lib/filter.cc:78 +msgid "Median deinterlacer" +msgstr "Median deinterlacer" + +#: src/lib/filter.cc:74 src/lib/filter.cc:85 src/lib/filter.cc:86 +#: src/lib/filter.cc:87 src/lib/filter.cc:90 +msgid "Misc" +msgstr "Miscelánea" + +#: src/lib/filter.cc:81 +msgid "Motion compensating deinterlacer" +msgstr "Motion compensating deinterlacer" + +#: src/lib/ffmpeg_content.cc:181 +msgid "Movie: %1" +msgstr "" + +#: src/lib/filter.cc:84 src/lib/filter.cc:88 src/lib/filter.cc:89 +#: src/lib/filter.cc:91 +msgid "Noise reduction" +msgstr "Reducción de ruido" + +#: src/lib/job.cc:316 +msgid "OK (ran for %1)" +msgstr "OK (ejecución %1)" + +#: src/lib/filter.cc:91 +msgid "Overcomplete wavelet denoiser" +msgstr "Overcomplete wavelet denoiser" + +#: src/lib/dcp_content_type.cc:51 +msgid "Policy" +msgstr "Policy" + +#: src/lib/dcp_content_type.cc:52 +msgid "Public Service Announcement" +msgstr "Anuncio de servicio público" + +#: src/lib/dcp_content_type.cc:49 +msgid "Rating" +msgstr "Clasificación" + +#: src/lib/util.cc:699 +msgid "Right" +msgstr "" + +#: src/lib/util.cc:703 +msgid "Right surround" +msgstr "" + +#: src/lib/scp_dcp_job.cc:133 +msgid "SSH error (%1)" +msgstr "error SSH (%1)" + +#: src/lib/ratio.cc:54 +msgid "Scope" +msgstr "Scope" + +#: src/lib/dcp_content_type.cc:45 +msgid "Short" +msgstr "Cortometraje" + +#: src/lib/scaler.cc:67 +msgid "Sinc" +msgstr "Sinc" + +#: src/lib/sndfile_content.cc:57 +#, fuzzy +msgid "Sound file: %1" +msgstr "no se pudo abrir el fichero para lectura" + +#: src/lib/scaler.cc:68 +msgid "Spline" +msgstr "Spline" + +#: src/lib/dcp_content_type.cc:50 +msgid "Teaser" +msgstr "Teaser" + +#: src/lib/filter.cc:90 +msgid "Telecine filter" +msgstr "Filtro telecine" + +#: src/lib/filter.cc:84 +msgid "Temporal noise reducer" +msgstr "Temporal noise reducer" + +#: src/lib/dcp_content_type.cc:47 +msgid "Test" +msgstr "Test" + +#: src/lib/job.cc:78 +msgid "" +"The drive that the film is stored on is low in disc space. Free some more " +"space and try again." +msgstr "" +"En el dispositivo donde se encuentra la película queda poco espacio. Libere " +"espacio en el disco y pruebe de nuevo." + +#: src/lib/film.cc:364 +msgid "" +"This film was created with an older version of DCP-o-matic, and " +"unfortunately it cannot be loaded into this version. You will need to " +"create a new Film, re-add your content and set it up again. Sorry!" +msgstr "" + +#: src/lib/dcp_content_type.cc:46 +msgid "Trailer" +msgstr "Trailer" + +#: src/lib/transcode_job.cc:50 +msgid "Transcode %1" +msgstr "Codificar %1" + +#: src/lib/dcp_content_type.cc:48 +msgid "Transitional" +msgstr "Transitional" + +#: src/lib/job.cc:104 +msgid "Unknown error" +msgstr "Error desconocido" + +#: src/lib/ffmpeg_decoder.cc:264 +msgid "Unrecognised audio sample format (%1)" +msgstr "Formato de audio desconocido (%1)" + +#: src/lib/filter.cc:87 +msgid "Unsharp mask and Gaussian blur" +msgstr "Máscara de desenfoque Gaussiano" + +#: src/lib/filter.cc:69 +msgid "Vertical deblocking filter" +msgstr "Vertical deblocking filter" + +#: src/lib/filter.cc:71 +msgid "Vertical deblocking filter A" +msgstr "Vertical deblocking filter A" + +#: src/lib/scp_dcp_job.cc:101 +msgid "Waiting" +msgstr "Esperando" + +#: src/lib/scaler.cc:63 +msgid "X" +msgstr "X" + +#: src/lib/filter.cc:83 +msgid "Yet Another Deinterlacing Filter" +msgstr "Yet Another Deinterlacing Filter" + +#: src/lib/film.cc:273 +msgid "You must add some content to the DCP before creating it" +msgstr "" + +#: src/lib/film.cc:232 +msgid "cannot contain slashes" +msgstr "no puede contener barras" + +#: src/lib/util.cc:494 +msgid "connect timed out" +msgstr "tiempo de conexión agotado" + +#: src/lib/scp_dcp_job.cc:119 +msgid "connecting" +msgstr "conectando" + +#: src/lib/film.cc:269 +#, fuzzy +msgid "container" +msgstr "contenido" + +#: src/lib/film.cc:277 +msgid "content type" +msgstr "tipo de contenido" + +#: src/lib/scp_dcp_job.cc:168 +msgid "copying %1" +msgstr "copiando %1" + +#: src/lib/exceptions.cc:36 +#, fuzzy +msgid "could not create file %1" +msgstr "No se pudo escribir el fichero remoto (%1)" + +#: src/lib/ffmpeg.cc:128 +msgid "could not find audio decoder" +msgstr "no se encontró el decodificador de audio" + +#: src/lib/ffmpeg.cc:76 +msgid "could not find stream information" +msgstr "no se pudo encontrar información del flujo" + +#: src/lib/ffmpeg_decoder.cc:498 +msgid "could not find subtitle decoder" +msgstr "no se pudo encontrar decodificador de subtítutlos" + +#: src/lib/ffmpeg.cc:107 +msgid "could not find video decoder" +msgstr "no se pudo encontrar decodificador de vídeo" + +#: src/lib/sndfile_decoder.cc:45 +#, fuzzy +msgid "could not open audio file for reading" +msgstr "no se pudo abrir el fichero para lectura" + +#: src/lib/exceptions.cc:29 +#, fuzzy +msgid "could not open file %1" +msgstr "no se pudo abrir el fichero para lectura" + +#: src/lib/dcp_video_frame.cc:263 +msgid "could not open file for reading" +msgstr "no se pudo abrir el fichero para lectura" + +#: src/lib/exceptions.cc:44 +#, fuzzy +msgid "could not read from file %1 (%2)" +msgstr "No se pudo crear la carpeta remota %1 (%2)" + +#: src/lib/resampler.cc:76 src/lib/resampler.cc:96 +msgid "could not run sample-rate converter" +msgstr "no se pudo ejecutar el conversor de velocidad" + +#: src/lib/scp_dcp_job.cc:86 +msgid "could not start SCP session (%1)" +msgstr "no se pudo abrir la sesión SCP (%1)" + +#: src/lib/scp_dcp_job.cc:52 +msgid "could not start SSH session" +msgstr "no se pudo abrir la sesión SSH" + +#: src/lib/exceptions.cc:50 +#, fuzzy +msgid "could not write to file %1 (%2)" +msgstr "No se pudo escribir el fichero remoto (%1)" + +#: src/lib/transcode_job.cc:94 +msgid "frames per second" +msgstr "fotogramas por segundo" + +#: src/lib/util.cc:146 +msgid "hour" +msgstr "hora" + +#: src/lib/util.cc:143 src/lib/util.cc:148 +msgid "hours" +msgstr "horas" + +#: src/lib/util.cc:153 +msgid "minute" +msgstr "minuto" + +#: src/lib/util.cc:155 +msgid "minutes" +msgstr "minutos" + +#: src/lib/util.cc:621 +msgid "missing key %1 in key-value set" +msgstr "falta la clave %1 en el par clave-valor" + +#: src/lib/exceptions.cc:54 +msgid "missing required setting %1" +msgstr "" + +#: src/lib/ffmpeg_decoder.cc:530 +msgid "multi-part subtitles not yet supported" +msgstr "todavía no se soportan subtítulos en múltiples partes" + +#: src/lib/film.cc:232 src/lib/film.cc:281 +msgid "name" +msgstr "nombre" + +#: src/lib/ffmpeg_decoder.cc:545 +msgid "non-bitmap subtitles not yet supported" +msgstr "todavía no se soportan subtítulos que no son en mapas de bits" + +#. / TRANSLATORS: remaining here follows an amount of time that is remaining +#. / on an operation. +#: src/lib/job.cc:313 +msgid "remaining" +msgstr "pendiente" + +#: src/lib/util.cc:158 +msgid "seconds" +msgstr "segundos" + +#~ msgid "1.66 within Flat" +#~ msgstr "1.66 en Flat" + +#~ msgid "16:9 within Flat" +#~ msgstr "16:9 en Flat" + +#, fuzzy +#~ msgid "16:9 within Scope" +#~ msgstr "16:9 en Flat" + +#~ msgid "4:3 within Flat" +#~ msgstr "4:3 en Flat" + +#~ msgid "A/B transcode %1" +#~ msgstr "Codificación A/B %1" + +#~ msgid "Cannot resample audio as libswresample is not present" +#~ msgstr "" +#~ "No se puede redimensionar el sonido porque no se encuentra libswresample" + +#~ msgid "Examine content of %1" +#~ msgstr "Examinar contenido de %1" + +#~ msgid "Flat without stretch" +#~ msgstr "Flat sin deformación" + +#~ msgid "Rec 709" +#~ msgstr "Rec 709" + +#~ msgid "Scope without stretch" +#~ msgstr "Scope sin deformación" + +#~ msgid "could not open external audio file for reading" +#~ msgstr "no se pudo leer el fichero externo de audio" + +#~ msgid "external audio files have differing lengths" +#~ msgstr "los ficheros externos de sonido tienen duraciones diferentes" + +#~ msgid "external audio files must be mono" +#~ msgstr "los ficheros externos de sonido deben ser mono" + +#~ msgid "format" +#~ msgstr "formato" + +#~ msgid "no still image files found" +#~ msgstr "no se encuentran imágenes fijas" + +#~ msgid "sRGB" +#~ msgstr "sRGB" + +#~ msgid "still" +#~ msgstr "imagen fija" + +#~ msgid "video" +#~ msgstr "vídeo" + +#~ msgid "1.33" +#~ msgstr "1.33" + +#~ msgid "Source scaled to 1.19:1" +#~ msgstr "Fuente escalada a 1.19:1" + +#~ msgid "Source scaled to 1.33:1" +#~ msgstr "Fuente escalada a 1.33:1" + +#~ msgid "Source scaled to 1.33:1 then pillarboxed to Flat" +#~ msgstr "Fuente escalada a 1.33:1 con bandas hasta Flat" + +#~ msgid "Source scaled to 1.375:1" +#~ msgstr "Fuente escalada a 1.375:1" + +#~ msgid "Source scaled to 1.37:1 (Academy ratio)" +#~ msgstr "Fuente escalada a 1.37:1 (Academy)" + +#~ msgid "Source scaled to 1.66:1" +#~ msgstr "Fuente escalada a 1.66:1" + +#~ msgid "Source scaled to 1.66:1 then pillarboxed to Flat" +#~ msgstr "Fuente escalada a 1.66:1 con bandas hasta Flat" + +#~ msgid "Source scaled to 1.78:1" +#~ msgstr "Fuente escalada a 1.78:1" + +#~ msgid "Source scaled to 1.78:1 then pillarboxed to Flat" +#~ msgstr "Fuente escalada a 1.78:1 con bandas hasta Flat" + +#~ msgid "Source scaled to Flat (1.85:1)" +#~ msgstr "Fuente escalada a Flat (1.85:1)" + +#~ msgid "Source scaled to Scope (2.39:1)" +#~ msgstr "Fuente escalada a Scope (2.39:1)" + +#~ msgid "Source scaled to fit Flat preserving its aspect ratio" +#~ msgstr "Fuente escalada a Flat conservando el ratio de aspecto" + +#~ msgid "Source scaled to fit Scope preserving its aspect ratio" +#~ msgstr "Fuente escalada a Scope conservando el ratio de aspecto" + +#~ msgid "adding to queue of %1" +#~ msgstr "añadiendo a la cola de %1" diff --git a/src/lib/po/fr_FR.po b/src/lib/po/fr_FR.po new file mode 100644 index 000000000..d13472546 --- /dev/null +++ b/src/lib/po/fr_FR.po @@ -0,0 +1,664 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic FRENCH\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-07-16 23:11+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/lib/sndfile_content.cc:70 +msgid "%1 channels, %2kHz, %3 samples" +msgstr "%1 canaux, %2kHz, %3 samples" + +#: src/lib/ffmpeg_content.cc:193 +msgid "%1 frames; %2 frames per second" +msgstr "%1 images ; %2 images par seconde" + +#: src/lib/video_content.cc:131 +msgid "%1x%2 pixels (%3:1)" +msgstr "%1x%2 pixels (%3:1)" + +#: src/lib/transcode_job.cc:81 +msgid "0%" +msgstr "0%" + +#: src/lib/ratio.cc:47 +msgid "1.19" +msgstr "1.19" + +#: src/lib/ratio.cc:50 +msgid "1.375" +msgstr "1.375" + +#: src/lib/ratio.cc:51 +msgid "1.66" +msgstr "1.66" + +#: src/lib/ratio.cc:52 +msgid "16:9" +msgstr "16:9" + +#: src/lib/filter.cc:88 +msgid "3D denoiser" +msgstr "Débruitage 3D" + +#: src/lib/ratio.cc:48 +msgid "4:3" +msgstr "4:3" + +#: src/lib/ratio.cc:49 +msgid "Academy" +msgstr "Academy" + +#: src/lib/dcp_content_type.cc:53 +msgid "Advertisement" +msgstr "Advertisement" + +#: src/lib/job.cc:72 +msgid "An error occurred whilst handling the file %1." +msgstr "Une erreur s'est produite lors du traitement du fichier %1." + +#: src/lib/analyse_audio_job.cc:53 +msgid "Analyse audio of %1" +msgstr "Analyse du son de %1" + +#: src/lib/scaler.cc:64 +msgid "Area" +msgstr "Area" + +#: src/lib/scaler.cc:62 +msgid "Bicubic" +msgstr "Bicubique" + +#: src/lib/scaler.cc:69 +msgid "Bilinear" +msgstr "Bilinéaire" + +#: src/lib/job.cc:320 +msgid "Cancelled" +msgstr "Annulé" + +#: src/lib/exceptions.cc:60 +msgid "Cannot handle pixel format %1 during %2" +msgstr "Format du pixel %1 non géré par %2" + +#: src/lib/util.cc:700 +msgid "Centre" +msgstr "Centre" + +#: src/lib/scp_dcp_job.cc:109 +msgid "Copy DCP to TMS" +msgstr "Copier le DCP dans le TMS" + +#: src/lib/scp_dcp_job.cc:128 +msgid "Could not connect to server %1 (%2)" +msgstr "Connexion au serveur %1 (%2) impossible" + +#: src/lib/scp_dcp_job.cc:150 +msgid "Could not create remote directory %1 (%2)" +msgstr "Création du dossier distant %1 (%2) impossible" + +#: src/lib/scp_dcp_job.cc:175 +msgid "Could not open %1 to send" +msgstr "Ouverture de %1 pour envoi impossible" + +#: src/lib/scp_dcp_job.cc:145 +msgid "Could not start SCP session (%1)" +msgstr "Démarrage de session SCP (%1) impossible" + +#: src/lib/scp_dcp_job.cc:187 +msgid "Could not write to remote file (%1)" +msgstr "Écriture vers fichier distant (%1) impossible" + +#: src/lib/filter.cc:77 +msgid "Cubic interpolating deinterlacer" +msgstr "Désentrelacement cubique interpolé" + +#: src/lib/util.cc:723 +msgid "DCP and source have the same rate.\n" +msgstr "Le DCP et la source ont les mêmes cadences.\n" + +#: src/lib/util.cc:733 +#, fuzzy +msgid "DCP will run at %1%% of the source speed.\n" +msgstr "La cadence du DCP sera %1%% par rapport à la source.\n" + +#: src/lib/util.cc:726 +msgid "DCP will use every other frame of the source.\n" +msgstr "Le DCP utilisera une image sur deux de la source.\n" + +#: src/lib/filter.cc:68 +#: src/lib/filter.cc:69 +#: src/lib/filter.cc:70 +#: src/lib/filter.cc:71 +#: src/lib/filter.cc:72 +#: src/lib/filter.cc:73 +msgid "De-blocking" +msgstr "De-bloc" + +#: src/lib/filter.cc:75 +#: src/lib/filter.cc:76 +#: src/lib/filter.cc:77 +#: src/lib/filter.cc:78 +#: src/lib/filter.cc:79 +#: src/lib/filter.cc:80 +#: src/lib/filter.cc:81 +#: src/lib/filter.cc:82 +#: src/lib/filter.cc:83 +msgid "De-interlacing" +msgstr "Désentrelacement" + +#: src/lib/filter.cc:74 +msgid "Deringing filter" +msgstr "Filtre anti bourdonnement" + +#: src/lib/dolby_cp750.cc:27 +msgid "Dolby CP750" +msgstr "Dolby CP750" + +#: src/lib/util.cc:728 +msgid "Each source frame will be doubled in the DCP.\n" +msgstr "Chaque image source sera dupliquée dans le DCP.\n" + +#: src/lib/job.cc:318 +msgid "Error (%1)" +msgstr "Erreur (%1)" + +#: src/lib/examine_content_job.cc:45 +msgid "Examine content" +msgstr "Examen du contenu" + +#: src/lib/filter.cc:72 +msgid "Experimental horizontal deblocking filter 1" +msgstr "Filtre dé-bloc horizontal 1" + +#: src/lib/filter.cc:73 +msgid "Experimental vertical deblocking filter 1" +msgstr "Filtre dé-bloc vertical 1" + +#: src/lib/filter.cc:79 +msgid "FFMPEG deinterlacer" +msgstr "Désentrelaceur FFMPEG" + +#: src/lib/filter.cc:80 +msgid "FIR low-pass deinterlacer" +msgstr "Désentrelaceur passe-bas FIR" + +#: src/lib/scp_dcp_job.cc:138 +msgid "Failed to authenticate with server (%1)" +msgstr "L'authentification du serveur (%1) a échouée" + +#: src/lib/scaler.cc:70 +msgid "Fast Bilinear" +msgstr "Bilinéaire rapide" + +#: src/lib/dcp_content_type.cc:44 +msgid "Feature" +msgstr "Feature" + +#: src/lib/ratio.cc:53 +msgid "Flat" +msgstr "Flat" + +#: src/lib/filter.cc:85 +msgid "Force quantizer" +msgstr "Forcer la quantification" + +#: src/lib/ratio.cc:55 +msgid "Full frame" +msgstr "Pleine matrice" + +#: src/lib/scaler.cc:65 +msgid "Gaussian" +msgstr "Gaussien" + +#: src/lib/filter.cc:86 +msgid "Gradient debander" +msgstr "Corrections des bandes du dégradé" + +#: src/lib/filter.cc:89 +msgid "High quality 3D denoiser" +msgstr "Débruiteur 3D haute qualité" + +#: src/lib/filter.cc:68 +msgid "Horizontal deblocking filter" +msgstr "Filtre dé-bloc horizontal" + +#: src/lib/filter.cc:70 +msgid "Horizontal deblocking filter A" +msgstr "Filtre dé-bloc horizontal" + +#: src/lib/imagemagick_content.cc:50 +msgid "Image: %1" +msgstr "Image : %1" + +#: src/lib/job.cc:96 +#: src/lib/job.cc:105 +msgid "It is not known what caused this error. The best idea is to report the problem to the DCP-o-matic mailing list (carl@dcpomatic.com)" +msgstr "Erreur indéterminée. Merci de rapporter le problème à la liste DCP-o-matic (carl@dcpomatic.com)" + +#: src/lib/filter.cc:82 +msgid "Kernel deinterlacer" +msgstr "Désentrelaceur noyau" + +#: src/lib/scaler.cc:66 +msgid "Lanczos" +msgstr "Lanczos" + +#: src/lib/util.cc:698 +msgid "Left" +msgstr "Gauche" + +#: src/lib/util.cc:702 +msgid "Left surround" +msgstr "Arrière gauche" + +#: src/lib/util.cc:701 +msgid "Lfe (sub)" +msgstr "Basses fréquences" + +#: src/lib/filter.cc:75 +msgid "Linear blend deinterlacer" +msgstr "Désentrelaceur par mélange interpolé" + +#: src/lib/filter.cc:76 +msgid "Linear interpolating deinterlacer" +msgstr "Désentrelaceur linéaire interpolé" + +#: src/lib/filter.cc:78 +msgid "Median deinterlacer" +msgstr "Désentrelaceur médian" + +#: src/lib/filter.cc:74 +#: src/lib/filter.cc:85 +#: src/lib/filter.cc:86 +#: src/lib/filter.cc:87 +#: src/lib/filter.cc:90 +msgid "Misc" +msgstr "Divers" + +#: src/lib/filter.cc:81 +msgid "Motion compensating deinterlacer" +msgstr "Désentrelaceur par compensation de mouvement" + +#: src/lib/ffmpeg_content.cc:181 +msgid "Movie: %1" +msgstr "Film : %1" + +#: src/lib/filter.cc:84 +#: src/lib/filter.cc:88 +#: src/lib/filter.cc:89 +#: src/lib/filter.cc:91 +msgid "Noise reduction" +msgstr "Réduction de bruit" + +#: src/lib/job.cc:316 +msgid "OK (ran for %1)" +msgstr "OK (processus %1)" + +#: src/lib/filter.cc:91 +msgid "Overcomplete wavelet denoiser" +msgstr "Réduction de bruit par ondelettes" + +#: src/lib/dcp_content_type.cc:51 +msgid "Policy" +msgstr "Policy" + +#: src/lib/dcp_content_type.cc:52 +msgid "Public Service Announcement" +msgstr "Public Service Announcement" + +#: src/lib/dcp_content_type.cc:49 +msgid "Rating" +msgstr "Classification" + +#: src/lib/util.cc:699 +msgid "Right" +msgstr "Droite" + +#: src/lib/util.cc:703 +msgid "Right surround" +msgstr "Arrière droite" + +#: src/lib/scp_dcp_job.cc:133 +msgid "SSH error (%1)" +msgstr "Erreur SSH (%1)" + +#: src/lib/ratio.cc:54 +msgid "Scope" +msgstr "Scope" + +#: src/lib/dcp_content_type.cc:45 +msgid "Short" +msgstr "Short" + +#: src/lib/scaler.cc:67 +msgid "Sinc" +msgstr "Sinc" + +#: src/lib/sndfile_content.cc:57 +msgid "Sound file: %1" +msgstr "Fichier son : %1" + +#: src/lib/scaler.cc:68 +msgid "Spline" +msgstr "Spline" + +#: src/lib/dcp_content_type.cc:50 +msgid "Teaser" +msgstr "Teaser" + +#: src/lib/filter.cc:90 +msgid "Telecine filter" +msgstr "Filtre télécinéma" + +#: src/lib/filter.cc:84 +msgid "Temporal noise reducer" +msgstr "Réduction de bruit temporel" + +#: src/lib/dcp_content_type.cc:47 +msgid "Test" +msgstr "Test" + +#: src/lib/job.cc:78 +msgid "The drive that the film is stored on is low in disc space. Free some more space and try again." +msgstr "Le disque contenant le film est plein. Libérez de l'espace et essayez à nouveau." + +#: src/lib/film.cc:364 +msgid "This film was created with an older version of DCP-o-matic, and unfortunately it cannot be loaded into this version. You will need to create a new Film, re-add your content and set it up again. Sorry!" +msgstr "Ce projet a été créé avec une ancienne version de DCP-o-matic, chargement impossible. Créez un nouveau projet, ajoutez du contenu et reparamétrez. Désolé !" + +#: src/lib/dcp_content_type.cc:46 +msgid "Trailer" +msgstr "Trailer" + +#: src/lib/transcode_job.cc:50 +msgid "Transcode %1" +msgstr "Transcodage %1" + +#: src/lib/dcp_content_type.cc:48 +msgid "Transitional" +msgstr "Transitional" + +#: src/lib/job.cc:104 +msgid "Unknown error" +msgstr "Erreur inconnue" + +#: src/lib/ffmpeg_decoder.cc:264 +msgid "Unrecognised audio sample format (%1)" +msgstr "Échantillonnage audio (%1) inconnu" + +#: src/lib/filter.cc:87 +msgid "Unsharp mask and Gaussian blur" +msgstr "Adoucissement et flou Gaussien" + +#: src/lib/filter.cc:69 +msgid "Vertical deblocking filter" +msgstr "Filtre dé-bloc vertical" + +#: src/lib/filter.cc:71 +msgid "Vertical deblocking filter A" +msgstr "Filtre dé-bloc vertical A" + +#: src/lib/scp_dcp_job.cc:101 +msgid "Waiting" +msgstr "En cours" + +#: src/lib/scaler.cc:63 +msgid "X" +msgstr "X" + +#: src/lib/filter.cc:83 +msgid "Yet Another Deinterlacing Filter" +msgstr "Un autre filtre de désentrelacement" + +#: src/lib/film.cc:273 +msgid "You must add some content to the DCP before creating it" +msgstr "Ajoutez un contenu pour créer le DCP" + +#: src/lib/film.cc:232 +msgid "cannot contain slashes" +msgstr "slash interdit" + +#: src/lib/util.cc:494 +msgid "connect timed out" +msgstr "temps de connexion expiré" + +#: src/lib/scp_dcp_job.cc:119 +msgid "connecting" +msgstr "connexion" + +#: src/lib/film.cc:269 +#, fuzzy +msgid "container" +msgstr "contenu" + +#: src/lib/film.cc:277 +msgid "content type" +msgstr "type de contenu" + +#: src/lib/scp_dcp_job.cc:168 +msgid "copying %1" +msgstr "copie de %1" + +#: src/lib/exceptions.cc:36 +msgid "could not create file %1" +msgstr "Écriture vers fichier distant (%1) impossible" + +#: src/lib/ffmpeg.cc:128 +msgid "could not find audio decoder" +msgstr "décodeur audio introuvable" + +#: src/lib/ffmpeg.cc:76 +msgid "could not find stream information" +msgstr "information du flux introuvable" + +#: src/lib/ffmpeg_decoder.cc:498 +msgid "could not find subtitle decoder" +msgstr "décodeur de sous-titre introuvable" + +#: src/lib/ffmpeg.cc:107 +msgid "could not find video decoder" +msgstr "décodeur vidéo introuvable" + +#: src/lib/sndfile_decoder.cc:45 +#, fuzzy +msgid "could not open audio file for reading" +msgstr "lecture du fichier impossible" + +#: src/lib/exceptions.cc:29 +msgid "could not open file %1" +msgstr "lecture du fichier (%1) impossible" + +#: src/lib/dcp_video_frame.cc:263 +msgid "could not open file for reading" +msgstr "lecture du fichier impossible" + +#: src/lib/exceptions.cc:44 +msgid "could not read from file %1 (%2)" +msgstr "lecture du fichier impossible %1 (%2)" + +#: src/lib/resampler.cc:76 +#: src/lib/resampler.cc:96 +msgid "could not run sample-rate converter" +msgstr "conversion de la fréquence d'échantillonnage impossible" + +#: src/lib/scp_dcp_job.cc:86 +msgid "could not start SCP session (%1)" +msgstr "démarrage de session SCP (%1) impossible" + +#: src/lib/scp_dcp_job.cc:52 +msgid "could not start SSH session" +msgstr "démarrage de session SSH impossible" + +#: src/lib/exceptions.cc:50 +msgid "could not write to file %1 (%2)" +msgstr "Écriture vers fichier distant (%1) impossible (%2)" + +#: src/lib/transcode_job.cc:94 +msgid "frames per second" +msgstr "images par seconde" + +#: src/lib/util.cc:146 +msgid "hour" +msgstr "heure" + +#: src/lib/util.cc:143 +#: src/lib/util.cc:148 +msgid "hours" +msgstr "heures" + +#: src/lib/util.cc:153 +msgid "minute" +msgstr "minute" + +#: src/lib/util.cc:155 +msgid "minutes" +msgstr "minutes" + +#: src/lib/util.cc:621 +msgid "missing key %1 in key-value set" +msgstr "clé %1 non sélectionnée" + +#: src/lib/exceptions.cc:54 +msgid "missing required setting %1" +msgstr "paramètre %1 manquant" + +#: src/lib/ffmpeg_decoder.cc:530 +msgid "multi-part subtitles not yet supported" +msgstr "sous-titres en plusieurs parties non supportés" + +#: src/lib/film.cc:232 +#: src/lib/film.cc:281 +msgid "name" +msgstr "nom" + +#: src/lib/ffmpeg_decoder.cc:545 +msgid "non-bitmap subtitles not yet supported" +msgstr "sous-titres non-bitmap non supportés actuellement" + +#. / TRANSLATORS: remaining here follows an amount of time that is remaining +#. / on an operation. +#: src/lib/job.cc:313 +msgid "remaining" +msgstr "restant" + +#: src/lib/util.cc:158 +msgid "seconds" +msgstr "secondes" + +#~ msgid "1.66 within Flat" +#~ msgstr "1.66 dans Flat" + +#~ msgid "16:9 within Flat" +#~ msgstr "16:9 dans Flat" + +#, fuzzy +#~ msgid "16:9 within Scope" +#~ msgstr "16:9 dans Scope" + +#~ msgid "4:3 within Flat" +#~ msgstr "4:3 dans Flat" + +#~ msgid "A/B transcode %1" +#~ msgstr "Transcodage A/B %1" + +#~ msgid "Cannot resample audio as libswresample is not present" +#~ msgstr "Ré-échantillonnage du son impossible : libswresample est absent" + +#~ msgid "Examine content of %1" +#~ msgstr "Examen du contenu de %1" + +#~ msgid "Flat without stretch" +#~ msgstr "Flat sans déformation" + +#~ msgid "Rec 709" +#~ msgstr "Rec 709" + +#~ msgid "Scope without stretch" +#~ msgstr "Scope sans déformation" + +#~ msgid "could not open external audio file for reading" +#~ msgstr "lecture du fichier audio externe impossible" + +#~ msgid "external audio files have differing lengths" +#~ msgstr "Les fichiers audio externes ont des durées différentes" + +#~ msgid "external audio files must be mono" +#~ msgstr "les fichiers audio externes doivent être en mono" + +#~ msgid "format" +#~ msgstr "format" + +#~ msgid "no still image files found" +#~ msgstr "aucune image fixe trouvée" + +#~ msgid "sRGB" +#~ msgstr "sRGB" + +#~ msgid "still" +#~ msgstr "fixe" + +#~ msgid "video" +#~ msgstr "vidéo" + +#~ msgid "1.33" +#~ msgstr "1.33" + +#~ msgid "Source scaled to 1.19:1" +#~ msgstr "Source mise à l'échelle en 1.19:1" + +#~ msgid "Source scaled to 1.33:1" +#~ msgstr "Source mise à l'échelle en 1.33:1" + +#~ msgid "Source scaled to 1.33:1 then pillarboxed to Flat" +#~ msgstr "Source mise à l'échelle en 1.33:1 puis contenue dans Flat" + +#~ msgid "Source scaled to 1.375:1" +#~ msgstr "Source mise à l'échelle en 1.375:1" + +#~ msgid "Source scaled to 1.37:1 (Academy ratio)" +#~ msgstr "Source mise à l'échelle en 1.37:1 (ratio \"academy\")" + +#~ msgid "Source scaled to 1.66:1" +#~ msgstr "Source mise à l'échelle en 1.66:1" + +#~ msgid "Source scaled to 1.66:1 then pillarboxed to Flat" +#~ msgstr "Source mise à l'échelle en 1.66:1 puis contenue dans Flat" + +#~ msgid "Source scaled to 1.78:1" +#~ msgstr "Source mise à l'échelle en 1.78:1" + +#~ msgid "Source scaled to 1.78:1 then pillarboxed to Flat" +#~ msgstr "Source mise à l'échelle en 1.78:1 puis contenue dans Flat" + +#~ msgid "Source scaled to Flat (1.85:1)" +#~ msgstr "Source mise à l'échelle en Flat (1.85:1)" + +#~ msgid "Source scaled to Scope (2.39:1)" +#~ msgstr "Source mise à l'échelle en Scope (2.39:1)" + +#~ msgid "Source scaled to fit Flat preserving its aspect ratio" +#~ msgstr "Source réduite en Flat afin de préserver ses dimensions" + +#~ msgid "Source scaled to fit Scope preserving its aspect ratio" +#~ msgstr "Source réduite en Scope afin de préserver ses dimensions" + +#~ msgid "adding to queue of %1" +#~ msgstr "Mise en file d'attente de %1" + +#~ msgid "decoder sleeps with queue of %1" +#~ msgstr "décodeur en veille avec %1 en file d'attente" + +#~ msgid "decoder wakes with queue of %1" +#~ msgstr "reprise du décodage avec %1 en file d'attente" diff --git a/src/lib/po/it_IT.po b/src/lib/po/it_IT.po new file mode 100644 index 000000000..53a34e020 --- /dev/null +++ b/src/lib/po/it_IT.po @@ -0,0 +1,659 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: IT VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-28 10:26+0100\n" +"Last-Translator: Maci <macibro@gmail.com>\n" +"Language-Team: \n" +"Language: Italiano\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/lib/sndfile_content.cc:70 +msgid "%1 channels, %2kHz, %3 samples" +msgstr "" + +#: src/lib/ffmpeg_content.cc:193 +#, fuzzy +msgid "%1 frames; %2 frames per second" +msgstr "fotogrammi al secondo" + +#: src/lib/video_content.cc:131 +msgid "%1x%2 pixels (%3:1)" +msgstr "" + +#: src/lib/transcode_job.cc:81 +msgid "0%" +msgstr "0%" + +#: src/lib/ratio.cc:47 +msgid "1.19" +msgstr "1.19" + +#: src/lib/ratio.cc:50 +msgid "1.375" +msgstr "1.375" + +#: src/lib/ratio.cc:51 +msgid "1.66" +msgstr "1.66" + +#: src/lib/ratio.cc:52 +msgid "16:9" +msgstr "16:9" + +#: src/lib/filter.cc:88 +msgid "3D denoiser" +msgstr "Riduttore di rumore 3D" + +#: src/lib/ratio.cc:48 +msgid "4:3" +msgstr "4:3" + +#: src/lib/ratio.cc:49 +msgid "Academy" +msgstr "Academy" + +#: src/lib/dcp_content_type.cc:53 +msgid "Advertisement" +msgstr "Pubblicità" + +#: src/lib/job.cc:72 +msgid "An error occurred whilst handling the file %1." +msgstr "Errore durante l'elaborazione del file %1." + +#: src/lib/analyse_audio_job.cc:53 +msgid "Analyse audio of %1" +msgstr "Analizzo l'audio di %1" + +#: src/lib/scaler.cc:64 +msgid "Area" +msgstr "Area" + +#: src/lib/scaler.cc:62 +msgid "Bicubic" +msgstr "Bicubica" + +#: src/lib/scaler.cc:69 +msgid "Bilinear" +msgstr "Bilineare" + +#: src/lib/job.cc:320 +msgid "Cancelled" +msgstr "Cancellato" + +#: src/lib/exceptions.cc:60 +msgid "Cannot handle pixel format %1 during %2" +msgstr "Non posso gestire il formato di pixel %1 durante %2" + +#: src/lib/util.cc:700 +msgid "Centre" +msgstr "Centro" + +#: src/lib/scp_dcp_job.cc:109 +msgid "Copy DCP to TMS" +msgstr "Copia del DCP al TMS" + +#: src/lib/scp_dcp_job.cc:128 +msgid "Could not connect to server %1 (%2)" +msgstr "Non posso connetermi al server %1 (%2)" + +#: src/lib/scp_dcp_job.cc:150 +msgid "Could not create remote directory %1 (%2)" +msgstr "Non posso creare la directory remota %1 (%2)" + +#: src/lib/scp_dcp_job.cc:175 +msgid "Could not open %1 to send" +msgstr "Non posso aprire %1 da inviare" + +#: src/lib/scp_dcp_job.cc:145 +msgid "Could not start SCP session (%1)" +msgstr "Non posso avviare la sessione SCP (%1)" + +#: src/lib/scp_dcp_job.cc:187 +msgid "Could not write to remote file (%1)" +msgstr "Non posso scrivere il file remoto (%1)" + +#: src/lib/filter.cc:77 +msgid "Cubic interpolating deinterlacer" +msgstr "Deinterlacciatore cubico interpolato" + +#: src/lib/util.cc:723 +msgid "DCP and source have the same rate.\n" +msgstr "Il DCP e il sorgente hanno la stessa frequenza.\n" + +#: src/lib/util.cc:733 +msgid "DCP will run at %1%% of the source speed.\n" +msgstr "Il DCP andrà al %1%% della velocità del sorgente.\n" + +#: src/lib/util.cc:726 +msgid "DCP will use every other frame of the source.\n" +msgstr "Il DCP userà ogni altro fotogramma del sorgente.\n" + +#: src/lib/filter.cc:68 src/lib/filter.cc:69 src/lib/filter.cc:70 +#: src/lib/filter.cc:71 src/lib/filter.cc:72 src/lib/filter.cc:73 +msgid "De-blocking" +msgstr "Sbloccaggio" + +#: src/lib/filter.cc:75 src/lib/filter.cc:76 src/lib/filter.cc:77 +#: src/lib/filter.cc:78 src/lib/filter.cc:79 src/lib/filter.cc:80 +#: src/lib/filter.cc:81 src/lib/filter.cc:82 src/lib/filter.cc:83 +msgid "De-interlacing" +msgstr "De-interlacciamento" + +#: src/lib/filter.cc:74 +msgid "Deringing filter" +msgstr "Filtro deringing" + +#: src/lib/dolby_cp750.cc:27 +msgid "Dolby CP750" +msgstr "Dolby CP750" + +#: src/lib/util.cc:728 +msgid "Each source frame will be doubled in the DCP.\n" +msgstr "Ogni fotogramma del sorgente sarà raddoppiato nel DCP.\n" + +#: src/lib/job.cc:318 +msgid "Error (%1)" +msgstr "Errore (%1)" + +#: src/lib/examine_content_job.cc:45 +msgid "Examine content" +msgstr "Esamino il contenuto" + +#: src/lib/filter.cc:72 +msgid "Experimental horizontal deblocking filter 1" +msgstr "Filtro di sblocco sperimentale orizzontale 1" + +#: src/lib/filter.cc:73 +msgid "Experimental vertical deblocking filter 1" +msgstr "Filtro di sblocco sperimentale verticale 1" + +#: src/lib/filter.cc:79 +msgid "FFMPEG deinterlacer" +msgstr "Deinterlacciatore FFMPEG" + +#: src/lib/filter.cc:80 +msgid "FIR low-pass deinterlacer" +msgstr "Deinterlacciatore FIR low-pass" + +#: src/lib/scp_dcp_job.cc:138 +msgid "Failed to authenticate with server (%1)" +msgstr "Autenticazione col server fallita (%1) " + +#: src/lib/scaler.cc:70 +msgid "Fast Bilinear" +msgstr "Bilineare rapida" + +#: src/lib/dcp_content_type.cc:44 +msgid "Feature" +msgstr "Caratteristica" + +#: src/lib/ratio.cc:53 +msgid "Flat" +msgstr "Flat" + +#: src/lib/filter.cc:85 +msgid "Force quantizer" +msgstr "Forza quantizzatore" + +#: src/lib/ratio.cc:55 +msgid "Full frame" +msgstr "" + +#: src/lib/scaler.cc:65 +msgid "Gaussian" +msgstr "Gaussiana" + +#: src/lib/filter.cc:86 +msgid "Gradient debander" +msgstr "Gradiente debander" + +#: src/lib/filter.cc:89 +msgid "High quality 3D denoiser" +msgstr "Riduttore di rumore 3D di alta qualità" + +#: src/lib/filter.cc:68 +msgid "Horizontal deblocking filter" +msgstr "Filtro sblocco orizzontale" + +#: src/lib/filter.cc:70 +msgid "Horizontal deblocking filter A" +msgstr "Filtro A sblocco orizzontale" + +#: src/lib/imagemagick_content.cc:50 +msgid "Image: %1" +msgstr "" + +#: src/lib/job.cc:96 src/lib/job.cc:105 +msgid "" +"It is not known what caused this error. The best idea is to report the " +"problem to the DCP-o-matic mailing list (carl@dcpomatic.com)" +msgstr "" +"Non sappiamo cosa ha causato questo errore. La cosa migliore è inviare un " +"report del problema alla mailing list di DCP-o-matic (carl@dcpomatic.com)" + +#: src/lib/filter.cc:82 +msgid "Kernel deinterlacer" +msgstr "Deinterlacciatore Kernel" + +#: src/lib/scaler.cc:66 +msgid "Lanczos" +msgstr "Lanczos" + +#: src/lib/util.cc:698 +msgid "Left" +msgstr "Sinistro" + +#: src/lib/util.cc:702 +msgid "Left surround" +msgstr "Surround sinistro" + +#: src/lib/util.cc:701 +msgid "Lfe (sub)" +msgstr "Lfe(sub)" + +#: src/lib/filter.cc:75 +msgid "Linear blend deinterlacer" +msgstr "Deinterlacciatore lineare miscelato" + +#: src/lib/filter.cc:76 +msgid "Linear interpolating deinterlacer" +msgstr "Deinterlacciatore lineare interpolato" + +#: src/lib/filter.cc:78 +msgid "Median deinterlacer" +msgstr "Deinterlacciatore mediano" + +#: src/lib/filter.cc:74 src/lib/filter.cc:85 src/lib/filter.cc:86 +#: src/lib/filter.cc:87 src/lib/filter.cc:90 +msgid "Misc" +msgstr "Varie" + +#: src/lib/filter.cc:81 +msgid "Motion compensating deinterlacer" +msgstr "Dinterlacciatore compensativo di movimento" + +#: src/lib/ffmpeg_content.cc:181 +msgid "Movie: %1" +msgstr "" + +#: src/lib/filter.cc:84 src/lib/filter.cc:88 src/lib/filter.cc:89 +#: src/lib/filter.cc:91 +msgid "Noise reduction" +msgstr "Riduzione del rumore" + +#: src/lib/job.cc:316 +msgid "OK (ran for %1)" +msgstr "OK (eseguito in %1)" + +#: src/lib/filter.cc:91 +msgid "Overcomplete wavelet denoiser" +msgstr "Overcomplete wavelet denoiser" + +#: src/lib/dcp_content_type.cc:51 +msgid "Policy" +msgstr "Politica" + +#: src/lib/dcp_content_type.cc:52 +msgid "Public Service Announcement" +msgstr "Annuncio di pubblico servizio" + +#: src/lib/dcp_content_type.cc:49 +msgid "Rating" +msgstr "Punteggio" + +#: src/lib/util.cc:699 +msgid "Right" +msgstr "Destro" + +#: src/lib/util.cc:703 +msgid "Right surround" +msgstr "Surround destro" + +#: src/lib/scp_dcp_job.cc:133 +msgid "SSH error (%1)" +msgstr "Errore SSH (%1)" + +#: src/lib/ratio.cc:54 +msgid "Scope" +msgstr "Scope" + +#: src/lib/dcp_content_type.cc:45 +msgid "Short" +msgstr "Corto" + +#: src/lib/scaler.cc:67 +msgid "Sinc" +msgstr "Sinc" + +#: src/lib/sndfile_content.cc:57 +#, fuzzy +msgid "Sound file: %1" +msgstr "non riesco ad aprire %1" + +#: src/lib/scaler.cc:68 +msgid "Spline" +msgstr "Spline" + +#: src/lib/dcp_content_type.cc:50 +msgid "Teaser" +msgstr "Teaser" + +#: src/lib/filter.cc:90 +msgid "Telecine filter" +msgstr "Filtro telecinema" + +#: src/lib/filter.cc:84 +msgid "Temporal noise reducer" +msgstr "Riduttore temporale di rumore" + +#: src/lib/dcp_content_type.cc:47 +msgid "Test" +msgstr "Prova" + +#: src/lib/job.cc:78 +msgid "" +"The drive that the film is stored on is low in disc space. Free some more " +"space and try again." +msgstr "" +"Sul disco dove è memorizzato il film non c'è abbastanza spazio. Liberare " +"altro spazio e riprovare." + +#: src/lib/film.cc:364 +msgid "" +"This film was created with an older version of DCP-o-matic, and " +"unfortunately it cannot be loaded into this version. You will need to " +"create a new Film, re-add your content and set it up again. Sorry!" +msgstr "" + +#: src/lib/dcp_content_type.cc:46 +msgid "Trailer" +msgstr "Prossimamente" + +#: src/lib/transcode_job.cc:50 +msgid "Transcode %1" +msgstr "Transcodifica %1" + +#: src/lib/dcp_content_type.cc:48 +msgid "Transitional" +msgstr "Di transizione" + +#: src/lib/job.cc:104 +msgid "Unknown error" +msgstr "Errore sconosciuto" + +#: src/lib/ffmpeg_decoder.cc:264 +msgid "Unrecognised audio sample format (%1)" +msgstr "Formato di campionamento audio non riconosciuto (%1)" + +#: src/lib/filter.cc:87 +msgid "Unsharp mask and Gaussian blur" +msgstr "Maschera unsharp e sfocatura Gaussiana" + +#: src/lib/filter.cc:69 +msgid "Vertical deblocking filter" +msgstr "Filtro di sblocco verticale" + +#: src/lib/filter.cc:71 +msgid "Vertical deblocking filter A" +msgstr "Filtro A di sblocco verticale" + +#: src/lib/scp_dcp_job.cc:101 +msgid "Waiting" +msgstr "Aspetta" + +#: src/lib/scaler.cc:63 +msgid "X" +msgstr "X" + +#: src/lib/filter.cc:83 +msgid "Yet Another Deinterlacing Filter" +msgstr "Altro filtro di deinterlacciamento" + +#: src/lib/film.cc:273 +msgid "You must add some content to the DCP before creating it" +msgstr "" + +#: src/lib/film.cc:232 +msgid "cannot contain slashes" +msgstr "non può contenere barre" + +#: src/lib/util.cc:494 +msgid "connect timed out" +msgstr "connessione scaduta" + +#: src/lib/scp_dcp_job.cc:119 +msgid "connecting" +msgstr "mi sto connettendo" + +#: src/lib/film.cc:269 +#, fuzzy +msgid "container" +msgstr "contenuto" + +#: src/lib/film.cc:277 +msgid "content type" +msgstr "tipo di contenuto" + +#: src/lib/scp_dcp_job.cc:168 +msgid "copying %1" +msgstr "copia %1" + +#: src/lib/exceptions.cc:36 +msgid "could not create file %1" +msgstr "Non posso scrivere il file remoto (%1)" + +#: src/lib/ffmpeg.cc:128 +msgid "could not find audio decoder" +msgstr "non riesco a trovare il decoder audio" + +#: src/lib/ffmpeg.cc:76 +msgid "could not find stream information" +msgstr "non riesco a trovare informazioni sullo streaming" + +#: src/lib/ffmpeg_decoder.cc:498 +msgid "could not find subtitle decoder" +msgstr "non riesco a trovare il decoder dei sottotitoli" + +#: src/lib/ffmpeg.cc:107 +msgid "could not find video decoder" +msgstr "non riesco a trovare il decoder video" + +#: src/lib/sndfile_decoder.cc:45 +#, fuzzy +msgid "could not open audio file for reading" +msgstr "non riesco ad aprire il file per leggerlo" + +#: src/lib/exceptions.cc:29 +msgid "could not open file %1" +msgstr "non riesco ad aprire %1" + +#: src/lib/dcp_video_frame.cc:263 +msgid "could not open file for reading" +msgstr "non riesco ad aprire il file per leggerlo" + +#: src/lib/exceptions.cc:44 +msgid "could not read from file %1 (%2)" +msgstr "non posso leggere dal file %1 (%2)" + +#: src/lib/resampler.cc:76 src/lib/resampler.cc:96 +msgid "could not run sample-rate converter" +msgstr "non riesco a eseguire il convertitore della frequenza di campionamento" + +#: src/lib/scp_dcp_job.cc:86 +msgid "could not start SCP session (%1)" +msgstr "non posso avviare la sessione SCP (%1)" + +#: src/lib/scp_dcp_job.cc:52 +msgid "could not start SSH session" +msgstr "non posso avviare la sessione SSH" + +#: src/lib/exceptions.cc:50 +msgid "could not write to file %1 (%2)" +msgstr "non posso scrivere il file (%1)" + +#: src/lib/transcode_job.cc:94 +msgid "frames per second" +msgstr "fotogrammi al secondo" + +#: src/lib/util.cc:146 +msgid "hour" +msgstr "ora" + +#: src/lib/util.cc:143 src/lib/util.cc:148 +msgid "hours" +msgstr "ore" + +#: src/lib/util.cc:153 +msgid "minute" +msgstr "minuto" + +#: src/lib/util.cc:155 +msgid "minutes" +msgstr "minuti" + +#: src/lib/util.cc:621 +msgid "missing key %1 in key-value set" +msgstr "persa la chiave %1 tra i valori chiave" + +#: src/lib/exceptions.cc:54 +msgid "missing required setting %1" +msgstr "persa la regolazione richiesta %1" + +#: src/lib/ffmpeg_decoder.cc:530 +msgid "multi-part subtitles not yet supported" +msgstr "sottotitoli multi-part non ancora supportati" + +#: src/lib/film.cc:232 src/lib/film.cc:281 +msgid "name" +msgstr "nome" + +#: src/lib/ffmpeg_decoder.cc:545 +msgid "non-bitmap subtitles not yet supported" +msgstr "sottotitoli non-bitmap non ancora supportati" + +#. / TRANSLATORS: remaining here follows an amount of time that is remaining +#. / on an operation. +#: src/lib/job.cc:313 +msgid "remaining" +msgstr "restano" + +#: src/lib/util.cc:158 +msgid "seconds" +msgstr "secondi" + +#~ msgid "1.66 within Flat" +#~ msgstr "1.66 all'interno di Flat" + +#~ msgid "16:9 within Flat" +#~ msgstr "16:9 all'interno di Flat" + +#~ msgid "16:9 within Scope" +#~ msgstr "16:9 all'interno di Scope" + +#~ msgid "4:3 within Flat" +#~ msgstr "4:3 all'interno di Flat" + +#~ msgid "A/B transcode %1" +#~ msgstr "Transcodifica A/B %1" + +#~ msgid "Cannot resample audio as libswresample is not present" +#~ msgstr "Non posso ricampionare l'audio perchè libswresample non è presente" + +#~ msgid "Examine content of %1" +#~ msgstr "Esamino il contenuto di %1" + +#~ msgid "Flat without stretch" +#~ msgstr "Flat senza stiramento" + +#~ msgid "Rec 709" +#~ msgstr "Rec 709" + +#~ msgid "Scope without stretch" +#~ msgstr "Scope senza stiramento" + +#~ msgid "could not open external audio file for reading" +#~ msgstr "non riesco ad aprire il file dell'audio esterno per leggerlo" + +#~ msgid "external audio files have differing lengths" +#~ msgstr "i files dell'audio esterno hanno durata diversa" + +#~ msgid "external audio files must be mono" +#~ msgstr "i files dell'audio esterno devono essere mono" + +#~ msgid "format" +#~ msgstr "formato" + +#~ msgid "no still image files found" +#~ msgstr "file immagini statiche non trovati" + +#~ msgid "sRGB" +#~ msgstr "sRGB" + +#~ msgid "still" +#~ msgstr "ancora" + +#~ msgid "video" +#~ msgstr "video" + +#~ msgid "1.33" +#~ msgstr "1.33" + +#~ msgid "Source scaled to 1.19:1" +#~ msgstr "Sorgente scalato a 1.19:1" + +#~ msgid "Source scaled to 1.33:1" +#~ msgstr "Sorgente scalato a 1.33:1" + +#~ msgid "Source scaled to 1.33:1 then pillarboxed to Flat" +#~ msgstr "Sorgente scalato a 1.33:1 e poi inviato come Flat" + +#~ msgid "Source scaled to 1.375:1" +#~ msgstr "Sorgente scalato a 1.375:1" + +#~ msgid "Source scaled to 1.37:1 (Academy ratio)" +#~ msgstr "Sorgente scalato a 1.37:1 (Academy ratio)" + +#~ msgid "Source scaled to 1.66:1" +#~ msgstr "Sorgente scalato a 1.66:1" + +#~ msgid "Source scaled to 1.66:1 then pillarboxed to Flat" +#~ msgstr "Sorgente scalato a 1.66:1 e poi inviato come Flat" + +#~ msgid "Source scaled to 1.78:1" +#~ msgstr "Sorgente scalato a 1.78:1" + +#~ msgid "Source scaled to 1.78:1 then pillarboxed to Flat" +#~ msgstr "Sorgente scalato a 1.78:1 e poi inviato come Flat" + +#~ msgid "Source scaled to Flat (1.85:1)" +#~ msgstr "Sorgente scalato a Flat (1.85:1)" + +#~ msgid "Source scaled to Scope (2.39:1)" +#~ msgstr "Sorgente scalato a Scope (2.39:1)" + +#~ msgid "Source scaled to fit Flat preserving its aspect ratio" +#~ msgstr "" +#~ "Sorgente scalato per adattarsi a Flat mantentendo le sue proporzioni" + +#~ msgid "Source scaled to fit Scope preserving its aspect ratio" +#~ msgstr "" +#~ "Sorgente scalato per adattarsi a Scope mantentendo le sue proporzioni" + +#~ msgid "adding to queue of %1" +#~ msgstr "aggiungo alla coda %1" + +#~ msgid "decoder sleeps with queue of %1" +#~ msgstr "il decoder è in pausa con la coda di %1" + +#~ msgid "decoder wakes with queue of %1" +#~ msgstr "il decoder riparte con la coda di %1" diff --git a/src/lib/po/sv_SE.po b/src/lib/po/sv_SE.po new file mode 100644 index 000000000..13989e58e --- /dev/null +++ b/src/lib/po/sv_SE.po @@ -0,0 +1,656 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-10 15:35+0100\n" +"Last-Translator: Adam Klotblixt <adam.klotblixt@gmail.com>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/lib/sndfile_content.cc:70 +msgid "%1 channels, %2kHz, %3 samples" +msgstr "" + +#: src/lib/ffmpeg_content.cc:193 +#, fuzzy +msgid "%1 frames; %2 frames per second" +msgstr "bilder per sekund" + +#: src/lib/video_content.cc:131 +msgid "%1x%2 pixels (%3:1)" +msgstr "" + +#: src/lib/transcode_job.cc:81 +msgid "0%" +msgstr "0%" + +#: src/lib/ratio.cc:47 +msgid "1.19" +msgstr "1,19" + +#: src/lib/ratio.cc:50 +msgid "1.375" +msgstr "1,375" + +#: src/lib/ratio.cc:51 +msgid "1.66" +msgstr "1,66" + +#: src/lib/ratio.cc:52 +msgid "16:9" +msgstr "16:9" + +#: src/lib/filter.cc:88 +msgid "3D denoiser" +msgstr "3D brusreducering" + +#: src/lib/ratio.cc:48 +msgid "4:3" +msgstr "" + +#: src/lib/ratio.cc:49 +msgid "Academy" +msgstr "Academy" + +#: src/lib/dcp_content_type.cc:53 +msgid "Advertisement" +msgstr "Reklam" + +#: src/lib/job.cc:72 +msgid "An error occurred whilst handling the file %1." +msgstr "Ett fel inträffade vid hantering av filen %1" + +#: src/lib/analyse_audio_job.cc:53 +msgid "Analyse audio of %1" +msgstr "Analysera %1s audio" + +#: src/lib/scaler.cc:64 +msgid "Area" +msgstr "Yta" + +#: src/lib/scaler.cc:62 +msgid "Bicubic" +msgstr "Bikubisk" + +#: src/lib/scaler.cc:69 +msgid "Bilinear" +msgstr "Bilinjär" + +#: src/lib/job.cc:320 +msgid "Cancelled" +msgstr "Avbruten" + +#: src/lib/exceptions.cc:60 +msgid "Cannot handle pixel format %1 during %2" +msgstr "Kan inte hantera pixelformat %1 under %2" + +#: src/lib/util.cc:700 +msgid "Centre" +msgstr "Mitt" + +#: src/lib/scp_dcp_job.cc:109 +msgid "Copy DCP to TMS" +msgstr "Kopiera DCP till TMS" + +#: src/lib/scp_dcp_job.cc:128 +msgid "Could not connect to server %1 (%2)" +msgstr "Kunde inte ansluta till server %1 (%2)" + +#: src/lib/scp_dcp_job.cc:150 +msgid "Could not create remote directory %1 (%2)" +msgstr "Kunde inte skapa fjärrkatalog %1 (%2)" + +#: src/lib/scp_dcp_job.cc:175 +msgid "Could not open %1 to send" +msgstr "Kunde inte öppna %1 för att skicka" + +#: src/lib/scp_dcp_job.cc:145 +msgid "Could not start SCP session (%1)" +msgstr "Kunde inte starta SCP-session (%1)" + +#: src/lib/scp_dcp_job.cc:187 +msgid "Could not write to remote file (%1)" +msgstr "Kunde inte skriva till fjärrfil (%1)" + +#: src/lib/filter.cc:77 +msgid "Cubic interpolating deinterlacer" +msgstr "Kubiskt interpolerande avflätare" + +#: src/lib/util.cc:723 +msgid "DCP and source have the same rate.\n" +msgstr "DCP och källa har samma bildfrekvens.\n" + +#: src/lib/util.cc:733 +msgid "DCP will run at %1%% of the source speed.\n" +msgstr "DCP kommer att köras på %1%% av källans hastighet.\n" + +#: src/lib/util.cc:726 +msgid "DCP will use every other frame of the source.\n" +msgstr "DCP kommer att använda varannan bild från källan.\n" + +#: src/lib/filter.cc:68 src/lib/filter.cc:69 src/lib/filter.cc:70 +#: src/lib/filter.cc:71 src/lib/filter.cc:72 src/lib/filter.cc:73 +msgid "De-blocking" +msgstr "Kantighetsutjämning" + +#: src/lib/filter.cc:75 src/lib/filter.cc:76 src/lib/filter.cc:77 +#: src/lib/filter.cc:78 src/lib/filter.cc:79 src/lib/filter.cc:80 +#: src/lib/filter.cc:81 src/lib/filter.cc:82 src/lib/filter.cc:83 +msgid "De-interlacing" +msgstr "Avflätning" + +#: src/lib/filter.cc:74 +msgid "Deringing filter" +msgstr "Avringningsfilter" + +#: src/lib/dolby_cp750.cc:27 +msgid "Dolby CP750" +msgstr "Dolby CP750" + +#: src/lib/util.cc:728 +msgid "Each source frame will be doubled in the DCP.\n" +msgstr "Varje bild från källan kommer att användas två gånger i DCPn.\n" + +#: src/lib/job.cc:318 +msgid "Error (%1)" +msgstr "Fel (%1)" + +#: src/lib/examine_content_job.cc:45 +msgid "Examine content" +msgstr "Undersök innehållet" + +#: src/lib/filter.cc:72 +msgid "Experimental horizontal deblocking filter 1" +msgstr "Experimentellt filter för horisontal kantighetsutjämning 1" + +#: src/lib/filter.cc:73 +msgid "Experimental vertical deblocking filter 1" +msgstr "Experimentellt filter för vertikal kantighetsutjämning 1" + +#: src/lib/filter.cc:79 +msgid "FFMPEG deinterlacer" +msgstr "FFMPEG avflätare" + +#: src/lib/filter.cc:80 +msgid "FIR low-pass deinterlacer" +msgstr "FIR lågpass-avflätare" + +#: src/lib/scp_dcp_job.cc:138 +msgid "Failed to authenticate with server (%1)" +msgstr "Misslyckades att autentisera med server (%1)" + +#: src/lib/scaler.cc:70 +msgid "Fast Bilinear" +msgstr "Snabb bilinjär" + +#: src/lib/dcp_content_type.cc:44 +msgid "Feature" +msgstr "Långfilm" + +#: src/lib/ratio.cc:53 +msgid "Flat" +msgstr "Flat" + +#: src/lib/filter.cc:85 +msgid "Force quantizer" +msgstr "Tvinga kvantiserare" + +#: src/lib/ratio.cc:55 +msgid "Full frame" +msgstr "" + +#: src/lib/scaler.cc:65 +msgid "Gaussian" +msgstr "Gaussisk" + +#: src/lib/filter.cc:86 +msgid "Gradient debander" +msgstr "Gradientutjämnare" + +#: src/lib/filter.cc:89 +msgid "High quality 3D denoiser" +msgstr "Högkvalitets 3D-brusreducering" + +#: src/lib/filter.cc:68 +msgid "Horizontal deblocking filter" +msgstr "Filter för horisontal kantighetsutjämning" + +#: src/lib/filter.cc:70 +msgid "Horizontal deblocking filter A" +msgstr "Filter för horisontal kantighetsutjämning A" + +#: src/lib/imagemagick_content.cc:50 +msgid "Image: %1" +msgstr "" + +#: src/lib/job.cc:96 src/lib/job.cc:105 +msgid "" +"It is not known what caused this error. The best idea is to report the " +"problem to the DCP-o-matic mailing list (carl@dcpomatic.com)" +msgstr "" +"Det är inte känt vad som orsakade detta fel. Bästa sättet att rapportera " +"problemet är till DCP-o-matics mejl-lista (carl@dcpomatic.com)" + +#: src/lib/filter.cc:82 +msgid "Kernel deinterlacer" +msgstr "Kernel-avflätare" + +#: src/lib/scaler.cc:66 +msgid "Lanczos" +msgstr "Lanczos" + +#: src/lib/util.cc:698 +msgid "Left" +msgstr "Vänster" + +#: src/lib/util.cc:702 +msgid "Left surround" +msgstr "Vänster surround" + +#: src/lib/util.cc:701 +msgid "Lfe (sub)" +msgstr "Lfe (sub)" + +#: src/lib/filter.cc:75 +msgid "Linear blend deinterlacer" +msgstr "Linjär blandningsavflätare" + +#: src/lib/filter.cc:76 +msgid "Linear interpolating deinterlacer" +msgstr "Linjär interpolationsavflätare" + +#: src/lib/filter.cc:78 +msgid "Median deinterlacer" +msgstr "Median-avflätare" + +#: src/lib/filter.cc:74 src/lib/filter.cc:85 src/lib/filter.cc:86 +#: src/lib/filter.cc:87 src/lib/filter.cc:90 +msgid "Misc" +msgstr "Diverse" + +#: src/lib/filter.cc:81 +msgid "Motion compensating deinterlacer" +msgstr "Rörelsekompenserande avflätare" + +#: src/lib/ffmpeg_content.cc:181 +msgid "Movie: %1" +msgstr "" + +#: src/lib/filter.cc:84 src/lib/filter.cc:88 src/lib/filter.cc:89 +#: src/lib/filter.cc:91 +msgid "Noise reduction" +msgstr "Brusreducering" + +#: src/lib/job.cc:316 +msgid "OK (ran for %1)" +msgstr "OK (kördes %1)" + +#: src/lib/filter.cc:91 +msgid "Overcomplete wavelet denoiser" +msgstr "Överkomplett wavelet-brusreducering" + +#: src/lib/dcp_content_type.cc:51 +msgid "Policy" +msgstr "Policy" + +#: src/lib/dcp_content_type.cc:52 +msgid "Public Service Announcement" +msgstr "Offentligt Servicemeddelande" + +#: src/lib/dcp_content_type.cc:49 +msgid "Rating" +msgstr "Klassificeringsklipp" + +#: src/lib/util.cc:699 +msgid "Right" +msgstr "Höger" + +#: src/lib/util.cc:703 +msgid "Right surround" +msgstr "Höger surround" + +#: src/lib/scp_dcp_job.cc:133 +msgid "SSH error (%1)" +msgstr "SSH fel (%1)" + +#: src/lib/ratio.cc:54 +msgid "Scope" +msgstr "Scope" + +#: src/lib/dcp_content_type.cc:45 +msgid "Short" +msgstr "Kortfilm" + +#: src/lib/scaler.cc:67 +msgid "Sinc" +msgstr "Sinc" + +#: src/lib/sndfile_content.cc:57 +#, fuzzy +msgid "Sound file: %1" +msgstr "kunde inte öppna fil %1" + +#: src/lib/scaler.cc:68 +msgid "Spline" +msgstr "Spline" + +#: src/lib/dcp_content_type.cc:50 +msgid "Teaser" +msgstr "Teaser" + +#: src/lib/filter.cc:90 +msgid "Telecine filter" +msgstr "Telecine-filter" + +#: src/lib/filter.cc:84 +msgid "Temporal noise reducer" +msgstr "Temporal brusreducering" + +#: src/lib/dcp_content_type.cc:47 +msgid "Test" +msgstr "Test" + +#: src/lib/job.cc:78 +msgid "" +"The drive that the film is stored on is low in disc space. Free some more " +"space and try again." +msgstr "" +"Enheten som filmen lagras på har för lite ledigt utrymme. Frigör utrymme och " +"försök igen." + +#: src/lib/film.cc:364 +msgid "" +"This film was created with an older version of DCP-o-matic, and " +"unfortunately it cannot be loaded into this version. You will need to " +"create a new Film, re-add your content and set it up again. Sorry!" +msgstr "" + +#: src/lib/dcp_content_type.cc:46 +msgid "Trailer" +msgstr "Trailer" + +#: src/lib/transcode_job.cc:50 +msgid "Transcode %1" +msgstr "Konvertera %1" + +#: src/lib/dcp_content_type.cc:48 +msgid "Transitional" +msgstr "Övergångsklipp" + +#: src/lib/job.cc:104 +msgid "Unknown error" +msgstr "Okänt fel" + +# Svengelska +#: src/lib/ffmpeg_decoder.cc:264 +#, fuzzy +msgid "Unrecognised audio sample format (%1)" +msgstr "Okänt audio-sampelformat (%1)" + +#: src/lib/filter.cc:87 +msgid "Unsharp mask and Gaussian blur" +msgstr "Oskärpemask och Gaussisk suddighet" + +#: src/lib/filter.cc:69 +msgid "Vertical deblocking filter" +msgstr "Filter för vertikal kantighetsutjämning" + +#: src/lib/filter.cc:71 +msgid "Vertical deblocking filter A" +msgstr "Filter för vertikal kantighetsutjämning A" + +#: src/lib/scp_dcp_job.cc:101 +msgid "Waiting" +msgstr "Väntar" + +#: src/lib/scaler.cc:63 +msgid "X" +msgstr "X" + +# Filtret heter så, ska ej översättas +#: src/lib/filter.cc:83 +msgid "Yet Another Deinterlacing Filter" +msgstr "Yet Another Deinterlacing Filter" + +#: src/lib/film.cc:273 +msgid "You must add some content to the DCP before creating it" +msgstr "" + +#: src/lib/film.cc:232 +msgid "cannot contain slashes" +msgstr "får inte innehålla snedstreck" + +# Svengelska +#: src/lib/util.cc:494 +#, fuzzy +msgid "connect timed out" +msgstr "uppkopplingen tajmade ur" + +#: src/lib/scp_dcp_job.cc:119 +msgid "connecting" +msgstr "kopplar upp" + +#: src/lib/film.cc:269 +#, fuzzy +msgid "container" +msgstr "innehåll" + +#: src/lib/film.cc:277 +msgid "content type" +msgstr "innehållstyp" + +#: src/lib/scp_dcp_job.cc:168 +msgid "copying %1" +msgstr "kopierar %1" + +#: src/lib/exceptions.cc:36 +msgid "could not create file %1" +msgstr "kunde inte skapa fil %1" + +#: src/lib/ffmpeg.cc:128 +msgid "could not find audio decoder" +msgstr "kunde inte hitta audio-avkodare" + +#: src/lib/ffmpeg.cc:76 +msgid "could not find stream information" +msgstr "kunde inte hitta information om strömmen" + +#: src/lib/ffmpeg_decoder.cc:498 +msgid "could not find subtitle decoder" +msgstr "kunde inte hitta undertext-avkodare" + +#: src/lib/ffmpeg.cc:107 +msgid "could not find video decoder" +msgstr "kunde inte hitta video-avkodare" + +#: src/lib/sndfile_decoder.cc:45 +#, fuzzy +msgid "could not open audio file for reading" +msgstr "kunde inte öppna fil för läsning" + +#: src/lib/exceptions.cc:29 +msgid "could not open file %1" +msgstr "kunde inte öppna fil %1" + +#: src/lib/dcp_video_frame.cc:263 +msgid "could not open file for reading" +msgstr "kunde inte öppna fil för läsning" + +#: src/lib/exceptions.cc:44 +msgid "could not read from file %1 (%2)" +msgstr "kunde inte läsa från fil %1 (%2)" + +#: src/lib/resampler.cc:76 src/lib/resampler.cc:96 +msgid "could not run sample-rate converter" +msgstr "kunde inte köra sampelhastighetskonverteraren" + +#: src/lib/scp_dcp_job.cc:86 +msgid "could not start SCP session (%1)" +msgstr "kunde inte starta SCP-session (%1)" + +#: src/lib/scp_dcp_job.cc:52 +msgid "could not start SSH session" +msgstr "kunde inte starta SSH-session" + +#: src/lib/exceptions.cc:50 +msgid "could not write to file %1 (%2)" +msgstr "kunde inte skriva till fil %1 (%2)" + +#: src/lib/transcode_job.cc:94 +msgid "frames per second" +msgstr "bilder per sekund" + +#: src/lib/util.cc:146 +msgid "hour" +msgstr "timme" + +#: src/lib/util.cc:143 src/lib/util.cc:148 +msgid "hours" +msgstr "timmar" + +#: src/lib/util.cc:153 +msgid "minute" +msgstr "minut" + +#: src/lib/util.cc:155 +msgid "minutes" +msgstr "minuter" + +#: src/lib/util.cc:621 +msgid "missing key %1 in key-value set" +msgstr "saknad nyckel %1 i nyckel-värde grupp" + +#: src/lib/exceptions.cc:54 +msgid "missing required setting %1" +msgstr "saknad nödvändig inställning %1" + +#: src/lib/ffmpeg_decoder.cc:530 +msgid "multi-part subtitles not yet supported" +msgstr "undertexter i flera delar stöds inte ännu" + +#: src/lib/film.cc:232 src/lib/film.cc:281 +msgid "name" +msgstr "namn" + +#: src/lib/ffmpeg_decoder.cc:545 +msgid "non-bitmap subtitles not yet supported" +msgstr "icke-rastergrafiska undertexter stöds inte ännu" + +#. / TRANSLATORS: remaining here follows an amount of time that is remaining +#. / on an operation. +#: src/lib/job.cc:313 +msgid "remaining" +msgstr "återstående tid" + +#: src/lib/util.cc:158 +msgid "seconds" +msgstr "sekunder" + +#~ msgid "1.66 within Flat" +#~ msgstr "1,66 innanför Flat" + +#~ msgid "16:9 within Flat" +#~ msgstr "16:9 innanför Flat" + +#~ msgid "16:9 within Scope" +#~ msgstr "16:9 innanför Scope" + +#~ msgid "4:3 within Flat" +#~ msgstr "4:3 innanför Flat" + +#~ msgid "A/B transcode %1" +#~ msgstr "A/B konvertera %1" + +#~ msgid "Cannot resample audio as libswresample is not present" +#~ msgstr "" +#~ "Kan inte omsampla ljudet eftersom libswresample inte finns tillgängligt" + +#~ msgid "Examine content of %1" +#~ msgstr "Undersök innehållet i %1" + +#~ msgid "Flat without stretch" +#~ msgstr "Flat utan utsträckning" + +#~ msgid "Rec 709" +#~ msgstr "Rec 709" + +#~ msgid "Scope without stretch" +#~ msgstr "Scope utan utsträckning" + +#~ msgid "could not open external audio file for reading" +#~ msgstr "kunde inte öppna extern audio-fil för läsning" + +#~ msgid "external audio files have differing lengths" +#~ msgstr "externa audio-filer har olika längder" + +#~ msgid "external audio files must be mono" +#~ msgstr "externa audio-filer måste vara mono" + +#~ msgid "format" +#~ msgstr "format" + +#~ msgid "no still image files found" +#~ msgstr "inga stillbildsfiler hittade" + +#~ msgid "sRGB" +#~ msgstr "sRGB" + +#~ msgid "still" +#~ msgstr "stillbild" + +#~ msgid "video" +#~ msgstr "video" + +#~ msgid "1.33" +#~ msgstr "1,33" + +#~ msgid "Source scaled to 1.19:1" +#~ msgstr "Källan skalad till 1,19:1" + +#~ msgid "Source scaled to 1.33:1" +#~ msgstr "Källan skalad till 1,33:1" + +#~ msgid "Source scaled to 1.33:1 then pillarboxed to Flat" +#~ msgstr "Källan skalad till 1,33:1, med sorgkanter innanför Flat" + +#~ msgid "Source scaled to 1.375:1" +#~ msgstr "Källan skalad till 1,375:1" + +#~ msgid "Source scaled to 1.37:1 (Academy ratio)" +#~ msgstr "Källan skalad till 1,37:1 (Academy-förhållande)" + +#~ msgid "Source scaled to 1.66:1" +#~ msgstr "Källan skalad till 1,66:1" + +#~ msgid "Source scaled to 1.66:1 then pillarboxed to Flat" +#~ msgstr "Källan skalad till 1,66:1, med sorgkanter innanför Flat" + +#~ msgid "Source scaled to 1.78:1" +#~ msgstr "Källan skalad till 1,78:1" + +#~ msgid "Source scaled to 1.78:1 then pillarboxed to Flat" +#~ msgstr "Källan skalad till 1,78:1, med sorgkanter innanför Flat" + +#~ msgid "Source scaled to Flat (1.85:1)" +#~ msgstr "Källan skalad till Flat (1,85:1)" + +#~ msgid "Source scaled to Scope (2.39:1)" +#~ msgstr "Källan skalad till Scope (2,39:1)" + +#~ msgid "Source scaled to fit Flat preserving its aspect ratio" +#~ msgstr "" +#~ "Källan skalad för att rymmas inom Flat utan att ändra bildförhållandet" + +#~ msgid "Source scaled to fit Scope preserving its aspect ratio" +#~ msgstr "" +#~ "Källan skalad för att rymmas inom Scope utan att ändra bildförhållandet" diff --git a/src/lib/ffmpeg_compatibility.h b/src/lib/position.h index 772d22c33..8768bf5a8 100644 --- a/src/lib/ffmpeg_compatibility.h +++ b/src/lib/position.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,15 +17,30 @@ */ -struct AVFilterInOut; +#ifndef DCPOMATIC_POSITION_H +#define DCPOMATIC_POSITION_H + +/** @struct Position + * @brief A position. + */ +template <class T> +class Position +{ +public: + Position () + : x (0) + , y (0) + {} + + Position (T x_, T y_) + : x (x_) + , y (y_) + {} + + /** x coordinate */ + T x; + /** y coordinate */ + T y; +}; -extern AVFilter* get_sink (); -extern AVFilterInOut* avfilter_inout_alloc (); - -#ifndef HAVE_AV_PIXEL_FORMAT -#define AVPixelFormat PixelFormat -#endif - -#ifndef HAVE_AV_FRAME_GET_BEST_EFFORT_TIMESTAMP -extern int64_t av_frame_get_best_effort_timestamp (AVFrame const *); #endif diff --git a/src/lib/processor.h b/src/lib/processor.h deleted file mode 100644 index 19d7c4b0c..000000000 --- a/src/lib/processor.h +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/processor.h - * @brief Parent class for classes which accept and then emit video or audio data. - */ - -#ifndef DVDOMATIC_PROCESSOR_H -#define DVDOMATIC_PROCESSOR_H - -#include "video_source.h" -#include "video_sink.h" -#include "audio_source.h" -#include "audio_sink.h" - -class Log; - -/** @class Processor - * @brief Base class for processors. - */ -class Processor -{ -public: - /** Construct a Processor. - * @param log Log to use. - */ - Processor (Log* log) - : _log (log) - {} - - virtual ~Processor() {} - - /** Will be called at the end of a processing run */ - virtual void process_end () {} - -protected: - Log* _log; ///< log to write to -}; - -/** @class AudioVideoProcessor - * @brief A processor which handles both video and audio data. - */ -class AudioVideoProcessor : public Processor, public VideoSource, public VideoSink, public AudioSource, public AudioSink -{ -public: - /** Construct an AudioVideoProcessor. - * @param log Log to write to. - */ - AudioVideoProcessor (Log* log) - : Processor (log) - {} -}; - -/** @class AudioProcessor - * @brief A processor which handles just audio data. - */ -class AudioProcessor : public Processor, public AudioSource, public AudioSink -{ -public: - /** Construct an AudioProcessor. - * @param log Log to write to. - */ - AudioProcessor (Log* log) - : Processor (log) - {} -}; - -/** @class VideoProcessor - * @brief A processor which handles just video data. - */ -class VideoProcessor : public Processor, public VideoSource, public VideoSink -{ -public: - /** Construct an VideoProcessor. - * @param log Log to write to. - */ - VideoProcessor (Log* log) - : Processor (log) - {} -}; - -#endif diff --git a/src/lib/ratio.cc b/src/lib/ratio.cc new file mode 100644 index 000000000..5988b3418 --- /dev/null +++ b/src/lib/ratio.cc @@ -0,0 +1,71 @@ +/* + 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 <libdcp/types.h> +#include "ratio.h" + +#include "i18n.h" + +using std::string; +using std::stringstream; +using std::vector; + +vector<Ratio const *> Ratio::_ratios; + +libdcp::Size +Ratio::size (libdcp::Size full_frame) const +{ + if (_ratio < static_cast<float>(full_frame.width) / full_frame.height) { + return libdcp::Size (full_frame.height * _ratio, full_frame.height); + } else { + return libdcp::Size (full_frame.width, full_frame.width / _ratio); + } + + return libdcp::Size (); +} + + +void +Ratio::setup_ratios () +{ + _ratios.push_back (new Ratio (float(1285) / 1080, "119", _("1.19"), "F")); + _ratios.push_back (new Ratio (float(1436) / 1080, "133", _("4:3"), "F")); + _ratios.push_back (new Ratio (float(1480) / 1080, "137", _("Academy"), "F")); + _ratios.push_back (new Ratio (float(1485) / 1080, "138", _("1.375"), "F")); + _ratios.push_back (new Ratio (float(1793) / 1080, "166", _("1.66"), "F")); + _ratios.push_back (new Ratio (float(1920) / 1080, "178", _("16:9"), "F")); + _ratios.push_back (new Ratio (float(1998) / 1080, "185", _("Flat"), "F")); + _ratios.push_back (new Ratio (float(2048) / 858, "239", _("Scope"), "S")); + _ratios.push_back (new Ratio (float(2048) / 1080, "full-frame", _("Full frame"), "C")); +} + +Ratio const * +Ratio::from_id (string i) +{ + vector<Ratio const *>::iterator j = _ratios.begin (); + while (j != _ratios.end() && (*j)->id() != i) { + ++j; + } + + if (j == _ratios.end ()) { + return 0; + } + + return *j; +} diff --git a/src/lib/ratio.h b/src/lib/ratio.h new file mode 100644 index 000000000..c331edabe --- /dev/null +++ b/src/lib/ratio.h @@ -0,0 +1,72 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_RATIO_H +#define DCPOMATIC_RATIO_H + +#include <vector> +#include <boost/utility.hpp> +#include <libdcp/util.h> + +class Ratio : public boost::noncopyable +{ +public: + Ratio (float ratio, std::string id, std::string n, std::string d) + : _ratio (ratio) + , _id (id) + , _nickname (n) + , _dci_name (d) + {} + + libdcp::Size size (libdcp::Size) const; + + std::string id () const { + return _id; + } + + std::string nickname () const { + return _nickname; + } + + std::string dci_name () const { + return _dci_name; + } + + float ratio () const { + return _ratio; + } + + static void setup_ratios (); + static Ratio const * from_id (std::string i); + static std::vector<Ratio const *> all () { + return _ratios; + } + +private: + float _ratio; + /** id for use in metadata */ + std::string _id; + /** nickname (e.g. Flat, Scope) */ + std::string _nickname; + std::string _dci_name; + + static std::vector<Ratio const *> _ratios; +}; + +#endif diff --git a/src/lib/rect.h b/src/lib/rect.h new file mode 100644 index 000000000..6f4709c08 --- /dev/null +++ b/src/lib/rect.h @@ -0,0 +1,79 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_RECT_H +#define DCPOMATIC_RECT_H + +#include "position.h" + +/* Put this inside a namespace as Apple put a Rect in the global namespace */ + +namespace dcpomatic +{ + +/** @struct Rect + * @brief A rectangle. + */ +template <class T> +class Rect +{ +public: + + Rect () + : x (0) + , y (0) + , width (0) + , height (0) + {} + + Rect (T x_, T y_, T w_, T h_) + : x (x_) + , y (y_) + , width (w_) + , height (h_) + {} + + T x; + T y; + T width; + T height; + + Position<T> position () const { + return Position<T> (x, y); + } + + Rect<T> intersection (Rect<T> const & other) const { + T const tx = max (x, other.x); + T const ty = max (y, other.y); + + return Rect ( + tx, ty, + min (x + width, other.x + other.width) - tx, + min (y + height, other.y + other.height) - ty + ); + } + + bool contains (Position<T> p) const { + return (p.x >= x && p.x <= (x + width) && p.y >= y && p.y <= (y + height)); + } +}; + +} + +#endif diff --git a/src/lib/resampler.cc b/src/lib/resampler.cc new file mode 100644 index 000000000..7bc933fd0 --- /dev/null +++ b/src/lib/resampler.cc @@ -0,0 +1,113 @@ +/* + 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. + +*/ + +extern "C" { +#include "libavutil/channel_layout.h" +} +#include "resampler.h" +#include "audio_buffers.h" +#include "exceptions.h" + +#include "i18n.h" + +using std::cout; +using std::pair; +using std::make_pair; +using boost::shared_ptr; + +Resampler::Resampler (int in, int out, int channels) + : _in_rate (in) + , _out_rate (out) + , _channels (channels) +{ + /* We will be using planar float data when we call the + resampler. As far as I can see, the audio channel + layout is not necessary for our purposes; it seems + only to be used get the number of channels and + decide if rematrixing is needed. It won't be, since + input and output layouts are the same. + */ + + _swr_context = swr_alloc_set_opts ( + 0, + av_get_default_channel_layout (_channels), + AV_SAMPLE_FMT_FLTP, + _out_rate, + av_get_default_channel_layout (_channels), + AV_SAMPLE_FMT_FLTP, + _in_rate, + 0, 0 + ); + + swr_init (_swr_context); +} + +Resampler::~Resampler () +{ + swr_free (&_swr_context); +} + +pair<shared_ptr<const AudioBuffers>, AudioContent::Frame> +Resampler::run (shared_ptr<const AudioBuffers> in, AudioContent::Frame frame) +{ + 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)); + + int const resampled_frames = swr_convert ( + _swr_context, (uint8_t **) resampled->data(), max_resampled_frames, (uint8_t const **) in->data(), in->frames() + ); + + if (resampled_frames < 0) { + throw EncodeError (_("could not run sample-rate converter")); + } + + resampled->set_frames (resampled_frames); + return make_pair (resampled, resamp_time); +} + +shared_ptr<const AudioBuffers> +Resampler::flush () +{ + shared_ptr<AudioBuffers> out (new AudioBuffers (_channels, 0)); + int out_offset = 0; + int64_t const pass_size = 256; + shared_ptr<AudioBuffers> pass (new AudioBuffers (_channels, 256)); + + while (1) { + int const frames = swr_convert (_swr_context, (uint8_t **) pass->data(), pass_size, 0, 0); + + if (frames < 0) { + throw EncodeError (_("could not run sample-rate converter")); + } + + if (frames == 0) { + break; + } + + out->ensure_size (out_offset + frames); + out->copy_from (pass.get(), frames, 0, out_offset); + out_offset += frames; + out->set_frames (out_offset); + } + + return out; +} diff --git a/src/lib/resampler.h b/src/lib/resampler.h new file mode 100644 index 000000000..69ec83ba9 --- /dev/null +++ b/src/lib/resampler.h @@ -0,0 +1,44 @@ +/* + 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 <boost/shared_ptr.hpp> +#include <boost/utility.hpp> +extern "C" { +#include <libswresample/swresample.h> +} +#include "types.h" +#include "audio_content.h" + +class AudioBuffers; + +class Resampler : public boost::noncopyable +{ +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> flush (); + +private: + SwrContext* _swr_context; + int _in_rate; + int _out_rate; + int _channels; +}; diff --git a/src/lib/scaler.cc b/src/lib/scaler.cc index c81456a15..40a0f05b9 100644 --- a/src/lib/scaler.cc +++ b/src/lib/scaler.cc @@ -28,6 +28,8 @@ extern "C" { } #include "scaler.h" +#include "i18n.h" + using namespace std; vector<Scaler const *> Scaler::_scalers; @@ -57,15 +59,15 @@ Scaler::all () void Scaler::setup_scalers () { - _scalers.push_back (new Scaler (SWS_BICUBIC, "bicubic", "Bicubic")); - _scalers.push_back (new Scaler (SWS_X, "x", "X")); - _scalers.push_back (new Scaler (SWS_AREA, "area", "Area")); - _scalers.push_back (new Scaler (SWS_GAUSS, "gauss", "Gaussian")); - _scalers.push_back (new Scaler (SWS_LANCZOS, "lanczos", "Lanczos")); - _scalers.push_back (new Scaler (SWS_SINC, "sinc", "Sinc")); - _scalers.push_back (new Scaler (SWS_SPLINE, "spline", "Spline")); - _scalers.push_back (new Scaler (SWS_BILINEAR, "bilinear", "Bilinear")); - _scalers.push_back (new Scaler (SWS_FAST_BILINEAR, "fastbilinear", "Fast Bilinear")); + _scalers.push_back (new Scaler (SWS_BICUBIC, N_("bicubic"), _("Bicubic"))); + _scalers.push_back (new Scaler (SWS_X, N_("x"), _("X"))); + _scalers.push_back (new Scaler (SWS_AREA, N_("area"), _("Area"))); + _scalers.push_back (new Scaler (SWS_GAUSS, N_("gauss"), _("Gaussian"))); + _scalers.push_back (new Scaler (SWS_LANCZOS, N_("lanczos"), _("Lanczos"))); + _scalers.push_back (new Scaler (SWS_SINC, N_("sinc"), _("Sinc"))); + _scalers.push_back (new Scaler (SWS_SPLINE, N_("spline"), _("Spline"))); + _scalers.push_back (new Scaler (SWS_BILINEAR, N_("bilinear"), _("Bilinear"))); + _scalers.push_back (new Scaler (SWS_FAST_BILINEAR, N_("fastbilinear"), _("Fast Bilinear"))); } /** @param id One of our ids. diff --git a/src/lib/scaler.h b/src/lib/scaler.h index c80f4b7db..6a039edd8 100644 --- a/src/lib/scaler.h +++ b/src/lib/scaler.h @@ -21,16 +21,17 @@ * @brief A class to describe one of FFmpeg's software scalers. */ -#ifndef DVDOMATIC_SCALER_H -#define DVDOMATIC_SCALER_H +#ifndef DCPOMATIC_SCALER_H +#define DCPOMATIC_SCALER_H #include <string> #include <vector> +#include <boost/utility.hpp> /** @class Scaler * @brief Class to describe one of FFmpeg's software scalers */ -class Scaler +class Scaler : public boost::noncopyable { public: Scaler (int f, std::string i, std::string n); diff --git a/src/lib/scp_dcp_job.cc b/src/lib/scp_dcp_job.cc index 22129f56c..528d393a3 100644 --- a/src/lib/scp_dcp_job.cc +++ b/src/lib/scp_dcp_job.cc @@ -34,6 +34,8 @@ #include "log.h" #include "film.h" +#include "i18n.h" + using std::string; using std::stringstream; using std::min; @@ -47,7 +49,7 @@ public: { session = ssh_new (); if (session == 0) { - throw NetworkError ("Could not start SSH session"); + throw NetworkError (_("could not start SSH session")); } } @@ -81,7 +83,7 @@ public: { scp = ssh_scp_new (s, SSH_SCP_WRITE | SSH_SCP_RECURSIVE, Config::instance()->tms_path().c_str ()); if (!scp) { - throw NetworkError (String::compose ("Could not start SCP session (%1)", ssh_get_error (s))); + throw NetworkError (String::compose (_("could not start SCP session (%1)"), ssh_get_error (s))); } } @@ -94,9 +96,9 @@ public: }; -SCPDCPJob::SCPDCPJob (shared_ptr<Film> f, shared_ptr<Job> req) - : Job (f, req) - , _status ("Waiting") +SCPDCPJob::SCPDCPJob (shared_ptr<const Film> f) + : Job (f) + , _status (_("Waiting")) { } @@ -104,17 +106,17 @@ SCPDCPJob::SCPDCPJob (shared_ptr<Film> f, shared_ptr<Job> req) string SCPDCPJob::name () const { - return "Copy DCP to TMS"; + return _("Copy DCP to TMS"); } void SCPDCPJob::run () { - _film->log()->log ("SCP DCP job starting"); + _film->log()->log (N_("SCP DCP job starting")); SSHSession ss; - set_status ("connecting"); + set_status (_("connecting")); ssh_options_set (ss.session, SSH_OPTIONS_HOST, Config::instance()->tms_ip().c_str ()); ssh_options_set (ss.session, SSH_OPTIONS_USER, Config::instance()->tms_user().c_str ()); @@ -123,29 +125,29 @@ SCPDCPJob::run () int r = ss.connect (); if (r != SSH_OK) { - throw NetworkError (String::compose ("Could not connect to server %1 (%2)", Config::instance()->tms_ip(), ssh_get_error (ss.session))); + throw NetworkError (String::compose (_("Could not connect to server %1 (%2)"), Config::instance()->tms_ip(), ssh_get_error (ss.session))); } int const state = ssh_is_server_known (ss.session); if (state == SSH_SERVER_ERROR) { - throw NetworkError (String::compose ("SSH error (%1)", ssh_get_error (ss.session))); + throw NetworkError (String::compose (_("SSH error (%1)"), ssh_get_error (ss.session))); } r = ssh_userauth_password (ss.session, 0, Config::instance()->tms_password().c_str ()); if (r != SSH_AUTH_SUCCESS) { - throw NetworkError (String::compose ("Failed to authenticate with server (%1)", ssh_get_error (ss.session))); + throw NetworkError (String::compose (_("Failed to authenticate with server (%1)"), ssh_get_error (ss.session))); } SSHSCP sc (ss.session); r = ssh_scp_init (sc.scp); if (r != SSH_OK) { - throw NetworkError (String::compose ("Could not start SCP session (%1)", ssh_get_error (ss.session))); + throw NetworkError (String::compose (_("Could not start SCP session (%1)"), ssh_get_error (ss.session))); } r = ssh_scp_push_directory (sc.scp, _film->dcp_name().c_str(), S_IRWXU); if (r != SSH_OK) { - throw NetworkError (String::compose ("Could not create remote directory %1 (%2)", _film->dcp_name(), ssh_get_error (ss.session))); + throw NetworkError (String::compose (_("Could not create remote directory %1 (%2)"), _film->dcp_name(), ssh_get_error (ss.session))); } string const dcp_dir = _film->dir (_film->dcp_name()); @@ -161,33 +163,30 @@ SCPDCPJob::run () for (boost::filesystem::directory_iterator i = boost::filesystem::directory_iterator (dcp_dir); i != boost::filesystem::directory_iterator(); ++i) { - /* Aah, the sweet smell of progress */ -#if BOOST_FILESYSTEM_VERSION == 3 string const leaf = boost::filesystem::path(*i).leaf().generic_string (); -#else - string const leaf = i->leaf (); -#endif - set_status ("copying " + leaf); + set_status (String::compose (_("copying %1"), leaf)); boost::uintmax_t to_do = boost::filesystem::file_size (*i); ssh_scp_push_file (sc.scp, leaf.c_str(), to_do, S_IRUSR | S_IWUSR); - FILE* f = fopen (boost::filesystem::path (*i).string().c_str(), "rb"); + FILE* f = fopen (boost::filesystem::path (*i).string().c_str(), N_("rb")); if (f == 0) { - throw NetworkError (String::compose ("Could not open %1 to send", *i)); + throw NetworkError (String::compose (_("Could not open %1 to send"), *i)); } while (to_do > 0) { int const t = min (to_do, buffer_size); size_t const read = fread (buffer, 1, t, f); if (read != size_t (t)) { + fclose (f); throw ReadFileError (boost::filesystem::path (*i).string()); } r = ssh_scp_write (sc.scp, buffer, t); if (r != SSH_OK) { - throw NetworkError (String::compose ("Could not write to remote file (%1)", ssh_get_error (ss.session))); + fclose (f); + throw NetworkError (String::compose (_("Could not write to remote file (%1)"), ssh_get_error (ss.session))); } to_do -= t; bytes_transferred += t; @@ -199,7 +198,7 @@ SCPDCPJob::run () } set_progress (1); - set_status (""); + set_status (N_("")); set_state (FINISHED_OK); } @@ -210,7 +209,7 @@ SCPDCPJob::status () const stringstream s; s << Job::status (); if (!_status.empty ()) { - s << "; " << _status; + s << N_("; ") << _status; } return s.str (); } diff --git a/src/lib/scp_dcp_job.h b/src/lib/scp_dcp_job.h index 5d0bfe7b4..bdc83af18 100644 --- a/src/lib/scp_dcp_job.h +++ b/src/lib/scp_dcp_job.h @@ -26,7 +26,7 @@ class SCPDCPJob : public Job { public: - SCPDCPJob (boost::shared_ptr<Film>, boost::shared_ptr<Job> req); + SCPDCPJob (boost::shared_ptr<const Film>); std::string name () const; void run (); @@ -34,7 +34,7 @@ public: private: void set_status (std::string); - + mutable boost::mutex _status_mutex; std::string _status; }; diff --git a/src/lib/server.cc b/src/lib/server.cc index bea75cff8..0212dbbed 100644 --- a/src/lib/server.cc +++ b/src/lib/server.cc @@ -28,13 +28,16 @@ #include <iostream> #include <boost/algorithm/string.hpp> #include <boost/lexical_cast.hpp> +#include <boost/scoped_array.hpp> +#include <libcxml/cxml.h> #include "server.h" #include "util.h" #include "scaler.h" #include "image.h" #include "dcp_video_frame.h" #include "config.h" -#include "subtitle.h" + +#include "i18n.h" using std::string; using std::stringstream; @@ -45,34 +48,41 @@ using boost::algorithm::is_any_of; using boost::algorithm::split; using boost::thread; using boost::bind; +using boost::scoped_array; +using boost::optional; +using libdcp::Size; + +ServerDescription::ServerDescription (shared_ptr<const cxml::Node> node) +{ + _host_name = node->string_child ("HostName"); + _threads = node->number_child<int> ("Threads"); +} + +void +ServerDescription::as_xml (xmlpp::Node* root) const +{ + root->add_child("HostName")->add_child_text (_host_name); + root->add_child("Threads")->add_child_text (boost::lexical_cast<string> (_threads)); +} /** Create a server description from a string of metadata returned from as_metadata(). * @param v Metadata. * @return ServerDescription, or 0. */ -ServerDescription * +optional<ServerDescription> ServerDescription::create_from_metadata (string v) { vector<string> b; - split (b, v, is_any_of (" ")); + split (b, v, is_any_of (N_(" "))); if (b.size() != 2) { - return 0; + return optional<ServerDescription> (); } - return new ServerDescription (b[0], atoi (b[1].c_str ())); + return ServerDescription (b[0], atoi (b[1].c_str ())); } -/** @return Description of this server as text */ -string -ServerDescription::as_metadata () const -{ - stringstream s; - s << _host_name << " " << _threads; - return s.str (); -} - -Server::Server (Log* log) +Server::Server (shared_ptr<Log> log) : _log (log) { @@ -81,53 +91,26 @@ Server::Server (Log* log) int Server::process (shared_ptr<Socket> socket) { - char buffer[512]; - socket->read_indefinite ((uint8_t *) buffer, sizeof (buffer), 30); - socket->consume (strlen (buffer) + 1); + uint32_t length = socket->read_uint32 (); + scoped_array<char> buffer (new char[length]); + socket->read (reinterpret_cast<uint8_t*> (buffer.get()), length); - stringstream s (buffer); - multimap<string, string> kv = read_key_value (s); - - if (get_required_string (kv, "encode") != "please") { + stringstream s (buffer.get()); + shared_ptr<cxml::Document> xml (new cxml::Document ("EncodingRequest")); + xml->read_stream (s); + if (xml->number_child<int> ("Version") != SERVER_LINK_VERSION) { + _log->log ("Mismatched server/client versions"); return -1; } - Size in_size (get_required_int (kv, "input_width"), get_required_int (kv, "input_height")); - int pixel_format_int = get_required_int (kv, "input_pixel_format"); - Size out_size (get_required_int (kv, "output_width"), get_required_int (kv, "output_height")); - int padding = get_required_int (kv, "padding"); - int subtitle_offset = get_required_int (kv, "subtitle_offset"); - float subtitle_scale = get_required_float (kv, "subtitle_scale"); - string scaler_id = get_required_string (kv, "scaler"); - int frame = get_required_int (kv, "frame"); - int frames_per_second = get_required_int (kv, "frames_per_second"); - string post_process = get_optional_string (kv, "post_process"); - int colour_lut_index = get_required_int (kv, "colour_lut"); - int j2k_bandwidth = get_required_int (kv, "j2k_bandwidth"); - Position subtitle_position (get_optional_int (kv, "subtitle_x"), get_optional_int (kv, "subtitle_y")); - Size subtitle_size (get_optional_int (kv, "subtitle_width"), get_optional_int (kv, "subtitle_height")); - - /* This checks that colour_lut_index is within range */ - colour_lut_index_to_name (colour_lut_index); - - PixelFormat pixel_format = (PixelFormat) pixel_format_int; - Scaler const * scaler = Scaler::from_id (scaler_id); - - shared_ptr<Image> image (new SimpleImage (pixel_format, in_size, true)); - - image->read_from_socket (socket); + libdcp::Size size ( + xml->number_child<int> ("Width"), xml->number_child<int> ("Height") + ); - shared_ptr<Subtitle> sub; - if (subtitle_size.width && subtitle_size.height) { - shared_ptr<Image> subtitle_image (new SimpleImage (PIX_FMT_RGBA, subtitle_size, true)); - subtitle_image->read_from_socket (socket); - sub.reset (new Subtitle (subtitle_position, subtitle_image)); - } + shared_ptr<Image> image (new Image (PIX_FMT_RGB24, size, true)); - DCPVideoFrame dcp_video_frame ( - image, sub, out_size, padding, subtitle_offset, subtitle_scale, - scaler, frame, frames_per_second, post_process, colour_lut_index, j2k_bandwidth, _log - ); + image->read_from_socket (socket); + DCPVideoFrame dcp_video_frame (image, xml, _log); shared_ptr<EncodedData> encoded = dcp_video_frame.encode_locally (); try { @@ -135,13 +118,13 @@ Server::process (shared_ptr<Socket> socket) } catch (std::exception& e) { _log->log (String::compose ( "Send failed; frame %1, data size %2, pixel format %3, image size %4x%5, %6 components", - frame, encoded->size(), image->pixel_format(), image->size().width, image->size().height, image->components() + dcp_video_frame.frame(), encoded->size(), image->pixel_format(), image->size().width, image->size().height, image->components() ) ); throw; } - return frame; + return dcp_video_frame.frame (); } void @@ -166,7 +149,7 @@ Server::worker_thread () try { frame = process (socket); } catch (std::exception& e) { - _log->log (String::compose ("Error: %1", e.what())); + _log->log (String::compose (N_("Error: %1"), e.what())); } socket.reset (); @@ -176,7 +159,7 @@ Server::worker_thread () if (frame >= 0) { struct timeval end; gettimeofday (&end, 0); - _log->log (String::compose ("Encoded frame %1 in %2", frame, seconds (end) - seconds (start))); + _log->log (String::compose (N_("Encoded frame %1 in %2"), frame, seconds (end) - seconds (start))); } _worker_condition.notify_all (); @@ -186,7 +169,7 @@ Server::worker_thread () void Server::run (int num_threads) { - _log->log (String::compose ("Server starting with %1 threads", num_threads)); + _log->log (String::compose (N_("Server starting with %1 threads"), num_threads)); for (int i = 0; i < num_threads; ++i) { _worker_threads.push_back (new thread (bind (&Server::worker_thread, this))); diff --git a/src/lib/server.h b/src/lib/server.h index 32ba8dc4b..77b51d079 100644 --- a/src/lib/server.h +++ b/src/lib/server.h @@ -17,6 +17,9 @@ */ +#ifndef DCPOMATIC_SERVER_H +#define DCPOMATIC_SERVER_H + /** @file src/server.h * @brief Class to describe a server to which we can send * encoding work, and a class to implement such a server. @@ -26,16 +29,27 @@ #include <boost/thread.hpp> #include <boost/asio.hpp> #include <boost/thread/condition.hpp> +#include <boost/optional.hpp> +#include <libxml++/libxml++.h> #include "log.h" class Socket; +namespace cxml { + class Node; +} + /** @class ServerDescription * @brief Class to describe a server to which we can send encoding work. */ class ServerDescription { public: + ServerDescription () + : _host_name ("") + , _threads (1) + {} + /** @param h Server host name or IP address in string form. * @param t Number of threads to use on the server. */ @@ -44,6 +58,10 @@ public: , _threads (t) {} + ServerDescription (boost::shared_ptr<const cxml::Node>); + + /* Default copy constructor is fine */ + /** @return server's host name or IP address in string form */ std::string host_name () const { return _host_name; @@ -62,9 +80,9 @@ public: _threads = t; } - std::string as_metadata () const; + void as_xml (xmlpp::Node *) const; - static ServerDescription * create_from_metadata (std::string v); + static boost::optional<ServerDescription> create_from_metadata (std::string); private: /** server's host name */ @@ -73,10 +91,10 @@ private: int _threads; }; -class Server +class Server : public boost::noncopyable { public: - Server (Log* log); + Server (boost::shared_ptr<Log> log); void run (int num_threads); @@ -88,5 +106,7 @@ private: std::list<boost::shared_ptr<Socket> > _queue; boost::mutex _worker_mutex; boost::condition _worker_condition; - Log* _log; + boost::shared_ptr<Log> _log; }; + +#endif diff --git a/src/lib/sndfile_content.cc b/src/lib/sndfile_content.cc new file mode 100644 index 000000000..d57cf04e3 --- /dev/null +++ b/src/lib/sndfile_content.cc @@ -0,0 +1,170 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "sndfile_content.h" +#include "sndfile_decoder.h" +#include "film.h" +#include "compose.hpp" +#include "job.h" + +#include "i18n.h" + +using std::string; +using std::stringstream; +using std::cout; +using boost::shared_ptr; +using boost::lexical_cast; + +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) +{ + +} + +SndfileContent::SndfileContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) + , AudioContent (f, node) + , _audio_mapping (node->node_child ("AudioMapping")) +{ + _audio_channels = node->number_child<int> ("AudioChannels"); + _audio_length = node->number_child<AudioContent::Frame> ("AudioLength"); + _audio_frame_rate = node->number_child<int> ("AudioFrameRate"); +} + +string +SndfileContent::summary () const +{ + /* Get the string() here so that the name does not have quotes around it */ + return String::compose (_("%1 [audio]"), path().filename().string()); +} + +string +SndfileContent::technical_summary () const +{ + return Content::technical_summary() + " - " + + AudioContent::technical_summary () + + " - sndfile"; +} + +string +SndfileContent::information () const +{ + if (_audio_frame_rate == 0) { + return ""; + } + + stringstream s; + + s << String::compose ( + _("%1 channels, %2kHz, %3 samples"), + audio_channels(), + content_audio_frame_rate() / 1000.0, + audio_length() + ); + + return s.str (); +} + +bool +SndfileContent::valid_file (boost::filesystem::path f) +{ + /* XXX: more extensions */ + string ext = f.extension().string(); + transform (ext.begin(), ext.end(), ext.begin(), ::tolower); + return (ext == ".wav" || ext == ".aif" || ext == ".aiff"); +} + +void +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 (lexical_cast<string> (audio_channels ())); + node->add_child("AudioLength")->add_child_text (lexical_cast<string> (audio_length ())); + node->add_child("AudioFrameRate")->add_child_text (lexical_cast<string> (content_audio_frame_rate ())); + _audio_mapping.as_xml (node->add_child("AudioMapping")); +} + +Time +SndfileContent::full_length () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + return film->audio_frames_to_time (audio_length ()); +} + +int +SndfileContent::output_audio_frame_rate () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + return film->audio_frame_rate (); +} + +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 new file mode 100644 index 000000000..191d62527 --- /dev/null +++ b/src/lib/sndfile_content.h @@ -0,0 +1,83 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_SNDFILE_CONTENT_H +#define DCPOMATIC_SNDFILE_CONTENT_H + +extern "C" { +#include <libavutil/audioconvert.h> +} +#include "audio_content.h" + +namespace cxml { + class Node; +} + +class SndfileContent : public AudioContent +{ +public: + SndfileContent (boost::shared_ptr<const Film>, boost::filesystem::path); + SndfileContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + boost::shared_ptr<SndfileContent> shared_from_this () { + return boost::dynamic_pointer_cast<SndfileContent> (Content::shared_from_this ()); + } + + 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; + } + + int output_audio_frame_rate () const; + + 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 new file mode 100644 index 000000000..1fc1ecaf2 --- /dev/null +++ b/src/lib/sndfile_decoder.cc @@ -0,0 +1,120 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <iostream> +#include <sndfile.h> +#include "sndfile_content.h" +#include "sndfile_decoder.h" +#include "film.h" +#include "exceptions.h" +#include "audio_buffers.h" + +#include "i18n.h" + +using std::vector; +using std::string; +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) + , _sndfile_content (c) + , _deinterleave_buffer (0) +{ + _info.format = 0; + _sndfile = sf_open (_sndfile_content->path().string().c_str(), SFM_READ, &_info); + if (!_sndfile) { + throw DecodeError (_("could not open audio file for reading")); + } + + _done = 0; + _remaining = _info.frames; +} + +SndfileDecoder::~SndfileDecoder () +{ + sf_close (_sndfile); + delete[] _deinterleave_buffer; +} + +void +SndfileDecoder::pass () +{ + /* Do things in half second blocks as I think there may be limits + to what FFmpeg (and in particular the resampler) can cope with. + */ + sf_count_t const block = _sndfile_content->content_audio_frame_rate() / 2; + sf_count_t const this_time = min (block, _remaining); + + int const channels = _sndfile_content->audio_channels (); + + shared_ptr<AudioBuffers> data (new AudioBuffers (channels, this_time)); + + if (_sndfile_content->audio_channels() == 1) { + /* No de-interleaving required */ + sf_read_float (_sndfile, data->data(0), this_time); + } else { + /* Deinterleave */ + if (!_deinterleave_buffer) { + _deinterleave_buffer = new float[block * channels]; + } + sf_readf_float (_sndfile, _deinterleave_buffer, this_time); + vector<float*> out_ptr (channels); + for (int i = 0; i < channels; ++i) { + out_ptr[i] = data->data(i); + } + float* in_ptr = _deinterleave_buffer; + for (int i = 0; i < this_time; ++i) { + for (int j = 0; j < channels; ++j) { + *out_ptr[j]++ = *in_ptr++; + } + } + } + + data->set_frames (this_time); + audio (data, _done); + _done += this_time; + _remaining -= this_time; +} + +int +SndfileDecoder::audio_channels () const +{ + return _info.channels; +} + +AudioContent::Frame +SndfileDecoder::audio_length () const +{ + return _info.frames; +} + +int +SndfileDecoder::audio_frame_rate () const +{ + return _info.samplerate; +} + +bool +SndfileDecoder::done () const +{ + return _audio_position >= _sndfile_content->audio_length (); +} diff --git a/src/lib/sndfile_decoder.h b/src/lib/sndfile_decoder.h new file mode 100644 index 000000000..77fa6d177 --- /dev/null +++ b/src/lib/sndfile_decoder.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2012-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 <sndfile.h> +#include "decoder.h" +#include "audio_decoder.h" + +class SndfileContent; + +class SndfileDecoder : public AudioDecoder +{ +public: + SndfileDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const SndfileContent>); + ~SndfileDecoder (); + + void pass (); + bool done () const; + + int audio_channels () const; + AudioContent::Frame audio_length () const; + int audio_frame_rate () const; + +private: + boost::shared_ptr<const SndfileContent> _sndfile_content; + SNDFILE* _sndfile; + SF_INFO _info; + AudioContent::Frame _done; + AudioContent::Frame _remaining; + float* _deinterleave_buffer; +}; diff --git a/src/lib/sound_processor.h b/src/lib/sound_processor.h index 2edf38840..8f2652243 100644 --- a/src/lib/sound_processor.h +++ b/src/lib/sound_processor.h @@ -21,16 +21,17 @@ * @brief A class to describe a sound processor. */ -#ifndef DVDOMATIC_SOUND_PROCESSOR_H -#define DVDOMATIC_SOUND_PROCESSOR_H +#ifndef DCPOMATIC_SOUND_PROCESSOR_H +#define DCPOMATIC_SOUND_PROCESSOR_H #include <string> #include <vector> +#include <boost/utility.hpp> /** @class SoundProcessor * @brief Class to describe a sound processor. */ -class SoundProcessor +class SoundProcessor : public boost::noncopyable { public: SoundProcessor (std::string i, std::string n); diff --git a/src/lib/stack.cpp b/src/lib/stack.cpp new file mode 100644 index 000000000..a8183d344 --- /dev/null +++ b/src/lib/stack.cpp @@ -0,0 +1,461 @@ +// Copyright 2007 Edd Dawson. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#include <cassert> +#include <cstring> +#include <cstdlib> +#include <iomanip> +#include <ostream> +#include <stdexcept> +#include <sstream> + +#include "stack.hpp" + +#if defined(_WIN32) +# include <windows.h> +# include <imagehlp.h> + +# if defined(__MINGW32__) +# define PACKAGE 1 +# define PACKAGE_VERSION 1 +# include <bfd.h> // link against libbfd and libiberty +# include <psapi.h> // link against psapi +# include <cxxabi.h> +# endif + +#elif defined(__GNUC__) +# include <dlfcn.h> +# include <cxxabi.h> +#endif + +namespace +{ + const char * const unknown_function = "[unknown function]"; + const char * const unknown_module = "[unknown module]"; + +#if defined(__GNUC__) + std::string demangle(const char *name) + { + if (!name) + return unknown_function; + + int status = 0; + char *d = 0; + std::string ret = name; + try + { + if ((d = abi::__cxa_demangle(name, 0, 0, &status))) + ret = d; + } + catch (const std::bad_alloc &) { } + + std::free(d); + return ret; + } +#endif + +#if defined(_WIN32) + + // Derive from this to disallow copying of your class. + // c.f. boost::noncopyable + class uncopyable + { + protected: + uncopyable() { } + + private: + uncopyable(const uncopyable &); + uncopyable &operator= (const uncopyable &); + }; + +#if defined(__MINGW32__) + + // Provides a means to translate a program counter offset in to the name of the corresponding function. + class bfd_context : uncopyable + { + private: + struct find_data + { + std::string func; + unsigned int line; + asymbol **symbol_table; + bfd_vma counter; + }; + + public: + bfd_context() : + abfd_(0), + sec_(0), + symbol_table_(0) + { + char procname[MAX_PATH]; + GetModuleFileNameA(NULL, procname, sizeof procname); + + bfd_init(); + abfd_ = bfd_openr(procname, 0); + if (!abfd_) + throw std::runtime_error("Failed to parse object data for the executable"); + + char **formats = 0; + bool b1 = bfd_check_format(abfd_, bfd_object); + bool b2 = bfd_check_format_matches(abfd_, bfd_object, &formats); + bool b3 = bfd_get_file_flags(abfd_) & HAS_SYMS; + + if (!(b1 && b2 && b3)) + { + bfd_close(abfd_); + free(formats); + throw std::runtime_error("Failed to parse object data for the executable"); + } + free(formats); + + // Load symbol table + unsigned dummy = 0; + if (bfd_read_minisymbols(abfd_, FALSE, reinterpret_cast<void **>(&symbol_table_), &dummy) == 0 && + bfd_read_minisymbols(abfd_, TRUE, reinterpret_cast<void **>(&symbol_table_), &dummy) < 0) + { + free(symbol_table_); + bfd_close(abfd_); + throw std::runtime_error("Failed to parse object data for the executable"); + } + } + + ~bfd_context() + { + free(symbol_table_); + bfd_close(abfd_); + } + + std::pair<std::string, unsigned int> get_function_name_and_line(DWORD offset) + { + find_data data; + data.symbol_table = symbol_table_; + data.counter = offset; + + bfd_map_over_sections(abfd_, &find_function_name_in_section, &data); + + return std::make_pair(data.func, data.line); + } + + private: + static void find_function_name_in_section(bfd *abfd, asection *sec, void *opaque_data) + { + assert(sec); + assert(opaque_data); + find_data &data = *static_cast<find_data *>(opaque_data); + + if (!data.func.empty()) return; // already found it + + if (!(bfd_get_section_flags(abfd, sec) & SEC_ALLOC)) return; + + bfd_vma vma = bfd_get_section_vma(abfd, sec); + if (data.counter < vma || vma + bfd_get_section_size(sec) <= data.counter) return; + + const char *func = 0; + const char *file = 0; + unsigned line = 0; + + if (bfd_find_nearest_line(abfd, sec, data.symbol_table, data.counter - vma, &file, &func, &line) && func) { + data.func = demangle(func); + data.line = line; + } + } + + private: + bfd *abfd_; + asection *sec_; + asymbol **symbol_table_; + }; + +#endif // __MINGW32__ + + // g++ spouts warnings if you use {0} to initialize PODs. So we use this instead: + const struct + { + template<typename POD> + operator POD () const { POD p; std::memset(&p, 0, sizeof p); return p; } + } + empty_pod = { }; + + // Wraps a FARPROC. Implicitly convertible to any kind of pointer-to-function. + // Avoids having reinterpret casts all over the place. + struct auto_cast_function_ptr + { + auto_cast_function_ptr(FARPROC f) : fptr_(f) { } + + template<typename FuncPtr> + operator FuncPtr() const { return reinterpret_cast<FuncPtr>(fptr_); } + + FARPROC fptr_; + }; + + // A wrapper around a DLL. Can dynamically get function pointers with the function() function! + class windows_dll : uncopyable + { + public: + explicit windows_dll(const std::string &libname) : + name_(libname), + lib_(LoadLibraryA(name_.c_str())) + { + if (!lib_) throw std::runtime_error("Failed to load dll " + name_); + } + + ~windows_dll() { FreeLibrary(lib_); } + + const std::string &name() const { return name_; } + + auto_cast_function_ptr function(const char *func_name) const + { + FARPROC proc = GetProcAddress(lib_, func_name); + if (!proc) throw std::runtime_error(std::string("failed to load function ") + func_name + " from library " + name_); + + return proc; + } + + private: + std::string name_; + HMODULE lib_; + }; + + // An object that makes sure debugging symbols are available + class symbol_context : uncopyable + { + public: + symbol_context() + { + if (!SymInitialize(GetCurrentProcess(), 0, TRUE)) + throw std::runtime_error("Failed to initialize symbol context"); + } + ~symbol_context() { SymCleanup(GetCurrentProcess()); } + }; + + // A simple Windows mutex class. Use a lock object to lock the mutex for the duration of a scope. + class mutex : uncopyable + { + public: + mutex() { InitializeCriticalSection(&cs_); } + ~mutex() { DeleteCriticalSection(&cs_); } + + private: + friend class lock; + void lock() { EnterCriticalSection(&cs_); } + void unlock() { LeaveCriticalSection(&cs_); } + + CRITICAL_SECTION cs_; + } + g_fill_frames_mtx; + + // A lock for the mutex + class lock : uncopyable + { + public: + lock(mutex &m) : m_(m) { m.lock(); } + ~lock() { m_.unlock(); } + private: + mutex &m_; + }; + + + void fill_frames(std::list<dbg::stack_frame> &frames, dbg::stack::depth_type limit) + { + lock lk(g_fill_frames_mtx); + + symbol_context sc; +#ifdef __MINGW32__ + bfd_context bfdc; +#endif + + STACKFRAME frame = empty_pod; + CONTEXT context = empty_pod; + context.ContextFlags = CONTEXT_FULL; + + windows_dll kernel32("kernel32.dll"); + void (WINAPI *RtlCaptureContext_)(CONTEXT*) = kernel32.function("RtlCaptureContext"); + + RtlCaptureContext_(&context); + +#if defined(_M_AMD64) + frame.AddrPC.Offset = context.Rip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Rsp; + frame.AddrStack.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Rbp; + frame.AddrFrame.Mode = AddrModeFlat; +#else + frame.AddrPC.Offset = context.Eip; + frame.AddrPC.Mode = AddrModeFlat; + frame.AddrStack.Offset = context.Esp; + frame.AddrStack.Mode = AddrModeFlat; + frame.AddrFrame.Offset = context.Ebp; + frame.AddrFrame.Mode = AddrModeFlat; +#endif + + HANDLE process = GetCurrentProcess(); + HANDLE thread = GetCurrentThread(); + + bool skip = true; + bool has_limit = limit != 0; + char symbol_buffer[sizeof(IMAGEHLP_SYMBOL) + 255]; + char module_name_raw[MAX_PATH]; + +#if defined(_M_AMD64) + const DWORD machine = IMAGE_FILE_MACHINE_AMD64; +#else + const DWORD machine = IMAGE_FILE_MACHINE_I386; +#endif + + while(StackWalk(machine, process, thread, &frame, &context, 0, SymFunctionTableAccess, SymGetModuleBase, 0)) + { + if (skip) + { + skip = false; + continue; + } + + if (has_limit && limit-- == 0) break; + + IMAGEHLP_SYMBOL *symbol = reinterpret_cast<IMAGEHLP_SYMBOL *>(symbol_buffer); + symbol->SizeOfStruct = (sizeof *symbol) + 255; + symbol->MaxNameLength = 254; + +#if defined(_WIN64) + DWORD64 module_base = SymGetModuleBase(process, frame.AddrPC.Offset); +#else + DWORD module_base = SymGetModuleBase(process, frame.AddrPC.Offset); +#endif + std::string module_name = unknown_module; + if (module_base && GetModuleFileNameA(reinterpret_cast<HINSTANCE>(module_base), module_name_raw, MAX_PATH)) + module_name = module_name_raw; + +#if defined(__MINGW32__) + std::pair<std::string, unsigned int> func_and_line = bfdc.get_function_name_and_line(frame.AddrPC.Offset); + + if (func_and_line.first.empty()) + { +#if defined(_WIN64) + DWORD64 dummy = 0; +#else + DWORD dummy = 0; +#endif + BOOL got_symbol = SymGetSymFromAddr(process, frame.AddrPC.Offset, &dummy, symbol); + func_and_line.first = got_symbol ? symbol->Name : unknown_function; + } +#else + DWORD dummy = 0; + BOOL got_symbol = SymGetSymFromAddr(process, frame.AddrPC.Offset, &dummy, symbol); + std::string func = got_symbol ? symbol->Name : unknown_function; +#endif + + dbg::stack_frame f(reinterpret_cast<const void *>(frame.AddrPC.Offset), func_and_line.first, func_and_line.second, module_name); + frames.push_back(f); + } + } +#elif defined(__GNUC__) +# if defined(__i386__) || defined(__amd64__) + + void fill_frames(std::list<dbg::stack_frame> &frames, dbg::stack::depth_type limit) + { + // Based on code found at: + // http://www.tlug.org.za/wiki/index.php/Obtaining_a_stack_trace_in_C_upon_SIGSEGV + + Dl_info info; + void **frame = static_cast<void **>(__builtin_frame_address(0)); + void **bp = static_cast<void **>(*frame); + void *ip = frame[1]; + + bool has_limit = limit != 0; + bool skip = true; + + while(bp && ip && dladdr(ip, &info)) + { + if (skip) + skip = false; + else + { + if (has_limit && limit-- == 0) break; + frames.push_back(dbg::stack_frame(ip, demangle(info.dli_sname), info.dli_fname)); + + if(info.dli_sname && !std::strcmp(info.dli_sname, "main")) break; + } + + ip = bp[1]; + bp = static_cast<void**>(bp[0]); + } + } + +# elif defined(__ppc__) + + void fill_frames(std::list<dbg::stack_frame> &frames, dbg::stack::depth_type limit) + { + // Based on code found at: + // http://www.informit.com/articles/article.aspx?p=606582&seqNum=4&rl=1 + + void *ip = __builtin_return_address(0); + void **frame = static_cast<void **>(__builtin_frame_address(1)); + bool has_limit = limit != 0; + Dl_info info; + + do + { + if (has_limit && limit-- == 0) break; + + if (dladdr(ip, &info)) + frames.push_back(dbg::stack_frame(ip, demangle(info.dli_sname), info.dli_fname)); + + if (frame && (frame = static_cast<void**>(*frame))) ip = *(frame + 2); + } + while (frame && ip); + } + +# else + // GNU, but not x86, x64 nor PPC +# error "Sorry but dbg::stack is not supported on this architecture" +# endif +#else + // Unsupported compiler +# error "Sorry but dbg::stack is not supported on this compiler" +#endif + +} // close anonymous namespace + + + +namespace dbg +{ + stack_frame::stack_frame(const void *instruction, const std::string &function, unsigned int line, const std::string &module) : + instruction(instruction), + function(function), + line(line), + module(module) + { + } + + std::ostream &operator<< (std::ostream &out, const stack_frame &frame) + { + return out << frame.instruction << ": " << frame.function << ":" << frame.line << " in " << frame.module; + } + + stack::stack(depth_type limit) + { + fill_frames(frames_, limit); + } + + stack::const_iterator stack::begin() const + { + return frames_.begin(); + } + + stack::const_iterator stack::end() const + { + return frames_.end(); + } + + stack::depth_type stack::depth() const + { + return frames_.size(); + } + +} // close namespace dbg + diff --git a/src/lib/stack.hpp b/src/lib/stack.hpp new file mode 100644 index 000000000..73a13bf85 --- /dev/null +++ b/src/lib/stack.hpp @@ -0,0 +1,58 @@ +// Copyright 2007 Edd Dawson. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// http://www.boost.org/LICENSE_1_0.txt) + +#ifndef STACK_HPP_0022_01092007 +#define STACK_HPP_0022_01092007 + +#include <string> +#include <list> +#include <iosfwd> + +namespace dbg +{ + //! stack_frame objects are collected by a stack object. They contain information about the instruction pointer, + //! the name of the corresponding function and the "module" (executable or library) in which the function resides. + struct stack_frame + { + stack_frame(const void *instruction, const std::string &function, unsigned int line, const std::string &module); + + const void *instruction; + std::string function; + unsigned int line; + std::string module; + }; + + //! Allows you to write a stack_frame object to an std::ostream + std::ostream &operator<< (std::ostream &out, const stack_frame &frame); + + //! Instantiate a dbg::stack object to collect information about the current call stack. Once created, a stack object + //! may be freely copied about and will continue to contain the information about the scope in which collection occurred. + class stack + { + public: + typedef std::list<stack_frame>::size_type depth_type; + typedef std::list<stack_frame>::const_iterator const_iterator; + + //! Collect information about the current call stack. Information on the most recent frames will be collected + //! up to the specified limit. 0 means unlimited. + //! An std::runtime_error may be thrown on failure. + stack(depth_type limit = 0); + + //! Returns an iterator referring to the "top" stack frame + const_iterator begin() const; + + //! Returns an iterator referring to one past the "bottom" stack frame + const_iterator end() const; + + //! Returns the number of frames collected + depth_type depth() const; + + private: + std::list<stack_frame> frames_; + }; + +} // close namespace dbg + +#endif // STACK_HPP_0022_01092007 diff --git a/src/lib/still_image.h b/src/lib/still_image.h new file mode 100644 index 000000000..366d69331 --- /dev/null +++ b/src/lib/still_image.h @@ -0,0 +1,40 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_STILL_IMAGE_H +#define DCPOMATIC_STILL_IMAGE_H + +class StillImageContent; + +class StillImage +{ +public: + StillImage (boost::shared_ptr<const StillImageContent> c) + : _still_image_content (c) + {} + + boost::shared_ptr<const StillImageContent> content () const { + return _still_image_content; + } + +protected: + boost::shared_ptr<const StillImageContent> _still_image_content; +}; + +#endif diff --git a/src/lib/still_image_content.cc b/src/lib/still_image_content.cc new file mode 100644 index 000000000..0cf80b546 --- /dev/null +++ b/src/lib/still_image_content.cc @@ -0,0 +1,113 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "still_image_content.h" +#include "still_image_examiner.h" +#include "config.h" +#include "compose.hpp" +#include "film.h" + +#include "i18n.h" + +using std::string; +using std::cout; +using std::stringstream; +using boost::shared_ptr; + +StillImageContent::StillImageContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , VideoContent (f, p) +{ + +} + +StillImageContent::StillImageContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) + , VideoContent (f, node) +{ + +} + +string +StillImageContent::summary () const +{ + /* Get the string() here so that the name does not have quotes around it */ + return String::compose (_("%1 [still]"), path().filename().string()); +} + +string +StillImageContent::technical_summary () const +{ + return Content::technical_summary() + " - " + + VideoContent::technical_summary() + " - " + + "still"; +} + +void +StillImageContent::as_xml (xmlpp::Node* node) const +{ + node->add_child("Type")->add_child_text ("StillImage"); + Content::as_xml (node); + VideoContent::as_xml (node); +} + +void +StillImageContent::examine (shared_ptr<Job> job) +{ + Content::examine (job); + + shared_ptr<const Film> film = _film.lock (); + assert (film); + + shared_ptr<StillImageExaminer> examiner (new StillImageExaminer (film, shared_from_this())); + + take_from_video_examiner (examiner); + set_video_length (Config::instance()->default_still_length() * video_frame_rate()); +} + +void +StillImageContent::set_video_length (VideoContent::Frame len) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _video_length = len; + } + + signal_changed (ContentProperty::LENGTH); +} + +Time +StillImageContent::full_length () const +{ + shared_ptr<const Film> film = _film.lock (); + assert (film); + + FrameRateConversion frc (video_frame_rate(), film->video_frame_rate ()); + return video_length() * frc.factor() * TIME_HZ / video_frame_rate(); +} + +string +StillImageContent::identifier () const +{ + stringstream s; + s << VideoContent::identifier (); + s << "_" << video_length(); + return s.str (); +} diff --git a/src/lib/still_image_content.h b/src/lib/still_image_content.h new file mode 100644 index 000000000..ccd7fbc03 --- /dev/null +++ b/src/lib/still_image_content.h @@ -0,0 +1,52 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_STILL_IMAGE_CONTENT_H +#define DCPOMATIC_STILL_IMAGE_CONTENT_H + +#include <boost/enable_shared_from_this.hpp> +#include "video_content.h" + +namespace cxml { + class Node; +} + +/** A single image which is to be held on screen for some time (i.e. a slide) */ +class StillImageContent : public VideoContent +{ +public: + StillImageContent (boost::shared_ptr<const Film>, boost::filesystem::path); + StillImageContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + boost::shared_ptr<StillImageContent> shared_from_this () { + return boost::dynamic_pointer_cast<StillImageContent> (Content::shared_from_this ()); + }; + + void examine (boost::shared_ptr<Job>); + std::string summary () const; + std::string technical_summary () const; + void as_xml (xmlpp::Node *) const; + Time full_length () const; + + std::string identifier () const; + + void set_video_length (VideoContent::Frame); +}; + +#endif diff --git a/src/lib/still_image_decoder.cc b/src/lib/still_image_decoder.cc new file mode 100644 index 000000000..6e82f9a55 --- /dev/null +++ b/src/lib/still_image_decoder.cc @@ -0,0 +1,89 @@ +/* + Copyright (C) 2012-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 <iostream> +#include <boost/filesystem.hpp> +#include <Magick++.h> +#include "still_image_content.h" +#include "still_image_decoder.h" +#include "image.h" +#include "film.h" +#include "exceptions.h" + +#include "i18n.h" + +using std::cout; +using boost::shared_ptr; +using libdcp::Size; + +StillImageDecoder::StillImageDecoder (shared_ptr<const Film> f, shared_ptr<const StillImageContent> c) + : Decoder (f) + , VideoDecoder (f, c) + , StillImage (c) +{ + +} + +void +StillImageDecoder::pass () +{ + if (_video_position >= _still_image_content->video_length ()) { + return; + } + + if (_image) { + video (_image, true, _video_position); + return; + } + + Magick::Image* magick_image = new Magick::Image (_still_image_content->path().string ()); + _video_size = libdcp::Size (magick_image->columns(), magick_image->rows()); + + _image.reset (new Image (PIX_FMT_RGB24, _video_size.get(), true)); + + using namespace MagickCore; + + uint8_t* p = _image->data()[0]; + for (int y = 0; y < _video_size->height; ++y) { + uint8_t* q = p; + for (int x = 0; x < _video_size->width; ++x) { + Magick::Color c = magick_image->pixelColor (x, y); + *q++ = c.redQuantum() * 255 / QuantumRange; + *q++ = c.greenQuantum() * 255 / QuantumRange; + *q++ = c.blueQuantum() * 255 / QuantumRange; + } + p += _image->stride()[0]; + } + + delete magick_image; + + video (_image, false, _video_position); +} + +void +StillImageDecoder::seek (VideoContent::Frame frame, bool) +{ + _video_position = frame; +} + +bool +StillImageDecoder::done () const +{ + return _video_position >= _still_image_content->video_length (); +} diff --git a/src/lib/still_image_decoder.h b/src/lib/still_image_decoder.h new file mode 100644 index 000000000..db41b0357 --- /dev/null +++ b/src/lib/still_image_decoder.h @@ -0,0 +1,44 @@ +/* + Copyright (C) 2012-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 "video_decoder.h" +#include "still_image.h" + +namespace Magick { + class Image; +} + +class StillImageContent; + +class StillImageDecoder : public VideoDecoder, public StillImage +{ +public: + StillImageDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const StillImageContent>); + + /* Decoder */ + + void pass (); + void seek (VideoContent::Frame, bool); + bool done () const; + +private: + boost::shared_ptr<Image> _image; + mutable boost::optional<libdcp::Size> _video_size; +}; + diff --git a/src/lib/still_image_examiner.cc b/src/lib/still_image_examiner.cc new file mode 100644 index 000000000..5f45d6365 --- /dev/null +++ b/src/lib/still_image_examiner.cc @@ -0,0 +1,63 @@ +/* + 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 <iostream> +#include <Magick++.h> +#include "still_image_content.h" +#include "still_image_examiner.h" +#include "film.h" + +#include "i18n.h" + +using std::cout; +using boost::shared_ptr; + +StillImageExaminer::StillImageExaminer (shared_ptr<const Film> f, shared_ptr<const StillImageContent> c) + : StillImage (c) + , _film (f) +{ + using namespace MagickCore; + Magick::Image* image = new Magick::Image (_still_image_content->path().string()); + _video_size = libdcp::Size (image->columns(), image->rows()); + delete image; +} + +libdcp::Size +StillImageExaminer::video_size () const +{ + return _video_size; +} + +int +StillImageExaminer::video_length () const +{ + return _still_image_content->video_length (); +} + +float +StillImageExaminer::video_frame_rate () const +{ + boost::shared_ptr<const Film> f = _film.lock (); + if (!f) { + return 24; + } + + return f->video_frame_rate (); +} + diff --git a/src/lib/check_hashes_job.h b/src/lib/still_image_examiner.h index c41af9d3f..fa3456e8a 100644 --- a/src/lib/check_hashes_job.h +++ b/src/lib/still_image_examiner.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,27 +17,25 @@ */ -#include "job.h" +#include "still_image.h" +#include "video_examiner.h" -class DecodeOptions; -class EncodeOptions; +namespace Magick { + class Image; +} -class CheckHashesJob : public Job +class StillImageContent; + +class StillImageExaminer : public StillImage, public VideoExaminer { public: - CheckHashesJob ( - boost::shared_ptr<Film> f, - boost::shared_ptr<const DecodeOptions> od, - boost::shared_ptr<const EncodeOptions> oe, - boost::shared_ptr<Job> req - ); + StillImageExaminer (boost::shared_ptr<const Film>, boost::shared_ptr<const StillImageContent>); - std::string name () const; - void run (); - std::string status () const; + float video_frame_rate () const; + libdcp::Size video_size () const; + VideoContent::Frame video_length () const; private: - boost::shared_ptr<const DecodeOptions> _decode_opt; - boost::shared_ptr<const EncodeOptions> _encode_opt; - int _bad; + boost::weak_ptr<const Film> _film; + libdcp::Size _video_size; }; diff --git a/src/lib/stream.cc b/src/lib/stream.cc deleted file mode 100644 index 4f12f41b9..000000000 --- a/src/lib/stream.cc +++ /dev/null @@ -1,90 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <sstream> -#include "compose.hpp" -#include "stream.h" -#include "ffmpeg_decoder.h" -#include "external_audio_decoder.h" - -using std::string; -using std::stringstream; -using boost::shared_ptr; -using boost::optional; - -/** Construct a SubtitleStream from a value returned from to_string(). - * @param t String returned from to_string(). - * @param v State file version. - */ -SubtitleStream::SubtitleStream (string t, boost::optional<int>) -{ - stringstream n (t); - n >> _id; - - size_t const s = t.find (' '); - if (s != string::npos) { - _name = t.substr (s + 1); - } -} - -/** @return A canonical string representation of this stream */ -string -SubtitleStream::to_string () const -{ - return String::compose ("%1 %2", _id, _name); -} - -/** Create a SubtitleStream from a value returned from to_string(). - * @param t String returned from to_string(). - * @param v State file version. - */ -shared_ptr<SubtitleStream> -SubtitleStream::create (string t, optional<int> v) -{ - return shared_ptr<SubtitleStream> (new SubtitleStream (t, v)); -} - -/** Create an AudioStream from a string returned from to_string(). - * @param t String returned from to_string(). - * @param v State file version. - * @return AudioStream, or 0. - */ -shared_ptr<AudioStream> -audio_stream_factory (string t, optional<int> v) -{ - shared_ptr<AudioStream> s; - - s = FFmpegAudioStream::create (t, v); - if (!s) { - s = ExternalAudioStream::create (t, v); - } - - return s; -} - -/** Create a SubtitleStream from a string returned from to_string(). - * @param t String returned from to_string(). - * @param v State file version. - * @return SubtitleStream, or 0. - */ -shared_ptr<SubtitleStream> -subtitle_stream_factory (string t, optional<int> v) -{ - return SubtitleStream::create (t, v); -} diff --git a/src/lib/stream.h b/src/lib/stream.h deleted file mode 100644 index 16b06e4bc..000000000 --- a/src/lib/stream.h +++ /dev/null @@ -1,121 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/lib/stream.h - * @brief Representations of audio and subtitle streams. - * - * Some content may have multiple `streams' of audio and/or subtitles; perhaps - * for multiple languages, or for stereo / surround mixes. These classes represent - * those streams, and know about their details. - */ - -#ifndef DVDOMATIC_STREAM_H -#define DVDOMATIC_STREAM_H - -#include <stdint.h> -#include <boost/shared_ptr.hpp> -#include <boost/optional.hpp> -extern "C" { -#include <libavutil/audioconvert.h> -} - -/** @class Stream - * @brief Parent class for streams. - */ -class Stream -{ -public: - virtual ~Stream () {} - virtual std::string to_string () const = 0; -}; - -/** @class AudioStream - * @brief A stream of audio data. - */ -struct AudioStream : public Stream -{ -public: - AudioStream (int r, int64_t l) - : _sample_rate (r) - , _channel_layout (l) - {} - - /* Only used for backwards compatibility for state file version < 1 */ - void set_sample_rate (int s) { - _sample_rate = s; - } - - int channels () const { - return av_get_channel_layout_nb_channels (_channel_layout); - } - - int sample_rate () const { - return _sample_rate; - } - - int64_t channel_layout () const { - return _channel_layout; - } - -protected: - AudioStream () - : _sample_rate (0) - , _channel_layout (0) - {} - - int _sample_rate; - int64_t _channel_layout; -}; - -/** @class SubtitleStream - * @brief A stream of subtitle data. - */ -class SubtitleStream : public Stream -{ -public: - SubtitleStream (std::string n, int i) - : _name (n) - , _id (i) - {} - - std::string to_string () const; - - std::string name () const { - return _name; - } - - int id () const { - return _id; - } - - static boost::shared_ptr<SubtitleStream> create (std::string t, boost::optional<int> v); - -private: - friend class stream_test; - - SubtitleStream (std::string t, boost::optional<int> v); - - std::string _name; - int _id; -}; - -boost::shared_ptr<AudioStream> audio_stream_factory (std::string t, boost::optional<int> version); -boost::shared_ptr<SubtitleStream> subtitle_stream_factory (std::string t, boost::optional<int> version); - -#endif diff --git a/src/lib/subtitle.cc b/src/lib/subtitle.cc deleted file mode 100644 index c52d3ac66..000000000 --- a/src/lib/subtitle.cc +++ /dev/null @@ -1,147 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/subtitle.cc - * @brief Representations of subtitles. - */ - -#include "subtitle.h" -#include "image.h" -#include "exceptions.h" - -using namespace std; -using namespace boost; - -/** Construct a TimedSubtitle. This is a subtitle image, position, - * and a range of time over which it should be shown. - * @param sub AVSubtitle to read. - */ -TimedSubtitle::TimedSubtitle (AVSubtitle const & sub) -{ - assert (sub.rects > 0); - - /* Subtitle PTS in seconds (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; - - /* hence start time for this sub */ - _from = packet_time + (double (sub.start_display_time) / 1e3); - _to = packet_time + (double (sub.end_display_time) / 1e3); - - if (sub.num_rects > 1) { - throw DecodeError ("multi-part subtitles not yet supported"); - } - - AVSubtitleRect const * rect = sub.rects[0]; - - if (rect->type != SUBTITLE_BITMAP) { - throw DecodeError ("non-bitmap subtitles not yet supported"); - } - - shared_ptr<Image> image (new SimpleImage (PIX_FMT_RGBA, Size (rect->w, rect->h), true)); - - /* Start of the first line in the subtitle */ - uint8_t* sub_p = rect->pict.data[0]; - /* sub_p looks up into a RGB palette which is here */ - uint32_t const * palette = (uint32_t *) rect->pict.data[1]; - /* Start of the output data */ - uint32_t* out_p = (uint32_t *) image->data()[0]; - - for (int y = 0; y < rect->h; ++y) { - uint8_t* sub_line_p = sub_p; - uint32_t* out_line_p = out_p; - for (int x = 0; x < rect->w; ++x) { - *out_line_p++ = palette[*sub_line_p++]; - } - sub_p += rect->pict.linesize[0]; - out_p += image->stride()[0] / sizeof (uint32_t); - } - - _subtitle.reset (new Subtitle (Position (rect->x, rect->y), image)); -} - -/** @param t Time in seconds from the start of the source */ -bool -TimedSubtitle::displayed_at (double t) const -{ - return t >= _from && t <= _to; -} - -/** Construct a subtitle, which is an image and a position. - * @param p Position within the (uncropped) source frame. - * @param i Image of the subtitle (should be RGBA). - */ -Subtitle::Subtitle (Position p, shared_ptr<Image> i) - : _position (p) - , _image (i) -{ - -} - -/** Given the area of a subtitle, work out the area it should - * take up when its video frame is scaled up, and it is optionally - * itself scaled and offset. - * @param target_x_scale the x scaling of the video frame that the subtitle is in. - * @param target_y_scale the y scaling of the video frame that the subtitle is in. - * @param sub_area The area of the subtitle within the original source. - * @param subtitle_offset y offset to apply to the subtitle position (+ve is down) - * in the coordinate space of the source. - * @param subtitle_scale scaling factor to apply to the subtitle image. - */ -Rect -subtitle_transformed_area ( - float target_x_scale, float target_y_scale, - Rect sub_area, int subtitle_offset, float subtitle_scale - ) -{ - Rect tx; - - sub_area.y += subtitle_offset; - - /* We will scale the subtitle by the same amount as the video frame, and also by the additional - subtitle_scale - */ - tx.width = sub_area.width * target_x_scale * subtitle_scale; - tx.height = sub_area.height * target_y_scale * subtitle_scale; - - /* Then we need a corrective translation, consisting of two parts: - * - * 1. that which is the result of the scaling of the subtitle by target_x_scale and target_y_scale; this will be - * sub_area.x * target_x_scale and sub_area.y * target_y_scale. - * - * 2. that to shift the origin of the scale by subtitle_scale to the centre of the subtitle; this will be - * (width_before_subtitle_scale * (1 - subtitle_scale) / 2) and - * (height_before_subtitle_scale * (1 - subtitle_scale) / 2). - * - * Combining these two translations gives these expressions. - */ - - tx.x = target_x_scale * (sub_area.x + (sub_area.width * (1 - subtitle_scale) / 2)); - tx.y = target_y_scale * (sub_area.y + (sub_area.height * (1 - subtitle_scale) / 2)); - - return tx; -} - -/** @return area that this subtitle takes up, in the original uncropped source's coordinate space */ -Rect -Subtitle::area () const -{ - return Rect (_position.x, _position.y, _image->size().width, _image->size().height); -} diff --git a/src/lib/subtitle.h b/src/lib/subtitle.h deleted file mode 100644 index 38ba4e70e..000000000 --- a/src/lib/subtitle.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -/** @file src/subtitle.h - * @brief Representations of subtitles. - */ - -#include <list> -#include <boost/shared_ptr.hpp> -#include "util.h" - -struct AVSubtitle; -class Image; - -/** A subtitle, consisting of an image and a position */ -class Subtitle -{ -public: - Subtitle (Position p, boost::shared_ptr<Image> i); - - void set_position (Position p) { - _position = p; - } - - Position position () const { - return _position; - } - - boost::shared_ptr<Image> image () const { - return _image; - } - - Rect area () const; - -private: - Position _position; - boost::shared_ptr<Image> _image; -}; - -Rect -subtitle_transformed_area ( - float target_x_scale, float target_y_scale, - Rect sub_area, int subtitle_offset, float subtitle_scale - ); - -/** A Subtitle class with details of the time over which it should be shown */ -class TimedSubtitle -{ -public: - TimedSubtitle (AVSubtitle const &); - - bool displayed_at (double t) const; - - boost::shared_ptr<Subtitle> subtitle () const { - return _subtitle; - } - -private: - /** the subtitle */ - boost::shared_ptr<Subtitle> _subtitle; - /** display from time in seconds from the start of the film */ - double _from; - /** display to time in seconds from the start of the film */ - double _to; -}; diff --git a/src/lib/subtitle_content.cc b/src/lib/subtitle_content.cc new file mode 100644 index 000000000..9fefbbfcd --- /dev/null +++ b/src/lib/subtitle_content.cc @@ -0,0 +1,72 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <libcxml/cxml.h> +#include "subtitle_content.h" + +using std::string; +using boost::shared_ptr; +using boost::lexical_cast; + +int const SubtitleContentProperty::SUBTITLE_OFFSET = 500; +int const SubtitleContentProperty::SUBTITLE_SCALE = 501; + +SubtitleContent::SubtitleContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , _subtitle_offset (0) + , _subtitle_scale (1) +{ + +} + +SubtitleContent::SubtitleContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : Content (f, node) + , _subtitle_offset (0) + , _subtitle_scale (1) +{ + _subtitle_offset = node->number_child<float> ("SubtitleOffset"); + _subtitle_scale = node->number_child<float> ("SubtitleScale"); +} + +void +SubtitleContent::as_xml (xmlpp::Node* root) const +{ + root->add_child("SubtitleOffset")->add_child_text (lexical_cast<string> (_subtitle_offset)); + root->add_child("SubtitleScale")->add_child_text (lexical_cast<string> (_subtitle_scale)); +} + +void +SubtitleContent::set_subtitle_offset (double o) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _subtitle_offset = o; + } + signal_changed (SubtitleContentProperty::SUBTITLE_OFFSET); +} + +void +SubtitleContent::set_subtitle_scale (double s) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _subtitle_scale = s; + } + signal_changed (SubtitleContentProperty::SUBTITLE_SCALE); +} diff --git a/src/lib/subtitle_content.h b/src/lib/subtitle_content.h new file mode 100644 index 000000000..c29485fee --- /dev/null +++ b/src/lib/subtitle_content.h @@ -0,0 +1,64 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_SUBTITLE_CONTENT_H +#define DCPOMATIC_SUBTITLE_CONTENT_H + +#include "content.h" + +class SubtitleContentProperty +{ +public: + static int const SUBTITLE_OFFSET; + static int const SUBTITLE_SCALE; +}; + +class SubtitleContent : public virtual Content +{ +public: + SubtitleContent (boost::shared_ptr<const Film>, boost::filesystem::path); + SubtitleContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + + void set_subtitle_offset (double); + void set_subtitle_scale (double); + + double subtitle_offset () const { + boost::mutex::scoped_lock lm (_mutex); + return _subtitle_offset; + } + + double subtitle_scale () const { + boost::mutex::scoped_lock lm (_mutex); + return _subtitle_scale; + } + +private: + friend class ffmpeg_pts_offset_test; + + /** y offset for placing subtitles, as a proportion of the container height; + +ve is further down the frame, -ve is further up. + */ + double _subtitle_offset; + /** scale factor to apply to subtitles */ + double _subtitle_scale; +}; + +#endif diff --git a/src/lib/gain.cc b/src/lib/subtitle_decoder.cc index cec3b3c62..c06f3d718 100644 --- a/src/lib/gain.cc +++ b/src/lib/subtitle_decoder.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,29 +17,23 @@ */ -#include "gain.h" +#include <boost/shared_ptr.hpp> +#include "subtitle_decoder.h" using boost::shared_ptr; -/** @param gain gain in dB */ -Gain::Gain (Log* log, float gain) - : AudioProcessor (log) - , _gain (gain) +SubtitleDecoder::SubtitleDecoder (shared_ptr<const Film> f) + : Decoder (f) { } + +/** Called by subclasses when a subtitle is ready. + * Image may be 0 to say that there is no current subtitle. + */ void -Gain::process_audio (shared_ptr<AudioBuffers> b) +SubtitleDecoder::subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, Time from, Time to) { - if (_gain != 0) { - float const linear_gain = pow (10, _gain / 20); - for (int i = 0; i < b->channels(); ++i) { - for (int j = 0; j < b->frames(); ++j) { - b->data(i)[j] *= linear_gain; - } - } - } - - Audio (b); + Subtitle (image, rect, from, to); } diff --git a/src/lib/subtitle_decoder.h b/src/lib/subtitle_decoder.h new file mode 100644 index 000000000..eeeadbd3f --- /dev/null +++ b/src/lib/subtitle_decoder.h @@ -0,0 +1,38 @@ +/* + 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 <boost/signals2.hpp> +#include "decoder.h" +#include "rect.h" +#include "types.h" + +class Film; +class TimedSubtitle; +class Image; + +class SubtitleDecoder : public virtual Decoder +{ +public: + SubtitleDecoder (boost::shared_ptr<const Film>); + + boost::signals2::signal<void (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time)> Subtitle; + +protected: + void subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, Time, Time); +}; diff --git a/src/lib/timer.cc b/src/lib/timer.cc index a45e80dcb..69a7e3aa9 100644 --- a/src/lib/timer.cc +++ b/src/lib/timer.cc @@ -26,6 +26,8 @@ #include "timer.h" #include "util.h" +#include "i18n.h" + using namespace std; /** @param n Name to use when giving output */ @@ -40,7 +42,7 @@ PeriodTimer::~PeriodTimer () { struct timeval stop; gettimeofday (&stop, 0); - cout << "T: " << _name << ": " << (seconds (stop) - seconds (_start)) << "\n"; + cout << N_("T: ") << _name << N_(": ") << (seconds (stop) - seconds (_start)) << N_("\n"); } /** @param n Name to use when giving output. @@ -80,10 +82,10 @@ StateTimer::~StateTimer () } - set_state (""); + set_state (N_("")); - cout << _name << ":\n"; + cout << _name << N_(":\n"); for (map<string, double>::iterator i = _totals.begin(); i != _totals.end(); ++i) { - cout << "\t" << i->first << " " << i->second << "\n"; + cout << N_("\t") << i->first << " " << i->second << N_("\n"); } } diff --git a/src/lib/timer.h b/src/lib/timer.h index f509a7492..4a5aa12de 100644 --- a/src/lib/timer.h +++ b/src/lib/timer.h @@ -22,8 +22,8 @@ * @brief Some timing classes for debugging and profiling. */ -#ifndef DVDOMATIC_TIMER_H -#define DVDOMATIC_TIMER_H +#ifndef DCPOMATIC_TIMER_H +#define DCPOMATIC_TIMER_H #include <string> #include <map> @@ -53,7 +53,7 @@ private: * spends in one of a set of states. * * Once constructed, the caller can call set_state() whenever - * its state changes. When StateTimer is destroyed, it will + * its state changes. When StateTimer is destroyed, it will * output (to cout) a summary of the time spent in each state. */ class StateTimer diff --git a/src/lib/transcode_job.cc b/src/lib/transcode_job.cc index dfb9b1071..c9ec2053d 100644 --- a/src/lib/transcode_job.cc +++ b/src/lib/transcode_job.cc @@ -25,10 +25,10 @@ #include <iomanip> #include "transcode_job.h" #include "film.h" -#include "format.h" #include "transcoder.h" #include "log.h" -#include "encoder.h" + +#include "i18n.h" using std::string; using std::stringstream; @@ -37,13 +37,9 @@ using std::setprecision; using boost::shared_ptr; /** @param s Film to use. - * @param o Options. - * @param req Job that must be completed before this job is run. */ -TranscodeJob::TranscodeJob (shared_ptr<Film> f, shared_ptr<const DecodeOptions> od, shared_ptr<const EncodeOptions> oe, shared_ptr<Job> req) - : Job (f, req) - , _decode_opt (od) - , _encode_opt (oe) +TranscodeJob::TranscodeJob (shared_ptr<const Film> f) + : Job (f) { } @@ -51,7 +47,7 @@ TranscodeJob::TranscodeJob (shared_ptr<Film> f, shared_ptr<const DecodeOptions> string TranscodeJob::name () const { - return String::compose ("Transcode %1", _film->name()); + return String::compose (_("Transcode %1"), _film->name()); } void @@ -59,22 +55,20 @@ TranscodeJob::run () { try { - _film->log()->log ("Transcode job starting"); - _film->log()->log (String::compose ("Audio delay is %1ms", _film->audio_delay())); + _film->log()->log (N_("Transcode job starting")); - _encoder.reset (new Encoder (_film, _encode_opt)); - Transcoder w (_film, _decode_opt, this, _encoder); - w.go (); + _transcoder.reset (new Transcoder (_film, shared_from_this ())); + _transcoder->go (); set_progress (1); set_state (FINISHED_OK); - _film->log()->log ("Transcode job completed successfully"); + _film->log()->log (N_("Transcode job completed successfully")); } catch (std::exception& e) { set_progress (1); set_state (FINISHED_ERROR); - _film->log()->log (String::compose ("Transcode job failed (%1)", e.what())); + _film->log()->log (String::compose (N_("Transcode job failed (%1)"), e.what())); throw; } @@ -83,16 +77,11 @@ TranscodeJob::run () string TranscodeJob::status () const { - if (!_encoder) { - return "0%"; + if (!_transcoder) { + return _("0%"); } - if (_encoder->skipping () && !finished ()) { - return "skipping already-encoded frames"; - } - - - float const fps = _encoder->current_frames_per_second (); + float const fps = _transcoder->current_encoding_rate (); if (fps == 0) { return Job::status (); } @@ -102,7 +91,12 @@ TranscodeJob::status () const s << Job::status (); if (!finished ()) { - s << "; " << fixed << setprecision (1) << fps << " frames per second"; + if (_transcoder->state() == Encoder::TRANSCODING) { + s << "; " << fixed << setprecision (1) << fps << N_(" ") << _("frames per second"); + } else { + /* TRANSLATORS: this means `computing a hash' as in a digest of a block of data */ + s << "; " << _("hashing"); + } } return s.str (); @@ -111,16 +105,17 @@ TranscodeJob::status () const int TranscodeJob::remaining_time () const { - float fps = _encoder->current_frames_per_second (); - if (fps == 0) { + if (!_transcoder) { return 0; } + + float fps = _transcoder->current_encoding_rate (); - if (!_film->dcp_length()) { + if (fps == 0) { return 0; } - /* We assume that dcp_length() is valid, if it is set */ - SourceFrame const left = _film->dcp_trim_start() + _film->dcp_length().get() - _encoder->video_frame(); + /* Compute approximate proposed length here, as it's only here that we need it */ + OutputVideoFrame const left = _film->time_to_video_frames (_film->length ()) - _transcoder->video_frames_out(); return left / fps; } diff --git a/src/lib/transcode_job.h b/src/lib/transcode_job.h index 97f655e15..9128206d2 100644 --- a/src/lib/transcode_job.h +++ b/src/lib/transcode_job.h @@ -24,9 +24,7 @@ #include <boost/shared_ptr.hpp> #include "job.h" -class Encoder; -class DecodeOptions; -class EncodeOptions; +class Transcoder; /** @class TranscodeJob * @brief A job which transcodes from one format to another. @@ -34,17 +32,14 @@ class EncodeOptions; class TranscodeJob : public Job { public: - TranscodeJob (boost::shared_ptr<Film> f, boost::shared_ptr<const DecodeOptions> od, boost::shared_ptr<const EncodeOptions> oe, boost::shared_ptr<Job> req); + TranscodeJob (boost::shared_ptr<const Film> f); std::string name () const; void run (); std::string status () const; -protected: +private: int remaining_time () const; -private: - boost::shared_ptr<const DecodeOptions> _decode_opt; - boost::shared_ptr<const EncodeOptions> _encode_opt; - boost::shared_ptr<Encoder> _encoder; + boost::shared_ptr<Transcoder> _transcoder; }; diff --git a/src/lib/transcoder.cc b/src/lib/transcoder.cc index 87a1fb3f2..63ba77939 100644 --- a/src/lib/transcoder.cc +++ b/src/lib/transcoder.cc @@ -28,101 +28,71 @@ #include <boost/signals2.hpp> #include "transcoder.h" #include "encoder.h" -#include "decoder_factory.h" #include "film.h" -#include "matcher.h" -#include "delay_line.h" -#include "options.h" -#include "gain.h" #include "video_decoder.h" #include "audio_decoder.h" +#include "player.h" +#include "job.h" using std::string; -using std::cout; using boost::shared_ptr; +using boost::weak_ptr; using boost::dynamic_pointer_cast; +static void +video_proxy (weak_ptr<Encoder> encoder, shared_ptr<const Image> image, Eyes eyes, ColourConversion conversion, bool same) +{ + shared_ptr<Encoder> e = encoder.lock (); + if (e) { + e->process_video (image, eyes, conversion, 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 o Decode options. * @param j Job that we are running under, or 0. * @param e Encoder to use. */ -Transcoder::Transcoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j, shared_ptr<Encoder> e) +Transcoder::Transcoder (shared_ptr<const Film> f, shared_ptr<Job> j) : _job (j) - , _encoder (e) - , _decoders (decoder_factory (f, o, j)) + , _player (f->make_player ()) + , _encoder (new Encoder (f, j)) { - assert (_encoder); - - if (f->audio_stream()) { - shared_ptr<AudioStream> st = f->audio_stream(); - _matcher.reset (new Matcher (f->log(), st->sample_rate(), f->frames_per_second())); - _delay_line.reset (new DelayLine (f->log(), st->channels(), f->audio_delay() * st->sample_rate() / 1000)); - _gain.reset (new Gain (f->log(), f->audio_gain())); - } - - /* Set up the decoder to use the film's set streams */ - _decoders.video->set_subtitle_stream (f->subtitle_stream ()); - if (_decoders.audio) { - _decoders.audio->set_audio_stream (f->audio_stream ()); - } - - if (_matcher) { - _decoders.video->connect_video (_matcher); - _matcher->connect_video (_encoder); - } else { - _decoders.video->connect_video (_encoder); - } - - if (_matcher && _delay_line && _decoders.audio) { - _decoders.audio->connect_audio (_delay_line); - _delay_line->connect_audio (_matcher); - _matcher->connect_audio (_gain); - _gain->connect_audio (_encoder); - } + _player->Video.connect (bind (video_proxy, _encoder, _1, _2, _3, _4)); + _player->Audio.connect (bind (audio_proxy, _encoder, _1)); } -/** Run the decoder, passing its output to the encoder, until the decoder - * has no more data to present. - */ void Transcoder::go () { _encoder->process_begin (); - try { - bool done[2] = { false, false }; - - while (1) { - if (!done[0]) { - done[0] = _decoders.video->pass (); - _decoders.video->set_progress (); - } + while (!_player->pass ()) {} + _encoder->process_end (); +} - if (!done[1] && _decoders.audio && dynamic_pointer_cast<Decoder> (_decoders.audio) != dynamic_pointer_cast<Decoder> (_decoders.video)) { - done[1] = _decoders.audio->pass (); - } else { - done[1] = true; - } +float +Transcoder::current_encoding_rate () const +{ + return _encoder->current_encoding_rate (); +} - if (done[0] && done[1]) { - break; - } - } - - } catch (...) { - _encoder->process_end (); - throw; - } - - if (_delay_line) { - _delay_line->process_end (); - } - if (_matcher) { - _matcher->process_end (); - } - if (_gain) { - _gain->process_end (); - } - _encoder->process_end (); +int +Transcoder::video_frames_out () const +{ + return _encoder->video_frames_out (); +} + +Encoder::State +Transcoder::state () const +{ + return _encoder->state (); } diff --git a/src/lib/transcoder.h b/src/lib/transcoder.h index b50113742..7bf214a88 100644 --- a/src/lib/transcoder.h +++ b/src/lib/transcoder.h @@ -17,58 +17,33 @@ */ -/** @file src/transcoder.h - * @brief A class which takes a FilmState and some Options, then uses those to transcode a Film. - * - * A decoder is selected according to the content type, and the encoder can be specified - * as a parameter to the constructor. - */ - -#include "decoder_factory.h" +#include "types.h" +#include "encoder.h" class Film; class Job; class Encoder; -class FilmState; -class Matcher; class VideoFilter; -class Gain; -class VideoDecoder; -class AudioDecoder; -class DelayLine; -class EncodeOptions; -class DecodeOptions; +class Player; -/** @class Transcoder - * @brief A class which takes a FilmState and some Options, then uses those to transcode a Film. - * - * A decoder is selected according to the content type, and the encoder can be specified - * as a parameter to the constructor. - */ -class Transcoder +/** @class Transcoder */ +class Transcoder : public boost::noncopyable { public: Transcoder ( - boost::shared_ptr<Film> f, - boost::shared_ptr<const DecodeOptions> o, - Job* j, - boost::shared_ptr<Encoder> e + boost::shared_ptr<const Film> f, + boost::shared_ptr<Job> j ); void go (); - boost::shared_ptr<VideoDecoder> video_decoder () const { - return _decoders.video; - } + float current_encoding_rate () const; + Encoder::State state () const; + int video_frames_out () const; -protected: +private: /** A Job that is running this Transcoder, or 0 */ - Job* _job; - /** The encoder that we will use */ + boost::shared_ptr<Job> _job; + boost::shared_ptr<Player> _player; boost::shared_ptr<Encoder> _encoder; - /** The decoders that we will use */ - Decoders _decoders; - boost::shared_ptr<Matcher> _matcher; - boost::shared_ptr<DelayLine> _delay_line; - boost::shared_ptr<Gain> _gain; }; diff --git a/src/lib/types.cc b/src/lib/types.cc new file mode 100644 index 000000000..bc4f5f8d9 --- /dev/null +++ b/src/lib/types.cc @@ -0,0 +1,67 @@ +/* + 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 "types.h" + +using std::max; +using std::min; +using std::string; + +bool operator== (Crop const & a, Crop const & b) +{ + return (a.left == b.left && a.right == b.right && a.top == b.top && a.bottom == b.bottom); +} + +bool operator!= (Crop const & a, Crop const & b) +{ + return !(a == b); +} + +/** @param r Resolution. + * @return Untranslated string representation. + */ +string +resolution_to_string (Resolution r) +{ + switch (r) { + case RESOLUTION_2K: + return "2K"; + case RESOLUTION_4K: + return "4K"; + } + + assert (false); + return ""; +} + + +Resolution +string_to_resolution (string s) +{ + if (s == "2K") { + return RESOLUTION_2K; + } + + if (s == "4K") { + return RESOLUTION_4K; + } + + assert (false); + return RESOLUTION_2K; +} diff --git a/src/lib/types.h b/src/lib/types.h new file mode 100644 index 000000000..01560ba81 --- /dev/null +++ b/src/lib/types.h @@ -0,0 +1,103 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_TYPES_H +#define DCPOMATIC_TYPES_H + +#include <vector> +#include <stdint.h> +#include <boost/shared_ptr.hpp> +#include <libdcp/util.h> + +class Content; +class AudioBuffers; + +/** The version number of the protocol used to communicate + * with servers. Intended to be bumped when incompatibilities + * are introduced. + */ +#define SERVER_LINK_VERSION 1 + +typedef int64_t Time; +#define TIME_MAX INT64_MAX +#define TIME_HZ ((Time) 96000) +typedef int64_t OutputAudioFrame; +typedef int OutputVideoFrame; +typedef std::vector<boost::shared_ptr<Content> > ContentList; + +template<class T> +struct TimedAudioBuffers +{ + TimedAudioBuffers () + : time (0) + {} + + TimedAudioBuffers (boost::shared_ptr<AudioBuffers> a, T t) + : audio (a) + , time (t) + {} + + boost::shared_ptr<AudioBuffers> audio; + T time; +}; + +enum VideoFrameType +{ + VIDEO_FRAME_TYPE_2D, + VIDEO_FRAME_TYPE_3D_LEFT_RIGHT +}; + +enum Eyes +{ + EYES_BOTH, + EYES_LEFT, + EYES_RIGHT, + EYES_COUNT +}; + +/** @struct Crop + * @brief A description of the crop of an image or video. + */ +struct Crop +{ + Crop () : left (0), right (0), top (0), bottom (0) {} + Crop (int l, int r, int t, int b) : left (l), right (r), top (t), bottom (b) {} + + /** Number of pixels to remove from the left-hand side */ + int left; + /** Number of pixels to remove from the right-hand side */ + int right; + /** Number of pixels to remove from the top */ + int top; + /** Number of pixels to remove from the bottom */ + int bottom; +}; + +extern bool operator== (Crop const & a, Crop const & b); +extern bool operator!= (Crop const & a, Crop const & b); + +enum Resolution { + RESOLUTION_2K, + RESOLUTION_4K +}; + +std::string resolution_to_string (Resolution); +Resolution string_to_resolution (std::string); + +#endif diff --git a/src/lib/ui_signaller.h b/src/lib/ui_signaller.h index 221bcbe95..7e0f57513 100644 --- a/src/lib/ui_signaller.h +++ b/src/lib/ui_signaller.h @@ -17,8 +17,8 @@ */ -#ifndef DVDOMATIC_UI_SIGNALLER_H -#define DVDOMATIC_UI_SIGNALLER_H +#ifndef DCPOMATIC_UI_SIGNALLER_H +#define DCPOMATIC_UI_SIGNALLER_H #include <boost/bind.hpp> #include <boost/asio.hpp> @@ -27,7 +27,7 @@ /** A class to allow signals to be emitted from non-UI threads and handled * by a UI thread. */ -class UISignaller +class UISignaller : public boost::noncopyable { public: /** Create a UISignaller. Must be called from the UI thread */ @@ -60,7 +60,10 @@ public: } /** This should wake the UI and make it call ui_idle() */ - virtual void wake_ui () = 0; + virtual void wake_ui () { + /* This is only a sensible implementation when there is no GUI... */ + ui_idle (); + } private: /** A io_service which is used as the conduit for messages */ diff --git a/src/lib/util.cc b/src/lib/util.cc index ef6f46575..b8bc1fc9e 100644 --- a/src/lib/util.cc +++ b/src/lib/util.cc @@ -26,7 +26,8 @@ #include <iomanip> #include <iostream> #include <fstream> -#ifdef DVDOMATIC_POSIX +#include <climits> +#ifdef DCPOMATIC_POSIX #include <execinfo.h> #include <cxxabi.h> #endif @@ -38,6 +39,7 @@ #include <boost/lexical_cast.hpp> #include <boost/thread.hpp> #include <boost/filesystem.hpp> +#include <glib.h> #include <openjpeg.h> #include <openssl/md5.h> #include <magick/MagickCore.h> @@ -55,15 +57,44 @@ extern "C" { #include "util.h" #include "exceptions.h" #include "scaler.h" -#include "format.h" #include "dcp_content_type.h" #include "filter.h" #include "sound_processor.h" +#include "config.h" +#include "ratio.h" +#include "job.h" +#ifdef DCPOMATIC_WINDOWS +#include "stack.hpp" +#endif -using namespace std; -using namespace boost; - -thread::id ui_thread; +#include "i18n.h" + +using std::string; +using std::stringstream; +using std::setfill; +using std::ostream; +using std::endl; +using std::vector; +using std::hex; +using std::setw; +using std::ifstream; +using std::ios; +using std::min; +using std::max; +using std::list; +using std::multimap; +using std::istream; +using std::numeric_limits; +using std::pair; +using std::ofstream; +using boost::shared_ptr; +using boost::thread; +using boost::lexical_cast; +using boost::optional; +using libdcp::Size; + +static boost::thread::id ui_thread; +static boost::filesystem::path backtrace_file; /** Convert some number of seconds to a string representation * in hours, minutes and seconds. @@ -81,11 +112,11 @@ seconds_to_hms (int s) m -= (h * 60); stringstream hms; - hms << h << ":"; + hms << h << N_(":"); hms.width (2); - hms << setfill ('0') << m << ":"; + hms << std::setfill ('0') << m << N_(":"); hms.width (2); - hms << setfill ('0') << s; + hms << std::setfill ('0') << s; return hms.str (); } @@ -105,40 +136,40 @@ seconds_to_approximate_hms (int s) if (h > 0) { if (m > 30) { - ap << (h + 1) << " hours"; + ap << (h + 1) << N_(" ") << _("hours"); } else { if (h == 1) { - ap << "1 hour"; + ap << N_("1 ") << _("hour"); } else { - ap << h << " hours"; + ap << h << N_(" ") << _("hours"); } } } else if (m > 0) { if (m == 1) { - ap << "1 minute"; + ap << N_("1 ") << _("minute"); } else { - ap << m << " minutes"; + ap << m << N_(" ") << _("minutes"); } } else { - ap << s << " seconds"; + ap << s << N_(" ") << _("seconds"); } return ap.str (); } -#ifdef DVDOMATIC_POSIX +#ifdef DCPOMATIC_POSIX /** @param l Mangled C++ identifier. * @return Demangled version. */ static string demangle (string l) { - string::size_type const b = l.find_first_of ("("); + string::size_type const b = l.find_first_of (N_("(")); if (b == string::npos) { return l; } - string::size_type const p = l.find_last_of ("+"); + string::size_type const p = l.find_last_of (N_("+")); if (p == string::npos) { return l; } @@ -172,16 +203,12 @@ void stacktrace (ostream& out, int levels) { void *array[200]; - size_t size; - char **strings; - size_t i; - - size = backtrace (array, 200); - strings = backtrace_symbols (array, size); + size_t size = backtrace (array, 200); + char** strings = backtrace_symbols (array, size); if (strings) { - for (i = 0; i < size && (levels == 0 || i < size_t(levels)); i++) { - out << " " << demangle (strings[i]) << endl; + for (size_t i = 0; i < size && (levels == 0 || i < size_t(levels)); i++) { + out << N_(" ") << demangle (strings[i]) << "\n"; } free (strings); @@ -196,7 +223,7 @@ static string ffmpeg_version_to_string (int v) { stringstream s; - s << ((v & 0xff0000) >> 16) << "." << ((v & 0xff00) >> 8) << "." << (v & 0xff); + s << ((v & 0xff0000) >> 16) << N_(".") << ((v & 0xff00) >> 8) << N_(".") << (v & 0xff); return s.str (); } @@ -205,16 +232,16 @@ string dependency_version_summary () { stringstream s; - s << "libopenjpeg " << opj_version () << ", " - << "libavcodec " << ffmpeg_version_to_string (avcodec_version()) << ", " - << "libavfilter " << ffmpeg_version_to_string (avfilter_version()) << ", " - << "libavformat " << ffmpeg_version_to_string (avformat_version()) << ", " - << "libavutil " << ffmpeg_version_to_string (avutil_version()) << ", " - << "libpostproc " << ffmpeg_version_to_string (postproc_version()) << ", " - << "libswscale " << ffmpeg_version_to_string (swscale_version()) << ", " - << MagickVersion << ", " - << "libssh " << ssh_version (0) << ", " - << "libdcp " << libdcp::version << " git " << libdcp::git_commit; + 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_("libpostproc ") << ffmpeg_version_to_string (postproc_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 (); } @@ -225,33 +252,82 @@ seconds (struct timeval t) return t.tv_sec + (double (t.tv_usec) / 1e6); } -/** Call the required functions to set up DVD-o-matic's static arrays, etc. +#ifdef DCPOMATIC_WINDOWS +LONG WINAPI exception_handler(struct _EXCEPTION_POINTERS *) +{ + dbg::stack s; + ofstream f (backtrace_file.string().c_str()); + std::copy(s.begin(), s.end(), std::ostream_iterator<dbg::stack_frame>(f, "\n")); + return EXCEPTION_CONTINUE_SEARCH; +} +#endif + +/** Call the required functions to set up DCP-o-matic's static arrays, etc. * Must be called from the UI thread, if there is one. */ void -dvdomatic_setup () +dcpomatic_setup () { - libdcp::init (); +#ifdef DCPOMATIC_WINDOWS + backtrace_file /= g_get_user_config_dir (); + backtrace_file /= "backtrace.txt"; + SetUnhandledExceptionFilter(exception_handler); +#endif + + avfilter_register_all (); - Format::setup_formats (); + Ratio::setup_ratios (); DCPContentType::setup_dcp_content_types (); Scaler::setup_scalers (); Filter::setup_filters (); SoundProcessor::setup_sound_processors (); - ui_thread = this_thread::get_id (); + ui_thread = boost::this_thread::get_id (); } -/** @param start Start position for the crop within the image. - * @param size Size of the cropped area. - * @return FFmpeg crop filter string. - */ -string -crop_string (Position start, Size size) +#ifdef DCPOMATIC_WINDOWS +boost::filesystem::path +mo_path () { - stringstream s; - s << "crop=" << size.width << ":" << size.height << ":" << start.x << ":" << start.y; - return s.str (); + wchar_t buffer[512]; + GetModuleFileName (0, buffer, 512 * sizeof(wchar_t)); + boost::filesystem::path p (buffer); + p = p.parent_path (); + p = p.parent_path (); + p /= "locale"; + return p; +} +#endif + +void +dcpomatic_setup_gettext_i18n (string lang) +{ +#ifdef DCPOMATIC_POSIX + lang += ".UTF8"; +#endif + + if (!lang.empty ()) { + /* Override our environment language; this is essential on + Windows. + */ + char cmd[64]; + snprintf (cmd, sizeof(cmd), "LANGUAGE=%s", lang.c_str ()); + putenv (cmd); + snprintf (cmd, sizeof(cmd), "LANG=%s", lang.c_str ()); + putenv (cmd); + } + + setlocale (LC_ALL, ""); + textdomain ("libdcpomatic"); + +#ifdef DCPOMATIC_WINDOWS + bindtextdomain ("libdcpomatic", mo_path().string().c_str()); + bind_textdomain_codeset ("libdcpomatic", "UTF8"); +#endif + +#ifdef DCPOMATIC_POSIX + bindtextdomain ("libdcpomatic", POSIX_LOCALE_PREFIX); +#endif } /** @param s A string. @@ -266,7 +342,7 @@ split_at_spaces_considering_quotes (string s) for (string::size_type i = 0; i < s.length(); ++i) { if (s[i] == ' ' && !in_quotes) { out.push_back (c); - c = ""; + c = N_(""); } else if (s[i] == '"') { in_quotes = !in_quotes; } else { @@ -289,7 +365,7 @@ md5_digest (void const * data, int size) stringstream s; for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) { - s << hex << setfill('0') << setw(2) << ((int) digest[i]); + s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]); } return s.str (); @@ -299,16 +375,16 @@ md5_digest (void const * data, int size) * @return MD5 digest of file's contents. */ string -md5_digest (string file) +md5_digest (boost::filesystem::path file) { - ifstream f (file.c_str(), ios::binary); + ifstream f (file.string().c_str(), std::ios::binary); if (!f.good ()) { - throw OpenFileError (file); + throw OpenFileError (file.string()); } - f.seekg (0, ios::end); + f.seekg (0, std::ios::end); int bytes = f.tellg (); - f.seekg (0, ios::beg); + f.seekg (0, std::ios::beg); int const buffer_size = 64 * 1024; char buffer[buffer_size]; @@ -327,275 +403,197 @@ md5_digest (string file) stringstream s; for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) { - s << hex << setfill('0') << setw(2) << ((int) digest[i]); + s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]); } return s.str (); } -/** @param fps Arbitrary frames-per-second value. - * @return DCPFrameRate for this frames-per-second. - */ -DCPFrameRate -dcp_frame_rate (float fps) +/** @param job Optional job for which to report progress */ +string +md5_digest_directory (boost::filesystem::path directory, shared_ptr<Job> job) { - DCPFrameRate dfr; + int const buffer_size = 64 * 1024; + char buffer[buffer_size]; - dfr.run_fast = (fps != rint (fps)); - dfr.frames_per_second = rint (fps); - dfr.skip = 1; + MD5_CTX md5_context; + MD5_Init (&md5_context); - /* XXX: somewhat arbitrary */ - if (fps == 50) { - dfr.frames_per_second = 25; - dfr.skip = 2; + int files = 0; + if (job) { + for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) { + ++files; + } } - return dfr; -} + int j = 0; + for (boost::filesystem::directory_iterator i(directory); i != boost::filesystem::directory_iterator(); ++i) { + ifstream f (i->path().string().c_str(), std::ios::binary); + if (!f.good ()) { + throw OpenFileError (i->path().string()); + } + + f.seekg (0, std::ios::end); + int bytes = f.tellg (); + f.seekg (0, std::ios::beg); + + while (bytes > 0) { + int const t = min (bytes, buffer_size); + f.read (buffer, t); + MD5_Update (&md5_context, buffer, t); + bytes -= t; + } -/** @param An arbitrary sampling rate. - * @return The appropriate DCP-approved sampling rate (48kHz or 96kHz). - */ -int -dcp_audio_sample_rate (int fs) -{ - if (fs <= 48000) { - return 48000; + if (job) { + job->set_progress (float (j) / files); + ++j; + } } - return 96000; -} + unsigned char digest[MD5_DIGEST_LENGTH]; + MD5_Final (digest, &md5_context); -int -dcp_audio_channels (int f) -{ - if (f == 1) { - /* The source is mono, so to put the mono channel into - the centre we need to generate a 5.1 soundtrack. - */ - return 6; + stringstream s; + for (int i = 0; i < MD5_DIGEST_LENGTH; ++i) { + s << std::hex << std::setfill('0') << std::setw(2) << ((int) digest[i]); } - return f; + return s.str (); } - -bool operator== (Size const & a, Size const & b) +static bool +about_equal (float a, float b) { - return (a.width == b.width && a.height == b.height); -} + /* A film of F seconds at f FPS will be Ff frames; + Consider some delta FPS d, so if we run the same + film at (f + d) FPS it will last F(f + d) seconds. -bool operator!= (Size const & a, Size const & b) -{ - return !(a == b); -} + Hence the difference in length over the length of the film will + be F(f + d) - Ff frames + = Ff + Fd - Ff frames + = Fd frames + = Fd/f seconds + + So if we accept a difference of 1 frame, ie 1/f seconds, we can + say that -bool operator== (Crop const & a, Crop const & b) -{ - return (a.left == b.left && a.right == b.right && a.top == b.top && a.bottom == b.bottom); -} + 1/f = Fd/f + ie 1 = Fd + ie d = 1/F + + So for a 3hr film, ie F = 3 * 60 * 60 = 10800, the acceptable + FPS error is 1/F ~= 0.0001 ~= 10-e4 + */ -bool operator!= (Crop const & a, Crop const & b) -{ - return !(a == b); + return (fabs (a - b) < 1e-4); } -/** @param index Colour LUT index. - * @return Human-readable name. +/** @param An arbitrary audio frame rate. + * @return The appropriate DCP-approved frame rate (48kHz or 96kHz). */ -string -colour_lut_index_to_name (int index) +int +dcp_audio_frame_rate (int fs) { - switch (index) { - case 0: - return "sRGB"; - case 1: - return "Rec 709"; + if (fs <= 48000) { + return 48000; } - assert (false); - return ""; + return 96000; } -Socket::Socket () +Socket::Socket (int timeout) : _deadline (_io_service) , _socket (_io_service) - , _buffer_data (0) + , _timeout (timeout) { - _deadline.expires_at (posix_time::pos_infin); + _deadline.expires_at (boost::posix_time::pos_infin); check (); } void Socket::check () { - if (_deadline.expires_at() <= asio::deadline_timer::traits_type::now ()) { + if (_deadline.expires_at() <= boost::asio::deadline_timer::traits_type::now ()) { _socket.close (); - _deadline.expires_at (posix_time::pos_infin); + _deadline.expires_at (boost::posix_time::pos_infin); } _deadline.async_wait (boost::bind (&Socket::check, this)); } -/** Blocking connect with timeout. +/** Blocking connect. * @param endpoint End-point to connect to. - * @param timeout Time-out in seconds. */ void -Socket::connect (asio::ip::basic_resolver_entry<asio::ip::tcp> const & endpoint, int timeout) +Socket::connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint) { - _deadline.expires_from_now (posix_time::seconds (timeout)); - system::error_code ec = asio::error::would_block; - _socket.async_connect (endpoint, lambda::var(ec) = lambda::_1); + _deadline.expires_from_now (boost::posix_time::seconds (_timeout)); + boost::system::error_code ec = boost::asio::error::would_block; + _socket.async_connect (endpoint, boost::lambda::var(ec) = boost::lambda::_1); do { _io_service.run_one(); - } while (ec == asio::error::would_block); + } while (ec == boost::asio::error::would_block); if (ec || !_socket.is_open ()) { - throw NetworkError ("connect timed out"); + throw NetworkError (_("connect timed out")); } } -/** Blocking write with timeout. +/** Blocking write. * @param data Buffer to write. * @param size Number of bytes to write. - * @param timeout Time-out, in seconds. */ void -Socket::write (uint8_t const * data, int size, int timeout) +Socket::write (uint8_t const * data, int size) { - _deadline.expires_from_now (posix_time::seconds (timeout)); - system::error_code ec = asio::error::would_block; + _deadline.expires_from_now (boost::posix_time::seconds (_timeout)); + boost::system::error_code ec = boost::asio::error::would_block; - asio::async_write (_socket, asio::buffer (data, size), lambda::var(ec) = lambda::_1); + boost::asio::async_write (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1); + do { _io_service.run_one (); - } while (ec == asio::error::would_block); - - if (ec) { - throw NetworkError ("write timed out"); - } -} - -/** Blocking read with timeout. - * @param data Buffer to read to. - * @param size Number of bytes to read. - * @param timeout Time-out, in seconds. - */ -int -Socket::read (uint8_t* data, int size, int timeout) -{ - _deadline.expires_from_now (posix_time::seconds (timeout)); - system::error_code ec = asio::error::would_block; + } while (ec == boost::asio::error::would_block); - int amount_read = 0; - - _socket.async_read_some ( - asio::buffer (data, size), - (lambda::var(ec) = lambda::_1, lambda::var(amount_read) = lambda::_2) - ); - - do { - _io_service.run_one (); - } while (ec == asio::error::would_block); - if (ec) { - amount_read = 0; + throw NetworkError (ec.message ()); } - - return amount_read; } -/** Mark some data as being `consumed', so that it will not be returned - * as data again. - * @param size Amount of data to consume, in bytes. - */ void -Socket::consume (int size) +Socket::write (uint32_t v) { - assert (_buffer_data >= size); - - _buffer_data -= size; - if (_buffer_data > 0) { - /* Shift still-valid data to the start of the buffer */ - memmove (_buffer, _buffer + size, _buffer_data); - } + v = htonl (v); + write (reinterpret_cast<uint8_t*> (&v), 4); } -/** Read a definite amount of data from our socket, and mark - * it as consumed. - * @param data Where to put the data. +/** Blocking read. + * @param data Buffer to read to. * @param size Number of bytes to read. */ void -Socket::read_definite_and_consume (uint8_t* data, int size, int timeout) -{ - int const from_buffer = min (_buffer_data, size); - if (from_buffer > 0) { - /* Get data from our buffer */ - memcpy (data, _buffer, from_buffer); - consume (from_buffer); - /* Update our output state */ - data += from_buffer; - size -= from_buffer; - } - - /* read() the rest */ - while (size > 0) { - int const n = read (data, size, timeout); - if (n <= 0) { - throw NetworkError ("could not read"); - } - - data += n; - size -= n; - } -} - -/** Read as much data as is available, up to some limit. - * @param data Where to put the data. - * @param size Maximum amount of data to read. - */ -void -Socket::read_indefinite (uint8_t* data, int size, int timeout) +Socket::read (uint8_t* data, int size) { - assert (size < int (sizeof (_buffer))); + _deadline.expires_from_now (boost::posix_time::seconds (_timeout)); + boost::system::error_code ec = boost::asio::error::would_block; - /* Amount of extra data we need to read () */ - int to_read = size - _buffer_data; - while (to_read > 0) { - /* read as much of it as we can (into our buffer) */ - int const n = read (_buffer + _buffer_data, to_read, timeout); - if (n <= 0) { - throw NetworkError ("could not read"); - } + boost::asio::async_read (_socket, boost::asio::buffer (data, size), boost::lambda::var(ec) = boost::lambda::_1); - to_read -= n; - _buffer_data += n; + do { + _io_service.run_one (); + } while (ec == boost::asio::error::would_block); + + if (ec) { + throw NetworkError (ec.message ()); } - - assert (_buffer_data >= size); - - /* copy data into the output buffer */ - assert (size >= _buffer_data); - memcpy (data, _buffer, size); } -/** @param other A Rect. - * @return The intersection of this with `other'. - */ -Rect -Rect::intersection (Rect const & other) const +uint32_t +Socket::read_uint32 () { - int const tx = max (x, other.x); - int const ty = max (y, other.y); - - return Rect ( - tx, ty, - min (x + width, other.x + other.width) - tx, - min (y + height, other.y + other.height) - ty - ); + uint32_t v; + read (reinterpret_cast<uint8_t *> (&v), 4); + return ntohl (v); } /** Round a number up to the nearest multiple of another number. @@ -611,12 +609,6 @@ stride_round_up (int c, int const * stride, int t) return a - (a % t); } -int -stride_lookup (int c, int const * stride) -{ - return stride[c]; -} - /** 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 @@ -658,13 +650,13 @@ string get_required_string (multimap<string, string> const & kv, string k) { if (kv.count (k) > 1) { - throw StringError ("unexpected multiple keys in key-value set"); + throw StringError (N_("unexpected multiple keys in key-value set")); } multimap<string, string>::const_iterator i = kv.find (k); if (i == kv.end ()) { - throw StringError (String::compose ("missing key %1 in key-value set", k)); + throw StringError (String::compose (_("missing key %1 in key-value set"), k)); } return i->second; @@ -688,12 +680,12 @@ string get_optional_string (multimap<string, string> const & kv, string k) { if (kv.count (k) > 1) { - throw StringError ("unexpected multiple keys in key-value set"); + throw StringError (N_("unexpected multiple keys in key-value set")); } multimap<string, string>::const_iterator i = kv.find (k); if (i == kv.end ()) { - return ""; + return N_(""); } return i->second; @@ -703,7 +695,7 @@ int get_optional_int (multimap<string, string> const & kv, string k) { if (kv.count (k) > 1) { - throw StringError ("unexpected multiple keys in key-value set"); + throw StringError (N_("unexpected multiple keys in key-value set")); } multimap<string, string>::const_iterator i = kv.find (k); @@ -714,193 +706,97 @@ get_optional_int (multimap<string, string> const & kv, string k) return lexical_cast<int> (i->second); } -/** Construct an AudioBuffers. Audio data is undefined after this constructor. - * @param channels Number of channels. - * @param frames Number of frames to reserve space for. - */ -AudioBuffers::AudioBuffers (int channels, int frames) - : _channels (channels) - , _frames (frames) - , _allocated_frames (frames) -{ - _data = new float*[_channels]; - for (int i = 0; i < _channels; ++i) { - _data[i] = new float[frames]; - } -} - -/** Copy constructor. - * @param other Other AudioBuffers; data is copied. - */ -AudioBuffers::AudioBuffers (AudioBuffers const & other) - : _channels (other._channels) - , _frames (other._frames) - , _allocated_frames (other._frames) -{ - _data = new float*[_channels]; - for (int i = 0; i < _channels; ++i) { - _data[i] = new float[_frames]; - memcpy (_data[i], other._data[i], _frames * sizeof (float)); - } -} - -/** AudioBuffers destructor */ -AudioBuffers::~AudioBuffers () +/** Trip an assert if the caller is not in the UI thread */ +void +ensure_ui_thread () { - for (int i = 0; i < _channels; ++i) { - delete[] _data[i]; - } - - delete[] _data; + assert (boost::this_thread::get_id() == ui_thread); } -/** @param c Channel index. - * @return Buffer for this channel. +/** @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'. */ -float* -AudioBuffers::data (int c) const +int64_t +video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second) { - assert (c >= 0 && c < _channels); - return _data[c]; + return ((int64_t) v * audio_sample_rate / frames_per_second); } -/** Set the number of frames that these AudioBuffers will report themselves - * as having. - * @param f Frames; must be less than or equal to the number of allocated frames. - */ -void -AudioBuffers::set_frames (int f) +string +audio_channel_name (int c) { - assert (f <= _allocated_frames); - _frames = f; -} + assert (MAX_AUDIO_CHANNELS == 6); -/** Make all samples on all channels silent */ -void -AudioBuffers::make_silent () -{ - for (int i = 0; i < _channels; ++i) { - make_silent (i); - } + /* TRANSLATORS: these are the names of audio channels; Lfe (sub) is the low-frequency + enhancement channel (sub-woofer)./ + */ + string const channels[] = { + _("Left"), + _("Right"), + _("Centre"), + _("Lfe (sub)"), + _("Left surround"), + _("Right surround"), + }; + + return channels[c]; } -/** Make all samples on a given channel silent. - * @param c Channel. - */ -void -AudioBuffers::make_silent (int c) +FrameRateConversion::FrameRateConversion (float source, int dcp) + : skip (false) + , repeat (false) + , change_speed (false) { - assert (c >= 0 && c < _channels); - - for (int i = 0; i < _frames; ++i) { - _data[c][i] = 0; + if (fabs (source / 2.0 - dcp) < (fabs (source - dcp))) { + skip = true; + } else if (fabs (source * 2 - dcp) < fabs (source - dcp)) { + repeat = true; } -} -/** Copy data from another AudioBuffers to this one. All channels are copied. - * @param from AudioBuffers to copy from; must have the same number of channels as this. - * @param frames_to_copy Number of frames to copy. - * @param read_offset Offset to read from in `from'. - * @param write_offset Offset to write to in `to'. - */ -void -AudioBuffers::copy_from (AudioBuffers* from, int frames_to_copy, int read_offset, int write_offset) -{ - assert (from->channels() == channels()); + change_speed = !about_equal (source * factor(), dcp); - assert (from); - assert (read_offset >= 0 && (read_offset + frames_to_copy) <= from->_allocated_frames); - assert (write_offset >= 0 && (write_offset + frames_to_copy) <= _allocated_frames); + if (!skip && !repeat && !change_speed) { + description = _("Content and DCP have the same rate.\n"); + } else { + if (skip) { + description = _("DCP will use every other frame of the content.\n"); + } else if (repeat) { + description = _("Each content frame will be doubled in the DCP.\n"); + } - for (int i = 0; i < _channels; ++i) { - memcpy (_data[i] + write_offset, from->_data[i] + read_offset, frames_to_copy * sizeof(float)); + if (change_speed) { + float const pc = dcp * 100 / (source * factor()); + description += String::compose (_("DCP will run at %1%% of the content speed.\n"), pc); + } } } -/** Move audio data around. - * @param from Offset to move from. - * @param to Offset to move to. - * @param frames Number of frames to move. - */ - -void -AudioBuffers::move (int from, int to, int frames) +LocaleGuard::LocaleGuard () + : _old (0) { - if (frames == 0) { - return; - } - - assert (from >= 0); - assert (from < _frames); - assert (to >= 0); - assert (to < _frames); - assert (frames > 0); - assert (frames <= _frames); - assert ((from + frames) <= _frames); - assert ((to + frames) <= _frames); - - for (int i = 0; i < _channels; ++i) { - memmove (_data[i] + to, _data[i] + from, frames * sizeof(float)); - } -} + char const * old = setlocale (LC_NUMERIC, 0); -/** Trip an assert if the caller is not in the UI thread */ -void -ensure_ui_thread () -{ - assert (this_thread::get_id() == ui_thread); + if (old) { + _old = strdup (old); + if (strcmp (_old, "C")) { + setlocale (LC_NUMERIC, "C"); + } + } } -/** @param v Source 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 (SourceFrame v, float audio_sample_rate, float frames_per_second) +LocaleGuard::~LocaleGuard () { - return ((int64_t) v * audio_sample_rate / frames_per_second); + setlocale (LC_NUMERIC, _old); + free (_old); } -/** @param f Filename. - * @return true if this file is a still image, false if it is something else. - */ bool -still_image_file (string f) +valid_image_file (boost::filesystem::path f) { -#if BOOST_FILESYSTEM_VERSION == 3 - string ext = boost::filesystem::path(f).extension().string(); -#else - string ext = boost::filesystem::path(f).extension(); -#endif - + string ext = f.extension().string(); transform (ext.begin(), ext.end(), ext.begin(), ::tolower); - - return (ext == ".tif" || ext == ".tiff" || ext == ".jpg" || ext == ".jpeg" || ext == ".png"); + return (ext == ".tif" || ext == ".tiff" || ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".tga"); } -/** @return A pair containing CPU model name and the number of processors */ -pair<string, int> -cpu_info () -{ - pair<string, int> info; - info.second = 0; - -#ifdef DVDOMATIC_POSIX - ifstream f ("/proc/cpuinfo"); - while (f.good ()) { - string l; - getline (f, l); - if (boost::algorithm::starts_with (l, "model name")) { - string::size_type const c = l.find (':'); - if (c != string::npos) { - info.first = l.substr (c + 2); - } - } else if (boost::algorithm::starts_with (l, "processor")) { - ++info.second; - } - } -#endif - - return info; -} diff --git a/src/lib/util.h b/src/lib/util.h index 024c40fb5..a83426206 100644 --- a/src/lib/util.h +++ b/src/lib/util.h @@ -22,172 +22,92 @@ * @brief Some utility functions and classes. */ -#ifndef DVDOMATIC_UTIL_H -#define DVDOMATIC_UTIL_H +#ifndef DCPOMATIC_UTIL_H +#define DCPOMATIC_UTIL_H #include <string> #include <vector> #include <boost/shared_ptr.hpp> #include <boost/asio.hpp> +#include <boost/optional.hpp> +#include <boost/filesystem.hpp> +#include <libdcp/util.h> extern "C" { #include <libavcodec/avcodec.h> #include <libavfilter/avfilter.h> } #include "compose.hpp" +#include "types.h" +#include "video_content.h" -#ifdef DVDOMATIC_DEBUG +#ifdef DCPOMATIC_DEBUG #define TIMING(...) _film->log()->microsecond_log (String::compose (__VA_ARGS__), Log::TIMING); #else #define TIMING(...) #endif +#undef check + /** The maximum number of audio channels that we can cope with */ #define MAX_AUDIO_CHANNELS 6 -class Scaler; +class Job; extern std::string seconds_to_hms (int); extern std::string seconds_to_approximate_hms (int); extern void stacktrace (std::ostream &, int); extern std::string dependency_version_summary (); extern double seconds (struct timeval); -extern void dvdomatic_setup (); +extern void dcpomatic_setup (); +extern void dcpomatic_setup_gettext_i18n (std::string); extern std::vector<std::string> split_at_spaces_considering_quotes (std::string); -extern std::string md5_digest (std::string); +extern std::string md5_digest (boost::filesystem::path); +extern std::string md5_digest_directory (boost::filesystem::path, boost::shared_ptr<Job>); extern std::string md5_digest (void const *, int); extern void ensure_ui_thread (); +extern std::string audio_channel_name (int); +extern bool valid_image_file (boost::filesystem::path); +#ifdef DCPOMATIC_WINDOWS +extern boost::filesystem::path mo_path (); +#endif -typedef int SourceFrame; - -struct DCPFrameRate -{ - /** frames per second for the DCP */ - int frames_per_second; - /** Skip every `skip' frames. e.g. if this is 1, we skip nothing; - * if it's 2, we skip every other frame. - */ - int skip; - /** true if this DCP will run its video faster than the source - * (e.g. if the source is 29.97fps and we will run the DCP at 30fps) - */ - bool run_fast; -}; - -enum ContentType { - STILL, ///< content is still images - VIDEO ///< content is a video -}; - -/** @class Size - * @brief Representation of the size of something */ -struct Size -{ - /** Construct a zero Size */ - Size () - : width (0) - , height (0) - {} - - /** @param w Width. - * @param h Height. - */ - Size (int w, int h) - : width (w) - , height (h) - {} - - /** width */ - int width; - /** height */ - int height; -}; - -extern bool operator== (Size const & a, Size const & b); -extern bool operator!= (Size const & a, Size const & b); - -/** @struct Crop - * @brief A description of the crop of an image or video. - */ -struct Crop -{ - Crop () : left (0), right (0), top (0), bottom (0) {} - - /** Number of pixels to remove from the left-hand side */ - int left; - /** Number of pixels to remove from the right-hand side */ - int right; - /** Number of pixels to remove from the top */ - int top; - /** Number of pixels to remove from the bottom */ - int bottom; -}; - -extern bool operator== (Crop const & a, Crop const & b); -extern bool operator!= (Crop const & a, Crop const & b); - -/** @struct Position - * @brief A position. - */ -struct Position -{ - Position () - : x (0) - , y (0) - {} - - Position (int x_, int y_) - : x (x_) - , y (y_) - {} - - /** x coordinate */ - int x; - /** y coordinate */ - int y; -}; - -/** @struct Rect - * @brief A rectangle. - */ -struct Rect +struct FrameRateConversion { - Rect () - : x (0) - , y (0) - , width (0) - , height (0) - {} - - Rect (int x_, int y_, int w_, int h_) - : x (x_) - , y (y_) - , width (w_) - , height (h_) - {} - - int x; - int y; - int width; - int height; - - Position position () const { - return Position (x, y); + FrameRateConversion (float, int); + + /** @return factor by which to multiply a source frame rate + to get the effective rate after any skip or repeat has happened. + */ + float factor () const { + if (skip) { + return 0.5; + } else if (repeat) { + return 2; + } + + return 1; } - Size size () const { - return Size (width, height); - } + /** true to skip every other frame */ + bool skip; + /** true to repeat every frame once */ + bool repeat; + /** true if this DCP will run its video faster or slower than the source + * without taking into account `repeat' nor `skip'. + * (e.g. change_speed will be true if + * source is 29.97fps, DCP is 30fps + * source is 14.50fps, DCP is 30fps + * but not if + * source is 15.00fps, DCP is 30fps + * source is 12.50fps, DCP is 25fps) + */ + bool change_speed; - Rect intersection (Rect const & other) const; + std::string description; }; -extern std::string crop_string (Position, Size); -extern int dcp_audio_sample_rate (int); -extern DCPFrameRate dcp_frame_rate (float); -extern int dcp_audio_channels (int); -extern std::string colour_lut_index_to_name (int index); +extern int dcp_audio_frame_rate (int); extern int stride_round_up (int, int const *, int); -extern int stride_lookup (int c, int const * stride); 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); @@ -197,90 +117,52 @@ extern std::string get_optional_string (std::multimap<std::string, std::string> /** @class Socket * @brief A class to wrap a boost::asio::ip::tcp::socket with some things - * that are useful for DVD-o-matic. + * that are useful for DCP-o-matic. * * This class wraps some things that I could not work out how to do with boost; - * most notably, sync read/write calls with timeouts, and the ability to peek into - * data being read. + * most notably, sync read/write calls with timeouts. */ class Socket { public: - Socket (); + Socket (int timeout = 30); /** @return Our underlying socket */ boost::asio::ip::tcp::socket& socket () { return _socket; } - void connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint, int timeout); - void write (uint8_t const * data, int size, int timeout); + void connect (boost::asio::ip::basic_resolver_entry<boost::asio::ip::tcp> const & endpoint); + + void write (uint32_t n); + void write (uint8_t const * data, int size); - void read_definite_and_consume (uint8_t* data, int size, int timeout); - void read_indefinite (uint8_t* data, int size, int timeout); - void consume (int amount); + void read (uint8_t* data, int size); + uint32_t read_uint32 (); private: void check (); - int read (uint8_t* data, int size, int timeout); Socket (Socket const &); boost::asio::io_service _io_service; boost::asio::deadline_timer _deadline; boost::asio::ip::tcp::socket _socket; - /** a buffer for small reads */ - uint8_t _buffer[1024]; - /** amount of valid data in the buffer */ - int _buffer_data; + int _timeout; }; -/** @class AudioBuffers - * @brief A class to hold multi-channel audio data in float format. - */ -class AudioBuffers +extern int64_t video_frames_to_audio_frames (VideoContent::Frame v, float audio_sample_rate, float frames_per_second); + +class LocaleGuard { public: - AudioBuffers (int channels, int frames); - AudioBuffers (AudioBuffers const &); - ~AudioBuffers (); - - float** data () const { - return _data; - } + LocaleGuard (); + ~LocaleGuard (); - float* data (int) const; - - int channels () const { - return _channels; - } - - int frames () const { - return _frames; - } - - void set_frames (int f); - - void make_silent (); - void make_silent (int c); - - void copy_from (AudioBuffers* from, int frames_to_copy, int read_offset, int write_offset); - void move (int from, int to, int frames); - private: - /** Number of channels */ - int _channels; - /** Number of frames (where a frame is one sample across all channels) */ - int _frames; - /** Number of frames that _data can hold */ - int _allocated_frames; - /** Audio data (so that, e.g. _data[2][6] is channel 2, sample 6) */ - float** _data; + char* _old; }; -extern int64_t video_frames_to_audio_frames (SourceFrame v, float audio_sample_rate, float frames_per_second); -extern bool still_image_file (std::string); -extern std::pair<std::string, int> cpu_info (); #endif diff --git a/src/lib/version.h b/src/lib/version.h index 71639e3bc..b70be8343 100644 --- a/src/lib/version.h +++ b/src/lib/version.h @@ -1,3 +1,4 @@ -extern char const * dvdomatic_version; -extern char const * dvdomatic_git_commit; +extern char const * dcpomatic_version; +extern char const * dcpomatic_git_commit; +extern char const * dcpomatic_cxx_flags; diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc new file mode 100644 index 000000000..3f6e171a5 --- /dev/null +++ b/src/lib/video_content.cc @@ -0,0 +1,278 @@ +/* + 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 <iomanip> +#include <libcxml/cxml.h> +#include "video_content.h" +#include "video_examiner.h" +#include "ratio.h" +#include "compose.hpp" +#include "config.h" +#include "colour_conversion.h" + +#include "i18n.h" + +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_RATIO = 4; +int const VideoContentProperty::COLOUR_CONVERSION = 5; + +using std::string; +using std::stringstream; +using std::setprecision; +using std::cout; +using boost::shared_ptr; +using boost::lexical_cast; +using boost::optional; + +VideoContent::VideoContent (shared_ptr<const Film> f, Time s, VideoContent::Frame len) + : Content (f, s) + , _video_length (len) + , _video_frame_rate (0) + , _video_frame_type (VIDEO_FRAME_TYPE_2D) + , _ratio (Ratio::from_id ("185")) + , _colour_conversion (Config::instance()->colour_conversions().front().conversion) +{ + +} + +VideoContent::VideoContent (shared_ptr<const Film> f, boost::filesystem::path p) + : Content (f, p) + , _video_length (0) + , _video_frame_rate (0) + , _video_frame_type (VIDEO_FRAME_TYPE_2D) + , _ratio (Ratio::from_id ("185")) + , _colour_conversion (Config::instance()->colour_conversions().front().conversion) +{ + +} + +VideoContent::VideoContent (shared_ptr<const Film> f, shared_ptr<const cxml::Node> node) + : 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"); + _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"); + _crop.top = node->number_child<int> ("TopCrop"); + _crop.bottom = node->number_child<int> ("BottomCrop"); + optional<string> r = node->optional_string_child ("Ratio"); + if (r) { + _ratio = Ratio::from_id (r.get ()); + } + _colour_conversion = ColourConversion (node->node_child ("ColourConversion")); +} + +void +VideoContent::as_xml (xmlpp::Node* node) const +{ + boost::mutex::scoped_lock lm (_mutex); + node->add_child("VideoLength")->add_child_text (lexical_cast<string> (_video_length)); + node->add_child("VideoWidth")->add_child_text (lexical_cast<string> (_video_size.width)); + node->add_child("VideoHeight")->add_child_text (lexical_cast<string> (_video_size.height)); + node->add_child("VideoFrameRate")->add_child_text (lexical_cast<string> (_video_frame_rate)); + node->add_child("VideoFrameType")->add_child_text (lexical_cast<string> (static_cast<int> (_video_frame_type))); + node->add_child("LeftCrop")->add_child_text (boost::lexical_cast<string> (_crop.left)); + node->add_child("RightCrop")->add_child_text (boost::lexical_cast<string> (_crop.right)); + node->add_child("TopCrop")->add_child_text (boost::lexical_cast<string> (_crop.top)); + node->add_child("BottomCrop")->add_child_text (boost::lexical_cast<string> (_crop.bottom)); + if (_ratio) { + node->add_child("Ratio")->add_child_text (_ratio->id ()); + } + _colour_conversion.as_xml (node->add_child("ColourConversion")); +} + +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 (); + float const vfr = d->video_frame_rate (); + + { + boost::mutex::scoped_lock lm (_mutex); + _video_size = vs; + _video_frame_rate = vfr; + } + + signal_changed (VideoContentProperty::VIDEO_SIZE); + signal_changed (VideoContentProperty::VIDEO_FRAME_RATE); +} + + +string +VideoContent::information () const +{ + if (video_size().width == 0 || video_size().height == 0) { + return ""; + } + + stringstream s; + + s << String::compose ( + _("%1x%2 pixels (%3:1)"), + video_size().width, + video_size().height, + setprecision (3), float (video_size().width) / video_size().height + ); + + return s.str (); +} + +void +VideoContent::set_left_crop (int c) +{ + { + boost::mutex::scoped_lock lm (_mutex); + + if (_crop.left == c) { + return; + } + + _crop.left = c; + } + + signal_changed (VideoContentProperty::VIDEO_CROP); +} + +void +VideoContent::set_right_crop (int c) +{ + { + boost::mutex::scoped_lock lm (_mutex); + if (_crop.right == c) { + return; + } + + _crop.right = c; + } + + signal_changed (VideoContentProperty::VIDEO_CROP); +} + +void +VideoContent::set_top_crop (int c) +{ + { + boost::mutex::scoped_lock lm (_mutex); + if (_crop.top == c) { + return; + } + + _crop.top = c; + } + + signal_changed (VideoContentProperty::VIDEO_CROP); +} + +void +VideoContent::set_bottom_crop (int c) +{ + { + boost::mutex::scoped_lock lm (_mutex); + if (_crop.bottom == c) { + return; + } + + _crop.bottom = c; + } + + signal_changed (VideoContentProperty::VIDEO_CROP); +} + +void +VideoContent::set_ratio (Ratio const * r) +{ + { + boost::mutex::scoped_lock lm (_mutex); + if (_ratio == r) { + return; + } + + _ratio = r; + } + + signal_changed (VideoContentProperty::VIDEO_RATIO); +} + +/** @return string which includes everything about how this content looks */ +string +VideoContent::identifier () const +{ + stringstream s; + s << Content::digest() + << "_" << crop().left + << "_" << crop().right + << "_" << crop().top + << "_" << crop().bottom + << "_" << colour_conversion().identifier (); + + if (ratio()) { + s << "_" << ratio()->id (); + } + + return s.str (); +} + +void +VideoContent::set_video_frame_type (VideoFrameType t) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _video_frame_type = t; + } + + signal_changed (VideoContentProperty::VIDEO_FRAME_TYPE); +} + +string +VideoContent::technical_summary () const +{ + return String::compose ("video: length %1, size %2x%3, rate %4", video_length(), video_size().width, video_size().height, video_frame_rate()); +} + +libdcp::Size +VideoContent::video_size_after_3d_split () const +{ + libdcp::Size const s = video_size (); + switch (video_frame_type ()) { + case VIDEO_FRAME_TYPE_2D: + return s; + case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT: + return libdcp::Size (s.width / 2, s.height); + } + + assert (false); +} + +void +VideoContent::set_colour_conversion (ColourConversion c) +{ + { + boost::mutex::scoped_lock lm (_mutex); + _colour_conversion = c; + } + + signal_changed (VideoContentProperty::COLOUR_CONVERSION); +} diff --git a/src/lib/video_content.h b/src/lib/video_content.h new file mode 100644 index 000000000..72c72625b --- /dev/null +++ b/src/lib/video_content.h @@ -0,0 +1,121 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_VIDEO_CONTENT_H +#define DCPOMATIC_VIDEO_CONTENT_H + +#include "content.h" +#include "colour_conversion.h" + +class VideoExaminer; +class Ratio; + +class VideoContentProperty +{ +public: + static int const VIDEO_SIZE; + static int const VIDEO_FRAME_RATE; + static int const VIDEO_FRAME_TYPE; + static int const VIDEO_CROP; + static int const VIDEO_RATIO; + static int const COLOUR_CONVERSION; +}; + +class VideoContent : public virtual Content +{ +public: + typedef int Frame; + + VideoContent (boost::shared_ptr<const Film>, Time, VideoContent::Frame); + VideoContent (boost::shared_ptr<const Film>, boost::filesystem::path); + VideoContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>); + + void as_xml (xmlpp::Node *) const; + std::string technical_summary () const; + virtual std::string information () const; + virtual std::string identifier () const; + + VideoContent::Frame video_length () const { + boost::mutex::scoped_lock lm (_mutex); + return _video_length; + } + + libdcp::Size video_size () const { + boost::mutex::scoped_lock lm (_mutex); + return _video_size; + } + + float video_frame_rate () const { + boost::mutex::scoped_lock lm (_mutex); + return _video_frame_rate; + } + + void set_video_frame_type (VideoFrameType); + + void set_left_crop (int); + void set_right_crop (int); + void set_top_crop (int); + void set_bottom_crop (int); + + void set_colour_conversion (ColourConversion); + + VideoFrameType video_frame_type () const { + boost::mutex::scoped_lock lm (_mutex); + return _video_frame_type; + } + + Crop crop () const { + boost::mutex::scoped_lock lm (_mutex); + return _crop; + } + + void set_ratio (Ratio const *); + + Ratio const * ratio () const { + boost::mutex::scoped_lock lm (_mutex); + return _ratio; + } + + ColourConversion colour_conversion () const { + boost::mutex::scoped_lock lm (_mutex); + return _colour_conversion; + } + + libdcp::Size video_size_after_3d_split () const; + +protected: + void take_from_video_examiner (boost::shared_ptr<VideoExaminer>); + + VideoContent::Frame _video_length; + +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; + + libdcp::Size _video_size; + float _video_frame_rate; + VideoFrameType _video_frame_type; + Crop _crop; + Ratio const * _ratio; + ColourConversion _colour_conversion; +}; + +#endif diff --git a/src/lib/video_decoder.cc b/src/lib/video_decoder.cc index e0a7576ee..eaa4534e4 100644 --- a/src/lib/video_decoder.cc +++ b/src/lib/video_decoder.cc @@ -18,84 +18,37 @@ */ #include "video_decoder.h" -#include "subtitle.h" -#include "film.h" #include "image.h" -#include "log.h" -#include "options.h" -#include "job.h" +#include "i18n.h" + +using std::cout; using boost::shared_ptr; -using boost::optional; -VideoDecoder::VideoDecoder (shared_ptr<Film> f, shared_ptr<const DecodeOptions> o, Job* j) - : Decoder (f, o, j) - , _video_frame (0) - , _last_source_time (0) +VideoDecoder::VideoDecoder (shared_ptr<const Film> f, shared_ptr<const VideoContent> c) + : Decoder (f) + , _video_content (c) + , _video_position (0) { } -/** Called by subclasses to tell the world that some video data is ready. - * We find a subtitle then emit it for listeners. - * @param image frame to emit. - * @param t Time of the frame within the source, in seconds. - */ void -VideoDecoder::emit_video (shared_ptr<Image> image, double t) +VideoDecoder::video (shared_ptr<const Image> image, bool same, VideoContent::Frame frame) { - shared_ptr<Subtitle> sub; - if (_timed_subtitle && _timed_subtitle->displayed_at (t)) { - sub = _timed_subtitle->subtitle (); + switch (_video_content->video_frame_type ()) { + case VIDEO_FRAME_TYPE_2D: + Video (image, EYES_BOTH, same, frame); + break; + case VIDEO_FRAME_TYPE_3D_LEFT_RIGHT: + { + int const half = image->size().width / 2; + Video (image->crop (Crop (0, half, 0, 0), true), EYES_LEFT, same, frame); + Video (image->crop (Crop (half, 0, 0, 0), true), EYES_RIGHT, same, frame); + break; } - - signal_video (image, false, sub); - _last_source_time = t; -} - -void -VideoDecoder::repeat_last_video () -{ - if (!_last_image) { - _last_image.reset (new SimpleImage (pixel_format(), native_size(), false)); - _last_image->make_black (); } - - signal_video (_last_image, true, _last_subtitle); -} - -void -VideoDecoder::signal_video (shared_ptr<Image> image, bool same, shared_ptr<Subtitle> sub) -{ - TIMING ("Decoder emits %1", _video_frame); - Video (image, same, sub); - ++_video_frame; - - _last_image = image; - _last_subtitle = sub; -} - -void -VideoDecoder::emit_subtitle (shared_ptr<TimedSubtitle> s) -{ - _timed_subtitle = s; - if (_timed_subtitle) { - Position const p = _timed_subtitle->subtitle()->position (); - _timed_subtitle->subtitle()->set_position (Position (p.x - _film->crop().left, p.y - _film->crop().top)); - } -} - -void -VideoDecoder::set_subtitle_stream (shared_ptr<SubtitleStream> s) -{ - _subtitle_stream = s; + _video_position = frame + 1; } -void -VideoDecoder::set_progress () const -{ - if (_job && _film->length()) { - _job->set_progress (float (_video_frame) / _film->length().get()); - } -} diff --git a/src/lib/video_decoder.h b/src/lib/video_decoder.h index 7726d2057..142320a04 100644 --- a/src/lib/video_decoder.h +++ b/src/lib/video_decoder.h @@ -17,73 +17,41 @@ */ -#ifndef DVDOMATIC_VIDEO_DECODER_H -#define DVDOMATIC_VIDEO_DECODER_H +#ifndef DCPOMATIC_VIDEO_DECODER_H +#define DCPOMATIC_VIDEO_DECODER_H -#include "video_source.h" -#include "stream.h" +#include <boost/signals2.hpp> +#include <boost/shared_ptr.hpp> #include "decoder.h" +#include "video_content.h" +#include "util.h" -class VideoDecoder : public VideoSource, public virtual Decoder +class VideoContent; +class Image; + +class VideoDecoder : public virtual Decoder { public: - VideoDecoder (boost::shared_ptr<Film>, boost::shared_ptr<const DecodeOptions>, Job *); - - /** @return video frames per second, or 0 if unknown */ - virtual float frames_per_second () const = 0; - /** @return native size in pixels */ - virtual Size native_size () const = 0; - /** @return length (in source video frames), according to our content's header */ - virtual SourceFrame length () const = 0; - - virtual int time_base_numerator () const = 0; - virtual int time_base_denominator () const = 0; - virtual int sample_aspect_ratio_numerator () const = 0; - virtual int sample_aspect_ratio_denominator () const = 0; - - virtual void set_subtitle_stream (boost::shared_ptr<SubtitleStream>); - - void set_progress () const; + 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 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 Image>, Eyes, bool, VideoContent::Frame)> Video; - int video_frame () const { - return _video_frame; - } - - boost::shared_ptr<SubtitleStream> subtitle_stream () const { - return _subtitle_stream; - } - - std::vector<boost::shared_ptr<SubtitleStream> > subtitle_streams () const { - return _subtitle_streams; - } - - double last_source_time () const { - return _last_source_time; - } - protected: - - virtual PixelFormat pixel_format () const = 0; - - void emit_video (boost::shared_ptr<Image>, double); - void emit_subtitle (boost::shared_ptr<TimedSubtitle>); - void repeat_last_video (); - - /** Subtitle stream to use when decoding */ - boost::shared_ptr<SubtitleStream> _subtitle_stream; - /** Subtitle streams that this decoder's content has */ - std::vector<boost::shared_ptr<SubtitleStream> > _subtitle_streams; - -private: - void signal_video (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>); - - int _video_frame; - double _last_source_time; - - boost::shared_ptr<TimedSubtitle> _timed_subtitle; - boost::shared_ptr<Image> _last_image; - boost::shared_ptr<Subtitle> _last_subtitle; + void video (boost::shared_ptr<const Image>, bool, VideoContent::Frame); + boost::shared_ptr<const VideoContent> _video_content; + VideoContent::Frame _video_position; }; #endif diff --git a/src/lib/video_examiner.h b/src/lib/video_examiner.h new file mode 100644 index 000000000..039c494b5 --- /dev/null +++ b/src/lib/video_examiner.h @@ -0,0 +1,31 @@ +/* + 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 <libdcp/types.h> +#include "types.h" +#include "video_content.h" + +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; +}; diff --git a/src/lib/writer.cc b/src/lib/writer.cc new file mode 100644 index 000000000..5f94d5d6b --- /dev/null +++ b/src/lib/writer.cc @@ -0,0 +1,525 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <fstream> +#include <cerrno> +#include <libdcp/picture_asset.h> +#include <libdcp/sound_asset.h> +#include <libdcp/picture_frame.h> +#include <libdcp/reel.h> +#include <libdcp/dcp.h> +#include <libdcp/cpl.h> +#include "writer.h" +#include "compose.hpp" +#include "film.h" +#include "ratio.h" +#include "log.h" +#include "dcp_video_frame.h" +#include "dcp_content_type.h" +#include "player.h" +#include "audio_mapping.h" +#include "config.h" +#include "job.h" + +#include "i18n.h" + +using std::make_pair; +using std::pair; +using std::string; +using std::ifstream; +using std::list; +using std::cout; +using boost::shared_ptr; + +int const Writer::_maximum_frames_in_memory = 8; + +Writer::Writer (shared_ptr<const Film> f, shared_ptr<Job> j) + : _film (f) + , _job (j) + , _first_nonexistant_frame (0) + , _thread (0) + , _finish (false) + , _queued_full_in_memory (0) + , _last_written_frame (-1) + , _last_written_eyes (EYES_RIGHT) + , _full_written (0) + , _fake_written (0) + , _repeat_written (0) + , _pushed_to_disk (0) +{ + /* Remove any old DCP */ + boost::filesystem::remove_all (_film->dir (_film->dcp_name ())); + + check_existing_picture_mxf (); + + /* Create our picture asset in a subdirectory, named according to those + film's parameters which affect the video output. We will hard-link + it into the DCP later. + */ + + if (f->three_d ()) { + _picture_asset.reset ( + new libdcp::StereoPictureAsset ( + _film->internal_video_mxf_dir (), + _film->internal_video_mxf_filename (), + _film->video_frame_rate (), + _film->container()->size (_film->full_frame ()) + ) + ); + + } else { + _picture_asset.reset ( + new libdcp::MonoPictureAsset ( + _film->internal_video_mxf_dir (), + _film->internal_video_mxf_filename (), + _film->video_frame_rate (), + _film->container()->size (_film->full_frame ()) + ) + ); + + } + + _picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0, _film->interop ()); + + _sound_asset.reset ( + new libdcp::SoundAsset ( + _film->dir (_film->dcp_name()), + _film->audio_mxf_filename (), + _film->video_frame_rate (), + _film->audio_channels (), + _film->audio_frame_rate () + ) + ); + + _sound_asset_writer = _sound_asset->start_write (_film->interop ()); + + _thread = new boost::thread (boost::bind (&Writer::thread, this)); + + _job->descend (0.9); +} + +void +Writer::write (shared_ptr<const EncodedData> encoded, int frame, Eyes eyes) +{ + boost::mutex::scoped_lock lock (_mutex); + + QueueItem qi; + qi.type = QueueItem::FULL; + qi.encoded = encoded; + qi.frame = frame; + + if (_film->three_d() && eyes == EYES_BOTH) { + /* 2D material in a 3D DCP; fake the 3D */ + qi.eyes = EYES_LEFT; + _queue.push_back (qi); + ++_queued_full_in_memory; + qi.eyes = EYES_RIGHT; + _queue.push_back (qi); + ++_queued_full_in_memory; + } else { + qi.eyes = eyes; + _queue.push_back (qi); + ++_queued_full_in_memory; + } + + _condition.notify_all (); +} + +void +Writer::fake_write (int frame, Eyes eyes) +{ + boost::mutex::scoped_lock lock (_mutex); + + ifstream ifi (_film->info_path (frame, eyes).c_str()); + libdcp::FrameInfo info (ifi); + + QueueItem qi; + qi.type = QueueItem::FAKE; + qi.size = info.size; + qi.frame = frame; + if (_film->three_d() && eyes == EYES_BOTH) { + qi.eyes = EYES_LEFT; + _queue.push_back (qi); + qi.eyes = EYES_RIGHT; + _queue.push_back (qi); + } else { + qi.eyes = eyes; + _queue.push_back (qi); + } + + _condition.notify_all (); +} + +/** This method is not thread safe */ +void +Writer::write (shared_ptr<const AudioBuffers> audio) +{ + _sound_asset_writer->write (audio->data(), audio->frames()); +} + +/** This must be called from Writer::thread() with an appropriate lock held, + * and with _queue sorted. + */ +bool +Writer::have_sequenced_image_at_queue_head () const +{ + if (_queue.empty ()) { + return false; + } + + /* The queue should contain only EYES_LEFT/EYES_RIGHT pairs or EYES_BOTH */ + + if (_queue.front().eyes == EYES_BOTH) { + /* 2D */ + return _queue.front().frame == (_last_written_frame + 1); + } + + /* 3D */ + + if (_last_written_eyes == EYES_LEFT && _queue.front().frame == _last_written_frame && _queue.front().eyes == EYES_RIGHT) { + return true; + } + + if (_last_written_eyes == EYES_RIGHT && _queue.front().frame == (_last_written_frame + 1) && _queue.front().eyes == EYES_LEFT) { + return true; + } + + return false; +} + +void +Writer::thread () +try +{ + while (1) + { + boost::mutex::scoped_lock lock (_mutex); + + while (1) { + + _queue.sort (); + + if (_finish || _queued_full_in_memory > _maximum_frames_in_memory || have_sequenced_image_at_queue_head ()) { + break; + } + + TIMING (N_("writer sleeps with a queue of %1"), _queue.size()); + _condition.wait (lock); + TIMING (N_("writer wakes with a queue of %1"), _queue.size()); + } + + if (_finish && _queue.empty()) { + return; + } + + /* Write any frames that we can write; i.e. those that are in sequence */ + while (have_sequenced_image_at_queue_head ()) { + QueueItem qi = _queue.front (); + _queue.pop_front (); + if (qi.type == QueueItem::FULL && qi.encoded) { + --_queued_full_in_memory; + } + + lock.unlock (); + switch (qi.type) { + case QueueItem::FULL: + { + _film->log()->log (String::compose (N_("Writer FULL-writes %1 to MXF"), qi.frame)); + if (!qi.encoded) { + 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()); + qi.encoded->write_info (_film, qi.frame, qi.eyes, fin); + _last_written[qi.eyes] = qi.encoded; + ++_full_written; + break; + } + case QueueItem::FAKE: + _film->log()->log (String::compose (N_("Writer FAKE-writes %1 to MXF"), qi.frame)); + _picture_asset_writer->fake_write (qi.size); + _last_written[qi.eyes].reset (); + ++_fake_written; + break; + case QueueItem::REPEAT: + { + _film->log()->log (String::compose (N_("Writer REPEAT-writes %1 to MXF"), qi.frame)); + libdcp::FrameInfo fin = _picture_asset_writer->write ( + _last_written[qi.eyes]->data(), + _last_written[qi.eyes]->size() + ); + + _last_written[qi.eyes]->write_info (_film, qi.frame, qi.eyes, fin); + ++_repeat_written; + break; + } + } + lock.lock (); + + _last_written_frame = qi.frame; + _last_written_eyes = qi.eyes; + + if (_film->length()) { + _job->set_progress ( + float (_full_written + _fake_written + _repeat_written) / _film->time_to_video_frames (_film->length()) + ); + } + } + + while (_queued_full_in_memory > _maximum_frames_in_memory) { + /* Too many frames in memory which can't yet be written to the stream. + Write some FULL frames to disk. + */ + + /* Find one */ + list<QueueItem>::reverse_iterator i = _queue.rbegin (); + while (i != _queue.rend() && (i->type != QueueItem::FULL || !i->encoded)) { + ++i; + } + + assert (i != _queue.rend()); + QueueItem qi = *i; + + ++_pushed_to_disk; + + lock.unlock (); + + _film->log()->log ( + String::compose ( + "Writer full (awaiting %1 [last eye was %2]); pushes %3 to disk", + _last_written_frame + 1, + _last_written_eyes, qi.frame) + ); + + qi.encoded->write (_film, qi.frame, qi.eyes); + lock.lock (); + qi.encoded.reset (); + --_queued_full_in_memory; + } + } +} +catch (...) +{ + store_current (); +} + +void +Writer::finish () +{ + if (!_thread) { + return; + } + + boost::mutex::scoped_lock lock (_mutex); + _finish = true; + _condition.notify_all (); + lock.unlock (); + + _thread->join (); + if (thrown ()) { + rethrow (); + } + + delete _thread; + _thread = 0; + + _picture_asset_writer->finalize (); + _sound_asset_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 from; + from /= _film->internal_video_mxf_dir(); + from /= _film->internal_video_mxf_filename(); + + boost::filesystem::path to; + to /= _film->dir (_film->dcp_name()); + to /= _film->video_mxf_filename (); + + boost::system::error_code ec; + boost::filesystem::create_hard_link (from, to, ec); + if (ec) { + /* hard link failed; copy instead */ + boost::filesystem::copy_file (from, to); + _film->log()->log ("Hard-link failed; fell back to copying"); + } + + /* And update the asset */ + + _picture_asset->set_directory (_film->dir (_film->dcp_name ())); + _picture_asset->set_file_name (_film->video_mxf_filename ()); + _sound_asset->set_duration (frames); + + libdcp::DCP dcp (_film->dir (_film->dcp_name())); + + shared_ptr<libdcp::CPL> cpl ( + new libdcp::CPL ( + _film->dir (_film->dcp_name()), + _film->dcp_name(), + _film->dcp_content_type()->libdcp_kind (), + frames, + _film->video_frame_rate () + ) + ); + + dcp.add_cpl (cpl); + + cpl->add_reel (shared_ptr<libdcp::Reel> (new libdcp::Reel ( + _picture_asset, + _sound_asset, + shared_ptr<libdcp::SubtitleAsset> () + ) + )); + + /* Compute the digests for the assets now so that we can keep track of progress. + We did _job->descend (0.9) in our constructor */ + _job->ascend (); + + _job->descend (0.1); + _picture_asset->compute_digest (boost::bind (&Job::set_progress, _job.get(), _1)); + _job->ascend (); + + _job->descend (0.1); + _sound_asset->compute_digest (boost::bind (&Job::set_progress, _job.get(), _1)); + _job->ascend (); + + libdcp::XMLMetadata meta = Config::instance()->dcp_metadata (); + meta.set_issue_date_now (); + dcp.write_xml (_film->interop (), meta); + + _film->log()->log (String::compose (N_("Wrote %1 FULL, %2 FAKE, %3 REPEAT; %4 pushed to disk"), _full_written, _fake_written, _repeat_written, _pushed_to_disk)); +} + +/** 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); + + 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); + } + + _condition.notify_all (); +} + +bool +Writer::check_existing_picture_mxf_frame (FILE* mxf, int f, Eyes eyes) +{ + /* Read the frame info as written */ + ifstream ifi (_film->info_path (f, eyes).c_str()); + libdcp::FrameInfo info (ifi); + if (info.size == 0) { + _film->log()->log (String::compose ("Existing frame %1 has no info file", f)); + return false; + } + + /* Read the data from the MXF and hash it */ + fseek (mxf, info.offset, SEEK_SET); + EncodedData data (info.size); + size_t const read = fread (data.data(), 1, data.size(), mxf); + if (read != static_cast<size_t> (data.size ())) { + _film->log()->log (String::compose ("Existing frame %1 is incomplete", f)); + return false; + } + + string const existing_hash = md5_digest (data.data(), data.size()); + if (existing_hash != info.hash) { + _film->log()->log (String::compose ("Existing frame %1 failed hash check", f)); + return false; + } + + return true; +} + +void +Writer::check_existing_picture_mxf () +{ + /* Try to open the existing MXF */ + boost::filesystem::path p; + p /= _film->internal_video_mxf_dir (); + p /= _film->internal_video_mxf_filename (); + FILE* mxf = fopen (p.string().c_str(), "rb"); + if (!mxf) { + _film->log()->log (String::compose ("Could not open existing MXF at %1 (errno=%2)", p.string(), errno)); + return; + } + + while (1) { + + if (_film->three_d ()) { + if (!check_existing_picture_mxf_frame (mxf, _first_nonexistant_frame, EYES_LEFT)) { + break; + } + if (!check_existing_picture_mxf_frame (mxf, _first_nonexistant_frame, EYES_RIGHT)) { + break; + } + } else { + if (!check_existing_picture_mxf_frame (mxf, _first_nonexistant_frame, EYES_BOTH)) { + break; + } + } + + _film->log()->log (String::compose ("Have existing frame %1", _first_nonexistant_frame)); + ++_first_nonexistant_frame; + } + + fclose (mxf); +} + +/** @param frame Frame index. + * @return true if we can fake-write this frame. + */ +bool +Writer::can_fake_write (int frame) const +{ + /* We have to do a proper write of the first frame so that we can set up the JPEG2000 + parameters in the MXF writer. + */ + return (frame != 0 && frame < _first_nonexistant_frame); +} + +bool +operator< (QueueItem const & a, QueueItem const & b) +{ + if (a.frame != b.frame) { + return a.frame < b.frame; + } + + return static_cast<int> (a.eyes) < static_cast<int> (b.eyes); +} + +bool +operator== (QueueItem const & a, QueueItem const & b) +{ + return a.frame == b.frame && a.eyes == b.eyes; +} diff --git a/src/lib/writer.h b/src/lib/writer.h new file mode 100644 index 000000000..d922cfce0 --- /dev/null +++ b/src/lib/writer.h @@ -0,0 +1,133 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <list> +#include <boost/shared_ptr.hpp> +#include <boost/thread.hpp> +#include <boost/thread/condition.hpp> +#include "exceptions.h" +#include "types.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; +} + +struct QueueItem +{ +public: + enum Type { + /** a normal frame with some JPEG200 data */ + FULL, + /** a frame whose data already exists in the MXF, + and we fake-write it; i.e. we update the writer's + 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 */ + boost::shared_ptr<const EncodedData> encoded; + /** size of data for FAKE */ + int size; + /** frame index */ + int frame; + Eyes eyes; +}; + +bool operator< (QueueItem const & a, QueueItem const & b); +bool operator== (QueueItem const & a, QueueItem const & b); + +class Writer : public ExceptionStore, public boost::noncopyable +{ +public: + Writer (boost::shared_ptr<const Film>, boost::shared_ptr<Job>); + + bool can_fake_write (int) const; + + void write (boost::shared_ptr<const EncodedData>, int, Eyes); + void fake_write (int, Eyes); + void write (boost::shared_ptr<const AudioBuffers>); + void repeat (int f, Eyes); + void finish (); + +private: + + void thread (); + void check_existing_picture_mxf (); + bool check_existing_picture_mxf_frame (FILE *, int, Eyes); + bool have_sequenced_image_at_queue_head () const; + + /** our Film */ + boost::shared_ptr<const Film> _film; + boost::shared_ptr<Job> _job; + /** the first frame index that does not already exist in our MXF */ + int _first_nonexistant_frame; + + /** our thread, or 0 */ + boost::thread* _thread; + /** true if our thread should finish */ + bool _finish; + /** queue of things to write to disk */ + std::list<QueueItem> _queue; + /** number of FULL frames whose JPEG200 data is currently held in RAM */ + int _queued_full_in_memory; + /** mutex for thread state */ + mutable boost::mutex _mutex; + /** condition to manage thread wakeups */ + boost::condition _condition; + /** the data of the last written frame, or 0 if there isn't one */ + boost::shared_ptr<const EncodedData> _last_written[EYES_COUNT]; + /** the index of the last written frame */ + int _last_written_frame; + Eyes _last_written_eyes; + /** maximum number of frames to hold in memory, for when we are managing + ordering + */ + static const int _maximum_frames_in_memory; + + /** number of FULL written frames */ + 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; +}; diff --git a/src/lib/wscript b/src/lib/wscript index b2b639f06..6c45d8b1e 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -1,61 +1,94 @@ +import os +import i18n + +sources = """ + analyse_audio_job.cc + audio_analysis.cc + audio_buffers.cc + audio_content.cc + audio_decoder.cc + audio_mapping.cc + colour_conversion.cc + config.cc + content.cc + content_factory.cc + cross.cc + dci_metadata.cc + dcp_content_type.cc + dcp_video_frame.cc + decoder.cc + dolby_cp750.cc + encoder.cc + examine_content_job.cc + exceptions.cc + filter_graph.cc + ffmpeg.cc + ffmpeg_content.cc + ffmpeg_decoder.cc + ffmpeg_examiner.cc + film.cc + filter.cc + image.cc + job.cc + job_manager.cc + log.cc + moving_image_content.cc + moving_image_decoder.cc + moving_image_examiner.cc + player.cc + playlist.cc + ratio.cc + resampler.cc + scp_dcp_job.cc + scaler.cc + server.cc + sndfile_content.cc + sndfile_decoder.cc + sound_processor.cc + still_image_content.cc + still_image_decoder.cc + still_image_examiner.cc + subtitle_content.cc + subtitle_decoder.cc + timer.cc + transcode_job.cc + transcoder.cc + types.cc + ui_signaller.cc + util.cc + video_content.cc + video_decoder.cc + writer.cc + """ + def build(bld): if bld.env.STATIC: obj = bld(features = 'cxx cxxstlib') else: obj = bld(features = 'cxx cxxshlib') - obj.name = 'libdvdomatic' - obj.export_includes = ['.'] - obj.uselib = 'AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE SNDFILE BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2 OPENJPEG POSTPROC TIFF MAGICK SSH DCP GLIB' + obj.name = 'libdcpomatic' + 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 XML++ + """ + + obj.source = sources + ' version.cc' + if bld.env.TARGET_WINDOWS: - obj.uselib += ' WINSOCK2' - obj.source = """ - ab_transcode_job.cc - ab_transcoder.cc - audio_decoder.cc - audio_source.cc - check_hashes_job.cc - config.cc - combiner.cc - cross.cc - dcp_content_type.cc - dcp_video_frame.cc - decoder.cc - decoder_factory.cc - delay_line.cc - dolby_cp750.cc - encoder.cc - examine_content_job.cc - external_audio_decoder.cc - filter_graph.cc - ffmpeg_compatibility.cc - ffmpeg_decoder.cc - film.cc - filter.cc - format.cc - gain.cc - image.cc - imagemagick_decoder.cc - job.cc - job_manager.cc - log.cc - lut.cc - make_dcp_job.cc - matcher.cc - scp_dcp_job.cc - scaler.cc - server.cc - sound_processor.cc - stream.cc - subtitle.cc - timer.cc - transcode_job.cc - transcoder.cc - ui_signaller.cc - util.cc - version.cc - video_decoder.cc - video_source.cc - """ - - obj.target = 'dvdomatic' + obj.uselib += ' WINSOCK2 BFD DBGHELP IBERTY SHLWAPI' + obj.source += ' stack.cpp' + if bld.env.STATIC: + obj.uselib += ' XML++' + + obj.target = 'dcpomatic' + + i18n.po_to_mo(os.path.join('src', 'lib'), 'libdcpomatic', bld) + +def pot(bld): + i18n.pot(os.path.join('src', 'lib'), sources, 'libdcpomatic') + +def pot_merge(bld): + i18n.pot_merge(os.path.join('src', 'lib'), 'libdcpomatic') diff --git a/src/tools/dcpomatic.cc b/src/tools/dcpomatic.cc new file mode 100644 index 000000000..f61ef19e2 --- /dev/null +++ b/src/tools/dcpomatic.cc @@ -0,0 +1,624 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <iostream> +#include <fstream> +#include <boost/filesystem.hpp> +#ifdef __WXMSW__ +#include <shellapi.h> +#endif +#ifdef __WXOSX__ +#include <ApplicationServices/ApplicationServices.h> +#endif +#include <wx/generic/aboutdlgg.h> +#include <wx/stdpaths.h> +#include <wx/cmdline.h> +#include "wx/film_viewer.h" +#include "wx/film_editor.h" +#include "wx/job_manager_view.h" +#include "wx/config_dialog.h" +#include "wx/job_wrapper.h" +#include "wx/wx_util.h" +#include "wx/new_film_dialog.h" +#include "wx/properties_dialog.h" +#include "wx/wx_ui_signaller.h" +#include "wx/about_dialog.h" +#include "wx/kdm_dialog.h" +#include "lib/film.h" +#include "lib/config.h" +#include "lib/util.h" +#include "lib/version.h" +#include "lib/ui_signaller.h" +#include "lib/log.h" +#include "lib/job_manager.h" +#include "lib/transcode_job.h" +#include "lib/exceptions.h" + +using std::cout; +using std::string; +using std::wstring; +using std::stringstream; +using std::map; +using std::make_pair; +using std::list; +using std::exception; +using std::ofstream; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +static FilmEditor* film_editor = 0; +static FilmViewer* film_viewer = 0; +static shared_ptr<Film> film; +static std::string log_level; +static std::string film_to_load; +static std::string film_to_create; +static wxMenu* jobs_menu = 0; + +static void set_menu_sensitivity (); + +class FilmChangedDialog +{ +public: + FilmChangedDialog () + { + _dialog = new wxMessageDialog ( + 0, + wxString::Format (_("Save changes to film \"%s\" before closing?"), std_to_wx (film->name ()).data()), + _("Film changed"), + wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION + ); + } + + ~FilmChangedDialog () + { + _dialog->Destroy (); + } + + int run () + { + return _dialog->ShowModal (); + } + +private: + /* Not defined */ + FilmChangedDialog (FilmChangedDialog const &); + + wxMessageDialog* _dialog; +}; + + +void +maybe_save_then_delete_film () +{ + if (!film) { + return; + } + + if (film->dirty ()) { + FilmChangedDialog d; + switch (d.run ()) { + case wxID_NO: + break; + case wxID_YES: + film->write_metadata (); + break; + } + } + + film.reset (); +} + +#define ALWAYS 0x0 +#define NEEDS_FILM 0x1 +#define NOT_DURING_DCP_CREATION 0x2 + +map<wxMenuItem*, int> menu_items; + +void +add_item (wxMenu* menu, wxString text, int id, int sens) +{ + wxMenuItem* item = menu->Append (id, text); + menu_items.insert (make_pair (item, sens)); +} + +void +set_menu_sensitivity () +{ + list<shared_ptr<Job> > jobs = JobManager::instance()->get (); + list<shared_ptr<Job> >::iterator i = jobs.begin(); + while (i != jobs.end() && dynamic_pointer_cast<TranscodeJob> (*i) == 0) { + ++i; + } + bool const dcp_creation = (i != jobs.end ()); + + for (map<wxMenuItem*, int>::iterator j = menu_items.begin(); j != menu_items.end(); ++j) { + + bool enabled = true; + + if ((j->second & NEEDS_FILM) && film == 0) { + enabled = false; + } + + if ((j->second & NOT_DURING_DCP_CREATION) && dcp_creation) { + enabled = false; + } + + j->first->Enable (enabled); + } +} + +enum { + ID_file_new = 1, + ID_file_open, + ID_file_save, + ID_file_properties, + ID_jobs_make_dcp, + ID_jobs_make_kdms, + ID_jobs_send_dcp_to_tms, + ID_jobs_show_dcp, +}; + +void +setup_menu (wxMenuBar* m) +{ + wxMenu* file = new wxMenu; + add_item (file, _("New..."), ID_file_new, ALWAYS); + add_item (file, _("&Open..."), ID_file_open, ALWAYS); + file->AppendSeparator (); + add_item (file, _("&Save"), ID_file_save, NEEDS_FILM); + file->AppendSeparator (); + add_item (file, _("&Properties..."), ID_file_properties, NEEDS_FILM); +#ifndef __WXOSX__ + file->AppendSeparator (); +#endif + +#ifdef __WXOSX__ + add_item (file, _("&Exit"), wxID_EXIT, ALWAYS); +#else + add_item (file, _("&Quit"), wxID_EXIT, ALWAYS); +#endif + + +#ifdef __WXOSX__ + add_item (file, _("&Preferences..."), wxID_PREFERENCES, ALWAYS); +#else + wxMenu* edit = new wxMenu; + add_item (edit, _("&Preferences..."), wxID_PREFERENCES, ALWAYS); +#endif + + jobs_menu = new wxMenu; + add_item (jobs_menu, _("&Make DCP"), ID_jobs_make_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION); + add_item (jobs_menu, _("Make &KDMs..."), ID_jobs_make_kdms, NEEDS_FILM); + add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM | NOT_DURING_DCP_CREATION); + add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM | NOT_DURING_DCP_CREATION); + + wxMenu* help = new wxMenu; +#ifdef __WXOSX__ + add_item (help, _("About DCP-o-matic"), wxID_ABOUT, ALWAYS); +#else + add_item (help, _("About"), wxID_ABOUT, ALWAYS); +#endif + + m->Append (file, _("&File")); +#ifndef __WXOSX__ + m->Append (edit, _("&Edit")); +#endif + m->Append (jobs_menu, _("&Jobs")); + m->Append (help, _("&Help")); +} + +class Frame : public wxFrame +{ +public: + Frame (wxString const & title) + : wxFrame (NULL, -1, title) + { + wxMenuBar* bar = new wxMenuBar; + setup_menu (bar); + SetMenuBar (bar); + + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_new, this), ID_file_new); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_open, this), ID_file_open); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_save, this), ID_file_save); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_properties, this), ID_file_properties); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_exit, this), wxID_EXIT); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::edit_preferences, this), wxID_PREFERENCES); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_dcp, this), ID_jobs_make_dcp); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_make_kdms, this), ID_jobs_make_kdms); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_send_dcp_to_tms, this), ID_jobs_send_dcp_to_tms); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::jobs_show_dcp, this), ID_jobs_show_dcp); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::help_about, this), wxID_ABOUT); + + Bind (wxEVT_MENU_OPEN, boost::bind (&Frame::menu_opened, this, _1)); + Bind (wxEVT_CLOSE_WINDOW, boost::bind (&Frame::close, this, _1)); + + /* Use a panel as the only child of the Frame so that we avoid + the dark-grey background on Windows. + */ + wxPanel* overall_panel = new wxPanel (this, wxID_ANY); + + film_editor = new FilmEditor (film, overall_panel); + film_viewer = new FilmViewer (film, overall_panel); + JobManagerView* job_manager_view = new JobManagerView (overall_panel, static_cast<JobManagerView::Buttons> (0)); + + wxBoxSizer* right_sizer = new wxBoxSizer (wxVERTICAL); + right_sizer->Add (film_viewer, 2, wxEXPAND | wxALL, 6); + right_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL, 6); + + wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL); + main_sizer->Add (film_editor, 1, wxEXPAND | wxALL, 6); + main_sizer->Add (right_sizer, 2, wxEXPAND | wxALL, 6); + + set_menu_sensitivity (); + + film_editor->FileChanged.connect (bind (&Frame::file_changed, this, _1)); + if (film) { + file_changed (film->directory ()); + } else { + file_changed (""); + } + + JobManager::instance()->ActiveJobsChanged.connect (boost::bind (set_menu_sensitivity)); + + set_film (); + overall_panel->SetSizer (main_sizer); + } + +private: + + void menu_opened (wxMenuEvent& ev) + { + if (ev.GetMenu() != jobs_menu) { + return; + } + + bool const have_dcp = false;//film && film->have_dcp(); + jobs_menu->Enable (ID_jobs_send_dcp_to_tms, have_dcp); + jobs_menu->Enable (ID_jobs_show_dcp, have_dcp); + } + + void set_film () + { + film_viewer->set_film (film); + film_editor->set_film (film); + set_menu_sensitivity (); + } + + void file_changed (string f) + { + stringstream s; + s << wx_to_std (_("DCP-o-matic")); + if (!f.empty ()) { + s << " - " << f; + } + + SetTitle (std_to_wx (s.str())); + } + + void file_new () + { + NewFilmDialog* d = new NewFilmDialog (this); + int const r = d->ShowModal (); + + if (r == wxID_OK) { + + if (boost::filesystem::is_directory (d->get_path()) && !boost::filesystem::is_empty(d->get_path())) { + if (!confirm_dialog ( + this, + std_to_wx ( + String::compose (wx_to_std (_("The directory %1 already exists and is not empty. " + "Are you sure you want to use it?")), + d->get_path().c_str()) + ) + )) { + return; + } + } else if (boost::filesystem::is_regular_file (d->get_path())) { + error_dialog ( + this, + String::compose (wx_to_std (_("%1 already exists as a file, so you cannot use it for a new film.")), d->get_path().c_str()) + ); + return; + } + + maybe_save_then_delete_film (); + film.reset (new Film (d->get_path ())); + film->write_metadata (); + film->log()->set_level (log_level); + film->set_name (boost::filesystem::path (d->get_path()).filename().generic_string()); + set_film (); + } + + d->Destroy (); + } + + void file_open () + { + wxDirDialog* c = new wxDirDialog (this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST); + int r; + while (1) { + r = c->ShowModal (); + if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { + error_dialog (this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); + } else { + break; + } + } + + if (r == wxID_OK) { + maybe_save_then_delete_film (); + try { + film.reset (new Film (wx_to_std (c->GetPath ()))); + film->read_metadata (); + film->log()->set_level (log_level); + set_film (); + } catch (std::exception& e) { + wxString p = c->GetPath (); + wxCharBuffer b = p.ToUTF8 (); + error_dialog (this, wxString::Format (_("Could not open film at %s (%s)"), p.data(), std_to_wx (e.what()).data())); + } + } + + c->Destroy (); + } + + void file_save () + { + film->write_metadata (); + } + + void file_properties () + { + PropertiesDialog* d = new PropertiesDialog (this, film); + d->ShowModal (); + d->Destroy (); + } + + void file_exit () + { + if (!should_close ()) { + return; + } + + maybe_save_then_delete_film (); + Close (true); + } + + void edit_preferences () + { + ConfigDialog* d = new ConfigDialog (this); + d->ShowModal (); + d->Destroy (); + Config::instance()->write (); + } + + void jobs_make_dcp () + { + JobWrapper::make_dcp (this, film); + } + + void jobs_make_kdms () + { + if (!film) { + return; + } + + KDMDialog* d = new KDMDialog (this); + if (d->ShowModal () == wxID_OK) { + try { + film->make_kdms ( + d->screens (), + d->from (), + d->until (), + d->directory () + ); + } catch (KDMError& e) { + error_dialog (this, e.what ()); + } + } + + d->Destroy (); + } + + void jobs_send_dcp_to_tms () + { + film->send_dcp_to_tms (); + } + + void jobs_show_dcp () + { +#ifdef __WXMSW__ + string d = film->directory(); + wstring w; + w.assign (d.begin(), d.end()); + ShellExecute (0, L"open", w.c_str(), 0, 0, SW_SHOWDEFAULT); +#else + int r = system ("which nautilus"); + if (WEXITSTATUS (r) == 0) { + r = system (string ("nautilus " + film->directory()).c_str ()); + if (WEXITSTATUS (r)) { + error_dialog (this, _("Could not show DCP (could not run nautilus)")); + } + } else { + int r = system ("which konqueror"); + if (WEXITSTATUS (r) == 0) { + r = system (string ("konqueror " + film->directory()).c_str ()); + if (WEXITSTATUS (r)) { + error_dialog (this, _("Could not show DCP (could not run konqueror)")); + } + } + } +#endif + } + + void help_about () + { + AboutDialog* d = new AboutDialog (this); + d->ShowModal (); + d->Destroy (); + } + + bool should_close () + { + if (!JobManager::instance()->work_to_do ()) { + return true; + } + + wxMessageDialog* d = new wxMessageDialog ( + 0, + _("There are unfinished jobs; are you sure you want to quit?"), + _("Unfinished jobs"), + wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION + ); + + bool const r = d->ShowModal() == wxID_YES; + d->Destroy (); + return r; + } + + void close (wxCloseEvent& ev) + { + if (!should_close ()) { + ev.Veto (); + return; + } + + ev.Skip (); + } +}; + +#if wxMINOR_VERSION == 9 +static const wxCmdLineEntryDesc command_line_description[] = { + { wxCMD_LINE_OPTION, "l", "log", "set log level (silent, verbose or timing)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_SWITCH, "n", "new", "create new film", wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_PARAM, 0, 0, "film to load or create", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 } +}; +#else +static const wxCmdLineEntryDesc command_line_description[] = { + { wxCMD_LINE_OPTION, wxT("l"), wxT("log"), wxT("set log level (silent, verbose or timing)"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_SWITCH, wxT("n"), wxT("new"), wxT("create new film"), wxCMD_LINE_VAL_NONE, wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_PARAM, 0, 0, wxT("film to load or create"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL }, + { wxCMD_LINE_NONE, wxT(""), wxT(""), wxT(""), wxCmdLineParamType (0), 0 } +}; +#endif + +class App : public wxApp +{ + bool OnInit () + try + { + if (!wxApp::OnInit()) { + return false; + } + +#ifdef DCPOMATIC_LINUX + unsetenv ("UBUNTU_MENUPROXY"); +#endif + +#ifdef __WXOSX__ + ProcessSerialNumber serial; + GetCurrentProcess (&serial); + TransformProcessType (&serial, kProcessTransformToForegroundApplication); +#endif + + wxInitAllImageHandlers (); + + /* Enable i18n; this will create a Config object + to look for a force-configured language. This Config + object will be wrong, however, because dcpomatic_setup + hasn't yet been called and there aren't any scalers, filters etc. + set up yet. + */ + dcpomatic_setup_i18n (); + + /* Set things up, including scalers / filters etc. + which will now be internationalised correctly. + */ + dcpomatic_setup (); + + /* Force the configuration to be re-loaded correctly next + time it is needed. + */ + Config::drop (); + + if (!film_to_load.empty() && boost::filesystem::is_directory (film_to_load)) { + try { + film.reset (new Film (film_to_load)); + film->read_metadata (); + film->log()->set_level (log_level); + } catch (exception& e) { + error_dialog (0, std_to_wx (String::compose (wx_to_std (_("Could not load film %1 (%2)")), film_to_load, e.what()))); + } + } + + if (!film_to_create.empty ()) { + film.reset (new Film (film_to_create)); + film->write_metadata (); + film->log()->set_level (log_level); + film->set_name (boost::filesystem::path (film_to_create).filename().generic_string ()); + } + + Frame* f = new Frame (_("DCP-o-matic")); + SetTopWindow (f); + f->Maximize (); + f->Show (); + + ui_signaller = new wxUISignaller (this); + this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this)); + + return true; + } + catch (exception& e) + { + error_dialog (0, wxString::Format ("DCP-o-matic could not start: %s", e.what ())); + return true; + } + + void OnInitCmdLine (wxCmdLineParser& parser) + { + parser.SetDesc (command_line_description); + parser.SetSwitchChars (wxT ("-")); + } + + bool OnCmdLineParsed (wxCmdLineParser& parser) + { + if (parser.GetParamCount() > 0) { + if (parser.Found (wxT ("new"))) { + film_to_create = wx_to_std (parser.GetParam (0)); + } else { + film_to_load = wx_to_std (parser.GetParam(0)); + } + } + + wxString log; + if (parser.Found (wxT ("log"), &log)) { + log_level = wx_to_std (log); + } + + return true; + } + + void idle () + { + ui_signaller->ui_idle (); + } +}; + +IMPLEMENT_APP (App) diff --git a/src/tools/dcpomatic_batch.cc b/src/tools/dcpomatic_batch.cc new file mode 100644 index 000000000..23d5a4819 --- /dev/null +++ b/src/tools/dcpomatic_batch.cc @@ -0,0 +1,240 @@ +/* + 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 <wx/aboutdlg.h> +#include <wx/stdpaths.h> +#include <wx/wx.h> +#include "lib/version.h" +#include "lib/compose.hpp" +#include "lib/config.h" +#include "lib/util.h" +#include "lib/film.h" +#include "lib/job_manager.h" +#include "wx/wx_util.h" +#include "wx/wx_ui_signaller.h" +#include "wx/job_manager_view.h" + +using boost::shared_ptr; + +enum { + ID_file_add_film = 1, + ID_file_quit, + ID_help_about +}; + +void +setup_menu (wxMenuBar* m) +{ + wxMenu* file = new wxMenu; + file->Append (ID_file_add_film, _("&Add Film...")); + file->Append (ID_file_quit, _("&Quit")); + + wxMenu* help = new wxMenu; + help->Append (ID_help_about, _("About")); + + m->Append (file, _("&File")); + m->Append (help, _("&Help")); +} + +class Frame : public wxFrame +{ +public: + Frame (wxString const & title) + : wxFrame (NULL, -1, title) + { + wxMenuBar* bar = new wxMenuBar; + setup_menu (bar); + SetMenuBar (bar); + + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_add_film, this), ID_file_add_film); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::file_quit, this), ID_file_quit); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&Frame::help_about, this), ID_help_about); + + wxPanel* panel = new wxPanel (this); + wxSizer* s = new wxBoxSizer (wxHORIZONTAL); + s->Add (panel, 1, wxEXPAND); + SetSizer (s); + + wxSizer* sizer = new wxBoxSizer (wxVERTICAL); + + JobManagerView* job_manager_view = new JobManagerView (panel, JobManagerView::PAUSE); + sizer->Add (job_manager_view, 1, wxALL | wxEXPAND, 6); + + wxSizer* buttons = new wxBoxSizer (wxHORIZONTAL); + wxButton* add = new wxButton (panel, wxID_ANY, _("Add Film...")); + add->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&Frame::add_film, this)); + buttons->Add (add, 1, wxALL, 6); + + sizer->Add (buttons, 0, wxALL, 6); + + panel->SetSizer (sizer); + + Bind (wxEVT_CLOSE_WINDOW, boost::bind (&Frame::close, this, _1)); + } + +private: + bool should_close () + { + if (!JobManager::instance()->work_to_do ()) { + return true; + } + + wxMessageDialog* d = new wxMessageDialog ( + 0, + _("There are unfinished jobs; are you sure you want to quit?"), + _("Unfinished jobs"), + wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION + ); + + bool const r = d->ShowModal() == wxID_YES; + d->Destroy (); + return r; + } + + void close (wxCloseEvent& ev) + { + if (!should_close ()) { + ev.Veto (); + return; + } + + ev.Skip (); + } + + void file_add_film () + { + add_film (); + } + + void file_quit () + { + if (should_close ()) { + Close (true); + } + } + + void help_about () + { + wxAboutDialogInfo info; + info.SetName (_("DCP-o-matic Batch Converter")); + if (strcmp (dcpomatic_git_commit, "release") == 0) { + info.SetVersion (std_to_wx (String::compose ("version %1", dcpomatic_version))); + } else { + info.SetVersion (std_to_wx (String::compose ("version %1 git %2", dcpomatic_version, dcpomatic_git_commit))); + } + info.SetDescription (_("Free, open-source DCP generation from almost anything.")); + info.SetCopyright (_("(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen")); + + wxArrayString authors; + authors.Add (wxT ("Carl Hetherington")); + authors.Add (wxT ("Terrence Meiczinger")); + authors.Add (wxT ("Paul Davis")); + authors.Add (wxT ("Ole Laursen")); + info.SetDevelopers (authors); + + wxArrayString translators; + translators.Add (wxT ("Olivier Perriere")); + translators.Add (wxT ("Lilian Lefranc")); + translators.Add (wxT ("Thierry Journet")); + translators.Add (wxT ("Massimiliano Broggi")); + translators.Add (wxT ("Manuel AC")); + translators.Add (wxT ("Adam Klotblixt")); + info.SetTranslators (translators); + + info.SetWebSite (wxT ("http://carlh.net/software/dcpomatic")); + wxAboutBox (info); + } + + void add_film () + { + wxDirDialog* c = new wxDirDialog (this, _("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST); + int r; + while (1) { + r = c->ShowModal (); + if (r == wxID_OK && c->GetPath() == wxStandardPaths::Get().GetDocumentsDir()) { + error_dialog (this, _("You did not select a folder. Make sure that you select a folder before clicking Open.")); + } else { + break; + } + } + + if (r == wxID_OK) { + try { + shared_ptr<Film> film (new Film (wx_to_std (c->GetPath ()))); + film->read_metadata (); + film->make_dcp (); + } catch (std::exception& e) { + wxString p = c->GetPath (); + wxCharBuffer b = p.ToUTF8 (); + error_dialog (this, wxString::Format (_("Could not open film at %s (%s)"), p.data(), std_to_wx (e.what()).data())); + } + } + + c->Destroy (); + } +}; + +class App : public wxApp +{ + bool OnInit () + { + if (!wxApp::OnInit()) { + return false; + } + +#ifdef DCPOMATIC_LINUX + unsetenv ("UBUNTU_MENUPROXY"); +#endif + + /* Enable i18n; this will create a Config object + to look for a force-configured language. This Config + object will be wrong, however, because dcpomatic_setup + hasn't yet been called and there aren't any scalers, filters etc. + set up yet. + */ + dcpomatic_setup_i18n (); + + /* Set things up, including scalers / filters etc. + which will now be internationalised correctly. + */ + dcpomatic_setup (); + + /* Force the configuration to be re-loaded correctly next + time it is needed. + */ + Config::drop (); + + Frame* f = new Frame (_("DCP-o-matic Batch Converter")); + SetTopWindow (f); + f->Maximize (); + f->Show (); + + ui_signaller = new wxUISignaller (this); + this->Bind (wxEVT_IDLE, boost::bind (&App::idle, this)); + + return true; + } + + void idle () + { + ui_signaller->ui_idle (); + } +}; + +IMPLEMENT_APP (App) diff --git a/src/tools/makedcp.cc b/src/tools/dcpomatic_cli.cc index 900c31bfc..7695e1e94 100644 --- a/src/tools/makedcp.cc +++ b/src/tools/dcpomatic_cli.cc @@ -20,21 +20,17 @@ #include <iostream> #include <iomanip> #include <getopt.h> -#include <libdcp/test_mode.h> #include <libdcp/version.h> -#include "format.h" -#include "film.h" -#include "filter.h" -#include "transcode_job.h" -#include "make_dcp_job.h" -#include "job_manager.h" -#include "ab_transcode_job.h" -#include "util.h" -#include "scaler.h" -#include "version.h" -#include "cross.h" -#include "config.h" -#include "log.h" +#include "lib/film.h" +#include "lib/filter.h" +#include "lib/transcode_job.h" +#include "lib/job_manager.h" +#include "lib/util.h" +#include "lib/scaler.h" +#include "lib/version.h" +#include "lib/cross.h" +#include "lib/config.h" +#include "lib/log.h" using std::string; using std::cerr; @@ -48,10 +44,10 @@ static void help (string n) { cerr << "Syntax: " << n << " [OPTION] <FILM>\n" - << " -v, --version show DVD-o-matic version\n" - << " -h, --help show this help\n" - << " -d, --deps list DVD-o-matic dependency details and quit\n" - << " -t, --test run in test mode (repeatable UUID generation, timestamps etc.)\n" + << " -v, --version show DCP-o-matic version\n" + << " -h, --help show this help\n" + << " -d, --deps list DCP-o-matic dependency details and quit\n" + << " -f, --flags show flags passed to C++ compiler on build\n" << " -n, --no-progress do not print progress to stdout\n" << " -r, --no-remote do not use any remote servers\n" << "\n" @@ -62,10 +58,9 @@ int main (int argc, char* argv[]) { string film_dir; - bool test_mode = false; bool progress = true; bool no_remote = false; - int log_level = 1; + int log_level = 0; int option_index = 0; while (1) { @@ -73,14 +68,14 @@ main (int argc, char* argv[]) { "version", no_argument, 0, 'v'}, { "help", no_argument, 0, 'h'}, { "deps", no_argument, 0, 'd'}, - { "test", no_argument, 0, 't'}, + { "flags", no_argument, 0, 'f'}, { "no-progress", no_argument, 0, 'n'}, { "no-remote", no_argument, 0, 'r'}, { "log-level", required_argument, 0, 'l' }, { 0, 0, 0, 0 } }; - int c = getopt_long (argc, argv, "vhdtnrl:", long_options, &option_index); + int c = getopt_long (argc, argv, "vhdfnrl:", long_options, &option_index); if (c == -1) { break; @@ -88,7 +83,7 @@ main (int argc, char* argv[]) switch (c) { case 'v': - cout << "dvdomatic version " << dvdomatic_version << " " << dvdomatic_git_commit << "\n"; + cout << "dcpomatic version " << dcpomatic_version << " " << dcpomatic_git_commit << "\n"; exit (EXIT_SUCCESS); case 'h': help (argv[0]); @@ -96,9 +91,9 @@ main (int argc, char* argv[]) case 'd': cout << dependency_version_summary () << "\n"; exit (EXIT_SUCCESS); - case 't': - test_mode = true; - break; + case 'f': + cout << dcpomatic_cxx_flags << "\n"; + exit (EXIT_SUCCESS); case 'n': progress = false; break; @@ -118,27 +113,23 @@ main (int argc, char* argv[]) film_dir = argv[optind]; - dvdomatic_setup (); + dcpomatic_setup (); if (no_remote) { - Config::instance()->set_servers (vector<ServerDescription*> ()); + Config::instance()->set_servers (vector<ServerDescription> ()); } - cout << "DVD-o-matic " << dvdomatic_version << " git " << dvdomatic_git_commit; + cout << "DCP-o-matic " << dcpomatic_version << " git " << dcpomatic_git_commit; char buf[256]; if (gethostname (buf, 256) == 0) { cout << " on " << buf; } cout << "\n"; - if (test_mode) { - libdcp::enable_test_mode (); - cout << dependency_version_summary() << "\n"; - } - shared_ptr<Film> film; try { - film.reset (new Film (film_dir, true)); + film.reset (new Film (film_dir)); + film->read_metadata (); } catch (std::exception& e) { cerr << argv[0] << ": error reading film `" << film_dir << "' (" << e.what() << ")\n"; exit (EXIT_FAILURE); @@ -146,23 +137,19 @@ main (int argc, char* argv[]) film->log()->set_level ((Log::Level) log_level); - cout << "\nMaking "; - if (film->dcp_ab()) { - cout << "A/B "; - } - cout << "DCP for " << film->name() << "\n"; - cout << "Test mode: " << (test_mode ? "yes" : "no") << "\n"; - cout << "Content: " << film->content() << "\n"; - pair<string, string> const f = Filter::ffmpeg_strings (film->filters ()); - cout << "Filters: " << f.first << " " << f.second << "\n"; + cout << "\nMaking DCP for " << film->name() << "\n"; +// cout << "Content: " << film->content() << "\n"; +// pair<string, string> const f = Filter::ffmpeg_strings (film->filters ()); +// cout << "Filters: " << f.first << " " << f.second << "\n"; - film->make_dcp (true); + film->make_dcp (); bool should_stop = false; bool first = true; + bool error = false; while (!should_stop) { - dvdomatic_sleep (5); + dcpomatic_sleep (5); list<shared_ptr<Job> > jobs = JobManager::instance()->get (); @@ -183,9 +170,9 @@ main (int argc, char* argv[]) float const p = (*i)->overall_progress (); if (p >= 0) { - cout << (*i)->status() << " \n"; + cout << (*i)->status() << " \n"; } else { - cout << ": Running \n"; + cout << ": Running \n"; } } @@ -195,6 +182,7 @@ main (int argc, char* argv[]) if ((*i)->finished_in_error ()) { ++finished_in_error; + error = true; } if (!progress && (*i)->finished_in_error ()) { @@ -210,7 +198,7 @@ main (int argc, char* argv[]) } } - return 0; + return error ? EXIT_FAILURE : EXIT_SUCCESS; } diff --git a/src/tools/servomatic_gui.cc b/src/tools/dcpomatic_server.cc index 610ba8005..78354c468 100644 --- a/src/tools/servomatic_gui.cc +++ b/src/tools/dcpomatic_server.cc @@ -20,13 +20,16 @@ #include <boost/thread.hpp> #include <wx/taskbar.h> #include <wx/icon.h> -#include "wx_util.h" +#include "wx/wx_util.h" #include "lib/util.h" #include "lib/server.h" #include "lib/config.h" -using namespace std; -using namespace boost; +using std::cout; +using std::string; +using boost::shared_ptr; +using boost::thread; +using boost::bind; enum { ID_status = 1, @@ -52,13 +55,13 @@ private: string _log; }; -static MemoryLog memory_log; +static shared_ptr<MemoryLog> memory_log (new MemoryLog); class StatusDialog : public wxDialog { public: StatusDialog () - : wxDialog (0, wxID_ANY, _("DVD-o-matic encode server"), wxDefaultPosition, wxSize (600, 80), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + : wxDialog (0, wxID_ANY, _("DCP-o-matic encode server"), wxDefaultPosition, wxSize (600, 80), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) , _timer (this, ID_timer) { _sizer = new wxFlexGridSizer (1, 6, 6); @@ -70,14 +73,14 @@ public: SetSizer (_sizer); _sizer->Layout (); - Connect (ID_timer, wxEVT_TIMER, wxTimerEventHandler (StatusDialog::update)); + Bind (wxEVT_TIMER, boost::bind (&StatusDialog::update, this), ID_timer); _timer.Start (1000); } private: - void update (wxTimerEvent &) + void update () { - _text->ChangeValue (std_to_wx (memory_log.get ())); + _text->ChangeValue (std_to_wx (memory_log->get ())); _sizer->Layout (); } @@ -91,11 +94,22 @@ class TaskBarIcon : public wxTaskBarIcon public: TaskBarIcon () { +#ifdef __WXMSW__ wxIcon icon (std_to_wx ("taskbar_icon")); - SetIcon (icon, std_to_wx ("DVD-o-matic encode server")); - - Connect (ID_status, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (TaskBarIcon::status)); - Connect (ID_quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (TaskBarIcon::quit)); +#endif +#ifdef __WXGTK__ + wxInitAllImageHandlers(); + wxBitmap bitmap (wxString::Format (wxT ("%s/taskbar_icon.png"), POSIX_ICON_PREFIX), wxBITMAP_TYPE_PNG); + wxIcon icon; + icon.CopyFromBitmap (bitmap); +#endif +#ifndef __WXOSX__ + /* XXX: fix this for OS X */ + SetIcon (icon, std_to_wx ("DCP-o-matic encode server")); +#endif + + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&TaskBarIcon::status, this), ID_status); + Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&TaskBarIcon::quit, this), ID_quit); } wxMenu* CreatePopupMenu () @@ -107,13 +121,13 @@ public: } private: - void status (wxCommandEvent &) + void status () { StatusDialog* d = new StatusDialog; d->Show (); } - void quit (wxCommandEvent &) + void quit () { wxTheApp->ExitMainLoop (); } @@ -125,27 +139,39 @@ public: App () : wxApp () , _thread (0) + , _icon (0) {} private: bool OnInit () { - dvdomatic_setup (); - - new TaskBarIcon; + if (!wxApp::OnInit ()) { + return false; + } + + dcpomatic_setup (); + _icon = new TaskBarIcon; _thread = new thread (bind (&App::main_thread, this)); + return true; } + int OnExit () + { + delete _icon; + return wxApp::OnExit (); + } + void main_thread () { - Server server (&memory_log); + Server server (memory_log); server.run (Config::instance()->num_local_encoding_threads ()); } boost::thread* _thread; + TaskBarIcon* _icon; }; IMPLEMENT_APP (App) diff --git a/src/tools/servomatic_cli.cc b/src/tools/dcpomatic_server_cli.cc index f8e713193..eff10a897 100644 --- a/src/tools/servomatic_cli.cc +++ b/src/tools/dcpomatic_server_cli.cc @@ -32,24 +32,27 @@ #include <boost/thread.hpp> #include <boost/thread/mutex.hpp> #include <boost/thread/condition.hpp> -#include "config.h" -#include "dcp_video_frame.h" -#include "exceptions.h" -#include "util.h" -#include "config.h" -#include "scaler.h" -#include "image.h" -#include "log.h" -#include "version.h" +#include "lib/config.h" +#include "lib/dcp_video_frame.h" +#include "lib/exceptions.h" +#include "lib/util.h" +#include "lib/config.h" +#include "lib/scaler.h" +#include "lib/image.h" +#include "lib/log.h" +#include "lib/version.h" -using namespace std; +using std::cerr; +using std::string; +using std::cout; +using boost::shared_ptr; static void help (string n) { cerr << "Syntax: " << n << " [OPTION]\n" - << " -v, --version show DVD-o-matic version\n" - << " -h, --help show this help\n" + << " -v, --version show DCP-o-matic version\n" + << " -h, --help show this help\n" << " -t, --threads number of parallel encoding threads to use\n"; } @@ -75,7 +78,7 @@ main (int argc, char* argv[]) switch (c) { case 'v': - cout << "dvdomatic version " << dvdomatic_version << " " << dvdomatic_git_commit << "\n"; + cout << "dcpomatic version " << dcpomatic_version << " " << dcpomatic_git_commit << "\n"; exit (EXIT_SUCCESS); case 'h': help (argv[0]); @@ -87,8 +90,8 @@ main (int argc, char* argv[]) } Scaler::setup_scalers (); - FileLog log ("servomatic.log"); - Server server (&log); + shared_ptr<FileLog> log (new FileLog ("servomatic.log")); + Server server (log); server.run (num_threads); return 0; } diff --git a/src/tools/dvdomatic.cc b/src/tools/dvdomatic.cc deleted file mode 100644 index b6662f281..000000000 --- a/src/tools/dvdomatic.cc +++ /dev/null @@ -1,466 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <iostream> -#include <boost/filesystem.hpp> -#include <wx/aboutdlg.h> -#include <wx/stdpaths.h> -#include <wx/cmdline.h> -#include "wx/film_viewer.h" -#include "wx/film_editor.h" -#include "wx/job_manager_view.h" -#include "wx/config_dialog.h" -#include "wx/job_wrapper.h" -#include "wx/wx_util.h" -#include "wx/new_film_dialog.h" -#include "wx/properties_dialog.h" -#include "wx/wx_ui_signaller.h" -#include "wx/kdm_dialog.h" -#include "lib/film.h" -#include "lib/format.h" -#include "lib/config.h" -#include "lib/filter.h" -#include "lib/util.h" -#include "lib/scaler.h" -#include "lib/exceptions.h" -#include "lib/version.h" -#include "lib/ui_signaller.h" -#include "lib/log.h" - -using std::cout; -using std::string; -using std::stringstream; -using std::map; -using std::make_pair; -using boost::shared_ptr; - -static FilmEditor* film_editor = 0; -static FilmViewer* film_viewer = 0; -static shared_ptr<Film> film; -static std::string log_level; -static std::string film_to_load; - -static void set_menu_sensitivity (); - -class FilmChangedDialog -{ -public: - FilmChangedDialog () - { - stringstream s; - s << "Save changes to film \"" << film->name() << "\" before closing?"; - _dialog = new wxMessageDialog (0, std_to_wx (s.str()), wxT ("Film changed"), wxYES_NO | wxYES_DEFAULT | wxICON_QUESTION); - } - - ~FilmChangedDialog () - { - _dialog->Destroy (); - } - - int run () - { - return _dialog->ShowModal (); - } - -private: - wxMessageDialog* _dialog; -}; - - -void -maybe_save_then_delete_film () -{ - if (!film) { - return; - } - - if (film->dirty ()) { - FilmChangedDialog d; - switch (d.run ()) { - case wxID_NO: - break; - case wxID_YES: - film->write_metadata (); - break; - } - } - - film.reset (); -} - -enum Sensitivity { - ALWAYS, - NEEDS_FILM -}; - -map<wxMenuItem*, Sensitivity> menu_items; - -void -add_item (wxMenu* menu, std::string text, int id, Sensitivity sens) -{ - wxMenuItem* item = menu->Append (id, std_to_wx (text)); - menu_items.insert (make_pair (item, sens)); -} - -void -set_menu_sensitivity () -{ - for (map<wxMenuItem*, Sensitivity>::iterator i = menu_items.begin(); i != menu_items.end(); ++i) { - if (i->second == NEEDS_FILM) { - i->first->Enable (film != 0); - } else { - i->first->Enable (true); - } - } -} - -enum { - ID_file_new = 1, - ID_file_open, - ID_file_save, - ID_file_properties, - ID_file_quit, - ID_edit_preferences, - ID_jobs_make_dcp, - ID_jobs_make_kdms, - ID_jobs_send_dcp_to_tms, - ID_jobs_examine_content, - ID_jobs_make_dcp_from_existing_transcode, - ID_help_about -}; - -void -setup_menu (wxMenuBar* m) -{ - wxMenu* file = new wxMenu; - add_item (file, "New...", ID_file_new, ALWAYS); - add_item (file, "&Open...", ID_file_open, ALWAYS); - file->AppendSeparator (); - add_item (file, "&Save", ID_file_save, NEEDS_FILM); - file->AppendSeparator (); - add_item (file, "&Properties...", ID_file_properties, NEEDS_FILM); - file->AppendSeparator (); - add_item (file, "&Quit", ID_file_quit, ALWAYS); - - wxMenu* edit = new wxMenu; - add_item (edit, "&Preferences...", ID_edit_preferences, ALWAYS); - - wxMenu* jobs = new wxMenu; - add_item (jobs, "&Make DCP", ID_jobs_make_dcp, NEEDS_FILM); - add_item (jobs, "Make &KDMs...", ID_jobs_make_kdms, NEEDS_FILM); - add_item (jobs, "&Send DCP to TMS", ID_jobs_send_dcp_to_tms, NEEDS_FILM); - jobs->AppendSeparator (); - add_item (jobs, "&Examine content", ID_jobs_examine_content, NEEDS_FILM); - add_item (jobs, "Make DCP from existing &transcode", ID_jobs_make_dcp_from_existing_transcode, NEEDS_FILM); - - wxMenu* help = new wxMenu; - add_item (help, "About", ID_help_about, ALWAYS); - - m->Append (file, _("&File")); - m->Append (edit, _("&Edit")); - m->Append (jobs, _("&Jobs")); - m->Append (help, _("&Help")); -} - -bool -window_closed (wxCommandEvent &) -{ - maybe_save_then_delete_film (); - return false; -} - -class Frame : public wxFrame -{ -public: - Frame (wxString const & title) - : wxFrame (NULL, -1, title) - { - wxMenuBar* bar = new wxMenuBar; - setup_menu (bar); - SetMenuBar (bar); - - Connect (ID_file_new, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_new)); - Connect (ID_file_open, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_open)); - Connect (ID_file_save, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_save)); - Connect (ID_file_properties, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_properties)); - Connect (ID_file_quit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::file_quit)); - Connect (ID_edit_preferences, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::edit_preferences)); - Connect (ID_jobs_make_dcp, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_make_dcp)); - Connect (ID_jobs_make_kdms, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_make_kdms)); - Connect (ID_jobs_send_dcp_to_tms, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_send_dcp_to_tms)); - Connect (ID_jobs_examine_content, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_examine_content)); - Connect (ID_jobs_make_dcp_from_existing_transcode, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_make_dcp_from_existing_transcode)); - Connect (ID_help_about, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::help_about)); - - wxPanel* panel = new wxPanel (this); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - s->Add (panel, 1, wxEXPAND); - SetSizer (s); - - film_editor = new FilmEditor (film, panel); - film_viewer = new FilmViewer (film, panel); - JobManagerView* job_manager_view = new JobManagerView (panel); - - wxSizer* rhs_sizer = new wxBoxSizer (wxVERTICAL); - rhs_sizer->Add (film_viewer, 3, wxEXPAND | wxALL); - rhs_sizer->Add (job_manager_view, 1, wxEXPAND | wxALL); - - wxBoxSizer* main_sizer = new wxBoxSizer (wxHORIZONTAL); - main_sizer->Add (film_editor, 0, wxALL, 6); - main_sizer->Add (rhs_sizer, 1, wxEXPAND | wxALL, 6); - panel->SetSizer (main_sizer); - - set_menu_sensitivity (); - - /* XXX: calling these here is a bit of a hack */ - film_editor->setup_visibility (); - - film_editor->FileChanged.connect (bind (&Frame::file_changed, this, _1)); - if (film) { - file_changed (film->directory ()); - } else { - file_changed (""); - } - - set_film (); - } - - void set_film () - { - film_viewer->set_film (film); - film_editor->set_film (film); - set_menu_sensitivity (); - } - - void file_changed (string f) - { - stringstream s; - s << "DVD-o-matic"; - if (!f.empty ()) { - s << " - " << f; - } - - SetTitle (std_to_wx (s.str())); - } - - void file_new (wxCommandEvent &) - { - NewFilmDialog* d = new NewFilmDialog (this); - int const r = d->ShowModal (); - - if (r == wxID_OK) { - - if (boost::filesystem::exists (d->get_path())) { - error_dialog (this, String::compose ("The directory %1 already exists.", d->get_path())); - return; - } - - maybe_save_then_delete_film (); - film.reset (new Film (d->get_path (), false)); - film->log()->set_level (log_level); -#if BOOST_FILESYSTEM_VERSION == 3 - film->set_name (boost::filesystem::path (d->get_path()).filename().generic_string()); -#else - film->set_name (boost::filesystem::path (d->get_path()).filename()); -#endif - set_film (); - } - - d->Destroy (); - } - - void file_open (wxCommandEvent &) - { - wxDirDialog* c = new wxDirDialog (this, wxT ("Select film to open"), wxStandardPaths::Get().GetDocumentsDir(), wxDEFAULT_DIALOG_STYLE | wxDD_DIR_MUST_EXIST); - int const r = c->ShowModal (); - - if (r == wxID_OK) { - maybe_save_then_delete_film (); - try { - film.reset (new Film (wx_to_std (c->GetPath ()))); - film->log()->set_level (log_level); - set_film (); - } catch (std::exception& e) { - error_dialog (this, String::compose ("Could not open film at %1 (%2)", wx_to_std (c->GetPath()), e.what())); - } - } - - c->Destroy (); - } - - void file_save (wxCommandEvent &) - { - film->write_metadata (); - } - - void file_properties (wxCommandEvent &) - { - PropertiesDialog* d = new PropertiesDialog (this, film); - d->ShowModal (); - d->Destroy (); - } - - void file_quit (wxCommandEvent &) - { - maybe_save_then_delete_film (); - Close (true); - } - - void edit_preferences (wxCommandEvent &) - { - ConfigDialog* d = new ConfigDialog (this); - d->ShowModal (); - d->Destroy (); - Config::instance()->write (); - } - - void jobs_make_dcp (wxCommandEvent &) - { - JobWrapper::make_dcp (this, film, true); - } - - void jobs_make_kdms (wxCommandEvent &) - { - if (!film) { - return; - } - - KDMDialog* d = new KDMDialog (this); - if (d->ShowModal () == wxID_OK) { - film->make_kdms ( - d->screens (), - d->from (), - d->until (), - d->directory () - ); - } - - d->Destroy (); - } - - void jobs_make_dcp_from_existing_transcode (wxCommandEvent &) - { - JobWrapper::make_dcp (this, film, false); - } - - void jobs_send_dcp_to_tms (wxCommandEvent &) - { - film->send_dcp_to_tms (); - } - - void jobs_examine_content (wxCommandEvent &) - { - film->examine_content (); - } - - void help_about (wxCommandEvent &) - { - wxAboutDialogInfo info; - info.SetName (_("DVD-o-matic")); - if (strcmp (dvdomatic_git_commit, "release") == 0) { - info.SetVersion (std_to_wx (String::compose ("version %1", dvdomatic_version))); - } else { - info.SetVersion (std_to_wx (String::compose ("version %1 git %2", dvdomatic_version, dvdomatic_git_commit))); - } - info.SetDescription (_("Free, open-source DCP generation from almost anything.")); - info.SetCopyright (_("(C) Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen")); - wxArrayString authors; - authors.Add (wxT ("Carl Hetherington")); - authors.Add (wxT ("Terrence Meiczinger")); - authors.Add (wxT ("Paul Davis")); - authors.Add (wxT ("Ole Laursen")); - info.SetDevelopers (authors); - info.SetWebSite (wxT ("http://carlh.net/software/dvdomatic")); - wxAboutBox (info); - } -}; - -#if wxMINOR_VERSION == 9 -static const wxCmdLineEntryDesc command_line_description[] = { - { wxCMD_LINE_OPTION, "l", "log", "set log level (silent, verbose or timing)", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, - { wxCMD_LINE_PARAM, 0, 0, "film to load", wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL }, - { wxCMD_LINE_NONE, "", "", "", wxCmdLineParamType (0), 0 } -}; -#else -static const wxCmdLineEntryDesc command_line_description[] = { - { wxCMD_LINE_OPTION, wxT("l"), wxT("log"), wxT("set log level (silent, verbose or timing)"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_OPTIONAL }, - { wxCMD_LINE_PARAM, 0, 0, wxT("film to load"), wxCMD_LINE_VAL_STRING, wxCMD_LINE_PARAM_MULTIPLE | wxCMD_LINE_PARAM_OPTIONAL }, - { wxCMD_LINE_NONE, wxT(""), wxT(""), wxT(""), wxCmdLineParamType (0), 0 } -}; -#endif - -class App : public wxApp -{ - bool OnInit () - { - if (!wxApp::OnInit()) { - return false; - } - -#ifdef DVDOMATIC_POSIX - unsetenv ("UBUNTU_MENUPROXY"); -#endif - - wxInitAllImageHandlers (); - - dvdomatic_setup (); - - if (!film_to_load.empty() && boost::filesystem::is_directory (film_to_load)) { - film.reset (new Film (film_to_load)); - film->log()->set_level (log_level); - } - - Frame* f = new Frame (_("DVD-o-matic")); - SetTopWindow (f); - f->Maximize (); - f->Show (); - - ui_signaller = new wxUISignaller (this); - this->Connect (-1, wxEVT_IDLE, wxIdleEventHandler (App::idle)); - - return true; - } - - void OnInitCmdLine (wxCmdLineParser& parser) - { - parser.SetDesc (command_line_description); - parser.SetSwitchChars (wxT ("-")); - } - - bool OnCmdLineParsed (wxCmdLineParser& parser) - { - if (parser.GetParamCount() > 0) { - film_to_load = wx_to_std (parser.GetParam(0)); - } - - wxString log; - if (parser.Found(wxT("log"), &log)) { - log_level = wx_to_std (log); - } - - return true; - } - - void idle (wxIdleEvent &) - { - ui_signaller->ui_idle (); - } -}; - -IMPLEMENT_APP (App) diff --git a/src/tools/po/es_ES.po b/src/tools/po/es_ES.po new file mode 100644 index 000000000..fb379ab33 --- /dev/null +++ b/src/tools/po/es_ES.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCPOMATIC\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-03-23 21:08-0500\n" +"Last-Translator: Manuel AC <manuel.acevedo@civantos.>\n" +"Language-Team: Manuel AC <manuel.acevedo@civantos.com>\n" +"Language: es-ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/tools/dcpomatic.cc:309 +msgid "%1 already exists as a file, so you cannot use it for a new film." +msgstr "" + +#: src/tools/dcpomatic.cc:196 +msgid "&Edit" +msgstr "&Editar" + +#: src/tools/dcpomatic.cc:169 +msgid "&Exit" +msgstr "" + +#: src/tools/dcpomatic.cc:194 +msgid "&File" +msgstr "&Archivo" + +#: src/tools/dcpomatic.cc:199 +msgid "&Help" +msgstr "&Ayuda" + +#: src/tools/dcpomatic.cc:198 +msgid "&Jobs" +msgstr "&Tareas" + +#: src/tools/dcpomatic.cc:183 +msgid "&Make DCP" +msgstr "&Crear DCP" + +#: src/tools/dcpomatic.cc:159 +msgid "&Open..." +msgstr "&Abrir..." + +#: src/tools/dcpomatic.cc:176 src/tools/dcpomatic.cc:179 +msgid "&Preferences..." +msgstr "&Preferencias..." + +#: src/tools/dcpomatic.cc:163 +msgid "&Properties..." +msgstr "&Propiedades..." + +#: src/tools/dcpomatic.cc:171 +msgid "&Quit" +msgstr "&Salir" + +#: src/tools/dcpomatic.cc:161 +msgid "&Save" +msgstr "&Guardar" + +#: src/tools/dcpomatic.cc:184 +msgid "&Send DCP to TMS" +msgstr "&Enviar DCP al TMS" + +#: src/tools/dcpomatic.cc:191 +msgid "About" +msgstr "Acerca de" + +#: src/tools/dcpomatic.cc:189 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DVD-o-matic" + +#: src/tools/dcpomatic.cc:479 +#, fuzzy +msgid "Could not load film %1 (%2)" +msgstr "No se pudo cargar la película %s (%s)" + +#: src/tools/dcpomatic.cc:348 +#, c-format +msgid "Could not open film at %s (%s)" +msgstr "No se pudo cargar la película en %s (%s)" + +#: src/tools/dcpomatic.cc:280 src/tools/dcpomatic.cc:490 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/tools/dcpomatic.cc:77 +msgid "Film changed" +msgstr "Película cambiada" + +#: src/tools/dcpomatic.cc:158 +msgid "New..." +msgstr "Nuevo..." + +#: src/tools/dcpomatic.cc:185 +msgid "S&how DCP" +msgstr "&Mostrar DCP" + +#: src/tools/dcpomatic.cc:76 +#, c-format +msgid "Save changes to film \"%s\" before closing?" +msgstr "" + +#: src/tools/dcpomatic.cc:327 +msgid "Select film to open" +msgstr "Selecciona la película a abrir" + +#: src/tools/dcpomatic.cc:299 +msgid "" +"The directory %1 already exists and is not empty. Are you sure you want to " +"use it?" +msgstr "" + +#: src/tools/dcpomatic.cc:332 +msgid "" +"You did not select a folder. Make sure that you select a folder before " +"clicking Open." +msgstr "" + +#~ msgid "&Analyse audio" +#~ msgstr "&Analizar audio" + +#~ msgid "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" +#~ msgstr "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" + +#~ msgid "Free, open-source DCP generation from almost anything." +#~ msgstr "" +#~ "Generación de DCP a partir de casi cualquier fuente, libre y de código " +#~ "abierto." + +#, fuzzy +#~ msgid "The directory %1 already exists." +#~ msgstr "La carpeta %s ya existe." diff --git a/src/tools/po/fr_FR.po b/src/tools/po/fr_FR.po new file mode 100644 index 000000000..71bd1550e --- /dev/null +++ b/src/tools/po/fr_FR.po @@ -0,0 +1,138 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic FRENCH\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-07-16 23:13+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/tools/dcpomatic.cc:309 +msgid "%1 already exists as a file, so you cannot use it for a new film." +msgstr "Le fichier %1 existe déjà, vous ne pouvez l'utiliser pour un nouveau projet." + +#: src/tools/dcpomatic.cc:196 +msgid "&Edit" +msgstr "&Edition" + +#: src/tools/dcpomatic.cc:169 +msgid "&Exit" +msgstr "&Quitter" + +#: src/tools/dcpomatic.cc:194 +msgid "&File" +msgstr "&Fichier" + +#: src/tools/dcpomatic.cc:199 +msgid "&Help" +msgstr "&Aide" + +#: src/tools/dcpomatic.cc:198 +msgid "&Jobs" +msgstr "&Travaux" + +#: src/tools/dcpomatic.cc:183 +msgid "&Make DCP" +msgstr "&Créer le DCP" + +#: src/tools/dcpomatic.cc:159 +msgid "&Open..." +msgstr "&Ouvrir..." + +#: src/tools/dcpomatic.cc:176 +#: src/tools/dcpomatic.cc:179 +msgid "&Preferences..." +msgstr "&Préférences..." + +#: src/tools/dcpomatic.cc:163 +msgid "&Properties..." +msgstr "&Propriétés..." + +#: src/tools/dcpomatic.cc:171 +msgid "&Quit" +msgstr "&Quitter" + +#: src/tools/dcpomatic.cc:161 +msgid "&Save" +msgstr "&Enregistrer" + +#: src/tools/dcpomatic.cc:184 +msgid "&Send DCP to TMS" +msgstr "&Envoyer le DCP dans le TMS" + +#: src/tools/dcpomatic.cc:191 +msgid "About" +msgstr "A Propos" + +#: src/tools/dcpomatic.cc:189 +msgid "About DCP-o-matic" +msgstr "À propos de DCP-o-matic" + +#: src/tools/dcpomatic.cc:479 +msgid "Could not load film %1 (%2)" +msgstr "Impossible de charger le film %1 (%2)" + +#: src/tools/dcpomatic.cc:348 +#, c-format +msgid "Could not open film at %s (%s)" +msgstr "Impossible d'ouvrir le film à %s (%s)" + +#: src/tools/dcpomatic.cc:280 +#: src/tools/dcpomatic.cc:490 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/tools/dcpomatic.cc:77 +msgid "Film changed" +msgstr "Film changé" + +#: src/tools/dcpomatic.cc:158 +msgid "New..." +msgstr "Nouveau..." + +#: src/tools/dcpomatic.cc:185 +msgid "S&how DCP" +msgstr "Voir le DCP" + +#: src/tools/dcpomatic.cc:76 +#, c-format +msgid "Save changes to film \"%s\" before closing?" +msgstr "Enregistrer les changements du film \"%s\" avant de fermer ?" + +#: src/tools/dcpomatic.cc:327 +msgid "Select film to open" +msgstr "Sélectionner le film à ouvrir" + +#: src/tools/dcpomatic.cc:299 +msgid "The directory %1 already exists and is not empty. Are you sure you want to use it?" +msgstr "Le dossier %1 existe et n'est pas vide. Etes-vous sûr de vouloir l'utiliser ?" + +#: src/tools/dcpomatic.cc:332 +msgid "You did not select a folder. Make sure that you select a folder before clicking Open." +msgstr "Aucun dossier sélectionné. Selectionnez un dossier avant de cliquer sur Ouvrir" + +#~ msgid "&Analyse audio" +#~ msgstr "&Analyser le son" + +#~ msgid "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" +#~ msgstr "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" + +#~ msgid "Free, open-source DCP generation from almost anything." +#~ msgstr "Création de DCP libre et open-source à partir de presque tout." + +#, fuzzy +#~ msgid "The directory %1 already exists." +#~ msgstr "Le dossier %s existe déjà." diff --git a/src/tools/po/it_IT.po b/src/tools/po/it_IT.po new file mode 100644 index 000000000..f32ce97db --- /dev/null +++ b/src/tools/po/it_IT.po @@ -0,0 +1,142 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: IT VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-28 10:31+0100\n" +"Last-Translator: Maci <macibro@gmail.com>\n" +"Language-Team: \n" +"Language: Italiano\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/tools/dcpomatic.cc:309 +msgid "%1 already exists as a file, so you cannot use it for a new film." +msgstr "" + +#: src/tools/dcpomatic.cc:196 +msgid "&Edit" +msgstr "&Modifica" + +#: src/tools/dcpomatic.cc:169 +msgid "&Exit" +msgstr "" + +#: src/tools/dcpomatic.cc:194 +msgid "&File" +msgstr "&File" + +#: src/tools/dcpomatic.cc:199 +msgid "&Help" +msgstr "&Aiuto" + +#: src/tools/dcpomatic.cc:198 +msgid "&Jobs" +msgstr "&Lavori" + +#: src/tools/dcpomatic.cc:183 +msgid "&Make DCP" +msgstr "&Crea DCP" + +#: src/tools/dcpomatic.cc:159 +msgid "&Open..." +msgstr "&Apri..." + +#: src/tools/dcpomatic.cc:176 src/tools/dcpomatic.cc:179 +msgid "&Preferences..." +msgstr "&Preferenze..." + +#: src/tools/dcpomatic.cc:163 +msgid "&Properties..." +msgstr "&Proprieta'..." + +#: src/tools/dcpomatic.cc:171 +msgid "&Quit" +msgstr "&Esci" + +#: src/tools/dcpomatic.cc:161 +msgid "&Save" +msgstr "&Salva" + +#: src/tools/dcpomatic.cc:184 +msgid "&Send DCP to TMS" +msgstr "&Invia DCP a TMS" + +#: src/tools/dcpomatic.cc:191 +msgid "About" +msgstr "Informazioni" + +#: src/tools/dcpomatic.cc:189 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DVD-o-matic" + +#: src/tools/dcpomatic.cc:479 +msgid "Could not load film %1 (%2)" +msgstr "Non posso caricare il film %s (%s)" + +#: src/tools/dcpomatic.cc:348 +#, c-format +msgid "Could not open film at %s (%s)" +msgstr "Non posso aprire il film in %s (%s)" + +#: src/tools/dcpomatic.cc:280 src/tools/dcpomatic.cc:490 +#, fuzzy +msgid "DCP-o-matic" +msgstr "DVD-o-matic" + +#: src/tools/dcpomatic.cc:77 +msgid "Film changed" +msgstr "Film modificato" + +#: src/tools/dcpomatic.cc:158 +msgid "New..." +msgstr "Nuovo" + +#: src/tools/dcpomatic.cc:185 +msgid "S&how DCP" +msgstr "&Mostra DCP" + +#: src/tools/dcpomatic.cc:76 +#, c-format +msgid "Save changes to film \"%s\" before closing?" +msgstr "Salvare i cambiamenti del film \"%s\" prima di chiudere?" + +#: src/tools/dcpomatic.cc:327 +msgid "Select film to open" +msgstr "Seleziona il film da aprire" + +#: src/tools/dcpomatic.cc:299 +msgid "" +"The directory %1 already exists and is not empty. Are you sure you want to " +"use it?" +msgstr "" + +#: src/tools/dcpomatic.cc:332 +msgid "" +"You did not select a folder. Make sure that you select a folder before " +"clicking Open." +msgstr "" + +#~ msgid "&Analyse audio" +#~ msgstr "&Analizza audio" + +#~ msgid "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" +#~ msgstr "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" + +#~ msgid "Free, open-source DCP generation from almost anything." +#~ msgstr "Genera DCP da quasi tutto, free e open-source." + +#~ msgid "The directory %1 already exists." +#~ msgstr "La directory %s esiste gia'." diff --git a/src/tools/po/sv_SE.po b/src/tools/po/sv_SE.po new file mode 100644 index 000000000..a7a5f82e5 --- /dev/null +++ b/src/tools/po/sv_SE.po @@ -0,0 +1,145 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-09 10:12+0100\n" +"Last-Translator: Adam Klotblixt <adam.klotblixt@gmail.com>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/tools/dcpomatic.cc:309 +msgid "%1 already exists as a file, so you cannot use it for a new film." +msgstr "" + +#: src/tools/dcpomatic.cc:196 +msgid "&Edit" +msgstr "&Redigera" + +#: src/tools/dcpomatic.cc:169 +msgid "&Exit" +msgstr "" + +#: src/tools/dcpomatic.cc:194 +msgid "&File" +msgstr "&Fil" + +#: src/tools/dcpomatic.cc:199 +msgid "&Help" +msgstr "&Hjälp" + +#: src/tools/dcpomatic.cc:198 +msgid "&Jobs" +msgstr "&Jobb" + +#: src/tools/dcpomatic.cc:183 +msgid "&Make DCP" +msgstr "&Skapa DCP" + +#: src/tools/dcpomatic.cc:159 +msgid "&Open..." +msgstr "&Öppna" + +#: src/tools/dcpomatic.cc:176 src/tools/dcpomatic.cc:179 +msgid "&Preferences..." +msgstr "&Inställningar" + +#: src/tools/dcpomatic.cc:163 +msgid "&Properties..." +msgstr "&Egenskaper" + +#: src/tools/dcpomatic.cc:171 +msgid "&Quit" +msgstr "&Avsluta" + +#: src/tools/dcpomatic.cc:161 +msgid "&Save" +msgstr "&Spara" + +#: src/tools/dcpomatic.cc:184 +msgid "&Send DCP to TMS" +msgstr "&Skicka DCP till TMS" + +#: src/tools/dcpomatic.cc:191 +msgid "About" +msgstr "Om" + +#: src/tools/dcpomatic.cc:189 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DVD-o-matic" + +#: src/tools/dcpomatic.cc:479 +msgid "Could not load film %1 (%2)" +msgstr "Kunde inte öppna filmen %1 (%2)" + +#: src/tools/dcpomatic.cc:348 +#, c-format +msgid "Could not open film at %s (%s)" +msgstr "Kunde inte öppna filmen vid %s (%s)" + +#: src/tools/dcpomatic.cc:280 src/tools/dcpomatic.cc:490 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/tools/dcpomatic.cc:77 +msgid "Film changed" +msgstr "Film ändrad" + +#: src/tools/dcpomatic.cc:158 +msgid "New..." +msgstr "Ny..." + +#: src/tools/dcpomatic.cc:185 +msgid "S&how DCP" +msgstr "&Visa DCP" + +#: src/tools/dcpomatic.cc:76 +#, fuzzy, c-format +msgid "Save changes to film \"%s\" before closing?" +msgstr "Spara ändringarna till filmen \"%s\" före avslut?" + +#: src/tools/dcpomatic.cc:327 +msgid "Select film to open" +msgstr "Välj film att öppna" + +#: src/tools/dcpomatic.cc:299 +msgid "" +"The directory %1 already exists and is not empty. Are you sure you want to " +"use it?" +msgstr "" + +#: src/tools/dcpomatic.cc:332 +msgid "" +"You did not select a folder. Make sure that you select a folder before " +"clicking Open." +msgstr "" +"Du har inte valt en folder. Se till att välja en folder innan du klickar på " +"Öppna." + +#~ msgid "&Analyse audio" +#~ msgstr "&Analysera audio" + +#~ msgid "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" +#~ msgstr "" +#~ "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole " +#~ "Laursen" + +#~ msgid "Free, open-source DCP generation from almost anything." +#~ msgstr "" +#~ "Fri, öppen-källkodsprogramvara för DCP-generering från nästan vad som " +#~ "helst." + +#~ msgid "The directory %1 already exists." +#~ msgstr "Katalogen %1 finns redan." diff --git a/src/tools/servomatictest.cc b/src/tools/server_test.cc index 88c2a833e..029e62614 100644 --- a/src/tools/servomatictest.cc +++ b/src/tools/server_test.cc @@ -21,41 +21,48 @@ #include <iomanip> #include <exception> #include <getopt.h> -#include "format.h" -#include "film.h" -#include "filter.h" -#include "util.h" -#include "scaler.h" -#include "server.h" -#include "dcp_video_frame.h" -#include "options.h" -#include "decoder.h" -#include "exceptions.h" -#include "scaler.h" -#include "log.h" -#include "decoder_factory.h" - -using namespace std; -using namespace boost; - -static Server* server; -static Log log_ ("servomatictest.log"); +#include "lib/ratio.h" +#include "lib/film.h" +#include "lib/filter.h" +#include "lib/util.h" +#include "lib/scaler.h" +#include "lib/server.h" +#include "lib/dcp_video_frame.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" + +using std::cout; +using std::cerr; +using std::string; +using std::pair; +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; void -process_video (shared_ptr<Image> image, bool, int frame) +process_video (shared_ptr<const Image> image, Eyes eyes, ColourConversion conversion, Time) { - shared_ptr<DCPVideoFrame> local (new DCPVideoFrame (image, Size (1024, 1024), 0, Scaler::from_id ("bicubic"), frame, 24, "", 0, 250000000, &log_)); - shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (image, Size (1024, 1024), 0, Scaler::from_id ("bicubic"), frame, 24, "", 0, 250000000, &log_)); + shared_ptr<DCPVideoFrame> local (new DCPVideoFrame (image, frame, eyes, conversion, film->video_frame_rate(), 250000000, log_)); + shared_ptr<DCPVideoFrame> remote (new DCPVideoFrame (image, frame, eyes, conversion, film->video_frame_rate(), 250000000, log_)); cout << "Frame " << frame << ": "; cout.flush (); + ++frame; + shared_ptr<EncodedData> local_encoded = local->encode_locally (); shared_ptr<EncodedData> remote_encoded; string remote_error; try { - remote_encoded = remote->encode_remotely (server); + remote_encoded = remote->encode_remotely (*server); } catch (NetworkError& e) { remote_error = e.what (); } @@ -128,19 +135,21 @@ main (int argc, char* argv[]) exit (EXIT_FAILURE); } - dvdomatic_setup (); + dcpomatic_setup (); - server = new Server (server_host, 1); - Film film (film_dir, true); + server = new ServerDescription (server_host, 1); + film.reset (new Film (film_dir)); + film->read_metadata (); - shared_ptr<Options> opt (new Options ("fred", "jim", "sheila")); - opt->out_size = Size (1024, 1024); - opt->decode_audio = false; + shared_ptr<Player> player = film->make_player (); + player->disable_audio (); - shared_ptr<Decoder> decoder = decoder_factory (film.state_copy(), opt, 0, &log_); try { - decoder->Video.connect (sigc::ptr_fun (process_video)); - decoder->go (); + player->Video.connect (boost::bind (process_video, _1, _2, _3, _5)); + bool done = false; + while (!done) { + done = player->pass (); + } } catch (std::exception& e) { cerr << "Error: " << e.what() << "\n"; } diff --git a/src/tools/wscript b/src/tools/wscript index 5a837f845..42fc90adb 100644 --- a/src/tools/wscript +++ b/src/tools/wscript @@ -1,19 +1,34 @@ +import os +import glob +from waflib import Logs +import i18n + def build(bld): - for t in ['makedcp', 'servomatic_cli']: + for t in ['dcpomatic_cli', 'dcpomatic_server_cli', 'server_test']: obj = bld(features = 'cxx cxxprogram') - obj.uselib = 'BOOST_THREAD OPENJPEG DCP AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC' + obj.uselib = 'BOOST_THREAD OPENJPEG DCP CXML AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC WXWIDGETS' obj.includes = ['..'] - obj.use = ['libdvdomatic'] + obj.use = ['libdcpomatic'] obj.source = '%s.cc' % t obj.target = t if not bld.env.DISABLE_GUI: - for t in ['dvdomatic', 'servomatic_gui']: + for t in ['dcpomatic', 'dcpomatic_batch', 'dcpomatic_server']: obj = bld(features = 'cxx cxxprogram') - obj.uselib = 'DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC' + obj.uselib = 'DCP OPENJPEG AVFORMAT AVFILTER AVCODEC AVUTIL SWSCALE POSTPROC CXML WXWIDGETS' + if bld.env.STATIC: + obj.uselib += ' GTK' obj.includes = ['..'] - obj.use = ['libdvdomatic', 'libdvdomatic-wx'] + obj.use = ['libdcpomatic', 'libdcpomatic-wx'] obj.source = '%s.cc' % t if bld.env.TARGET_WINDOWS: - obj.source += ' ../../windows/dvdomatic.rc' + obj.source += ' ../../platform/windows/dcpomatic.rc' obj.target = t + + i18n.po_to_mo(os.path.join('src', 'tools'), 'dcpomatic', bld) + +def pot(bld): + i18n.pot(os.path.join('src', 'tools'), 'dcpomatic.cc', 'dcpomatic') + +def pot_merge(bld): + i18n.pot_merge(os.path.join('src', 'tools'), 'dcpomatic') diff --git a/src/wscript b/src/wscript index 3f17b3e6c..a4cf349f9 100644 --- a/src/wscript +++ b/src/wscript @@ -7,3 +7,13 @@ def build(bld): bld.recurse('tools') if not bld.env.DISABLE_GUI: bld.recurse('wx') + +def pot(bld): + bld.recurse('lib') + bld.recurse('wx') + bld.recurse('tools') + +def pot_merge(bld): + bld.recurse('lib') + bld.recurse('wx') + bld.recurse('tools') diff --git a/src/wx/about_dialog.cc b/src/wx/about_dialog.cc new file mode 100644 index 000000000..45f2f6c37 --- /dev/null +++ b/src/wx/about_dialog.cc @@ -0,0 +1,159 @@ +/* + 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 <wx/notebook.h> +#include <wx/hyperlink.h> +#include "lib/version.h" +#include "lib/compose.hpp" +#include "about_dialog.h" +#include "wx_util.h" + +using std::vector; + +AboutDialog::AboutDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("About DCP-o-matic")) +{ + wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); + + wxFont title_font (*wxNORMAL_FONT); + title_font.SetPointSize (title_font.GetPointSize() + 12); + title_font.SetWeight (wxFONTWEIGHT_BOLD); + + wxFont subtitle_font (*wxNORMAL_FONT); + subtitle_font.SetPointSize (subtitle_font.GetPointSize() + 2); + + wxFont version_font (*wxNORMAL_FONT); + version_font.SetWeight (wxFONTWEIGHT_BOLD); + + wxStaticText* t = new wxStaticText (this, wxID_ANY, _("DCP-o-matic")); + t->SetFont (title_font); + sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 16)); + + wxString s; + if (strcmp (dcpomatic_git_commit, "release") == 0) { + t = new wxStaticText (this, wxID_ANY, std_to_wx (String::compose ("Version %1", dcpomatic_version))); + } else { + t = new wxStaticText (this, wxID_ANY, std_to_wx (String::compose ("Version %1 git %2", dcpomatic_version, dcpomatic_git_commit))); + } + t->SetFont (version_font); + sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 2)); + sizer->AddSpacer (12); + + t = new wxStaticText ( + this, wxID_ANY, + _("Free, open-source DCP generation from almost anything."), + wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER + ); + t->SetFont (subtitle_font); + + sizer->Add (t, wxSizerFlags().Centre().Border(wxALL, 8)); + + wxHyperlinkCtrl* h = new wxHyperlinkCtrl ( + this, wxID_ANY, + wxT ("dcpomatic.com"), + wxT ("http://dcpomatic.com") + ); + + sizer->Add (h, wxSizerFlags().Centre().Border(wxALL, 8)); + + t = new wxStaticText ( + this, wxID_ANY, + _("(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen"), + wxDefaultPosition, wxDefaultSize, wxALIGN_CENTER + ); + + sizer->Add (t, wxSizerFlags().Centre().Border(wxLEFT | wxRIGHT, 16)); + + _notebook = new wxNotebook (this, wxID_ANY); + + wxArrayString written_by; + written_by.Add (wxT ("Carl Hetherington")); + written_by.Add (wxT ("Terrence Meiczinger")); + written_by.Add (wxT ("Paul Davis")); + written_by.Add (wxT ("Ole Laursen")); + add_section (_("Written by"), written_by); + + wxArrayString translated_by; + translated_by.Add (wxT ("Olivier Perriere")); + translated_by.Add (wxT ("Lilian Lefranc")); + translated_by.Add (wxT ("Thierry Journet")); + translated_by.Add (wxT ("Massimiliano Broggi")); + translated_by.Add (wxT ("Manuel AC")); + translated_by.Add (wxT ("Adam Klotblixt")); + add_section (_("Translated by"), translated_by); + + wxArrayString supported_by; + supported_by.Add (wxT ("Carsten Kurz")); + supported_by.Add (wxT ("Wolfgang Woehl")); + supported_by.Add (wxT ("Manual AC")); + supported_by.Add (wxT ("Theo Lipfert")); + supported_by.Add (wxT ("Olivier Lemaire")); + supported_by.Add (wxT ("Mattias Mattsson")); + supported_by.Add (wxT ("Andrä Steiner")); + supported_by.Add (wxT ("Jonathan Jensen")); + supported_by.Add (wxT ("Kjarten Michaelsen")); + supported_by.Add (wxT ("Jussi Siponen")); + supported_by.Add (wxT ("Cinema Clarici")); + supported_by.Add (wxT ("Evan Freeze")); + supported_by.Add (wxT ("Flor Guillaume")); + supported_by.Add (wxT ("Adam Klotblixt ")); + supported_by.Add (wxT ("Lilian Lefranc")); + supported_by.Add (wxT ("Gavin Lewarne")); + supported_by.Add (wxT ("Lasse Salling")); + supported_by.Add (wxT ("Andres Fink")); + supported_by.Add (wxT ("Kieran Carroll")); + supported_by.Add (wxT ("Kambiz Afshar")); + supported_by.Add (wxT ("Sean Leigh")); + supported_by.Add (wxT ("Wolfram Weber")); + add_section (_("Supported by"), supported_by); + + sizer->Add (_notebook, wxSizerFlags().Centre().Border(wxALL, 16).Expand()); + + SetSizerAndFit (sizer); +} + +void +AboutDialog::add_section (wxString name, wxArrayString credits) +{ + static bool first = true; + int const N = 3; + + wxPanel* panel = new wxPanel (_notebook, wxID_ANY); + wxSizer* overall_sizer = new wxBoxSizer (wxHORIZONTAL); + + vector<wxSizer*> sizers; + + for (int i = 0; i < N; ++i) { + sizers.push_back (new wxBoxSizer (wxVERTICAL)); + overall_sizer->Add (sizers.back (), 1, wxEXPAND | wxALL, 6); + } + + int c = 0; + for (size_t i = 0; i < credits.Count(); ++i) { + add_label_to_sizer (sizers[c], panel, credits[i], false); + ++c; + if (c == N) { + c = 0; + } + } + + panel->SetSizerAndFit (overall_sizer); + _notebook->AddPage (panel, name, first); + first = false; +} diff --git a/src/lib/audio_sink.h b/src/wx/about_dialog.h index 11d578a60..a78abb93e 100644 --- a/src/lib/audio_sink.h +++ b/src/wx/about_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,14 +17,18 @@ */ -#ifndef DVDOMATIC_AUDIO_SINK_H -#define DVDOMATIC_AUDIO_SINK_H +#include <wx/wx.h> -class AudioSink +class wxNotebook; + +class AboutDialog : public wxDialog { public: - /** Call with some audio data */ - virtual void process_audio (boost::shared_ptr<AudioBuffers>) = 0; + AboutDialog (wxWindow *); + +private: + void add_section (wxString, wxArrayString); + + wxNotebook* _notebook; }; -#endif diff --git a/src/wx/audio_dialog.cc b/src/wx/audio_dialog.cc new file mode 100644 index 000000000..c7a0815f8 --- /dev/null +++ b/src/wx/audio_dialog.cc @@ -0,0 +1,177 @@ +/* + 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 <boost/filesystem.hpp> +#include "lib/audio_analysis.h" +#include "lib/film.h" +#include "lib/audio_content.h" +#include "audio_dialog.h" +#include "audio_plot.h" +#include "wx_util.h" + +using boost::shared_ptr; +using boost::bind; +using boost::optional; + +AudioDialog::AudioDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("Audio"), wxDefaultPosition, wxSize (640, 512), wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER | wxFULL_REPAINT_ON_RESIZE) + , _plot (0) +{ + wxBoxSizer* sizer = new wxBoxSizer (wxHORIZONTAL); + + _plot = new AudioPlot (this); + sizer->Add (_plot, 1, wxALL | wxEXPAND, 12); + + wxBoxSizer* side = new wxBoxSizer (wxVERTICAL); + + { + wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Channels")); + side->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16); + } + + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + _channel_checkbox[i] = new wxCheckBox (this, wxID_ANY, std_to_wx (audio_channel_name (i))); + side->Add (_channel_checkbox[i], 1, wxEXPAND | wxALL, 3); + _channel_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::channel_clicked, this, _1)); + } + + { + wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Type")); + side->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16); + } + + wxString const types[] = { + _("Peak"), + _("RMS") + }; + + for (int i = 0; i < AudioPoint::COUNT; ++i) { + _type_checkbox[i] = new wxCheckBox (this, wxID_ANY, types[i]); + side->Add (_type_checkbox[i], 1, wxEXPAND | wxALL, 3); + _type_checkbox[i]->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&AudioDialog::type_clicked, this, _1)); + } + + { + wxStaticText* m = new wxStaticText (this, wxID_ANY, _("Smoothing")); + side->Add (m, 1, wxALIGN_CENTER_VERTICAL | wxTOP, 16); + } + + _smoothing = new wxSlider (this, wxID_ANY, AudioPlot::max_smoothing / 2, 1, AudioPlot::max_smoothing); + _smoothing->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind (&AudioDialog::smoothing_changed, this)); + side->Add (_smoothing, 1, wxEXPAND); + + sizer->Add (side, 0, wxALL, 12); + + SetSizer (sizer); + sizer->Layout (); + sizer->SetSizeHints (this); +} + +void +AudioDialog::set_content (shared_ptr<AudioContent> c) +{ + _content_changed_connection.disconnect (); + + _content = c; + + try_to_load_analysis (); + _plot->set_gain (_content->audio_gain ()); + + _content_changed_connection = _content->Changed.connect (bind (&AudioDialog::content_changed, this, _2)); + + SetTitle (wxString::Format (_("DCP-o-matic audio - %s"), std_to_wx(_content->path().filename().string()).data())); +} + +void +AudioDialog::try_to_load_analysis () +{ + if (!boost::filesystem::exists (_content->audio_analysis_path()) && IsShown ()) { + _content->analyse_audio (bind (&AudioDialog::analysis_finished, this)); + return; + } + + shared_ptr<AudioAnalysis> a; + + a.reset (new AudioAnalysis (_content->audio_analysis_path ())); + _plot->set_analysis (a); + + if (_channel_checkbox[0]) { + _channel_checkbox[0]->SetValue (true); + } + _plot->set_channel_visible (0, true); + + for (int i = 0; i < AudioPoint::COUNT; ++i) { + _type_checkbox[i]->SetValue (true); + _plot->set_type_visible (i, true); + } +} + +void +AudioDialog::analysis_finished () +{ + if (!boost::filesystem::exists (_content->audio_analysis_path())) { + /* We analysed and still nothing showed up, so maybe it was cancelled or it failed. + Give up. + */ + _plot->set_message (_("Could not analyse audio.")); + return; + } + + try_to_load_analysis (); +} + +void +AudioDialog::channel_clicked (wxCommandEvent& ev) +{ + int c = 0; + while (c < MAX_AUDIO_CHANNELS && ev.GetEventObject() != _channel_checkbox[c]) { + ++c; + } + + assert (c < MAX_AUDIO_CHANNELS); + + _plot->set_channel_visible (c, _channel_checkbox[c]->GetValue ()); +} + +void +AudioDialog::content_changed (int p) +{ + if (p == AudioContentProperty::AUDIO_GAIN) { + _plot->set_gain (_content->audio_gain ()); + } +} + +void +AudioDialog::type_clicked (wxCommandEvent& ev) +{ + int t = 0; + while (t < AudioPoint::COUNT && ev.GetEventObject() != _type_checkbox[t]) { + ++t; + } + + assert (t < AudioPoint::COUNT); + + _plot->set_type_visible (t, _type_checkbox[t]->GetValue ()); +} + +void +AudioDialog::smoothing_changed () +{ + _plot->set_smoothing (_smoothing->GetValue ()); +} diff --git a/src/lib/video_source.h b/src/wx/audio_dialog.h index 893629160..8623192c4 100644 --- a/src/lib/video_source.h +++ b/src/wx/audio_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,36 +17,34 @@ */ -/** @file src/video_source.h - * @brief Parent class for classes which emit video data. - */ - -#ifndef DVDOMATIC_VIDEO_SOURCE_H -#define DVDOMATIC_VIDEO_SOURCE_H - #include <boost/shared_ptr.hpp> #include <boost/signals2.hpp> -#include "util.h" +#include <wx/wx.h> +#include "lib/film.h" +#include "lib/audio_analysis.h" -class VideoSink; -class Subtitle; -class Image; +class AudioPlot; +class Film; -/** @class VideoSink - * @param A class that emits video data. - */ -class VideoSource +class AudioDialog : public wxDialog { public: - - /** Emitted when a video frame is ready. - * First parameter is the video image. - * Second parameter is true if the image is the same as the last one that was emitted. - * Third parameter is either 0 or a subtitle that should be on this frame. - */ - boost::signals2::signal<void (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>)> Video; - - void connect_video (boost::shared_ptr<VideoSink>); + AudioDialog (wxWindow *); + + void set_content (boost::shared_ptr<AudioContent>); + +private: + void content_changed (int); + void channel_clicked (wxCommandEvent &); + void type_clicked (wxCommandEvent &); + void smoothing_changed (); + void try_to_load_analysis (); + void analysis_finished (); + + boost::shared_ptr<AudioContent> _content; + AudioPlot* _plot; + wxCheckBox* _channel_checkbox[MAX_AUDIO_CHANNELS]; + wxCheckBox* _type_checkbox[AudioPoint::COUNT]; + wxSlider* _smoothing; + boost::signals2::scoped_connection _content_changed_connection; }; - -#endif diff --git a/src/wx/audio_mapping_view.cc b/src/wx/audio_mapping_view.cc new file mode 100644 index 000000000..3136b8679 --- /dev/null +++ b/src/wx/audio_mapping_view.cc @@ -0,0 +1,207 @@ +/* + 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 <wx/wx.h> +#include <wx/renderer.h> +#include <wx/grid.h> +#include <libdcp/types.h> +#include "lib/audio_mapping.h" +#include "lib/util.h" +#include "audio_mapping_view.h" +#include "wx_util.h" + +using std::cout; +using std::list; +using boost::shared_ptr; + +/* This could go away with wxWidgets 2.9, which has an API call + to find these values. +*/ + +#ifdef __WXMSW__ +#define CHECKBOX_WIDTH 16 +#define CHECKBOX_HEIGHT 16 +#else +#define CHECKBOX_WIDTH 20 +#define CHECKBOX_HEIGHT 20 +#endif + +class NoSelectionStringRenderer : public wxGridCellStringRenderer +{ +public: + void Draw (wxGrid& grid, wxGridCellAttr& attr, wxDC& dc, const wxRect& rect, int row, int col, bool) + { + wxGridCellStringRenderer::Draw (grid, attr, dc, rect, row, col, false); + } +}; + +class CheckBoxRenderer : public wxGridCellRenderer +{ +public: + + void Draw (wxGrid& grid, wxGridCellAttr &, wxDC& dc, const wxRect& rect, int row, int col, bool) + { + dc.SetPen (*wxThePenList->FindOrCreatePen (wxColour (255, 255, 255), 0, wxPENSTYLE_SOLID)); + dc.DrawRectangle (rect); + + wxRendererNative::Get().DrawCheckBox ( + &grid, + dc, rect, + grid.GetCellValue (row, col) == wxT("1") ? static_cast<int>(wxCONTROL_CHECKED) : 0 + ); + } + + wxSize GetBestSize (wxGrid &, wxGridCellAttr &, wxDC &, int, int) + { + return wxSize (CHECKBOX_WIDTH + 4, CHECKBOX_HEIGHT + 4); + } + + wxGridCellRenderer* Clone () const + { + return new CheckBoxRenderer; + } +}; + + +AudioMappingView::AudioMappingView (wxWindow* parent) + : wxPanel (parent, wxID_ANY) +{ + _grid = new wxGrid (this, wxID_ANY); + + _grid->CreateGrid (0, 7); + _grid->HideRowLabels (); + _grid->DisableDragRowSize (); + _grid->DisableDragColSize (); + _grid->EnableEditing (false); + _grid->SetCellHighlightPenWidth (0); + _grid->SetDefaultRenderer (new NoSelectionStringRenderer); + + set_column_labels (); + + _sizer = new wxBoxSizer (wxVERTICAL); + _sizer->Add (_grid, 1, wxEXPAND | wxALL); + SetSizerAndFit (_sizer); + + Bind (wxEVT_GRID_CELL_LEFT_CLICK, boost::bind (&AudioMappingView::left_click, this, _1)); +} + +void +AudioMappingView::left_click (wxGridEvent& ev) +{ + if (ev.GetCol() == 0) { + return; + } + + if (_grid->GetCellValue (ev.GetRow(), ev.GetCol()) == wxT("1")) { + _grid->SetCellValue (ev.GetRow(), ev.GetCol(), wxT("0")); + } else { + _grid->SetCellValue (ev.GetRow(), ev.GetCol(), wxT("1")); + } + + _map = AudioMapping (_map.content_channels ()); + + for (int i = 0; i < _grid->GetNumberRows(); ++i) { + for (int j = 1; j < _grid->GetNumberCols(); ++j) { + if (_grid->GetCellValue (i, j) == wxT ("1")) { + _map.add (i, static_cast<libdcp::Channel> (j - 1)); + } + } + } + + Changed (_map); +} + +void +AudioMappingView::set (AudioMapping map) +{ + _map = map; + + if (_grid->GetNumberRows ()) { + _grid->DeleteRows (0, _grid->GetNumberRows ()); + } + + _grid->InsertRows (0, _map.content_channels ()); + + for (int r = 0; r < _map.content_channels(); ++r) { + for (int c = 1; c < 7; ++c) { + _grid->SetCellRenderer (r, c, new CheckBoxRenderer); + } + } + + for (int i = 0; i < _map.content_channels(); ++i) { + _grid->SetCellValue (i, 0, wxString::Format (wxT("%d"), i + 1)); + + list<libdcp::Channel> const d = _map.content_to_dcp (i); + for (list<libdcp::Channel>::const_iterator j = d.begin(); j != d.end(); ++j) { + int const c = static_cast<int>(*j) + 1; + if (c < _grid->GetNumberCols ()) { + _grid->SetCellValue (i, c, wxT("1")); + } + } + } +} + +void +AudioMappingView::set_channels (int c) +{ + c++; + + if (c < _grid->GetNumberCols ()) { + _grid->DeleteCols (c, _grid->GetNumberCols() - c); + } else if (c > _grid->GetNumberCols ()) { + _grid->InsertCols (_grid->GetNumberCols(), c - _grid->GetNumberCols()); + set_column_labels (); + } + + set (_map); +} + +void +AudioMappingView::set_column_labels () +{ + int const c = _grid->GetNumberCols (); + + _grid->SetColLabelValue (0, _("Content channel")); + + if (c > 0) { + _grid->SetColLabelValue (1, _("L")); + } + + if (c > 1) { + _grid->SetColLabelValue (2, _("R")); + } + + if (c > 2) { + _grid->SetColLabelValue (3, _("C")); + } + + if (c > 3) { + _grid->SetColLabelValue (4, _("Lfe")); + } + + if (c > 4) { + _grid->SetColLabelValue (5, _("Ls")); + } + + if (c > 5) { + _grid->SetColLabelValue (6, _("Rs")); + } + + _grid->AutoSize (); +} diff --git a/src/lib/audio_source.h b/src/wx/audio_mapping_view.h index 5a1510d3c..80534a613 100644 --- a/src/lib/audio_source.h +++ b/src/wx/audio_mapping_view.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,26 +17,26 @@ */ -/** @file src/audio_source.h - * @brief Parent class for classes which emit audio data. - */ - -#ifndef DVDOMATIC_AUDIO_SOURCE_H -#define DVDOMATIC_AUDIO_SOURCE_H - #include <boost/signals2.hpp> +#include <wx/wx.h> +#include <wx/grid.h> +#include "lib/audio_mapping.h" -class AudioBuffers; -class AudioSink; - -/** A class that emits audio data */ -class AudioSource +class AudioMappingView : public wxPanel { public: - /** Emitted when some audio data is ready */ - boost::signals2::signal<void (boost::shared_ptr<AudioBuffers>)> Audio; + AudioMappingView (wxWindow *); - void connect_audio (boost::shared_ptr<AudioSink>); -}; + void set (AudioMapping); + void set_channels (int); -#endif + boost::signals2::signal<void (AudioMapping)> Changed; + +private: + void left_click (wxGridEvent &); + void set_column_labels (); + + wxGrid* _grid; + wxSizer* _sizer; + AudioMapping _map; +}; diff --git a/src/wx/audio_panel.cc b/src/wx/audio_panel.cc new file mode 100644 index 000000000..b4921904c --- /dev/null +++ b/src/wx/audio_panel.cc @@ -0,0 +1,288 @@ +/* + Copyright (C) 2012-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 <boost/lexical_cast.hpp> +#include <wx/spinctrl.h> +#include "lib/config.h" +#include "lib/sound_processor.h" +#include "lib/ffmpeg_content.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" + +using std::vector; +using std::cout; +using std::string; +using boost::dynamic_pointer_cast; +using boost::lexical_cast; +using boost::shared_ptr; + +AudioPanel::AudioPanel (FilmEditor* e) + : FilmEditorPanel (e, _("Audio")) + , _audio_dialog (0) +{ + wxFlexGridSizer* grid = new wxFlexGridSizer (3, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _sizer->Add (grid, 0, wxALL, 8); + + _show = new wxButton (this, wxID_ANY, _("Show Audio...")); + grid->Add (_show, 1); + grid->AddSpacer (0); + grid->AddSpacer (0); + + add_label_to_sizer (grid, this, _("Audio Gain"), true); + { + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _gain = new wxSpinCtrl (this); + s->Add (_gain, 1); + add_label_to_sizer (s, this, _("dB"), false); + grid->Add (s, 1); + } + + _gain_calculate_button = new wxButton (this, wxID_ANY, _("Calculate...")); + grid->Add (_gain_calculate_button); + + add_label_to_sizer (grid, this, _("Audio Delay"), false); + { + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _delay = new wxSpinCtrl (this); + s->Add (_delay, 1); + /// TRANSLATORS: this is an abbreviation for milliseconds, the unit of time + add_label_to_sizer (s, this, _("ms"), false); + grid->Add (s); + } + + grid->AddSpacer (0); + + add_label_to_sizer (grid, this, _("Audio Stream"), true); + _stream = new wxChoice (this, wxID_ANY); + grid->Add (_stream, 1, wxEXPAND); + _description = new wxStaticText (this, wxID_ANY, wxT ("")); + grid->AddSpacer (0); + + grid->Add (_description, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, 8); + grid->AddSpacer (0); + grid->AddSpacer (0); + + _mapping = new AudioMappingView (this); + _sizer->Add (_mapping, 1, wxEXPAND | wxALL, 6); + + _gain->SetRange (-60, 60); + _delay->SetRange (-1000, 1000); + + _delay->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AudioPanel::delay_changed, 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->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&AudioPanel::gain_changed, this)); + _gain_calculate_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&AudioPanel::gain_calculate_button_clicked, this)); + + _mapping->Changed.connect (boost::bind (&AudioPanel::mapping_changed, this, _1)); +} + + +void +AudioPanel::film_changed (Film::Property property) +{ + switch (property) { + case Film::AUDIO_CHANNELS: + _mapping->set_channels (_editor->film()->audio_channels ()); + _sizer->Layout (); + break; + default: + break; + } +} + +void +AudioPanel::film_content_changed (shared_ptr<Content> c, int property) +{ + shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (c); + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + + if (_audio_dialog && _editor->selected_audio_content()) { + _audio_dialog->set_content (_editor->selected_audio_content ()); + } + + if (property == AudioContentProperty::AUDIO_GAIN) { + checked_set (_gain, ac ? ac->audio_gain() : 0); + } else if (property == AudioContentProperty::AUDIO_DELAY) { + checked_set (_delay, ac ? ac->audio_delay() : 0); + } else if (property == AudioContentProperty::AUDIO_MAPPING) { + _mapping->set (ac ? ac->audio_mapping () : AudioMapping ()); + _sizer->Layout (); + } else if (property == FFmpegContentProperty::AUDIO_STREAM) { + setup_stream_description (); + } else if (property == FFmpegContentProperty::AUDIO_STREAMS) { + _stream->Clear (); + if (fc) { + vector<shared_ptr<FFmpegAudioStream> > a = fc->audio_streams (); + for (vector<shared_ptr<FFmpegAudioStream> >::iterator i = a.begin(); i != a.end(); ++i) { + _stream->Append (std_to_wx ((*i)->name), new wxStringClientData (std_to_wx (lexical_cast<string> ((*i)->id)))); + } + + if (fc->audio_stream()) { + checked_set (_stream, lexical_cast<string> (fc->audio_stream()->id)); + setup_stream_description (); + } + } + } +} + +void +AudioPanel::gain_changed () +{ + shared_ptr<AudioContent> ac = _editor->selected_audio_content (); + if (!ac) { + return; + } + + ac->set_audio_gain (_gain->GetValue ()); +} + +void +AudioPanel::delay_changed () +{ + shared_ptr<AudioContent> ac = _editor->selected_audio_content (); + if (!ac) { + return; + } + + ac->set_audio_delay (_delay->GetValue ()); +} + +void +AudioPanel::gain_calculate_button_clicked () +{ + GainCalculatorDialog* d = new GainCalculatorDialog (this); + d->ShowModal (); + + if (d->wanted_fader() == 0 || d->actual_fader() == 0) { + d->Destroy (); + return; + } + + _gain->SetValue ( + Config::instance()->sound_processor()->db_for_fader_change ( + d->wanted_fader (), + d->actual_fader () + ) + ); + + /* This appears to be necessary, as the change is not signalled, + I think. + */ + gain_changed (); + + d->Destroy (); +} + +void +AudioPanel::show_clicked () +{ + if (_audio_dialog) { + _audio_dialog->Destroy (); + _audio_dialog = 0; + } + + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<AudioContent> ac = dynamic_pointer_cast<AudioContent> (c); + if (!ac) { + return; + } + + _audio_dialog = new AudioDialog (this); + _audio_dialog->Show (); + _audio_dialog->set_content (ac); +} + +void +AudioPanel::stream_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + if (!fc) { + return; + } + + if (_stream->GetSelection() == -1) { + return; + } + + vector<shared_ptr<FFmpegAudioStream> > a = fc->audio_streams (); + vector<shared_ptr<FFmpegAudioStream> >::iterator i = a.begin (); + string const s = string_client_data (_stream->GetClientObject (_stream->GetSelection ())); + while (i != a.end() && lexical_cast<string> ((*i)->id) != s) { + ++i; + } + + if (i != a.end ()) { + fc->set_audio_stream (*i); + } + + setup_stream_description (); +} + +void +AudioPanel::setup_stream_description () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + if (!fc) { + return; + } + + if (!fc->audio_stream ()) { + _description->SetLabel (wxT ("")); + } else { + wxString s; + if (fc->audio_channels() == 1) { + s << _("1 channel"); + } else { + s << fc->audio_channels() << wxT (" ") << _("channels"); + } + s << wxT (", ") << fc->content_audio_frame_rate() << _("Hz"); + _description->SetLabel (s); + } +} + +void +AudioPanel::mapping_changed (AudioMapping m) +{ + shared_ptr<AudioContent> c = _editor->selected_audio_content (); + if (!c) { + return; + } + + c->set_audio_mapping (m); +} + diff --git a/src/wx/audio_panel.h b/src/wx/audio_panel.h new file mode 100644 index 000000000..e1dc283e2 --- /dev/null +++ b/src/wx/audio_panel.h @@ -0,0 +1,55 @@ +/* + Copyright (C) 2012-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 "lib/audio_mapping.h" +#include "film_editor_panel.h" + +class wxSpinCtrl; +class wxButton; +class wxChoice; +class wxStaticText; +class AudioMappingView; +class AudioDialog; + +class AudioPanel : public FilmEditorPanel +{ +public: + AudioPanel (FilmEditor *); + + void film_changed (Film::Property); + void film_content_changed (boost::shared_ptr<Content>, int); + +private: + void gain_changed (); + void gain_calculate_button_clicked (); + void show_clicked (); + void delay_changed (); + void stream_changed (); + void mapping_changed (AudioMapping); + void setup_stream_description (); + + wxSpinCtrl* _gain; + wxButton* _gain_calculate_button; + wxButton* _show; + wxSpinCtrl* _delay; + wxChoice* _stream; + wxStaticText* _description; + AudioMappingView* _mapping; + AudioDialog* _audio_dialog; +}; diff --git a/src/wx/audio_plot.cc b/src/wx/audio_plot.cc new file mode 100644 index 000000000..7ed792351 --- /dev/null +++ b/src/wx/audio_plot.cc @@ -0,0 +1,281 @@ +/* + 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 <iostream> +#include <boost/bind.hpp> +#include <wx/graphics.h> +#include "audio_plot.h" +#include "lib/audio_decoder.h" +#include "lib/audio_analysis.h" +#include "wx/wx_util.h" + +using std::cout; +using std::vector; +using std::list; +using std::max; +using std::min; +using boost::bind; +using boost::shared_ptr; + +int const AudioPlot::_minimum = -70; +int const AudioPlot::max_smoothing = 128; + +AudioPlot::AudioPlot (wxWindow* parent) + : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE) + , _gain (0) + , _smoothing (max_smoothing / 2) + , _message (_("Please wait; audio is being analysed...")) +{ +#ifndef __WXOSX__ + SetDoubleBuffered (true); +#endif + + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + _channel_visible[i] = false; + } + + for (int i = 0; i < AudioPoint::COUNT; ++i) { + _type_visible[i] = false; + } + + _colours.push_back (wxColour ( 0, 0, 0)); + _colours.push_back (wxColour (255, 0, 0)); + _colours.push_back (wxColour ( 0, 255, 0)); + _colours.push_back (wxColour (139, 0, 204)); + _colours.push_back (wxColour ( 0, 0, 255)); + _colours.push_back (wxColour (100, 100, 100)); + + Bind (wxEVT_PAINT, boost::bind (&AudioPlot::paint, this)); + + SetMinSize (wxSize (640, 512)); +} + +void +AudioPlot::set_analysis (shared_ptr<AudioAnalysis> a) +{ + _analysis = a; + + for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { + _channel_visible[i] = false; + } + + for (int i = 0; i < AudioPoint::COUNT; ++i) { + _type_visible[i] = false; + } + + Refresh (); +} + +void +AudioPlot::set_channel_visible (int c, bool v) +{ + _channel_visible[c] = v; + Refresh (); +} + +void +AudioPlot::set_type_visible (int t, bool v) +{ + _type_visible[t] = v; + Refresh (); +} + +void +AudioPlot::set_message (wxString s) +{ + _message = s; + Refresh (); +} + +void +AudioPlot::paint () +{ + wxPaintDC dc (this); + + wxGraphicsContext* gc = wxGraphicsContext::Create (dc); + if (!gc) { + return; + } + + if (!_analysis || _analysis->channels() == 0) { + gc->SetFont (gc->CreateFont (*wxNORMAL_FONT)); + gc->DrawText (_message, 32, 32); + return; + } + + wxGraphicsPath grid = gc->CreatePath (); + gc->SetFont (gc->CreateFont (*wxSMALL_FONT)); + wxDouble db_label_height; + wxDouble db_label_descent; + wxDouble db_label_leading; + gc->GetTextExtent (wxT ("-80dB"), &_db_label_width, &db_label_height, &db_label_descent, &db_label_leading); + + _db_label_width += 8; + + int const data_width = GetSize().GetWidth() - _db_label_width; + /* Assume all channels have the same number of points */ + _x_scale = data_width / float (_analysis->points (0)); + _height = GetSize().GetHeight (); + _y_origin = 32; + _y_scale = (_height - _y_origin) / -_minimum; + + for (int i = _minimum; i <= 0; i += 10) { + int const y = (_height - (i - _minimum) * _y_scale) - _y_origin; + grid.MoveToPoint (_db_label_width - 4, y); + grid.AddLineToPoint (_db_label_width + data_width, y); + gc->DrawText (std_to_wx (String::compose ("%1dB", i)), 0, y - (db_label_height / 2)); + } + + gc->SetPen (*wxLIGHT_GREY_PEN); + gc->StrokePath (grid); + + gc->DrawText (_("Time"), data_width, _height - _y_origin + db_label_height / 2); + + + if (_type_visible[AudioPoint::PEAK]) { + for (int c = 0; c < MAX_AUDIO_CHANNELS; ++c) { + wxGraphicsPath p = gc->CreatePath (); + if (_channel_visible[c] && c < _analysis->channels()) { + plot_peak (p, c); + } + wxColour const col = _colours[c]; + gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (col.Red(), col.Green(), col.Blue(), col.Alpha() / 2), 1, wxPENSTYLE_SOLID)); + gc->StrokePath (p); + } + } + + if (_type_visible[AudioPoint::RMS]) { + for (int c = 0; c < MAX_AUDIO_CHANNELS; ++c) { + wxGraphicsPath p = gc->CreatePath (); + if (_channel_visible[c] && c < _analysis->channels()) { + plot_rms (p, c); + } + wxColour const col = _colours[c]; + gc->SetPen (*wxThePenList->FindOrCreatePen (col, 1, wxPENSTYLE_SOLID)); + gc->StrokePath (p); + } + } + + wxGraphicsPath axes = gc->CreatePath (); + axes.MoveToPoint (_db_label_width, 0); + axes.AddLineToPoint (_db_label_width, _height - _y_origin); + axes.AddLineToPoint (_db_label_width + data_width, _height - _y_origin); + gc->SetPen (*wxBLACK_PEN); + gc->StrokePath (axes); + + delete gc; +} + +float +AudioPlot::y_for_linear (float p) const +{ + return _height - (20 * log10(p) - _minimum + _gain) * _y_scale - _y_origin; +} + +void +AudioPlot::plot_peak (wxGraphicsPath& path, int channel) const +{ + if (_analysis->points (channel) == 0) { + return; + } + + path.MoveToPoint (_db_label_width, y_for_linear (_analysis->get_point(channel, 0)[AudioPoint::PEAK])); + + float peak = 0; + int const N = _analysis->points(channel); + for (int i = 0; i < N; ++i) { + float const p = _analysis->get_point(channel, i)[AudioPoint::PEAK]; + peak -= 0.01f * (1 - log10 (_smoothing) / log10 (max_smoothing)); + if (p > peak) { + peak = p; + } else if (peak < 0) { + peak = 0; + } + + path.AddLineToPoint (_db_label_width + i * _x_scale, y_for_linear (peak)); + } +} + +void +AudioPlot::plot_rms (wxGraphicsPath& path, int channel) const +{ + if (_analysis->points (channel) == 0) { + return; + } + + path.MoveToPoint (_db_label_width, y_for_linear (_analysis->get_point(channel, 0)[AudioPoint::RMS])); + + list<float> smoothing; + + int const N = _analysis->points(channel); + + float const first = _analysis->get_point(channel, 0)[AudioPoint::RMS]; + float const last = _analysis->get_point(channel, N - 1)[AudioPoint::RMS]; + + int const before = _smoothing / 2; + int const after = _smoothing - before; + + /* Pre-load the smoothing list */ + for (int i = 0; i < before; ++i) { + smoothing.push_back (first); + } + for (int i = 0; i < after; ++i) { + if (i < N) { + smoothing.push_back (_analysis->get_point(channel, i)[AudioPoint::RMS]); + } else { + smoothing.push_back (last); + } + } + + for (int i = 0; i < N; ++i) { + + int const next_for_window = i + after; + + if (next_for_window < N) { + smoothing.push_back (_analysis->get_point(channel, i)[AudioPoint::RMS]); + } else { + smoothing.push_back (last); + } + + smoothing.pop_front (); + + float p = 0; + for (list<float>::const_iterator j = smoothing.begin(); j != smoothing.end(); ++j) { + p += pow (*j, 2); + } + + p = sqrt (p / smoothing.size ()); + + path.AddLineToPoint (_db_label_width + i * _x_scale, y_for_linear (p)); + } +} + +void +AudioPlot::set_gain (float g) +{ + _gain = g; + Refresh (); +} + +void +AudioPlot::set_smoothing (int s) +{ + _smoothing = s; + Refresh (); +} diff --git a/src/wx/audio_plot.h b/src/wx/audio_plot.h new file mode 100644 index 000000000..094f8e1b0 --- /dev/null +++ b/src/wx/audio_plot.h @@ -0,0 +1,64 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <vector> +#include <boost/shared_ptr.hpp> +#include <wx/wx.h> +#include "lib/util.h" +#include "lib/audio_analysis.h" + +class AudioPlot : public wxPanel +{ +public: + AudioPlot (wxWindow *); + + void set_analysis (boost::shared_ptr<AudioAnalysis>); + void set_channel_visible (int c, bool v); + void set_type_visible (int t, bool v); + void set_gain (float); + void set_smoothing (int); + void set_message (wxString); + + static const int max_smoothing; + +private: + void paint (); + + boost::shared_ptr<AudioAnalysis> _analysis; + bool _channel_visible[MAX_AUDIO_CHANNELS]; + bool _type_visible[AudioPoint::COUNT]; + /** gain to apply in dB */ + float _gain; + int _smoothing; + std::vector<wxColour> _colours; + + void plot_peak (wxGraphicsPath &, int) const; + void plot_rms (wxGraphicsPath &, int) const; + float y_for_linear (float) const; + + double _db_label_width; + int _height; + int _y_origin; + float _x_scale; + float _y_scale; + + wxString _message; + + static const int _minimum; +}; diff --git a/src/wx/cinema_dialog.cc b/src/wx/cinema_dialog.cc index 9e3b1507d..2c0b0b4a4 100644 --- a/src/wx/cinema_dialog.cc +++ b/src/wx/cinema_dialog.cc @@ -28,11 +28,11 @@ CinemaDialog::CinemaDialog (wxWindow* parent, string title, string name, string wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6); table->AddGrowableCol (1, 1); - add_label_to_sizer (table, this, "Name"); + add_label_to_sizer (table, this, "Name", true); _name = new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (256, -1)); table->Add (_name, 1, wxEXPAND); - add_label_to_sizer (table, this, "Email address for KDM delivery"); + add_label_to_sizer (table, this, "Email address for KDM delivery", true); _email = new wxTextCtrl (this, wxID_ANY, std_to_wx (email), wxDefaultPosition, wxSize (256, -1)); table->Add (_email, 1, wxEXPAND); diff --git a/src/wx/colour_conversion_editor.cc b/src/wx/colour_conversion_editor.cc new file mode 100644 index 000000000..823f5ad38 --- /dev/null +++ b/src/wx/colour_conversion_editor.cc @@ -0,0 +1,152 @@ +/* + 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 <boost/lexical_cast.hpp> +#include <wx/spinctrl.h> +#include <wx/gbsizer.h> +#include "lib/colour_conversion.h" +#include "wx_util.h" +#include "colour_conversion_editor.h" + +using std::string; +using std::cout; +using std::stringstream; +using boost::shared_ptr; +using boost::lexical_cast; + +ColourConversionEditor::ColourConversionEditor (wxWindow* parent) + : wxPanel (parent, wxID_ANY) +{ + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + SetSizer (overall_sizer); + + wxGridBagSizer* table = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + + int r = 0; + + add_label_to_grid_bag_sizer (table, this, _("Input gamma"), true, wxGBPosition (r, 0)); + _input_gamma = new wxSpinCtrlDouble (this); + table->Add (_input_gamma, wxGBPosition (r, 1)); + ++r; + + _input_gamma_linearised = new wxCheckBox (this, wxID_ANY, _("Linearise input gamma curve for low values")); + table->Add (_input_gamma_linearised, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; + + wxClientDC dc (parent); + wxSize size = dc.GetTextExtent (wxT ("-0.12345678901")); + size.SetHeight (-1); + + wxTextValidator validator (wxFILTER_INCLUDE_CHAR_LIST); + wxArrayString list; + + wxString n (wxT ("0123456789.-")); + for (size_t i = 0; i < n.Length(); ++i) { + list.Add (n[i]); + } + + validator.SetIncludes (list); + + add_label_to_grid_bag_sizer (table, this, _("Matrix"), true, wxGBPosition (r, 0)); + wxFlexGridSizer* matrix_sizer = new wxFlexGridSizer (3, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + _matrix[i][j] = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, size, 0, validator); + matrix_sizer->Add (_matrix[i][j]); + } + } + table->Add (matrix_sizer, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (table, this, _("Output gamma"), true, wxGBPosition (r, 0)); + wxBoxSizer* output_sizer = new wxBoxSizer (wxHORIZONTAL); + /* TRANSLATORS: this means the mathematical reciprocal operation, i.e. we are dividing 1 by the control that + comes after it. + */ + add_label_to_sizer (output_sizer, this, _("1 / "), false); + _output_gamma = new wxSpinCtrlDouble (this); + output_sizer->Add (_output_gamma); + table->Add (output_sizer, wxGBPosition (r, 1)); + ++r; + + _input_gamma->SetRange(0.1, 4.0); + _input_gamma->SetDigits (1); + _input_gamma->SetIncrement (0.1); + _output_gamma->SetRange(0.1, 4.0); + _output_gamma->SetDigits (1); + _output_gamma->SetIncrement (0.1); + + _input_gamma->Bind (wxEVT_COMMAND_SPINCTRLDOUBLE_UPDATED, boost::bind (&ColourConversionEditor::changed, this)); + _input_gamma_linearised->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ColourConversionEditor::changed, this)); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + _matrix[i][j]->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ColourConversionEditor::changed, this)); + } + } + _output_gamma->Bind (wxEVT_COMMAND_SPINCTRLDOUBLE_UPDATED, boost::bind (&ColourConversionEditor::changed, this)); +} + +void +ColourConversionEditor::set (ColourConversion conversion) +{ + _input_gamma->SetValue (conversion.input_gamma); + _input_gamma_linearised->SetValue (conversion.input_gamma_linearised); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + stringstream s; + s.setf (std::ios::fixed, std::ios::floatfield); + s.precision (7); + s << conversion.matrix (i, j); + _matrix[i][j]->SetValue (std_to_wx (s.str ())); + } + } + _output_gamma->SetValue (conversion.output_gamma); +} + +ColourConversion +ColourConversionEditor::get () const +{ + ColourConversion conversion; + + conversion.input_gamma = _input_gamma->GetValue (); + conversion.input_gamma_linearised = _input_gamma_linearised->GetValue (); + + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + string const v = wx_to_std (_matrix[i][j]->GetValue ()); + if (v.empty ()) { + conversion.matrix (i, j) = 0; + } else { + conversion.matrix (i, j) = lexical_cast<double> (v); + } + } + } + + conversion.output_gamma = _output_gamma->GetValue (); + + return conversion; +} + +void +ColourConversionEditor::changed () +{ + Changed (); +} + diff --git a/src/wx/colour_conversion_editor.h b/src/wx/colour_conversion_editor.h new file mode 100644 index 000000000..8567cac22 --- /dev/null +++ b/src/wx/colour_conversion_editor.h @@ -0,0 +1,48 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#ifndef DCPOMATIC_COLOUR_CONVERSION_EDITOR_H +#define DCPOMATIC_COLOUR_CONVERSION_EDITOR_H + +#include <boost/signals2.hpp> +#include <wx/wx.h> + +class wxSpinCtrlDouble; +class ColourConversion; + +class ColourConversionEditor : public wxPanel +{ +public: + ColourConversionEditor (wxWindow *); + + void set (ColourConversion); + ColourConversion get () const; + + boost::signals2::signal<void ()> Changed; + +private: + void changed (); + + wxSpinCtrlDouble* _input_gamma; + wxCheckBox* _input_gamma_linearised; + wxTextCtrl* _matrix[3][3]; + wxSpinCtrlDouble* _output_gamma; +}; + +#endif diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc index 9de8e7001..7f1efa52f 100644 --- a/src/wx/config_dialog.cc +++ b/src/wx/config_dialog.cc @@ -18,292 +18,437 @@ */ /** @file src/config_dialog.cc - * @brief A dialogue to edit DVD-o-matic configuration. + * @brief A dialogue to edit DCP-o-matic configuration. */ #include <iostream> #include <boost/lexical_cast.hpp> #include <boost/filesystem.hpp> #include <wx/stdpaths.h> +#include <wx/notebook.h> +#include <libdcp/colour_matrix.h> #include "lib/config.h" #include "lib/server.h" -#include "lib/format.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 "config_dialog.h" #include "wx_util.h" #include "filter_dialog.h" #include "server_dialog.h" #include "dir_picker_ctrl.h" +#include "dci_metadata_dialog.h" +#include "preset_colour_conversion_dialog.h" -using namespace std; +using std::vector; +using std::string; +using std::list; using boost::bind; +using boost::shared_ptr; +using boost::lexical_cast; ConfigDialog::ConfigDialog (wxWindow* parent) - : wxDialog (parent, wxID_ANY, _("DVD-o-matic Preferences"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) + : wxDialog (parent, wxID_ANY, _("DCP-o-matic Preferences"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) { - wxFlexGridSizer* table = new wxFlexGridSizer (3, 6, 6); - table->AddGrowableCol (1, 1); + wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); + _notebook = new wxNotebook (this, wxID_ANY); + s->Add (_notebook, 1); + + make_misc_panel (); + _notebook->AddPage (_misc_panel, _("Miscellaneous"), true); + make_servers_panel (); + _notebook->AddPage (_servers_panel, _("Encoding servers"), false); + make_colour_conversions_panel (); + _notebook->AddPage (_colour_conversions_panel, _("Colour conversions"), false); + make_metadata_panel (); + _notebook->AddPage (_metadata_panel, _("Metadata"), false); + make_tms_panel (); + _notebook->AddPage (_tms_panel, _("TMS"), false); - add_label_to_sizer (table, this, "TMS IP address"); - _tms_ip = new wxTextCtrl (this, wxID_ANY); - table->Add (_tms_ip, 1, wxEXPAND); - table->AddSpacer (0); + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + overall_sizer->Add (s, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); - add_label_to_sizer (table, this, "TMS target path"); - _tms_path = new wxTextCtrl (this, wxID_ANY); - table->Add (_tms_path, 1, wxEXPAND); - table->AddSpacer (0); + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + if (buttons) { + overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } - add_label_to_sizer (table, this, "TMS user name"); - _tms_user = new wxTextCtrl (this, wxID_ANY); - table->Add (_tms_user, 1, wxEXPAND); - table->AddSpacer (0); + SetSizer (overall_sizer); + overall_sizer->Layout (); + overall_sizer->SetSizeHints (this); +} - add_label_to_sizer (table, this, "TMS password"); - _tms_password = new wxTextCtrl (this, wxID_ANY); - table->Add (_tms_password, 1, wxEXPAND); - table->AddSpacer (0); +void +ConfigDialog::make_misc_panel () +{ + _misc_panel = new wxPanel (_notebook); + wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); + _misc_panel->SetSizer (s); - add_label_to_sizer (table, this, "Threads to use for encoding on this host"); - _num_local_encoding_threads = new wxSpinCtrl (this); - table->Add (_num_local_encoding_threads, 1, wxEXPAND); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + s->Add (table, 1, wxALL | wxEXPAND, 8); + + _set_language = new wxCheckBox (_misc_panel, wxID_ANY, _("Set language")); + table->Add (_set_language, 1); + _language = new wxChoice (_misc_panel, wxID_ANY); + _language->Append (wxT ("English")); + _language->Append (wxT ("Français")); + _language->Append (wxT ("Italiano")); + _language->Append (wxT ("Español")); + _language->Append (wxT ("Svenska")); + table->Add (_language); + + wxStaticText* restart = add_label_to_sizer (table, _misc_panel, _("(restart DCP-o-matic to see language changes)"), false); + wxFont font = restart->GetFont(); + font.SetStyle (wxFONTSTYLE_ITALIC); + font.SetPointSize (font.GetPointSize() - 1); + restart->SetFont (font); table->AddSpacer (0); - add_label_to_sizer (table, this, "Default directory for new films"); -#ifdef __WXMSW__ - _default_directory = new DirPickerCtrl (this); + add_label_to_sizer (table, _misc_panel, _("Threads to use for encoding on this host"), true); + _num_local_encoding_threads = new wxSpinCtrl (_misc_panel); + table->Add (_num_local_encoding_threads, 1); + + { + add_label_to_sizer (table, _misc_panel, _("Default duration of still images"), true); + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _default_still_length = new wxSpinCtrl (_misc_panel); + s->Add (_default_still_length); + add_label_to_sizer (s, _misc_panel, _("s"), false); + table->Add (s, 1); + } + + add_label_to_sizer (table, _misc_panel, _("Default directory for new films"), true); +#ifdef DCPOMATIC_USE_OWN_DIR_PICKER + _default_directory = new DirPickerCtrl (_misc_panel); #else - _default_directory = new wxDirPickerCtrl (this, wxDD_DIR_MUST_EXIST); + _default_directory = new wxDirPickerCtrl (_misc_panel, wxDD_DIR_MUST_EXIST); #endif table->Add (_default_directory, 1, wxEXPAND); - table->AddSpacer (0); - add_label_to_sizer (table, this, "Reference scaler for A/B"); - _reference_scaler = new wxComboBox (this, wxID_ANY); - vector<Scaler const *> const sc = Scaler::all (); - for (vector<Scaler const *>::const_iterator i = sc.begin(); i != sc.end(); ++i) { - _reference_scaler->Append (std_to_wx ((*i)->name ())); - } + add_label_to_sizer (table, _misc_panel, _("Default DCI name details"), true); + _default_dci_metadata_button = new wxButton (_misc_panel, wxID_ANY, _("Edit...")); + table->Add (_default_dci_metadata_button); - table->Add (_reference_scaler, 1, wxEXPAND); - table->AddSpacer (0); + add_label_to_sizer (table, _misc_panel, _("Default container"), true); + _default_container = new wxChoice (_misc_panel, wxID_ANY); + table->Add (_default_container); + + add_label_to_sizer (table, _misc_panel, _("Default content type"), true); + _default_dcp_content_type = new wxChoice (_misc_panel, wxID_ANY); + table->Add (_default_dcp_content_type); { - add_label_to_sizer (table, this, "Reference filters for A/B"); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _reference_filters = new wxStaticText (this, wxID_ANY, wxT ("")); - s->Add (_reference_filters, 1, wxEXPAND); - _reference_filters_button = new wxButton (this, wxID_ANY, _("Edit...")); - s->Add (_reference_filters_button, 0); - table->Add (s, 1, wxEXPAND); - table->AddSpacer (0); + add_label_to_sizer (table, _misc_panel, _("Default JPEG2000 bandwidth"), true); + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _default_j2k_bandwidth = new wxSpinCtrl (_misc_panel); + s->Add (_default_j2k_bandwidth); + add_label_to_sizer (s, _misc_panel, _("MBps"), false); + table->Add (s, 1); } + + Config* config = Config::instance (); - add_label_to_sizer (table, this, "Encoding Servers"); - _servers = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (220, 100), wxLC_REPORT | wxLC_SINGLE_SEL); - wxListItem ip; - ip.SetId (0); - ip.SetText (_("IP address")); - ip.SetWidth (120); - _servers->InsertColumn (0, ip); - ip.SetId (1); - ip.SetText (_("Threads")); - ip.SetWidth (80); - _servers->InsertColumn (1, ip); - table->Add (_servers, 1, wxEXPAND | wxALL); + _set_language->SetValue (config->language ()); + + if (config->language().get_value_or ("") == "fr") { + _language->SetSelection (1); + } else if (config->language().get_value_or ("") == "it") { + _language->SetSelection (2); + } else if (config->language().get_value_or ("") == "es") { + _language->SetSelection (3); + } else if (config->language().get_value_or ("") == "sv") { + _language->SetSelection (4); + } else { + _language->SetSelection (0); + } - { - wxSizer* s = new wxBoxSizer (wxVERTICAL); - _add_server = new wxButton (this, wxID_ANY, _("Add")); - s->Add (_add_server); - _edit_server = new wxButton (this, wxID_ANY, _("Edit")); - s->Add (_edit_server); - _remove_server = new wxButton (this, wxID_ANY, _("Remove")); - s->Add (_remove_server); - table->Add (s, 0); + setup_language_sensitivity (); + + _set_language->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ConfigDialog::set_language_changed, this)); + _language->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ConfigDialog::language_changed, this)); + + _num_local_encoding_threads->SetRange (1, 128); + _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ()); + _num_local_encoding_threads->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::num_local_encoding_threads_changed, this)); + + _default_still_length->SetRange (1, 3600); + _default_still_length->SetValue (config->default_still_length ()); + _default_still_length->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::default_still_length_changed, this)); + + _default_directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())))); + _default_directory->Bind (wxEVT_COMMAND_DIRPICKER_CHANGED, boost::bind (&ConfigDialog::default_directory_changed, this)); + + _default_dci_metadata_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&ConfigDialog::edit_default_dci_metadata_clicked, this)); + + vector<Ratio const *> ratio = Ratio::all (); + int n = 0; + for (vector<Ratio const *>::iterator i = ratio.begin(); i != ratio.end(); ++i) { + _default_container->Append (std_to_wx ((*i)->nickname ())); + if (*i == config->default_container ()) { + _default_container->SetSelection (n); + } + ++n; } - - Config* config = Config::instance (); + _default_container->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ConfigDialog::default_container_changed, this)); + + vector<DCPContentType const *> const ct = DCPContentType::all (); + n = 0; + for (vector<DCPContentType const *>::const_iterator i = ct.begin(); i != ct.end(); ++i) { + _default_dcp_content_type->Append (std_to_wx ((*i)->pretty_name ())); + if (*i == config->default_dcp_content_type ()) { + _default_dcp_content_type->SetSelection (n); + } + ++n; + } + + _default_dcp_content_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ConfigDialog::default_dcp_content_type_changed, this)); + + _default_j2k_bandwidth->SetRange (50, 250); + _default_j2k_bandwidth->SetValue (config->default_j2k_bandwidth() / 1e6); + _default_j2k_bandwidth->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&ConfigDialog::default_j2k_bandwidth_changed, this)); +} + +void +ConfigDialog::make_tms_panel () +{ + _tms_panel = new wxPanel (_notebook); + wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); + _tms_panel->SetSizer (s); + + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + s->Add (table, 1, wxALL | wxEXPAND, 8); + + add_label_to_sizer (table, _tms_panel, _("IP address"), true); + _tms_ip = new wxTextCtrl (_tms_panel, wxID_ANY); + table->Add (_tms_ip, 1, wxEXPAND); + + add_label_to_sizer (table, _tms_panel, _("Target path"), true); + _tms_path = new wxTextCtrl (_tms_panel, wxID_ANY); + table->Add (_tms_path, 1, wxEXPAND); + + add_label_to_sizer (table, _tms_panel, _("User name"), true); + _tms_user = new wxTextCtrl (_tms_panel, wxID_ANY); + table->Add (_tms_user, 1, wxEXPAND); + + add_label_to_sizer (table, _tms_panel, _("Password"), true); + _tms_password = new wxTextCtrl (_tms_panel, wxID_ANY); + table->Add (_tms_password, 1, wxEXPAND); + + Config* config = Config::instance (); + _tms_ip->SetValue (std_to_wx (config->tms_ip ())); - _tms_ip->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ConfigDialog::tms_ip_changed), 0, this); + _tms_ip->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_ip_changed, this)); _tms_path->SetValue (std_to_wx (config->tms_path ())); - _tms_path->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ConfigDialog::tms_path_changed), 0, this); + _tms_path->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_path_changed, this)); _tms_user->SetValue (std_to_wx (config->tms_user ())); - _tms_user->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ConfigDialog::tms_user_changed), 0, this); + _tms_user->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_user_changed, this)); _tms_password->SetValue (std_to_wx (config->tms_password ())); - _tms_password->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ConfigDialog::tms_password_changed), 0, this); + _tms_password->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::tms_password_changed, this)); +} - _num_local_encoding_threads->SetRange (1, 128); - _num_local_encoding_threads->SetValue (config->num_local_encoding_threads ()); - _num_local_encoding_threads->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (ConfigDialog::num_local_encoding_threads_changed), 0, this); +void +ConfigDialog::make_metadata_panel () +{ + _metadata_panel = new wxPanel (_notebook); + wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); + _metadata_panel->SetSizer (s); - _default_directory->SetPath (std_to_wx (config->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())))); - _default_directory->Connect (wxID_ANY, wxEVT_COMMAND_DIRPICKER_CHANGED, wxCommandEventHandler (ConfigDialog::default_directory_changed), 0, this); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + s->Add (table, 1, wxALL | wxEXPAND, 8); - _reference_scaler->SetSelection (Scaler::as_index (config->reference_scaler ())); - _reference_scaler->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (ConfigDialog::reference_scaler_changed), 0, this); + add_label_to_sizer (table, _metadata_panel, _("Issuer"), true); + _issuer = new wxTextCtrl (_metadata_panel, wxID_ANY); + table->Add (_issuer, 1, wxEXPAND); - pair<string, string> p = Filter::ffmpeg_strings (config->reference_filters ()); - _reference_filters->SetLabel (std_to_wx (p.first + " " + p.second)); - _reference_filters_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ConfigDialog::edit_reference_filters_clicked), 0, this); + add_label_to_sizer (table, _metadata_panel, _("Creator"), true); + _creator = new wxTextCtrl (_metadata_panel, wxID_ANY); + table->Add (_creator, 1, wxEXPAND); - vector<ServerDescription*> servers = config->servers (); - for (vector<ServerDescription*>::iterator i = servers.begin(); i != servers.end(); ++i) { - add_server_to_control (*i); + Config* config = Config::instance (); + + _issuer->SetValue (std_to_wx (config->dcp_metadata().issuer)); + _issuer->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::issuer_changed, this)); + _creator->SetValue (std_to_wx (config->dcp_metadata().creator)); + _creator->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ConfigDialog::creator_changed, this)); +} + +static std::string +server_column (ServerDescription s, int c) +{ + switch (c) { + case 0: + return s.host_name (); + case 1: + return lexical_cast<string> (s.threads ()); } - - _add_server->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ConfigDialog::add_server_clicked), 0, this); - _edit_server->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ConfigDialog::edit_server_clicked), 0, this); - _remove_server->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (ConfigDialog::remove_server_clicked), 0, this); - _servers->Connect (wxID_ANY, wxEVT_COMMAND_LIST_ITEM_SELECTED, wxListEventHandler (ConfigDialog::server_selection_changed), 0, this); - _servers->Connect (wxID_ANY, wxEVT_COMMAND_LIST_ITEM_DESELECTED, wxListEventHandler (ConfigDialog::server_selection_changed), 0, this); - wxListEvent ev; - server_selection_changed (ev); + return ""; +} - wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); - overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6); +void +ConfigDialog::make_servers_panel () +{ + vector<string> columns; + columns.push_back (wx_to_std (_("IP address"))); + columns.push_back (wx_to_std (_("Threads"))); + _servers_panel = new EditableList<ServerDescription, ServerDialog> ( + _notebook, + columns, + boost::bind (&Config::servers, Config::instance()), + boost::bind (&Config::set_servers, Config::instance(), _1), + boost::bind (&server_column, _1, _2) + ); +} - wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); - if (buttons) { - overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); +void +ConfigDialog::language_changed () +{ + switch (_language->GetSelection ()) { + case 0: + Config::instance()->set_language ("en"); + break; + case 1: + Config::instance()->set_language ("fr"); + break; + case 2: + Config::instance()->set_language ("it"); + break; + case 3: + Config::instance()->set_language ("es"); + break; + case 4: + Config::instance()->set_language ("sv"); + break; } - - SetSizer (overall_sizer); - overall_sizer->Layout (); - overall_sizer->SetSizeHints (this); } void -ConfigDialog::tms_ip_changed (wxCommandEvent &) +ConfigDialog::tms_ip_changed () { Config::instance()->set_tms_ip (wx_to_std (_tms_ip->GetValue ())); } void -ConfigDialog::tms_path_changed (wxCommandEvent &) +ConfigDialog::tms_path_changed () { Config::instance()->set_tms_path (wx_to_std (_tms_path->GetValue ())); } void -ConfigDialog::tms_user_changed (wxCommandEvent &) +ConfigDialog::tms_user_changed () { Config::instance()->set_tms_user (wx_to_std (_tms_user->GetValue ())); } void -ConfigDialog::tms_password_changed (wxCommandEvent &) +ConfigDialog::tms_password_changed () { Config::instance()->set_tms_password (wx_to_std (_tms_password->GetValue ())); } void -ConfigDialog::num_local_encoding_threads_changed (wxCommandEvent &) +ConfigDialog::num_local_encoding_threads_changed () { Config::instance()->set_num_local_encoding_threads (_num_local_encoding_threads->GetValue ()); } void -ConfigDialog::default_directory_changed (wxCommandEvent &) +ConfigDialog::default_directory_changed () { Config::instance()->set_default_directory (wx_to_std (_default_directory->GetPath ())); } void -ConfigDialog::add_server_to_control (ServerDescription* s) -{ - wxListItem item; - int const n = _servers->GetItemCount (); - item.SetId (n); - _servers->InsertItem (item); - _servers->SetItem (n, 0, std_to_wx (s->host_name ())); - _servers->SetItem (n, 1, std_to_wx (boost::lexical_cast<string> (s->threads ()))); -} - -void -ConfigDialog::add_server_clicked (wxCommandEvent &) +ConfigDialog::edit_default_dci_metadata_clicked () { - ServerDialog* d = new ServerDialog (this, 0); + DCIMetadataDialog* d = new DCIMetadataDialog (this, Config::instance()->default_dci_metadata ()); d->ShowModal (); - ServerDescription* s = d->server (); + Config::instance()->set_default_dci_metadata (d->dci_metadata ()); d->Destroy (); - - add_server_to_control (s); - vector<ServerDescription*> o = Config::instance()->servers (); - o.push_back (s); - Config::instance()->set_servers (o); } void -ConfigDialog::edit_server_clicked (wxCommandEvent &) +ConfigDialog::set_language_changed () { - int i = _servers->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - if (i == -1) { - return; + setup_language_sensitivity (); + if (_set_language->GetValue ()) { + language_changed (); + } else { + Config::instance()->unset_language (); } +} - wxListItem item; - item.SetId (i); - item.SetColumn (0); - _servers->GetItem (item); - - vector<ServerDescription*> servers = Config::instance()->servers (); - assert (i >= 0 && i < int (servers.size ())); - - ServerDialog* d = new ServerDialog (this, servers[i]); - d->ShowModal (); - d->Destroy (); +void +ConfigDialog::setup_language_sensitivity () +{ + _language->Enable (_set_language->GetValue ()); +} - _servers->SetItem (i, 0, std_to_wx (servers[i]->host_name ())); - _servers->SetItem (i, 1, std_to_wx (boost::lexical_cast<string> (servers[i]->threads ()))); +void +ConfigDialog::default_still_length_changed () +{ + Config::instance()->set_default_still_length (_default_still_length->GetValue ()); } void -ConfigDialog::remove_server_clicked (wxCommandEvent &) +ConfigDialog::default_container_changed () { - int i = _servers->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - if (i >= 0) { - _servers->DeleteItem (i); - } + vector<Ratio const *> ratio = Ratio::all (); + Config::instance()->set_default_container (ratio[_default_container->GetSelection()]); +} - vector<ServerDescription*> o = Config::instance()->servers (); - o.erase (o.begin() + i); - Config::instance()->set_servers (o); +void +ConfigDialog::default_dcp_content_type_changed () +{ + vector<DCPContentType const *> ct = DCPContentType::all (); + Config::instance()->set_default_dcp_content_type (ct[_default_dcp_content_type->GetSelection()]); } void -ConfigDialog::server_selection_changed (wxListEvent &) +ConfigDialog::issuer_changed () { - int const i = _servers->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); - _edit_server->Enable (i >= 0); - _remove_server->Enable (i >= 0); + libdcp::XMLMetadata m = Config::instance()->dcp_metadata (); + m.issuer = wx_to_std (_issuer->GetValue ()); + Config::instance()->set_dcp_metadata (m); } void -ConfigDialog::reference_scaler_changed (wxCommandEvent &) +ConfigDialog::creator_changed () { - int const n = _reference_scaler->GetSelection (); - if (n >= 0) { - Config::instance()->set_reference_scaler (Scaler::from_index (n)); - } + libdcp::XMLMetadata m = Config::instance()->dcp_metadata (); + m.creator = wx_to_std (_creator->GetValue ()); + Config::instance()->set_dcp_metadata (m); } void -ConfigDialog::edit_reference_filters_clicked (wxCommandEvent &) +ConfigDialog::default_j2k_bandwidth_changed () { - FilterDialog* d = new FilterDialog (this, Config::instance()->reference_filters ()); - d->ActiveChanged.connect (boost::bind (&ConfigDialog::reference_filters_changed, this, _1)); - d->ShowModal (); - d->Destroy (); + Config::instance()->set_default_j2k_bandwidth (_default_j2k_bandwidth->GetValue() * 1e6); +} + +static std::string +colour_conversion_column (PresetColourConversion c) +{ + return c.name; } void -ConfigDialog::reference_filters_changed (vector<Filter const *> f) +ConfigDialog::make_colour_conversions_panel () { - Config::instance()->set_reference_filters (f); - pair<string, string> p = Filter::ffmpeg_strings (Config::instance()->reference_filters ()); - _reference_filters->SetLabel (std_to_wx (p.first + " " + p.second)); + vector<string> columns; + columns.push_back (wx_to_std (_("Name"))); + _colour_conversions_panel = new EditableList<PresetColourConversion, PresetColourConversionDialog> ( + _notebook, + columns, + boost::bind (&Config::colour_conversions, Config::instance()), + boost::bind (&Config::set_colour_conversions, Config::instance(), _1), + boost::bind (&colour_conversion_column, _1) + ); } diff --git a/src/wx/config_dialog.h b/src/wx/config_dialog.h index 32123a0d7..82c4ee2a0 100644 --- a/src/wx/config_dialog.h +++ b/src/wx/config_dialog.h @@ -18,20 +18,25 @@ */ /** @file src/config_dialog.h - * @brief A dialogue to edit DVD-o-matic configuration. + * @brief A dialogue to edit DCP-o-matic configuration. */ #include <wx/wx.h> #include <wx/spinctrl.h> #include <wx/listctrl.h> #include <wx/filepicker.h> +#include "wx_util.h" +#include "editable_list.h" class DirPickerCtrl; - +class wxNotebook; class ServerDescription; +class PresetColourConversion; +class PresetColourConversionDialog; +class ServerDialog; /** @class ConfigDialog - * @brief A dialogue to edit DVD-o-matic configuration. + * @brief A dialogue to edit DCP-o-matic configuration. */ class ConfigDialog : public wxDialog { @@ -39,38 +44,54 @@ public: ConfigDialog (wxWindow *); private: - void tms_ip_changed (wxCommandEvent &); - void tms_path_changed (wxCommandEvent &); - void tms_user_changed (wxCommandEvent &); - void tms_password_changed (wxCommandEvent &); - void num_local_encoding_threads_changed (wxCommandEvent &); - void default_directory_changed (wxCommandEvent &); - void reference_scaler_changed (wxCommandEvent &); - void edit_reference_filters_clicked (wxCommandEvent &); - void reference_filters_changed (std::vector<Filter const *>); - void add_server_clicked (wxCommandEvent &); - void edit_server_clicked (wxCommandEvent &); - void remove_server_clicked (wxCommandEvent &); - void server_selection_changed (wxListEvent &); + void set_language_changed (); + void language_changed (); + void tms_ip_changed (); + void tms_path_changed (); + void tms_user_changed (); + void tms_password_changed (); + void num_local_encoding_threads_changed (); + void default_still_length_changed (); + void default_directory_changed (); + void edit_default_dci_metadata_clicked (); + void default_container_changed (); + void default_dcp_content_type_changed (); + void issuer_changed (); + void creator_changed (); + void default_j2k_bandwidth_changed (); + + void setup_language_sensitivity (); + + void make_misc_panel (); + void make_tms_panel (); + void make_metadata_panel (); + void make_servers_panel (); + void make_colour_conversions_panel (); - void add_server_to_control (ServerDescription *); - + wxNotebook* _notebook; + wxPanel* _misc_panel; + wxPanel* _tms_panel; + EditableList<PresetColourConversion, PresetColourConversionDialog>* _colour_conversions_panel; + EditableList<ServerDescription, ServerDialog>* _servers_panel; + wxPanel* _metadata_panel; + wxCheckBox* _set_language; + wxChoice* _language; + wxChoice* _default_container; + wxChoice* _default_dcp_content_type; wxTextCtrl* _tms_ip; wxTextCtrl* _tms_path; wxTextCtrl* _tms_user; wxTextCtrl* _tms_password; wxSpinCtrl* _num_local_encoding_threads; -#ifdef __WXMSW__ + wxSpinCtrl* _default_still_length; +#ifdef DCPOMATIC_USE_OWN_DIR_PICKER DirPickerCtrl* _default_directory; #else wxDirPickerCtrl* _default_directory; -#endif - wxComboBox* _reference_scaler; - wxStaticText* _reference_filters; - wxButton* _reference_filters_button; - wxListCtrl* _servers; - wxButton* _add_server; - wxButton* _edit_server; - wxButton* _remove_server; +#endif + wxButton* _default_dci_metadata_button; + wxTextCtrl* _issuer; + wxTextCtrl* _creator; + wxSpinCtrl* _default_j2k_bandwidth; }; diff --git a/src/wx/content_colour_conversion_dialog.cc b/src/wx/content_colour_conversion_dialog.cc new file mode 100644 index 000000000..d8e768bcd --- /dev/null +++ b/src/wx/content_colour_conversion_dialog.cc @@ -0,0 +1,121 @@ +/* + 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 <wx/statline.h> +#include "lib/colour_conversion.h" +#include "lib/config.h" +#include "wx_util.h" +#include "content_colour_conversion_dialog.h" +#include "colour_conversion_editor.h" + +using std::string; +using std::vector; +using std::cout; +using boost::optional; + +ContentColourConversionDialog::ContentColourConversionDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("Colour conversion")) + , _editor (new ColourConversionEditor (this)) + , _setting (false) +{ + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + SetSizer (overall_sizer); + + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _preset_check = new wxCheckBox (this, wxID_ANY, _("Use preset")); + table->Add (_preset_check, 0, wxALIGN_CENTER_VERTICAL); + _preset_choice = new wxChoice (this, wxID_ANY); + table->Add (_preset_choice); + + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + overall_sizer->Add (new wxStaticLine (this, wxID_ANY), 0, wxEXPAND); + overall_sizer->Add (_editor); + + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + if (buttons) { + overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + overall_sizer->Layout (); + overall_sizer->SetSizeHints (this); + + _preset_check->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&ContentColourConversionDialog::preset_check_clicked, this)); + _preset_choice->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&ContentColourConversionDialog::preset_choice_changed, this)); + + _editor->Changed.connect (boost::bind (&ContentColourConversionDialog::check_for_preset, this)); + + vector<PresetColourConversion> presets = Config::instance()->colour_conversions (); + for (vector<PresetColourConversion>::const_iterator i = presets.begin(); i != presets.end(); ++i) { + _preset_choice->Append (std_to_wx (i->name)); + } +} + +ColourConversion +ContentColourConversionDialog::get () const +{ + return _editor->get (); +} + +void +ContentColourConversionDialog::set (ColourConversion c) +{ + _setting = true; + _editor->set (c); + _setting = false; + + check_for_preset (); +} + +void +ContentColourConversionDialog::check_for_preset () +{ + if (_setting) { + return; + } + + optional<size_t> preset = _editor->get().preset (); + + _preset_check->SetValue (preset); + _preset_choice->Enable (preset); + _preset_choice->SetSelection (preset.get_value_or (-1)); +} + +void +ContentColourConversionDialog::preset_check_clicked () +{ + if (_preset_check->GetValue ()) { + _preset_choice->SetSelection (0); + preset_choice_changed (); + } else { + _preset_choice->SetSelection (-1); + _preset_choice->Enable (false); + } +} + +void +ContentColourConversionDialog::preset_choice_changed () +{ + vector<PresetColourConversion> presets = Config::instance()->colour_conversions (); + int const s = _preset_choice->GetSelection(); + if (s != -1) { + set (presets[s].conversion); + } +} + + diff --git a/src/wx/content_colour_conversion_dialog.h b/src/wx/content_colour_conversion_dialog.h new file mode 100644 index 000000000..e6069f117 --- /dev/null +++ b/src/wx/content_colour_conversion_dialog.h @@ -0,0 +1,41 @@ +/* + 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 <wx/wx.h> + +class ColourConversionEditor; + +class ContentColourConversionDialog : public wxDialog +{ +public: + ContentColourConversionDialog (wxWindow *); + + void set (ColourConversion); + ColourConversion get () const; + +private: + void check_for_preset (); + void preset_check_clicked (); + void preset_choice_changed (); + + wxCheckBox* _preset_check; + wxChoice* _preset_choice; + ColourConversionEditor* _editor; + bool _setting; +}; diff --git a/src/wx/content_menu.cc b/src/wx/content_menu.cc new file mode 100644 index 000000000..1a409fa6c --- /dev/null +++ b/src/wx/content_menu.cc @@ -0,0 +1,96 @@ +/* + 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 <wx/wx.h> +#include "lib/playlist.h" +#include "lib/film.h" +#include "content_menu.h" +#include "repeat_dialog.h" + +using std::cout; +using boost::shared_ptr; + +enum { + ID_repeat, + ID_remove +}; + +ContentMenu::ContentMenu (shared_ptr<Film> f, wxWindow* p) + : _menu (new wxMenu) + , _film (f) + , _parent (p) +{ + _menu->Append (ID_repeat, _("Repeat...")); + _menu->AppendSeparator (); + _menu->Append (ID_remove, _("Remove")); + + _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, &ContentMenu::repeat, this, ID_repeat); + _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, &ContentMenu::remove, this, ID_remove); +} + +ContentMenu::~ContentMenu () +{ + delete _menu; +} + +void +ContentMenu::popup (ContentList c, wxPoint p) +{ + _content = c; + _parent->PopupMenu (_menu, p); +} + +void +ContentMenu::repeat (wxCommandEvent &) +{ + if (_content.empty ()) { + return; + } + + RepeatDialog d (_parent); + d.ShowModal (); + + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return; + } + + film->playlist()->repeat (_content, d.number ()); + d.Destroy (); + + _content.clear (); +} + +void +ContentMenu::remove (wxCommandEvent &) +{ + if (_content.empty ()) { + return; + } + + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return; + } + + film->playlist()->remove (_content); + + _content.clear (); +} + diff --git a/src/lib/video_sink.h b/src/wx/content_menu.h index 7c128cf73..127fbea1a 100644 --- a/src/lib/video_sink.h +++ b/src/wx/content_menu.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,24 +17,32 @@ */ -#ifndef DVDOMATIC_VIDEO_SINK_H -#define DVDOMATIC_VIDEO_SINK_H +#ifndef DCPOMATIC_CONTENT_MENU_H +#define DCPOMATIC_CONTENT_MENU_H +#include <wx/wx.h> #include <boost/shared_ptr.hpp> -#include "util.h" +#include <boost/weak_ptr.hpp> +#include "lib/types.h" -class Subtitle; -class Image; +class Film; -class VideoSink +class ContentMenu { public: - /** Call with a frame of video. - * @param i Video frame image. - * @param same true if i is the same as last time we were called. - * @param s A subtitle that should be on this frame, or 0. - */ - virtual void process_video (boost::shared_ptr<Image> i, bool same, boost::shared_ptr<Subtitle> s) = 0; + ContentMenu (boost::shared_ptr<Film>, wxWindow *); + ~ContentMenu (); + + void popup (ContentList, wxPoint); + +private: + void repeat (wxCommandEvent &); + void remove (wxCommandEvent &); + + wxMenu* _menu; + boost::weak_ptr<Film> _film; + wxWindow* _parent; + ContentList _content; }; #endif diff --git a/src/wx/dci_metadata_dialog.cc b/src/wx/dci_metadata_dialog.cc new file mode 100644 index 000000000..e28ddd855 --- /dev/null +++ b/src/wx/dci_metadata_dialog.cc @@ -0,0 +1,106 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <wx/wx.h> +#include <wx/sizer.h> +#include <wx/spinctrl.h> +#include "lib/film.h" +#include "dci_metadata_dialog.h" +#include "wx_util.h" + +using boost::shared_ptr; + +DCIMetadataDialog::DCIMetadataDialog (wxWindow* parent, DCIMetadata dm) + : wxDialog (parent, wxID_ANY, _("DCI name"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) +{ + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + + add_label_to_sizer (table, this, _("Content version"), true); + _content_version = new wxSpinCtrl (this, wxID_ANY); + table->Add (_content_version, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Audio Language (e.g. EN)"), true); + _audio_language = new wxTextCtrl (this, wxID_ANY); + table->Add (_audio_language, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Subtitle Language (e.g. FR)"), true); + _subtitle_language = new wxTextCtrl (this, wxID_ANY); + table->Add (_subtitle_language, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Territory (e.g. UK)"), true); + _territory = new wxTextCtrl (this, wxID_ANY); + table->Add (_territory, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Rating (e.g. 15)"), true); + _rating = new wxTextCtrl (this, wxID_ANY); + table->Add (_rating, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Studio (e.g. TCF)"), true); + _studio = new wxTextCtrl (this, wxID_ANY); + table->Add (_studio, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Facility (e.g. DLA)"), true); + _facility = new wxTextCtrl (this, wxID_ANY); + table->Add (_facility, 1, wxEXPAND); + + add_label_to_sizer (table, this, _("Package Type (e.g. OV)"), true); + _package_type = new wxTextCtrl (this, wxID_ANY); + table->Add (_package_type, 1, wxEXPAND); + + _content_version->SetRange (1, 1024); + + _content_version->SetValue (dm.content_version); + _audio_language->SetValue (std_to_wx (dm.audio_language)); + _subtitle_language->SetValue (std_to_wx (dm.subtitle_language)); + _territory->SetValue (std_to_wx (dm.territory)); + _rating->SetValue (std_to_wx (dm.rating)); + _studio->SetValue (std_to_wx (dm.studio)); + _facility->SetValue (std_to_wx (dm.facility)); + _package_type->SetValue (std_to_wx (dm.package_type)); + + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + if (buttons) { + overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + SetSizer (overall_sizer); + overall_sizer->Layout (); + overall_sizer->SetSizeHints (this); +} + +DCIMetadata +DCIMetadataDialog::dci_metadata () const +{ + DCIMetadata dm; + + dm.content_version = _content_version->GetValue (); + dm.audio_language = wx_to_std (_audio_language->GetValue ()); + dm.subtitle_language = wx_to_std (_subtitle_language->GetValue ()); + dm.territory = wx_to_std (_territory->GetValue ()); + dm.rating = wx_to_std (_rating->GetValue ()); + dm.studio = wx_to_std (_studio->GetValue ()); + dm.facility = wx_to_std (_facility->GetValue ()); + dm.package_type = wx_to_std (_package_type->GetValue ()); + + return dm; +} diff --git a/src/wx/dci_name_dialog.h b/src/wx/dci_metadata_dialog.h index 1fd5436b8..240d5535e 100644 --- a/src/wx/dci_name_dialog.h +++ b/src/wx/dci_metadata_dialog.h @@ -20,23 +20,20 @@ #include <wx/dialog.h> #include <wx/textctrl.h> #include <boost/shared_ptr.hpp> +#include "lib/dci_metadata.h" +class wxSpinCtrl; class Film; -class DCINameDialog : public wxDialog +class DCIMetadataDialog : public wxDialog { public: - DCINameDialog (wxWindow *, boost::shared_ptr<Film>); + DCIMetadataDialog (wxWindow *, DCIMetadata); + + DCIMetadata dci_metadata () const; private: - void audio_language_changed (wxCommandEvent &); - void subtitle_language_changed (wxCommandEvent &); - void territory_changed (wxCommandEvent &); - void rating_changed (wxCommandEvent &); - void studio_changed (wxCommandEvent &); - void facility_changed (wxCommandEvent &); - void package_type_changed (wxCommandEvent &); - + wxSpinCtrl* _content_version; wxTextCtrl* _audio_language; wxTextCtrl* _subtitle_language; wxTextCtrl* _territory; @@ -44,6 +41,4 @@ private: wxTextCtrl* _studio; wxTextCtrl* _facility; wxTextCtrl* _package_type; - - boost::shared_ptr<Film> _film; }; diff --git a/src/wx/dci_name_dialog.cc b/src/wx/dci_name_dialog.cc deleted file mode 100644 index 6927d6c9e..000000000 --- a/src/wx/dci_name_dialog.cc +++ /dev/null @@ -1,131 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <wx/sizer.h> -#include "dci_name_dialog.h" -#include "wx_util.h" -#include "film.h" - -using boost::shared_ptr; - -DCINameDialog::DCINameDialog (wxWindow* parent, shared_ptr<Film> film) - : wxDialog (parent, wxID_ANY, _("DCI name"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER) - , _film (film) -{ - wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6); - table->AddGrowableCol (1, 1); - - add_label_to_sizer (table, this, "Audio Language (e.g. EN)"); - _audio_language = new wxTextCtrl (this, wxID_ANY); - table->Add (_audio_language, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Subtitle Language (e.g. FR)"); - _subtitle_language = new wxTextCtrl (this, wxID_ANY); - table->Add (_subtitle_language, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Territory (e.g. UK)"); - _territory = new wxTextCtrl (this, wxID_ANY); - table->Add (_territory, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Rating (e.g. 15)"); - _rating = new wxTextCtrl (this, wxID_ANY); - table->Add (_rating, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Studio (e.g. TCF)"); - _studio = new wxTextCtrl (this, wxID_ANY); - table->Add (_studio, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Facility (e.g. DLA)"); - _facility = new wxTextCtrl (this, wxID_ANY); - table->Add (_facility, 1, wxEXPAND); - - add_label_to_sizer (table, this, "Package Type (e.g. OV)"); - _package_type = new wxTextCtrl (this, wxID_ANY); - table->Add (_package_type, 1, wxEXPAND); - - _audio_language->SetValue (std_to_wx (_film->audio_language ())); - _subtitle_language->SetValue (std_to_wx (_film->subtitle_language ())); - _territory->SetValue (std_to_wx (_film->territory ())); - _rating->SetValue (std_to_wx (_film->rating ())); - _studio->SetValue (std_to_wx (_film->studio ())); - _facility->SetValue (std_to_wx (_film->facility ())); - _package_type->SetValue (std_to_wx (_film->package_type ())); - - _audio_language->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::audio_language_changed), 0, this); - _subtitle_language->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::subtitle_language_changed), 0, this); - _territory->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::territory_changed), 0, this); - _rating->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::rating_changed), 0, this); - _studio->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::studio_changed), 0, this); - _facility->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::facility_changed), 0, this); - _package_type->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (DCINameDialog::package_type_changed), 0, this); - - wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); - overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6); - - wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); - if (buttons) { - overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); - } - - SetSizer (overall_sizer); - overall_sizer->Layout (); - overall_sizer->SetSizeHints (this); -} - -void -DCINameDialog::audio_language_changed (wxCommandEvent &) -{ - _film->set_audio_language (wx_to_std (_audio_language->GetValue ())); -} - -void -DCINameDialog::subtitle_language_changed (wxCommandEvent &) -{ - _film->set_subtitle_language (wx_to_std (_subtitle_language->GetValue ())); -} - -void -DCINameDialog::territory_changed (wxCommandEvent &) -{ - _film->set_territory (wx_to_std (_territory->GetValue ())); -} - -void -DCINameDialog::rating_changed (wxCommandEvent &) -{ - _film->set_rating (wx_to_std (_rating->GetValue ())); -} - -void -DCINameDialog::studio_changed (wxCommandEvent &) -{ - _film->set_studio (wx_to_std (_studio->GetValue ())); -} - -void -DCINameDialog::facility_changed (wxCommandEvent &) -{ - _film->set_facility (wx_to_std (_facility->GetValue ())); -} - -void -DCINameDialog::package_type_changed (wxCommandEvent &) -{ - _film->set_package_type (wx_to_std (_package_type->GetValue ())); -} diff --git a/src/wx/dir_picker_ctrl.cc b/src/wx/dir_picker_ctrl.cc index cb811fc10..47a546a8d 100644 --- a/src/wx/dir_picker_ctrl.cc +++ b/src/wx/dir_picker_ctrl.cc @@ -19,6 +19,7 @@ #include <wx/wx.h> #include <wx/stdpaths.h> +#include <wx/filepicker.h> #include <boost/filesystem.hpp> #include "dir_picker_ctrl.h" #include "wx_util.h" @@ -39,7 +40,7 @@ DirPickerCtrl::DirPickerCtrl (wxWindow* parent) SetSizerAndFit (_sizer); - _browse->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (DirPickerCtrl::browse_clicked), 0, this); + _browse->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&DirPickerCtrl::browse_clicked, this)); } void @@ -50,12 +51,11 @@ DirPickerCtrl::SetPath (wxString p) if (_path == wxStandardPaths::Get().GetDocumentsDir()) { _folder->SetLabel (_("My Documents")); } else { -#if BOOST_FILESYSTEM_VERSION == 3 _folder->SetLabel (std_to_wx (filesystem::path (wx_to_std (_path)).leaf().string())); -#else - _folder->SetLabel (std_to_wx (filesystem::path (wx_to_std (_path)).leaf())); -#endif } + + wxCommandEvent ev (wxEVT_COMMAND_DIRPICKER_CHANGED, wxID_ANY); + GetEventHandler()->ProcessEvent (ev); } wxString @@ -65,10 +65,11 @@ DirPickerCtrl::GetPath () const } void -DirPickerCtrl::browse_clicked (wxCommandEvent &) +DirPickerCtrl::browse_clicked () { wxDirDialog* d = new wxDirDialog (this); - d->ShowModal (); - SetPath (d->GetPath ()); + if (d->ShowModal () == wxID_OK) { + SetPath (d->GetPath ()); + } d->Destroy (); } diff --git a/src/wx/dir_picker_ctrl.h b/src/wx/dir_picker_ctrl.h index df7b25f7a..97c0d217a 100644 --- a/src/wx/dir_picker_ctrl.h +++ b/src/wx/dir_picker_ctrl.h @@ -28,7 +28,7 @@ public: void SetPath (wxString); private: - void browse_clicked (wxCommandEvent &); + void browse_clicked (); wxWindow* _parent; wxStaticText* _folder; diff --git a/src/wx/editable_list.h b/src/wx/editable_list.h new file mode 100644 index 000000000..98e7d0bfd --- /dev/null +++ b/src/wx/editable_list.h @@ -0,0 +1,176 @@ +/* + Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <wx/wx.h> + +template<class T, class S> +class EditableList : public wxPanel +{ +public: + EditableList ( + wxWindow* parent, + std::vector<std::string> columns, + boost::function<std::vector<T> ()> get, + boost::function<void (std::vector<T>)> set, + boost::function<std::string (T, int)> column + ) + : wxPanel (parent) + , _get (get) + , _set (set) + , _columns (columns.size ()) + , _column (column) + { + wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); + SetSizer (s); + + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (0, 1); + s->Add (table, 1, wxALL | wxEXPAND, 8); + + _list = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxSize (columns.size() * 200, 100), wxLC_REPORT | wxLC_SINGLE_SEL); + + for (size_t i = 0; i < columns.size(); ++i) { + wxListItem ip; + ip.SetId (i); + ip.SetText (std_to_wx (columns[i])); + ip.SetWidth (200); + _list->InsertColumn (i, ip); + } + + table->Add (_list, 1, wxEXPAND | wxALL); + + { + wxSizer* s = new wxBoxSizer (wxVERTICAL); + _add = new wxButton (this, wxID_ANY, _("Add...")); + s->Add (_add, 0, wxTOP | wxBOTTOM, 2); + _edit = new wxButton (this, wxID_ANY, _("Edit...")); + s->Add (_edit, 0, wxTOP | wxBOTTOM, 2); + _remove = new wxButton (this, wxID_ANY, _("Remove")); + s->Add (_remove, 0, wxTOP | wxBOTTOM, 2); + table->Add (s, 0); + } + + std::vector<T> current = _get (); + for (typename std::vector<T>::iterator i = current.begin (); i != current.end(); ++i) { + add_to_control (*i); + } + + _add->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&EditableList::add_clicked, this)); + _edit->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&EditableList::edit_clicked, this)); + _remove->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&EditableList::remove_clicked, this)); + + _list->Bind (wxEVT_COMMAND_LIST_ITEM_SELECTED, boost::bind (&EditableList::selection_changed, this)); + _list->Bind (wxEVT_COMMAND_LIST_ITEM_DESELECTED, boost::bind (&EditableList::selection_changed, this)); + _list->Bind (wxEVT_SIZE, boost::bind (&EditableList::resized, this, _1)); + selection_changed (); + + } + +private: + + void add_to_control (T item) + { + wxListItem list_item; + int const n = _list->GetItemCount (); + list_item.SetId (n); + _list->InsertItem (list_item); + + for (int i = 0; i < _columns; ++i) { + _list->SetItem (n, i, std_to_wx (_column (item, i))); + } + } + + void selection_changed () + { + int const i = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + _edit->Enable (i >= 0); + _remove->Enable (i >= 0); + } + + void add_clicked () + { + T new_item; + S* dialog = new S (this); + dialog->set (new_item); + dialog->ShowModal (); + + add_to_control (dialog->get ()); + + std::vector<T> all = _get (); + all.push_back (dialog->get ()); + _set (all); + + dialog->Destroy (); + } + + void edit_clicked () + { + int item = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (item == -1) { + return; + } + + std::vector<T> all = _get (); + assert (item >= 0 && item < int (all.size ())); + + S* dialog = new S (this); + dialog->set (all[item]); + dialog->ShowModal (); + all[item] = dialog->get (); + dialog->Destroy (); + + for (int i = 0; i < _columns; ++i) { + _list->SetItem (item, i, std_to_wx (_column (all[item], i))); + } + } + + void remove_clicked () + { + int i = _list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (i == -1) { + return; + } + + _list->DeleteItem (i); + std::vector<T> all = _get (); + all.erase (all.begin() + i); + _set (all); + + selection_changed (); + } + + void resized (wxSizeEvent& ev) + { + int const w = GetSize().GetWidth() / _columns; + for (int i = 0; i < _columns; ++i) { + _list->SetColumnWidth (i, w); + } + ev.Skip (); + } + + boost::function <std::vector<T> ()> _get; + boost::function <void (std::vector<T>)> _set; + int _columns; + boost::function<std::string (T, int)> _column; + + wxButton* _add; + wxButton* _edit; + wxButton* _remove; + wxListCtrl* _list; +}; diff --git a/src/wx/film_editor.cc b/src/wx/film_editor.cc index 9326227a3..56b697375 100644 --- a/src/wx/film_editor.cc +++ b/src/wx/film_editor.cc @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-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 @@ -25,26 +25,33 @@ #include <iomanip> #include <wx/wx.h> #include <wx/notebook.h> +#include <wx/listctrl.h> #include <boost/thread.hpp> #include <boost/filesystem.hpp> #include <boost/lexical_cast.hpp> -#include "lib/format.h" #include "lib/film.h" #include "lib/transcode_job.h" #include "lib/exceptions.h" -#include "lib/ab_transcode_job.h" #include "lib/job_manager.h" #include "lib/filter.h" +#include "lib/ratio.h" #include "lib/config.h" -#include "lib/ffmpeg_decoder.h" -#include "lib/external_audio_decoder.h" -#include "filter_dialog.h" +#include "lib/still_image_content.h" +#include "lib/moving_image_content.h" +#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 "timecode.h" #include "wx_util.h" #include "film_editor.h" -#include "gain_calculator_dialog.h" -#include "sound_processor.h" -#include "dci_name_dialog.h" -#include "scaler.h" +#include "dci_metadata_dialog.h" +#include "timeline_dialog.h" +#include "timing_panel.h" +#include "subtitle_panel.h" +#include "audio_panel.h" +#include "video_panel.h" using std::string; using std::cout; @@ -54,474 +61,257 @@ using std::fixed; using std::setprecision; using std::list; using std::vector; +using std::max; using boost::shared_ptr; +using boost::weak_ptr; using boost::dynamic_pointer_cast; +using boost::lexical_cast; /** @param f Film to edit */ FilmEditor::FilmEditor (shared_ptr<Film> f, wxWindow* parent) : wxPanel (parent) - , _film (f) + , _menu (f, this) , _generally_sensitive (true) + , _timeline_dialog (0) { wxBoxSizer* s = new wxBoxSizer (wxVERTICAL); - SetSizer (s); - _notebook = new wxNotebook (this, wxID_ANY); - s->Add (_notebook, 1); - - make_film_panel (); - _notebook->AddPage (_film_panel, _("Film"), true); - make_video_panel (); - _notebook->AddPage (_video_panel, _("Video"), false); - make_audio_panel (); - _notebook->AddPage (_audio_panel, _("Audio"), false); - make_subtitle_panel (); - _notebook->AddPage (_subtitle_panel, _("Subtitles"), false); - - set_film (_film); + + _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); + + set_film (f); connect_to_widgets (); JobManager::instance()->ActiveJobsChanged.connect ( bind (&FilmEditor::active_jobs_changed, this, _1) ); - setup_visibility (); - setup_formats (); + SetSizerAndFit (s); } void -FilmEditor::make_film_panel () +FilmEditor::make_dcp_panel () { - _film_panel = new wxPanel (_notebook); - _film_sizer = new wxFlexGridSizer (2, 4, 4); - wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL); - pad->Add (_film_sizer, 0, wxALL, 8); - _film_panel->SetSizer (pad); - - add_label_to_sizer (_film_sizer, _film_panel, "Name"); - _name = new wxTextCtrl (_film_panel, wxID_ANY); - _film_sizer->Add (_name, 1, wxEXPAND); - - add_label_to_sizer (_film_sizer, _film_panel, "DCP Name"); - _dcp_name = new wxStaticText (_film_panel, wxID_ANY, wxT ("")); - _film_sizer->Add (_dcp_name, 0, wxALIGN_CENTER_VERTICAL | wxSHRINK); - - _use_dci_name = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Use DCI name")); - _film_sizer->Add (_use_dci_name, 1, wxEXPAND); - _edit_dci_button = new wxButton (_film_panel, wxID_ANY, wxT ("Details...")); - _film_sizer->Add (_edit_dci_button, 0); - - add_label_to_sizer (_film_sizer, _film_panel, "Content"); - _content = new wxFilePickerCtrl (_film_panel, wxID_ANY, wxT (""), wxT ("Select Content File"), wxT("*.*")); - _film_sizer->Add (_content, 1, wxEXPAND); - - _trust_content_header = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Trust content's header")); - video_control (_trust_content_header); - _film_sizer->Add (_trust_content_header, 1); - _film_sizer->AddSpacer (0); - - add_label_to_sizer (_film_sizer, _film_panel, "Content Type"); - _dcp_content_type = new wxComboBox (_film_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY); - _film_sizer->Add (_dcp_content_type); - - video_control (add_label_to_sizer (_film_sizer, _film_panel, "Frames Per Second")); - _frames_per_second = new wxStaticText (_film_panel, wxID_ANY, wxT ("")); - _film_sizer->Add (video_control (_frames_per_second), 1, wxALIGN_CENTER_VERTICAL); + _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; - video_control (add_label_to_sizer (_film_sizer, _film_panel, "Original Size")); - _original_size = new wxStaticText (_film_panel, wxID_ANY, wxT ("")); - _film_sizer->Add (video_control (_original_size), 1, wxALIGN_CENTER_VERTICAL); + 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; - video_control (add_label_to_sizer (_film_sizer, _film_panel, "Length")); - _length = new wxStaticText (_film_panel, wxID_ANY, wxT ("")); - _film_sizer->Add (video_control (_length), 1, wxALIGN_CENTER_VERTICAL); - + 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; + + int flags = wxALIGN_CENTER_VERTICAL; +#ifdef __WXOSX__ + flags |= wxALIGN_RIGHT; +#endif + + _use_dci_name = new wxCheckBox (_dcp_panel, wxID_ANY, _("Use DCI name")); + grid->Add (_use_dci_name, wxGBPosition (r, 0), wxDefaultSpan, flags); + _edit_dci_button = new wxButton (_dcp_panel, wxID_ANY, _("Details...")); + grid->Add (_edit_dci_button, wxGBPosition (r, 1), wxDefaultSpan); + ++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; { - video_control (add_label_to_sizer (_film_sizer, _film_panel, "Trim frames")); + add_label_to_grid_bag_sizer (grid, _dcp_panel, _("Frame Rate"), true, wxGBPosition (r, 0)); wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - video_control (add_label_to_sizer (s, _film_panel, "Start")); - _dcp_trim_start = new wxSpinCtrl (_film_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (video_control (_dcp_trim_start)); - video_control (add_label_to_sizer (s, _film_panel, "End")); - _dcp_trim_end = new wxSpinCtrl (_film_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (video_control (_dcp_trim_end)); - - _film_sizer->Add (s); + _frame_rate = new wxChoice (_dcp_panel, wxID_ANY); + s->Add (_frame_rate, 1, wxALIGN_CENTER_VERTICAL); + _best_frame_rate = new wxButton (_dcp_panel, wxID_ANY, _("Use best")); + s->Add (_best_frame_rate, 1, wxALIGN_CENTER_VERTICAL | wxEXPAND); + grid->Add (s, wxGBPosition (r, 1)); } + ++r; - _encrypted = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Encrypted")); - _film_sizer->Add (_encrypted, 1); - _film_sizer->AddSpacer (0); + _encrypted = new wxCheckBox (_dcp_panel, wxID_ANY, wxT ("Encrypted")); + grid->Add (_encrypted, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; - _multiple_reels = new wxCheckBox (_film_panel, wxID_ANY, wxT ("Make multiple reels")); - _film_sizer->Add (_multiple_reels); + 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; - { - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _reel_size = new wxSpinCtrl (_film_panel, wxID_ANY); - s->Add (_reel_size); - add_label_to_sizer (s, _film_panel, "Gb each"); - _film_sizer->Add (s); - } + _three_d = new wxCheckBox (_dcp_panel, wxID_ANY, _("3D")); + grid->Add (_three_d, wxGBPosition (r, 0), wxGBSpan (1, 2)); + ++r; - _dcp_ab = new wxCheckBox (_film_panel, wxID_ANY, wxT ("A/B")); - video_control (_dcp_ab); - _film_sizer->Add (_dcp_ab, 1); - _film_sizer->AddSpacer (0); + 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; - /* STILL-only stuff */ { - still_control (add_label_to_sizer (_film_sizer, _film_panel, "Duration")); + add_label_to_grid_bag_sizer (grid, _dcp_panel, _("JPEG2000 bandwidth"), true, wxGBPosition (r, 0)); wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _still_duration = new wxSpinCtrl (_film_panel); - still_control (_still_duration); - s->Add (_still_duration, 1, wxEXPAND); - still_control (add_label_to_sizer (s, _film_panel, "s")); - _film_sizer->Add (s); - } - - 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 ())); - } - - _reel_size->SetRange(1, 1000); -} - -void -FilmEditor::connect_to_widgets () -{ - _name->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (FilmEditor::name_changed), 0, this); - _use_dci_name->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::use_dci_name_toggled), 0, this); - _edit_dci_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::edit_dci_button_clicked), 0, this); - _format->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::format_changed), 0, this); - _content->Connect (wxID_ANY, wxEVT_COMMAND_FILEPICKER_CHANGED, wxCommandEventHandler (FilmEditor::content_changed), 0, this); - _trust_content_header->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::trust_content_header_changed), 0, this); - _left_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::left_crop_changed), 0, this); - _right_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::right_crop_changed), 0, this); - _top_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::top_crop_changed), 0, this); - _bottom_crop->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::bottom_crop_changed), 0, this); - _filters_button->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::edit_filters_clicked), 0, this); - _scaler->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::scaler_changed), 0, this); - _dcp_content_type->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::dcp_content_type_changed), 0, this); - _dcp_ab->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::dcp_ab_toggled), 0, this); - _encrypted->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::encrypted_toggled), 0, this); - _still_duration->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::still_duration_changed), 0, this); - _dcp_trim_start->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::dcp_trim_start_changed), 0, this); - _dcp_trim_end->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::dcp_trim_end_changed), 0, this); - _multiple_reels->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::multiple_reels_toggled), 0, this); - _reel_size->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::reel_size_changed), 0, this); - _with_subtitles->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilmEditor::with_subtitles_toggled), 0, this); - _subtitle_offset->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::subtitle_offset_changed), 0, this); - _subtitle_scale->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::subtitle_scale_changed), 0, this); - _colour_lut->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::colour_lut_changed), 0, this); - _j2k_bandwidth->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::j2k_bandwidth_changed), 0, this); - _subtitle_stream->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::subtitle_stream_changed), 0, this); - _audio_stream->Connect (wxID_ANY, wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler (FilmEditor::audio_stream_changed), 0, this); - _audio_gain->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::audio_gain_changed), 0, this); - _audio_gain_calculate_button->Connect ( - wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::audio_gain_calculate_button_clicked), 0, this - ); - _audio_delay->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (FilmEditor::audio_delay_changed), 0, this); - _use_content_audio->Connect (wxID_ANY, wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler (FilmEditor::use_audio_changed), 0, this); - _use_external_audio->Connect (wxID_ANY, wxEVT_COMMAND_RADIOBUTTON_SELECTED, wxCommandEventHandler (FilmEditor::use_audio_changed), 0, this); - for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { - _external_audio[i]->Connect ( - wxID_ANY, wxEVT_COMMAND_FILEPICKER_CHANGED, wxCommandEventHandler (FilmEditor::external_audio_changed), 0, this - ); - } -} - -void -FilmEditor::make_video_panel () -{ - _video_panel = new wxPanel (_notebook); - _video_sizer = new wxFlexGridSizer (2, 4, 4); - wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL); - pad->Add (_video_sizer, 0, wxALL, 8); - _video_panel->SetSizer (pad); - - add_label_to_sizer (_video_sizer, _video_panel, "Format"); - _format = new wxComboBox (_video_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY); - _video_sizer->Add (_format); - - { - add_label_to_sizer (_video_sizer, _video_panel, "Crop"); - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - - add_label_to_sizer (s, _video_panel, "L"); - _left_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (_left_crop, 0); - add_label_to_sizer (s, _video_panel, "R"); - _right_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (_right_crop, 0); - add_label_to_sizer (s, _video_panel, "T"); - _top_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (_top_crop, 0); - add_label_to_sizer (s, _video_panel, "B"); - _bottom_crop = new wxSpinCtrl (_video_panel, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); - s->Add (_bottom_crop, 0); - - _video_sizer->Add (s); + _j2k_bandwidth = new wxSpinCtrl (_dcp_panel, wxID_ANY); + s->Add (_j2k_bandwidth, 1); + add_label_to_sizer (s, _dcp_panel, _("MBps"), false); + grid->Add (s, wxGBPosition (r, 1)); } + ++r; - /* VIDEO-only stuff */ - { - video_control (add_label_to_sizer (_video_sizer, _video_panel, "Filters")); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _filters = new wxStaticText (_video_panel, wxID_ANY, wxT ("None")); - video_control (_filters); - s->Add (_filters, 1, wxEXPAND | wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM | wxRIGHT, 6); - _filters_button = new wxButton (_video_panel, wxID_ANY, wxT ("Edit...")); - video_control (_filters_button); - s->Add (_filters_button, 0); - _video_sizer->Add (s, 1); - } + 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; - video_control (add_label_to_sizer (_video_sizer, _video_panel, "Scaler")); - _scaler = new wxComboBox (_video_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY); - _video_sizer->Add (video_control (_scaler), 1); + 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())); } - add_label_to_sizer (_video_sizer, _video_panel, "Colour look-up table"); - _colour_lut = new wxComboBox (_video_panel, wxID_ANY); - for (int i = 0; i < 2; ++i) { - _colour_lut->Append (std_to_wx (colour_lut_index_to_name (i))); - } - _colour_lut->SetSelection (0); - _video_sizer->Add (_colour_lut, 1, wxEXPAND); - - { - add_label_to_sizer (_video_sizer, _video_panel, "JPEG2000 bandwidth"); - wxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _j2k_bandwidth = new wxSpinCtrl (_video_panel, wxID_ANY); - s->Add (_j2k_bandwidth, 1); - add_label_to_sizer (s, _video_panel, "MBps"); - _video_sizer->Add (s, 1); + 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 ())); } - _left_crop->SetRange (0, 1024); - _top_crop->SetRange (0, 1024); - _right_crop->SetRange (0, 1024); - _bottom_crop->SetRange (0, 1024); - _still_duration->SetRange (1, 60 * 60); - _dcp_trim_start->SetRange (0, 100); - _dcp_trim_end->SetRange (0, 100); - _j2k_bandwidth->SetRange (50, 250); -} - -void -FilmEditor::make_audio_panel () -{ - _audio_panel = new wxPanel (_notebook); - _audio_sizer = new wxFlexGridSizer (2, 4, 4); - wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL); - pad->Add (_audio_sizer, 0, wxALL, 8); - _audio_panel->SetSizer (pad); - - { - video_control (add_label_to_sizer (_audio_sizer, _audio_panel, "Audio Gain")); - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _audio_gain = new wxSpinCtrl (_audio_panel); - s->Add (video_control (_audio_gain), 1); - video_control (add_label_to_sizer (s, _audio_panel, "dB")); - _audio_gain_calculate_button = new wxButton (_audio_panel, wxID_ANY, _("Calculate...")); - video_control (_audio_gain_calculate_button); - s->Add (_audio_gain_calculate_button, 1, wxEXPAND); - _audio_sizer->Add (s); + 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 ())); } - { - video_control (add_label_to_sizer (_audio_sizer, _audio_panel, "Audio Delay")); - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _audio_delay = new wxSpinCtrl (_audio_panel); - s->Add (video_control (_audio_delay), 1); - video_control (add_label_to_sizer (s, _audio_panel, "ms")); - _audio_sizer->Add (s); + list<int> const dfr = Config::instance()->allowed_dcp_frame_rates (); + for (list<int>::const_iterator i = dfr.begin(); i != dfr.end(); ++i) { + _frame_rate->Append (std_to_wx (boost::lexical_cast<string> (*i))); } - { - _use_content_audio = new wxRadioButton (_audio_panel, wxID_ANY, _("Use content's audio"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP); - _audio_sizer->Add (video_control (_use_content_audio)); - wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _audio_stream = new wxComboBox (_audio_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY); - s->Add (video_control (_audio_stream), 1); - _audio = new wxStaticText (_audio_panel, wxID_ANY, wxT ("")); - s->Add (video_control (_audio), 1, wxALIGN_CENTER_VERTICAL | wxLEFT, 8); - _audio_sizer->Add (s, 1, wxEXPAND); - } + _audio_channels->SetRange (0, MAX_AUDIO_CHANNELS); + _j2k_bandwidth->SetRange (1, 250); - _use_external_audio = new wxRadioButton (_audio_panel, wxID_ANY, _("Use external audio")); - _audio_sizer->Add (_use_external_audio); - _audio_sizer->AddSpacer (0); - - assert (MAX_AUDIO_CHANNELS == 6); - - char const * channels[] = { - "Left", - "Right", - "Centre", - "Lfe (sub)", - "Left surround", - "Right surround" - }; - - for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { - add_label_to_sizer (_audio_sizer, _audio_panel, channels[i]); - _external_audio[i] = new wxFilePickerCtrl (_audio_panel, wxID_ANY, wxT (""), wxT ("Select Audio File"), wxT ("*.wav")); - _audio_sizer->Add (_external_audio[i], 1, wxEXPAND); - } + _resolution->Append (_("2K")); + _resolution->Append (_("4K")); - _audio_gain->SetRange (-60, 60); - _audio_delay->SetRange (-1000, 1000); + _standard->Append (_("SMPTE")); + _standard->Append (_("Interop")); } void -FilmEditor::make_subtitle_panel () +FilmEditor::connect_to_widgets () { - _subtitle_panel = new wxPanel (_notebook); - _subtitle_sizer = new wxFlexGridSizer (2, 4, 4); - wxBoxSizer* pad = new wxBoxSizer (wxVERTICAL); - pad->Add (_subtitle_sizer, 0, wxALL, 8); - _subtitle_panel->SetSizer (pad); - - _with_subtitles = new wxCheckBox (_subtitle_panel, wxID_ANY, wxT("With Subtitles")); - video_control (_with_subtitles); - _subtitle_sizer->Add (_with_subtitles, 1); - - _subtitle_stream = new wxComboBox (_subtitle_panel, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, 0, wxCB_READONLY); - _subtitle_sizer->Add (video_control (_subtitle_stream)); - - video_control (add_label_to_sizer (_subtitle_sizer, _subtitle_panel, "Subtitle Offset")); - _subtitle_offset = new wxSpinCtrl (_subtitle_panel); - _subtitle_sizer->Add (video_control (_subtitle_offset), 1); + _name->Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&FilmEditor::name_changed, this)); + _use_dci_name->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilmEditor::use_dci_name_toggled, this)); + _edit_dci_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::edit_dci_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_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_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->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&FilmEditor::frame_rate_changed, this)); + _best_frame_rate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmEditor::best_frame_rate_clicked, 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); { - video_control (add_label_to_sizer (_subtitle_sizer, _subtitle_panel, "Subtitle Scale")); wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); - _subtitle_scale = new wxSpinCtrl (_subtitle_panel); - s->Add (video_control (_subtitle_scale)); - video_control (add_label_to_sizer (s, _subtitle_panel, "%")); - _subtitle_sizer->Add (s); - } - - _subtitle_offset->SetRange (-1024, 1024); - _subtitle_scale->SetRange (1, 1000); -} - -/** Called when the left crop widget has been changed */ -void -FilmEditor::left_crop_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } - - _film->set_left_crop (_left_crop->GetValue ()); -} - -/** Called when the right crop widget has been changed */ -void -FilmEditor::right_crop_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } - - _film->set_right_crop (_right_crop->GetValue ()); -} - -/** Called when the top crop widget has been changed */ -void -FilmEditor::top_crop_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } + + _content = new wxListCtrl (_content_panel, wxID_ANY, wxDefaultPosition, wxSize (320, 160), wxLC_REPORT | wxLC_NO_HEADER | wxLC_SINGLE_SEL); + s->Add (_content, 1, wxEXPAND | wxTOP | wxBOTTOM, 6); - _film->set_top_crop (_top_crop->GetValue ()); -} + _content->InsertColumn (0, wxT("")); + _content->SetColumnWidth (0, 512); -/** Called when the bottom crop value has been changed */ -void -FilmEditor::bottom_crop_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } + wxBoxSizer* b = new wxBoxSizer (wxVERTICAL); + _content_add_file = new wxButton (_content_panel, wxID_ANY, _("Add file(s)...")); + b->Add (_content_add_file, 1, wxEXPAND | wxLEFT | wxRIGHT); + _content_add_folder = new wxButton (_content_panel, wxID_ANY, _("Add folder...")); + b->Add (_content_add_folder, 1, wxEXPAND | wxLEFT | wxRIGHT); + _content_remove = new wxButton (_content_panel, wxID_ANY, _("Remove")); + b->Add (_content_remove, 1, wxEXPAND | wxLEFT | wxRIGHT); + _content_timeline = new wxButton (_content_panel, wxID_ANY, _("Timeline...")); + b->Add (_content_timeline, 1, wxEXPAND | wxLEFT | wxRIGHT); - _film->set_bottom_crop (_bottom_crop->GetValue ()); -} + s->Add (b, 0, wxALL, 4); -/** Called when the content filename has been changed */ -void -FilmEditor::content_changed (wxCommandEvent &) -{ - if (!_film) { - return; + _content_sizer->Add (s, 0.75, wxEXPAND | wxALL, 6); } - try { - _film->set_content (wx_to_std (_content->GetPath ())); - } catch (std::exception& e) { - _content->SetPath (std_to_wx (_film->directory ())); - error_dialog (this, String::compose ("Could not set content: %1", e.what ())); - } -} + _sequence_video = new wxCheckBox (_content_panel, wxID_ANY, _("Keep video in sequence")); + _content_sizer->Add (_sequence_video); -void -FilmEditor::trust_content_header_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } + _content_notebook = new wxNotebook (_content_panel, wxID_ANY); + _content_sizer->Add (_content_notebook, 1, wxEXPAND | wxTOP, 6); - _film->set_trust_content_header (_trust_content_header->GetValue ()); -} - -void -FilmEditor::multiple_reels_toggled (wxCommandEvent &) -{ - if (!_film) { - return; - } - - if (_multiple_reels->GetValue()) { - _film->set_reel_size (_reel_size->GetValue() * 1e9); - } else { - _film->unset_reel_size (); - } - - setup_reel_control_sensitivity (); + _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); } +/** Called when the name widget has been changed */ void -FilmEditor::reel_size_changed (wxCommandEvent &) +FilmEditor::name_changed () { if (!_film) { return; } - _film->set_reel_size (static_cast<uint64_t> (_reel_size->GetValue()) * 1e9); + _film->set_name (string (_name->GetValue().mb_str())); } -/** Called when the DCP A/B switch has been toggled */ void -FilmEditor::dcp_ab_toggled (wxCommandEvent &) +FilmEditor::j2k_bandwidth_changed () { if (!_film) { return; } - _film->set_dcp_ab (_dcp_ab->GetValue ()); + _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1e6); } void -FilmEditor::encrypted_toggled (wxCommandEvent &) +FilmEditor::encrypted_toggled () { if (!_film) { return; @@ -532,55 +322,48 @@ FilmEditor::encrypted_toggled (wxCommandEvent &) /** Called when the name widget has been changed */ void -FilmEditor::name_changed (wxCommandEvent &) +FilmEditor::frame_rate_changed () { if (!_film) { return; } - _film->set_name (string (_name->GetValue().mb_str())); + _film->set_video_frame_rate ( + boost::lexical_cast<int> ( + wx_to_std (_frame_rate->GetString (_frame_rate->GetSelection ())) + ) + ); } void -FilmEditor::subtitle_offset_changed (wxCommandEvent &) +FilmEditor::audio_channels_changed () { if (!_film) { return; } - _film->set_subtitle_offset (_subtitle_offset->GetValue ()); + _film->set_audio_channels (_audio_channels->GetValue ()); } void -FilmEditor::subtitle_scale_changed (wxCommandEvent &) +FilmEditor::resolution_changed () { if (!_film) { return; } - _film->set_subtitle_scale (_subtitle_scale->GetValue() / 100.0); -} - -void -FilmEditor::colour_lut_changed (wxCommandEvent &) -{ - if (!_film) { - return; - } - - _film->set_colour_lut (_colour_lut->GetSelection ()); + _film->set_resolution (_resolution->GetSelection() == 0 ? RESOLUTION_2K : RESOLUTION_4K); } void -FilmEditor::j2k_bandwidth_changed (wxCommandEvent &) +FilmEditor::standard_changed () { if (!_film) { return; } - - _film->set_j2k_bandwidth (_j2k_bandwidth->GetValue() * 1e6); -} + _film->set_interop (_standard->GetSelection() == 1); +} /** Called when the metadata stored in the Film object has changed; * so that we can update the GUI. @@ -596,201 +379,151 @@ FilmEditor::film_changed (Film::Property p) } stringstream 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: - checked_set (_content, _film->content ()); - setup_visibility (); - setup_formats (); - setup_subtitle_control_sensitivity (); - setup_streams (); + setup_content (); break; - case Film::TRUST_CONTENT_HEADER: - checked_set (_trust_content_header, _film->trust_content_header ()); + case Film::CONTAINER: + setup_container (); break; - case Film::SUBTITLE_STREAMS: - setup_subtitle_control_sensitivity (); - setup_streams (); - break; - case Film::CONTENT_AUDIO_STREAMS: - setup_streams (); - break; - case Film::FORMAT: - { - int n = 0; - vector<Format const *>::iterator i = _formats.begin (); - while (i != _formats.end() && *i != _film->format ()) { - ++i; - ++n; - } - if (i == _formats.end()) { - checked_set (_format, -1); - } else { - checked_set (_format, n); - } - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - break; - } - case Film::CROP: - checked_set (_left_crop, _film->crop().left); - checked_set (_right_crop, _film->crop().right); - checked_set (_top_crop, _film->crop().top); - checked_set (_bottom_crop, _film->crop().bottom); - break; - case Film::FILTERS: - { - pair<string, string> p = Filter::ffmpeg_strings (_film->filters ()); - if (p.first.empty () && p.second.empty ()) { - _filters->SetLabel (_("None")); - } else { - string const b = p.first + " " + p.second; - _filters->SetLabel (std_to_wx (b)); - } - _film_sizer->Layout (); - break; - } case Film::NAME: checked_set (_name, _film->name()); - _film->set_dci_date_today (); - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - break; - case Film::FRAMES_PER_SECOND: - s << fixed << setprecision(2) << _film->frames_per_second(); - _frames_per_second->SetLabel (std_to_wx (s.str ())); + setup_dcp_name (); break; - case Film::SIZE: - if (_film->size().width == 0 && _film->size().height == 0) { - _original_size->SetLabel (wxT ("")); - } else { - s << _film->size().width << " x " << _film->size().height; - _original_size->SetLabel (std_to_wx (s.str ())); - } - break; - case Film::LENGTH: - if (_film->frames_per_second() > 0 && _film->length()) { - s << _film->length().get() << " frames; " << seconds_to_hms (_film->length().get() / _film->frames_per_second()); - } else if (_film->length()) { - s << _film->length().get() << " frames"; - } - _length->SetLabel (std_to_wx (s.str ())); - if (_film->length()) { - _dcp_trim_start->SetRange (0, _film->length().get()); - _dcp_trim_end->SetRange (0, _film->length().get()); - } + 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 ())); - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - break; - case Film::DCP_AB: - checked_set (_dcp_ab, _film->dcp_ab ()); + setup_dcp_name (); break; case Film::SCALER: checked_set (_scaler, Scaler::as_index (_film->scaler ())); break; - case Film::DCP_TRIM_START: - checked_set (_dcp_trim_start, _film->dcp_trim_start()); - break; - case Film::DCP_TRIM_END: - checked_set (_dcp_trim_end, _film->dcp_trim_end()); - break; - case Film::REEL_SIZE: - if (_film->reel_size()) { - checked_set (_multiple_reels, true); - checked_set (_reel_size, _film->reel_size().get() / 1e9); - } else { - checked_set (_multiple_reels, false); - } - setup_reel_control_sensitivity (); - break; - case Film::AUDIO_GAIN: - checked_set (_audio_gain, _film->audio_gain ()); - break; - case Film::AUDIO_DELAY: - checked_set (_audio_delay, _film->audio_delay ()); - break; - case Film::STILL_DURATION: - checked_set (_still_duration, _film->still_duration ()); - break; - case Film::WITH_SUBTITLES: - checked_set (_with_subtitles, _film->with_subtitles ()); - setup_subtitle_control_sensitivity (); - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - break; - case Film::SUBTITLE_OFFSET: - checked_set (_subtitle_offset, _film->subtitle_offset ()); - break; - case Film::SUBTITLE_SCALE: - checked_set (_subtitle_scale, _film->subtitle_scale() * 100); - break; case Film::ENCRYPTED: checked_set (_encrypted, _film->encrypted ()); break; - case Film::COLOUR_LUT: - checked_set (_colour_lut, _film->colour_lut ()); + case Film::RESOLUTION: + checked_set (_resolution, _film->resolution() == RESOLUTION_2K ? 0 : 1); + setup_dcp_name (); break; case Film::J2K_BANDWIDTH: checked_set (_j2k_bandwidth, double (_film->j2k_bandwidth()) / 1e6); break; case Film::USE_DCI_NAME: checked_set (_use_dci_name, _film->use_dci_name ()); - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); + setup_dcp_name (); break; case Film::DCI_METADATA: - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); + setup_dcp_name (); break; - case Film::CONTENT_AUDIO_STREAM: - if (_film->content_audio_stream()) { - checked_set (_audio_stream, _film->content_audio_stream()->to_string()); + case Film::VIDEO_FRAME_RATE: + { + bool done = false; + for (unsigned int i = 0; i < _frame_rate->GetCount(); ++i) { + if (wx_to_std (_frame_rate->GetString(i)) == boost::lexical_cast<string> (_film->video_frame_rate())) { + checked_set (_frame_rate, i); + done = true; + break; + } + } + + if (!done) { + checked_set (_frame_rate, -1); } - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - setup_audio_details (); - setup_audio_control_sensitivity (); + + _best_frame_rate->Enable (_film->best_video_frame_rate () != _film->video_frame_rate ()); break; - case Film::USE_CONTENT_AUDIO: - checked_set (_use_content_audio, _film->use_content_audio()); - checked_set (_use_external_audio, !_film->use_content_audio()); - _dcp_name->SetLabel (std_to_wx (_film->dcp_name ())); - setup_audio_details (); - setup_audio_control_sensitivity (); + } + case Film::AUDIO_CHANNELS: + _audio_channels->SetValue (_film->audio_channels ()); + setup_dcp_name (); break; - case Film::SUBTITLE_STREAM: - if (_film->subtitle_stream()) { - checked_set (_subtitle_stream, _film->subtitle_stream()->to_string()); - } + case Film::SEQUENCE_VIDEO: + checked_set (_sequence_video, _film->sequence_video ()); break; - case Film::EXTERNAL_AUDIO: - { - vector<string> a = _film->external_audio (); - for (size_t i = 0; i < a.size() && i < MAX_AUDIO_CHANNELS; ++i) { - checked_set (_external_audio[i], a[i]); - } - setup_audio_details (); + 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; } +} + +void +FilmEditor::film_content_changed (weak_ptr<Content> weak_content, int property) +{ + ensure_ui_thread (); + + if (!_film) { + /* We call this method ourselves (as well as using it as a signal handler) + so _film can be 0. + */ + return; + } + + shared_ptr<Content> content = weak_content.lock (); + if (!content || content != selected_content ()) { + return; + } + + for (list<FilmEditorPanel*>::iterator i = _panels.begin(); i != _panels.end(); ++i) { + (*i)->film_content_changed (content, property); + } + + if (property == FFmpegContentProperty::AUDIO_STREAM) { + setup_dcp_name (); } } -/** Called when the format widget has been changed */ void -FilmEditor::format_changed (wxCommandEvent &) +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 = _format->GetSelection (); + int const n = _container->GetSelection (); if (n >= 0) { - assert (n < int (_formats.size())); - _film->set_format (_formats[n]); + 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 (wxCommandEvent &) +FilmEditor::dcp_content_type_changed () { if (!_film) { return; @@ -806,12 +539,17 @@ FilmEditor::dcp_content_type_changed (wxCommandEvent &) void FilmEditor::set_film (shared_ptr<Film> f) { - _film = f; + set_general_sensitivity (f != 0); - set_things_sensitive (_film != 0); + if (_film == f) { + return; + } + + _film = f; if (_film) { _film->Changed.connect (bind (&FilmEditor::film_changed, this, _1)); + _film->ContentChanged.connect (bind (&FilmEditor::film_content_changed, this, _1, _2)); } if (_film) { @@ -819,93 +557,67 @@ FilmEditor::set_film (shared_ptr<Film> f) } else { FileChanged (""); } - + film_changed (Film::NAME); film_changed (Film::USE_DCI_NAME); film_changed (Film::CONTENT); - film_changed (Film::TRUST_CONTENT_HEADER); film_changed (Film::DCP_CONTENT_TYPE); - film_changed (Film::FORMAT); - film_changed (Film::CROP); - film_changed (Film::FILTERS); + film_changed (Film::CONTAINER); + film_changed (Film::RESOLUTION); film_changed (Film::SCALER); - film_changed (Film::DCP_TRIM_START); - film_changed (Film::DCP_TRIM_END); - film_changed (Film::REEL_SIZE); - film_changed (Film::DCP_AB); - film_changed (Film::CONTENT_AUDIO_STREAM); - film_changed (Film::EXTERNAL_AUDIO); - film_changed (Film::USE_CONTENT_AUDIO); - film_changed (Film::AUDIO_GAIN); - film_changed (Film::AUDIO_DELAY); - film_changed (Film::STILL_DURATION); film_changed (Film::WITH_SUBTITLES); - film_changed (Film::SUBTITLE_OFFSET); - film_changed (Film::SUBTITLE_SCALE); film_changed (Film::ENCRYPTED); - film_changed (Film::COLOUR_LUT); film_changed (Film::J2K_BANDWIDTH); film_changed (Film::DCI_METADATA); - film_changed (Film::SIZE); - film_changed (Film::LENGTH); - film_changed (Film::CONTENT_AUDIO_STREAMS); - film_changed (Film::SUBTITLE_STREAMS); - film_changed (Film::FRAMES_PER_SECOND); + 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_selection_changed (); } -/** Updates the sensitivity of lots of widgets to a given value. - * @param s true to make sensitive, false to make insensitive. - */ void -FilmEditor::set_things_sensitive (bool s) +FilmEditor::set_general_sensitivity (bool s) { _generally_sensitive = s; - + + /* Stuff in the Content / DCP tabs */ _name->Enable (s); _use_dci_name->Enable (s); _edit_dci_button->Enable (s); - _format->Enable (s); _content->Enable (s); - _trust_content_header->Enable (s); - _left_crop->Enable (s); - _right_crop->Enable (s); - _top_crop->Enable (s); - _bottom_crop->Enable (s); - _filters_button->Enable (s); - _scaler->Enable (s); - _audio_stream->Enable (s); + _content_add_file->Enable (s); + _content_add_folder->Enable (s); + _content_remove->Enable (s); + _content_timeline->Enable (s); _dcp_content_type->Enable (s); - _dcp_trim_start->Enable (s); - _dcp_trim_end->Enable (s); - _multiple_reels->Enable (s); - _reel_size->Enable (s); - _dcp_ab->Enable (s); _encrypted->Enable (s); - _colour_lut->Enable (s); + _frame_rate->Enable (s); + _audio_channels->Enable (s); _j2k_bandwidth->Enable (s); - _audio_gain->Enable (s); - _audio_gain_calculate_button->Enable (s); - _audio_delay->Enable (s); - _still_duration->Enable (s); - - setup_subtitle_control_sensitivity (); - setup_audio_control_sensitivity (); - setup_reel_control_sensitivity (); -} + _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); -/** Called when the `Edit filters' button has been clicked */ -void -FilmEditor::edit_filters_clicked (wxCommandEvent &) -{ - FilterDialog* d = new FilterDialog (this, _film->filters()); - d->ActiveChanged.connect (bind (&Film::set_filters, _film, _1)); - d->ShowModal (); - d->Destroy (); + /* 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 (wxCommandEvent &) +FilmEditor::scaler_changed () { if (!_film) { return; @@ -918,322 +630,289 @@ FilmEditor::scaler_changed (wxCommandEvent &) } void -FilmEditor::audio_gain_changed (wxCommandEvent &) +FilmEditor::use_dci_name_toggled () { if (!_film) { return; } - _film->set_audio_gain (_audio_gain->GetValue ()); + _film->set_use_dci_name (_use_dci_name->GetValue ()); } void -FilmEditor::audio_delay_changed (wxCommandEvent &) +FilmEditor::edit_dci_button_clicked () { if (!_film) { return; } - _film->set_audio_delay (_audio_delay->GetValue ()); -} - -wxControl * -FilmEditor::video_control (wxControl* c) -{ - _video_controls.push_back (c); - return c; -} - -wxControl * -FilmEditor::still_control (wxControl* c) -{ - _still_controls.push_back (c); - return c; + DCIMetadataDialog* d = new DCIMetadataDialog (this, _film->dci_metadata ()); + d->ShowModal (); + _film->set_dci_metadata (d->dci_metadata ()); + d->Destroy (); } void -FilmEditor::setup_visibility () +FilmEditor::active_jobs_changed (bool a) { - ContentType c = VIDEO; - - if (_film) { - c = _film->content_type (); - } - - for (list<wxControl*>::iterator i = _video_controls.begin(); i != _video_controls.end(); ++i) { - (*i)->Show (c == VIDEO); - } - - for (list<wxControl*>::iterator i = _still_controls.begin(); i != _still_controls.end(); ++i) { - (*i)->Show (c == STILL); - } - - _notebook->InvalidateBestSize (); - - _film_sizer->Layout (); - _film_sizer->SetSizeHints (_film_panel); - _video_sizer->Layout (); - _video_sizer->SetSizeHints (_video_panel); - _audio_sizer->Layout (); - _audio_sizer->SetSizeHints (_audio_panel); - _subtitle_sizer->Layout (); - _subtitle_sizer->SetSizeHints (_subtitle_panel); - - _notebook->Fit (); - Fit (); + set_general_sensitivity (!a); } void -FilmEditor::still_duration_changed (wxCommandEvent &) +FilmEditor::setup_dcp_name () { - if (!_film) { - return; + 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)); } - - _film->set_still_duration (_still_duration->GetValue ()); } void -FilmEditor::dcp_trim_start_changed (wxCommandEvent &) +FilmEditor::best_frame_rate_clicked () { if (!_film) { return; } - - _film->set_dcp_trim_start (_dcp_trim_start->GetValue ()); + + _film->set_video_frame_rate (_film->best_video_frame_rate ()); } void -FilmEditor::dcp_trim_end_changed (wxCommandEvent &) +FilmEditor::setup_content () { - if (!_film) { - return; + 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 (); + for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { + int const t = _content->GetItemCount (); + _content->InsertItem (t, std_to_wx ((*i)->summary ())); + if ((*i)->summary() == selected_summary) { + _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); + } } - _film->set_dcp_trim_end (_dcp_trim_end->GetValue ()); + 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::audio_gain_calculate_button_clicked (wxCommandEvent &) +FilmEditor::content_add_file_clicked () { - GainCalculatorDialog* d = new GainCalculatorDialog (this); - d->ShowModal (); + wxFileDialog* d = new wxFileDialog (this, _("Choose a file or files"), wxT (""), wxT (""), wxT ("*.*"), wxFD_MULTIPLE); + int const r = d->ShowModal (); + d->Destroy (); - if (d->wanted_fader() == 0 || d->actual_fader() == 0) { - d->Destroy (); + if (r != wxID_OK) { return; } - - _audio_gain->SetValue ( - Config::instance()->sound_processor()->db_for_fader_change ( - d->wanted_fader (), - d->actual_fader () - ) - ); - /* This appears to be necessary, as the change is not signalled, - I think. - */ - wxCommandEvent dummy; - audio_gain_changed (dummy); - - d->Destroy (); -} + wxArrayString paths; + d->GetPaths (paths); -void -FilmEditor::setup_formats () -{ - ContentType c = VIDEO; + /* XXX: check for lots of files here and do something */ - if (_film) { - c = _film->content_type (); - } - - _formats.clear (); - - vector<Format const *> fmt = Format::all (); - for (vector<Format const *>::iterator i = fmt.begin(); i != fmt.end(); ++i) { - if (c == VIDEO && dynamic_cast<FixedFormat const *> (*i)) { - _formats.push_back (*i); - } else if (c == STILL && dynamic_cast<VariableFormat const *> (*i)) { - _formats.push_back (*i); + for (unsigned int i = 0; i < paths.GetCount(); ++i) { + boost::filesystem::path p (wx_to_std (paths[i])); + + shared_ptr<Content> c; + + if (valid_image_file (p)) { + c.reset (new StillImageContent (_film, p)); + } else if (SndfileContent::valid_file (p)) { + c.reset (new SndfileContent (_film, p)); + } else { + c.reset (new FFmpegContent (_film, p)); } - } - _format->Clear (); - for (vector<Format const *>::iterator i = _formats.begin(); i != _formats.end(); ++i) { - _format->Append (std_to_wx ((*i)->name ())); + _film->examine_and_add_content (c); } - - _film_sizer->Layout (); } void -FilmEditor::with_subtitles_toggled (wxCommandEvent &) +FilmEditor::content_add_folder_clicked () { - if (!_film) { + 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; } - _film->set_with_subtitles (_with_subtitles->GetValue ()); + _film->examine_and_add_content ( + shared_ptr<MovingImageContent> ( + new MovingImageContent (_film, boost::filesystem::path (wx_to_std (d->GetPath ()))) + ) + ); } void -FilmEditor::setup_subtitle_control_sensitivity () +FilmEditor::content_remove_clicked () { - bool h = false; - if (_generally_sensitive && _film) { - h = !_film->subtitle_streams().empty(); + shared_ptr<Content> c = selected_content (); + if (c) { + _film->remove_content (c); } - - _with_subtitles->Enable (h); - bool j = false; - if (_film) { - j = _film->with_subtitles (); - } - - _subtitle_stream->Enable (j); - _subtitle_offset->Enable (j); - _subtitle_scale->Enable (j); + content_selection_changed (); } void -FilmEditor::setup_audio_control_sensitivity () +FilmEditor::content_selection_changed () { - _use_content_audio->Enable (_generally_sensitive); - _use_external_audio->Enable (_generally_sensitive); - - bool const source = _generally_sensitive && _use_content_audio->GetValue(); - bool const external = _generally_sensitive && _use_external_audio->GetValue(); + setup_content_sensitivity (); + shared_ptr<Content> s = selected_content (); - _audio_stream->Enable (source); - for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { - _external_audio[i]->Enable (external); - } + /* All other sensitivity in content panels should be triggered by + one of these. + */ + film_content_changed (s, ContentProperty::POSITION); + film_content_changed (s, ContentProperty::LENGTH); + film_content_changed (s, ContentProperty::TRIM_START); + film_content_changed (s, ContentProperty::TRIM_END); + film_content_changed (s, VideoContentProperty::VIDEO_CROP); + film_content_changed (s, VideoContentProperty::VIDEO_RATIO); + film_content_changed (s, VideoContentProperty::VIDEO_FRAME_TYPE); + film_content_changed (s, VideoContentProperty::COLOUR_CONVERSION); + film_content_changed (s, AudioContentProperty::AUDIO_GAIN); + film_content_changed (s, AudioContentProperty::AUDIO_DELAY); + film_content_changed (s, AudioContentProperty::AUDIO_MAPPING); + film_content_changed (s, FFmpegContentProperty::AUDIO_STREAM); + film_content_changed (s, FFmpegContentProperty::AUDIO_STREAMS); + film_content_changed (s, FFmpegContentProperty::SUBTITLE_STREAM); + film_content_changed (s, FFmpegContentProperty::SUBTITLE_STREAMS); + film_content_changed (s, FFmpegContentProperty::FILTERS); + film_content_changed (s, SubtitleContentProperty::SUBTITLE_OFFSET); + film_content_changed (s, SubtitleContentProperty::SUBTITLE_SCALE); } +/** Set up broad sensitivity based on the type of content that is selected */ void -FilmEditor::use_dci_name_toggled (wxCommandEvent &) +FilmEditor::setup_content_sensitivity () { - if (!_film) { - return; - } + _content_add_file->Enable (_generally_sensitive); + _content_add_folder->Enable (_generally_sensitive); - _film->set_use_dci_name (_use_dci_name->GetValue ()); + shared_ptr<Content> selection = selected_content (); + + _content_remove->Enable (selection && _generally_sensitive); + _content_timeline->Enable (_generally_sensitive); + + _video_panel->Enable (selection && dynamic_pointer_cast<VideoContent> (selection) && _generally_sensitive); + _audio_panel->Enable (selection && dynamic_pointer_cast<AudioContent> (selection) && _generally_sensitive); + _subtitle_panel->Enable (selection && dynamic_pointer_cast<FFmpegContent> (selection) && _generally_sensitive); + _timing_panel->Enable (selection && _generally_sensitive); } -void -FilmEditor::edit_dci_button_clicked (wxCommandEvent &) +shared_ptr<Content> +FilmEditor::selected_content () { - if (!_film) { - return; + int const s = _content->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + if (s == -1) { + return shared_ptr<Content> (); } - DCINameDialog* d = new DCINameDialog (this, _film); - d->ShowModal (); - d->Destroy (); + ContentList c = _film->content (); + if (s < 0 || size_t (s) >= c.size ()) { + return shared_ptr<Content> (); + } + + return c[s]; } -void -FilmEditor::setup_streams () +shared_ptr<VideoContent> +FilmEditor::selected_video_content () { - _audio_stream->Clear (); - vector<shared_ptr<AudioStream> > a = _film->content_audio_streams (); - for (vector<shared_ptr<AudioStream> >::iterator i = a.begin(); i != a.end(); ++i) { - shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (*i); - assert (ffa); - _audio_stream->Append (std_to_wx (ffa->name()), new wxStringClientData (std_to_wx (ffa->to_string ()))); - } - - if (_film->use_content_audio() && _film->audio_stream()) { - checked_set (_audio_stream, _film->audio_stream()->to_string()); + shared_ptr<Content> c = selected_content (); + if (!c) { + return shared_ptr<VideoContent> (); } - _subtitle_stream->Clear (); - vector<shared_ptr<SubtitleStream> > s = _film->subtitle_streams (); - for (vector<shared_ptr<SubtitleStream> >::iterator i = s.begin(); i != s.end(); ++i) { - _subtitle_stream->Append (std_to_wx ((*i)->name()), new wxStringClientData (std_to_wx ((*i)->to_string ()))); - } - if (_film->subtitle_stream()) { - checked_set (_subtitle_stream, _film->subtitle_stream()->to_string()); - } else { - _subtitle_stream->SetValue (wxT ("")); + return dynamic_pointer_cast<VideoContent> (c); +} + +shared_ptr<AudioContent> +FilmEditor::selected_audio_content () +{ + shared_ptr<Content> c = selected_content (); + if (!c) { + return shared_ptr<AudioContent> (); } + + return dynamic_pointer_cast<AudioContent> (c); } -void -FilmEditor::audio_stream_changed (wxCommandEvent &) +shared_ptr<SubtitleContent> +FilmEditor::selected_subtitle_content () { - if (!_film) { - return; + shared_ptr<Content> c = selected_content (); + if (!c) { + return shared_ptr<SubtitleContent> (); } - _film->set_content_audio_stream ( - audio_stream_factory ( - string_client_data (_audio_stream->GetClientObject (_audio_stream->GetSelection ())), - Film::state_version - ) - ); + return dynamic_pointer_cast<SubtitleContent> (c); } void -FilmEditor::subtitle_stream_changed (wxCommandEvent &) +FilmEditor::content_timeline_clicked () { - if (!_film) { - return; + if (_timeline_dialog) { + _timeline_dialog->Destroy (); + _timeline_dialog = 0; } - - _film->set_subtitle_stream ( - subtitle_stream_factory ( - string_client_data (_subtitle_stream->GetClientObject (_subtitle_stream->GetSelection ())), - Film::state_version - ) - ); + + _timeline_dialog = new TimelineDialog (this, _film); + _timeline_dialog->Show (); } void -FilmEditor::setup_audio_details () +FilmEditor::set_selection (weak_ptr<Content> wc) { - if (!_film->audio_stream()) { - _audio->SetLabel (wxT ("")); - } else { - stringstream s; - if (_film->audio_stream()->channels() == 1) { - s << "1 channel"; + 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 { - s << _film->audio_stream()->channels () << " channels"; + _content->SetItemState (i, 0, wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED); } - s << ", " << _film->audio_stream()->sample_rate() << "Hz"; - _audio->SetLabel (std_to_wx (s.str ())); } } void -FilmEditor::active_jobs_changed (bool a) +FilmEditor::sequence_video_changed () { - set_things_sensitive (!a); + if (!_film) { + return; + } + + _film->set_sequence_video (_sequence_video->GetValue ()); } void -FilmEditor::use_audio_changed (wxCommandEvent &) +FilmEditor::content_right_click (wxListEvent& ev) { - _film->set_use_content_audio (_use_content_audio->GetValue()); + ContentList cl; + cl.push_back (selected_content ()); + _menu.popup (cl, ev.GetPoint ()); } void -FilmEditor::external_audio_changed (wxCommandEvent &) +FilmEditor::three_d_changed () { - vector<string> a; - for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) { - a.push_back (wx_to_std (_external_audio[i]->GetPath())); + if (!_film) { + return; } - _film->set_external_audio (a); -} - -void -FilmEditor::setup_reel_control_sensitivity () -{ - _reel_size->Enable (_multiple_reels->GetValue ()); + _film->set_three_d (_three_d->GetValue ()); } diff --git a/src/wx/film_editor.h b/src/wx/film_editor.h index b990ec40d..bb217211c 100644 --- a/src/wx/film_editor.h +++ b/src/wx/film_editor.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-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 @@ -16,7 +16,7 @@ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ - + /** @file src/film_editor.h * @brief A wx widget to edit a film's metadata, and perform various functions. */ @@ -27,10 +27,17 @@ #include <wx/collpane.h> #include <boost/signals2.hpp> #include "lib/film.h" +#include "content_menu.h" class wxNotebook; - +class wxListCtrl; +class wxListEvent; class Film; +class TimelineDialog; +class Ratio; +class Timecode; +class FilmEditorPanel; +class SubtitleContent; /** @class FilmEditor * @brief A wx widget to edit a film's metadata, and perform various functions. @@ -41,146 +48,108 @@ public: FilmEditor (boost::shared_ptr<Film>, wxWindow *); void set_film (boost::shared_ptr<Film>); - void setup_visibility (); + void set_selection (boost::weak_ptr<Content>); boost::signals2::signal<void (std::string)> FileChanged; + /* Stuff for panels */ + + wxNotebook* content_notebook () const { + return _content_notebook; + } + + boost::shared_ptr<Film> film () const { + return _film; + } + + boost::shared_ptr<Content> selected_content (); + boost::shared_ptr<VideoContent> selected_video_content (); + boost::shared_ptr<AudioContent> selected_audio_content (); + boost::shared_ptr<SubtitleContent> selected_subtitle_content (); + private: - void make_film_panel (); - void make_video_panel (); - void make_audio_panel (); - void make_subtitle_panel (); + void make_dcp_panel (); + void make_content_panel (); void connect_to_widgets (); /* Handle changes to the view */ - void name_changed (wxCommandEvent &); - void use_dci_name_toggled (wxCommandEvent &); - void edit_dci_button_clicked (wxCommandEvent &); - void left_crop_changed (wxCommandEvent &); - void right_crop_changed (wxCommandEvent &); - void top_crop_changed (wxCommandEvent &); - void bottom_crop_changed (wxCommandEvent &); - void content_changed (wxCommandEvent &); - void trust_content_header_changed (wxCommandEvent &); - void format_changed (wxCommandEvent &); - void dcp_trim_start_changed (wxCommandEvent &); - void dcp_trim_end_changed (wxCommandEvent &); - void multiple_reels_toggled (wxCommandEvent &); - void reel_size_changed (wxCommandEvent &); - void dcp_content_type_changed (wxCommandEvent &); - void encrypted_toggled (wxCommandEvent &); - void dcp_ab_toggled (wxCommandEvent &); - void scaler_changed (wxCommandEvent &); - void audio_gain_changed (wxCommandEvent &); - void audio_gain_calculate_button_clicked (wxCommandEvent &); - void audio_delay_changed (wxCommandEvent &); - void with_subtitles_toggled (wxCommandEvent &); - void subtitle_offset_changed (wxCommandEvent &); - void subtitle_scale_changed (wxCommandEvent &); - void colour_lut_changed (wxCommandEvent &); - void j2k_bandwidth_changed (wxCommandEvent &); - void still_duration_changed (wxCommandEvent &); - void audio_stream_changed (wxCommandEvent &); - void subtitle_stream_changed (wxCommandEvent &); - void use_audio_changed (wxCommandEvent &); - void external_audio_changed (wxCommandEvent &); + void name_changed (); + void use_dci_name_toggled (); + void edit_dci_button_clicked (); + void content_selection_changed (); + void content_add_file_clicked (); + void content_add_folder_clicked (); + void content_remove_clicked (); + void container_changed (); + void dcp_content_type_changed (); + void scaler_changed (); + void j2k_bandwidth_changed (); + void frame_rate_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 encrypted_toggled (); /* Handle changes to the model */ void film_changed (Film::Property); + void film_content_changed (boost::weak_ptr<Content>, int); - /* Button clicks */ - void edit_filters_clicked (wxCommandEvent &); - - void set_things_sensitive (bool); - void setup_formats (); - void setup_subtitle_control_sensitivity (); - void setup_audio_control_sensitivity (); - void setup_reel_control_sensitivity (); - void setup_streams (); - void setup_audio_details (); + void set_general_sensitivity (bool); + void setup_dcp_name (); + void setup_content (); + void setup_container (); + void setup_content_sensitivity (); - wxControl* video_control (wxControl *); - wxControl* still_control (wxControl *); - void active_jobs_changed (bool); - wxNotebook* _notebook; - wxPanel* _film_panel; - wxSizer* _film_sizer; - wxPanel* _video_panel; - wxSizer* _video_sizer; - wxPanel* _audio_panel; - wxSizer* _audio_sizer; - wxPanel* _subtitle_panel; - wxSizer* _subtitle_sizer; + 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; /** The film we are editing */ boost::shared_ptr<Film> _film; - /** The Film's name */ wxTextCtrl* _name; wxStaticText* _dcp_name; wxCheckBox* _use_dci_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_dci_button; - /** The Film's format */ - wxComboBox* _format; - /** The Film's content file */ - wxFilePickerCtrl* _content; - wxCheckBox* _trust_content_header; - /** The Film's left crop */ - wxSpinCtrl* _left_crop; - /** The Film's right crop */ - wxSpinCtrl* _right_crop; - /** The Film's top crop */ - wxSpinCtrl* _top_crop; - /** The Film's bottom crop */ - wxSpinCtrl* _bottom_crop; - /** Currently-applied filters */ - wxStaticText* _filters; - /** Button to open the filters dialogue */ - wxButton* _filters_button; - /** The Film's scaler */ - wxComboBox* _scaler; - wxRadioButton* _use_content_audio; - wxComboBox* _audio_stream; - wxRadioButton* _use_external_audio; - wxFilePickerCtrl* _external_audio[MAX_AUDIO_CHANNELS]; - /** The Film's audio gain */ - wxSpinCtrl* _audio_gain; - /** A button to open the gain calculation dialogue */ - wxButton* _audio_gain_calculate_button; - /** The Film's audio delay */ - wxSpinCtrl* _audio_delay; - wxCheckBox* _with_subtitles; - wxComboBox* _subtitle_stream; - wxSpinCtrl* _subtitle_offset; - wxSpinCtrl* _subtitle_scale; - wxComboBox* _colour_lut; - wxSpinCtrl* _j2k_bandwidth; - /** The Film's DCP content type */ - wxComboBox* _dcp_content_type; - /** The Film's frames per second */ - wxStaticText* _frames_per_second; - /** The Film's original size */ - wxStaticText* _original_size; - /** The Film's length */ - wxStaticText* _length; - /** The Film's audio details */ - wxStaticText* _audio; - /** The Film's duration for still sources */ - wxSpinCtrl* _still_duration; - - wxSpinCtrl* _dcp_trim_start; - wxSpinCtrl* _dcp_trim_end; + wxChoice* _scaler; + wxSpinCtrl* _j2k_bandwidth; + wxChoice* _dcp_content_type; + wxChoice* _frame_rate; + wxSpinCtrl* _audio_channels; + wxButton* _best_frame_rate; + wxCheckBox* _three_d; + wxChoice* _resolution; + wxChoice* _standard; wxCheckBox* _encrypted; - wxCheckBox* _multiple_reels; - wxSpinCtrl* _reel_size; - /** Selector to generate an A/B comparison DCP */ - wxCheckBox* _dcp_ab; - std::list<wxControl*> _video_controls; - std::list<wxControl*> _still_controls; + ContentMenu _menu; - std::vector<Format const *> _formats; + std::vector<Ratio const *> _ratios; bool _generally_sensitive; + TimelineDialog* _timeline_dialog; }; diff --git a/src/wx/film_editor_panel.cc b/src/wx/film_editor_panel.cc new file mode 100644 index 000000000..a637df1fe --- /dev/null +++ b/src/wx/film_editor_panel.cc @@ -0,0 +1,34 @@ +/* + Copyright (C) 2012-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 <wx/notebook.h> +#include "film_editor_panel.h" +#include "film_editor.h" + +using boost::shared_ptr; + +FilmEditorPanel::FilmEditorPanel (FilmEditor* e, wxString name) + : wxPanel (e->content_notebook (), wxID_ANY) + , _editor (e) + , _sizer (new wxBoxSizer (wxVERTICAL)) +{ + e->content_notebook()->AddPage (this, name, false); + SetSizer (_sizer); +} + diff --git a/src/lib/delay_line.h b/src/wx/film_editor_panel.h index fa2870ae7..8acb3efb6 100644 --- a/src/lib/delay_line.h +++ b/src/wx/film_editor_panel.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + Copyright (C) 2012-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 @@ -17,22 +17,27 @@ */ +#ifndef DCPOMATIC_FILM_EDITOR_PANEL_H +#define DCPOMATIC_FILM_EDITOR_PANEL_H + #include <boost/shared_ptr.hpp> -#include "processor.h" +#include <wx/wx.h> +#include "lib/film.h" -class AudioBuffers; +class FilmEditor; +class Content; -/** A delay line for audio */ -class DelayLine : public AudioProcessor +class FilmEditorPanel : public wxPanel { public: - DelayLine (Log* log, int channels, int frames); - - void process_audio (boost::shared_ptr<AudioBuffers>); - void process_end (); - -private: - boost::shared_ptr<AudioBuffers> _buffers; - int _negative_delay_remaining; ///< number of frames of negative delay that remain to emit - int _frames; + FilmEditorPanel (FilmEditor *, wxString); + + virtual void film_changed (Film::Property) {} + virtual void film_content_changed (boost::shared_ptr<Content>, int) = 0; + +protected: + FilmEditor* _editor; + wxSizer* _sizer; }; + +#endif diff --git a/src/wx/film_list.cc b/src/wx/film_list.cc deleted file mode 100644 index 05d9734f6..000000000 --- a/src/wx/film_list.cc +++ /dev/null @@ -1,65 +0,0 @@ -/* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. - -*/ - -#include <boost/filesystem.hpp> -#include "lib/film.h" -#include "film_list.h" - -using namespace std; -using namespace boost; - -FilmList::FilmList (string d) - : _directory (d) - , _list (1) -{ - for (filesystem::directory_iterator i = filesystem::directory_iterator (_directory); i != filesystem::directory_iterator(); ++i) { - if (is_directory (*i)) { - filesystem::path m = filesystem::path (*i) / filesystem::path ("metadata"); - if (is_regular_file (m)) { - Film* f = new Film (i->path().string()); - _films.push_back (f); - } - } - } - - for (vector<Film const *>::iterator i = _films.begin(); i != _films.end(); ++i) { - _list.append_text ((*i)->name ()); - } - - _list.set_headers_visible (false); - _list.get_selection()->signal_changed().connect (bind (&FilmList::selection_changed, this)); -} - -Gtk::Widget& -FilmList::widget () -{ - return _list; -} - -void -FilmList::selection_changed () -{ - Gtk::ListViewText::SelectionList s = _list.get_selected (); - if (s.empty ()) { - return; - } - - assert (s[0] < int (_films.size ())); - SelectionChanged (_films[s[0]]); -} diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index 3d8198457..3ba7ee7ce 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -25,42 +25,50 @@ #include <iomanip> #include <wx/tglbtn.h> #include "lib/film.h" -#include "lib/format.h" +#include "lib/ratio.h" #include "lib/util.h" #include "lib/job_manager.h" -#include "lib/options.h" -#include "lib/subtitle.h" #include "lib/image.h" #include "lib/scaler.h" #include "lib/exceptions.h" #include "lib/examine_content_job.h" +#include "lib/filter.h" +#include "lib/player.h" +#include "lib/video_content.h" +#include "lib/ffmpeg_content.h" +#include "lib/still_image_content.h" +#include "lib/video_decoder.h" #include "film_viewer.h" #include "wx_util.h" -#include "video_decoder.h" using std::string; using std::pair; +using std::min; using std::max; using std::cout; using std::list; +using std::make_pair; using boost::shared_ptr; +using boost::dynamic_pointer_cast; +using boost::weak_ptr; +using libdcp::Size; FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p) : wxPanel (p) , _panel (new wxPanel (this)) , _slider (new wxSlider (this, wxID_ANY, 0, 0, 4096)) - , _play_button (new wxToggleButton (this, wxID_ANY, wxT ("Play"))) + , _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) - , _out_width (0) - , _out_height (0) - , _panel_width (0) - , _panel_height (0) - , _clear_required (false) { +#ifndef __WXOSX__ _panel->SetDoubleBuffered (true); -#if wxMAJOR_VERSION == 2 && wxMINOR_VERSION >= 9 +#endif + _panel->SetBackgroundStyle (wxBG_STYLE_PAINT); -#endif _v_sizer = new wxBoxSizer (wxVERTICAL); SetSizer (_v_sizer); @@ -68,18 +76,32 @@ FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p) _v_sizer->Add (_panel, 1, wxEXPAND); wxBoxSizer* h_sizer = new wxBoxSizer (wxHORIZONTAL); + + wxBoxSizer* time_sizer = new wxBoxSizer (wxVERTICAL); + time_sizer->Add (_frame_number, 0, wxEXPAND); + time_sizer->Add (_timecode, 0, wxEXPAND); + + h_sizer->Add (_back_button, 0, wxALL, 2); + h_sizer->Add (time_sizer, 0, wxEXPAND); + h_sizer->Add (_forward_button, 0, wxALL, 2); h_sizer->Add (_play_button, 0, wxEXPAND); h_sizer->Add (_slider, 1, wxEXPAND); - _v_sizer->Add (h_sizer, 0, wxEXPAND); + _v_sizer->Add (h_sizer, 0, wxEXPAND | wxALL, 6); + + _frame_number->SetMinSize (wxSize (84, -1)); + _back_button->SetMinSize (wxSize (32, -1)); + _forward_button->SetMinSize (wxSize (32, -1)); - _panel->Connect (wxID_ANY, wxEVT_PAINT, wxPaintEventHandler (FilmViewer::paint_panel), 0, this); - _panel->Connect (wxID_ANY, wxEVT_SIZE, wxSizeEventHandler (FilmViewer::panel_sized), 0, this); - _slider->Connect (wxID_ANY, wxEVT_SCROLL_THUMBTRACK, wxScrollEventHandler (FilmViewer::slider_moved), 0, this); - _slider->Connect (wxID_ANY, wxEVT_SCROLL_PAGEUP, wxScrollEventHandler (FilmViewer::slider_moved), 0, this); - _slider->Connect (wxID_ANY, wxEVT_SCROLL_PAGEDOWN, wxScrollEventHandler (FilmViewer::slider_moved), 0, this); - _play_button->Connect (wxID_ANY, wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler (FilmViewer::play_clicked), 0, this); - _timer.Connect (wxID_ANY, wxEVT_TIMER, wxTimerEventHandler (FilmViewer::timer), 0, this); + _panel->Bind (wxEVT_PAINT, boost::bind (&FilmViewer::paint_panel, this)); + _panel->Bind (wxEVT_SIZE, boost::bind (&FilmViewer::panel_sized, this, _1)); + _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)); + _play_button->Bind (wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, boost::bind (&FilmViewer::play_clicked, this)); + _timer.Bind (wxEVT_TIMER, boost::bind (&FilmViewer::timer, this)); + _back_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmViewer::back_clicked, this)); + _forward_button->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&FilmViewer::forward_clicked, this)); set_film (f); @@ -89,95 +111,66 @@ FilmViewer::FilmViewer (shared_ptr<Film> f, wxWindow* p) } void -FilmViewer::film_changed (Film::Property p) -{ - switch (p) { - case Film::FORMAT: - calculate_sizes (); - update_from_raw (); - break; - case Film::CONTENT: - { - shared_ptr<DecodeOptions> o (new DecodeOptions); - o->decode_audio = false; - o->decode_subtitles = true; - o->video_sync = false; - _decoders = decoder_factory (_film, o, 0); - _decoders.video->Video.connect (bind (&FilmViewer::process_video, this, _1, _2, _3)); - _decoders.video->OutputChanged.connect (boost::bind (&FilmViewer::decoder_changed, this)); - _decoders.video->set_subtitle_stream (_film->subtitle_stream()); - calculate_sizes (); - get_frame (); - _panel->Refresh (); - _slider->Show (_film->content_type() == VIDEO); - _play_button->Show (_film->content_type() == VIDEO); - _v_sizer->Layout (); - break; - } - case Film::WITH_SUBTITLES: - case Film::SUBTITLE_OFFSET: - case Film::SUBTITLE_SCALE: - case Film::SCALER: - update_from_raw (); - break; - case Film::SUBTITLE_STREAM: - _decoders.video->set_subtitle_stream (_film->subtitle_stream ()); - break; - default: - break; - } -} - -void FilmViewer::set_film (shared_ptr<Film> f) { if (_film == f) { return; } - + _film = f; + _frame.reset (); + _queue.clear (); + + _slider->SetValue (0); + set_position_text (0); + if (!_film) { return; } - _film->Changed.connect (boost::bind (&FilmViewer::film_changed, this, _1)); + _player = f->make_player (); + _player->disable_audio (); + _player->Video.connect (boost::bind (&FilmViewer::process_video, this, _1, _2, _5)); + _player->Changed.connect (boost::bind (&FilmViewer::player_changed, this, _1)); - film_changed (Film::CONTENT); - film_changed (Film::CROP); - film_changed (Film::FORMAT); - film_changed (Film::WITH_SUBTITLES); - film_changed (Film::SUBTITLE_OFFSET); - film_changed (Film::SUBTITLE_SCALE); - film_changed (Film::SUBTITLE_STREAM); + calculate_sizes (); + fetch_current_frame_again (); } void -FilmViewer::decoder_changed () +FilmViewer::fetch_current_frame_again () { - if (_decoders.video->seek_to_last ()) { + if (!_player) { return; } - get_frame (); - _panel->Refresh (); - _panel->Update (); + /* Player::video_position is the time after the last frame that we received. + We want to see it again, so seek back one frame. + */ + + Time p = _player->video_position() - _film->video_frames_to_time (1); + if (p < 0) { + p = 0; + } + + _player->seek (p, true); + fetch_next_frame (); } void -FilmViewer::timer (wxTimerEvent &) +FilmViewer::timer () { - if (!_film) { + if (!_player) { return; } - _panel->Refresh (); - _panel->Update (); + fetch_next_frame (); - get_frame (); + Time const len = _film->length (); - if (_film->length()) { - int const new_slider_position = 4096 * _decoders.video->last_source_time() / (_film->length().get() / _film->frames_per_second()); + if (len) { + int const new_slider_position = 4096 * _player->video_position() / len; if (new_slider_position != _slider->GetValue()) { _slider->SetValue (new_slider_position); } @@ -186,125 +179,88 @@ FilmViewer::timer (wxTimerEvent &) void -FilmViewer::paint_panel (wxPaintEvent &) +FilmViewer::paint_panel () { wxPaintDC dc (_panel); - if (_clear_required) { - dc.Clear (); - _clear_required = false; - } - - if (!_display_frame || !_film || !_out_width || !_out_height) { + if (!_frame || !_film || !_out_size.width || !_out_size.height) { dc.Clear (); return; } - wxImage frame (_out_width, _out_height, _display_frame->data()[0], true); + shared_ptr<Image> packed_frame (new Image (_frame, false)); + + wxImage frame (_out_size.width, _out_size.height, packed_frame->data()[0], true); wxBitmap frame_bitmap (frame); dc.DrawBitmap (frame_bitmap, 0, 0); - if (_film->with_subtitles() && _display_sub) { - wxImage sub (_display_sub->size().width, _display_sub->size().height, _display_sub->data()[0], _display_sub->alpha(), true); - wxBitmap sub_bitmap (sub); - dc.DrawBitmap (sub_bitmap, _display_sub_position.x, _display_sub_position.y); + if (_out_size.width < _panel_size.width) { + wxPen p (GetBackgroundColour ()); + wxBrush b (GetBackgroundColour ()); + dc.SetPen (p); + dc.SetBrush (b); + dc.DrawRectangle (_out_size.width, 0, _panel_size.width - _out_size.width, _panel_size.height); } + + if (_out_size.height < _panel_size.height) { + wxPen p (GetBackgroundColour ()); + wxBrush b (GetBackgroundColour ()); + dc.SetPen (p); + dc.SetBrush (b); + dc.DrawRectangle (0, _out_size.height, _panel_size.width, _panel_size.height - _out_size.height); + } } void -FilmViewer::slider_moved (wxScrollEvent &) +FilmViewer::slider_moved () { - if (!_film || !_film->length()) { - return; + if (_film && _player) { + _player->seek (_slider->GetValue() * _film->length() / 4096, false); + fetch_next_frame (); } - - if (_decoders.video->seek (_slider->GetValue() * _film->length().get() / (4096 * _film->frames_per_second()))) { - return; - } - - get_frame (); - _panel->Refresh (); - _panel->Update (); } void FilmViewer::panel_sized (wxSizeEvent& ev) { - _panel_width = ev.GetSize().GetWidth(); - _panel_height = ev.GetSize().GetHeight(); + _panel_size.width = ev.GetSize().GetWidth(); + _panel_size.height = ev.GetSize().GetHeight(); calculate_sizes (); - update_from_raw (); -} - -void -FilmViewer::update_from_raw () -{ - if (!_raw_frame) { - return; - } - - raw_to_display (); - - _panel->Refresh (); - _panel->Update (); + fetch_current_frame_again (); } void -FilmViewer::raw_to_display () -{ - if (!_raw_frame || _out_width < 64 || _out_height < 64 || !_film) { - return; - } - - Size old_size; - if (_display_frame) { - old_size = _display_frame->size(); - } - - /* Get a compacted image as we have to feed it to wxWidgets */ - _display_frame = _raw_frame->scale_and_convert_to_rgb (Size (_out_width, _out_height), 0, _film->scaler(), false); - - if (old_size != _display_frame->size()) { - _clear_required = true; - } - - if (_raw_sub) { - Rect tx = subtitle_transformed_area ( - float (_out_width) / _film->size().width, - float (_out_height) / _film->size().height, - _raw_sub->area(), _film->subtitle_offset(), _film->subtitle_scale() - ); - - _display_sub.reset (new RGBPlusAlphaImage (_raw_sub->image()->scale (tx.size(), _film->scaler(), false))); - _display_sub_position = tx.position(); - } else { - _display_sub.reset (); - } -} - -void FilmViewer::calculate_sizes () { - if (!_film) { + if (!_film || !_player) { return; } + + Ratio const * container = _film->container (); - float const panel_ratio = static_cast<float> (_panel_width) / _panel_height; - float const film_ratio = _film->format() ? _film->format()->ratio_as_float(_film) : 1.78; + float const panel_ratio = static_cast<float> (_panel_size.width) / _panel_size.height; + float const film_ratio = container ? container->ratio () : 1.78; + if (panel_ratio < film_ratio) { /* panel is less widscreen than the film; clamp width */ - _out_width = _panel_width; - _out_height = _out_width / film_ratio; + _out_size.width = _panel_size.width; + _out_size.height = _out_size.width / film_ratio; } else { - /* panel is more widescreen than the film; clamp heignt */ - _out_height = _panel_height; - _out_width = _out_height * film_ratio; + /* panel is more widescreen than the film; clamp height */ + _out_size.height = _panel_size.height; + _out_size.width = _out_size.height * film_ratio; } + + /* Catch silly values */ + _out_size.width = max (64, _out_size.width); + _out_size.height = max (64, _out_size.height); + + _player->set_video_container_size (_out_size); } void -FilmViewer::play_clicked (wxCommandEvent &) +FilmViewer::play_clicked () { check_play_state (); } @@ -312,48 +268,88 @@ FilmViewer::play_clicked (wxCommandEvent &) void FilmViewer::check_play_state () { - if (!_film) { + if (!_film || _film->video_frame_rate() == 0) { return; } if (_play_button->GetValue()) { - _timer.Start (1000 / _film->frames_per_second()); + _timer.Start (1000 / _film->video_frame_rate()); } else { _timer.Stop (); } } void -FilmViewer::process_video (shared_ptr<Image> image, bool, shared_ptr<Subtitle> sub) +FilmViewer::process_video (shared_ptr<const Image> image, Eyes eyes, Time t) { - _raw_frame = image; - _raw_sub = sub; + if (eyes == EYES_RIGHT) { + return; + } + + if (_got_frame) { + /* This is an additional frame emitted by a single pass. Store it. */ + _queue.push_front (make_pair (image, t)); + return; + } + + _frame = image; + _got_frame = true; - raw_to_display (); + set_position_text (t); +} - _got_frame = true; +void +FilmViewer::set_position_text (Time t) +{ + if (!_film) { + _frame_number->SetLabel ("0"); + _timecode->SetLabel ("0:0:0.0"); + return; + } + + 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)); + + double w = static_cast<double>(t) / TIME_HZ; + int const h = (w / 3600); + w -= h * 3600; + int const m = (w / 60); + w -= m * 60; + int const s = floor (w); + w -= s; + int const f = rint (w * fps); + _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::get_frame () +FilmViewer::fetch_next_frame () { - /* Clear our raw frame in case we don't get a new one */ - _raw_frame.reset (); + /* Clear our frame in case we don't get a new one */ + _frame.reset (); + + if (!_player) { + return; + } + + _got_frame = false; - try { - _got_frame = false; - while (!_got_frame) { - if (_decoders.video->pass ()) { - /* We didn't get a frame before the decoder gave up, - so clear our display frame. - */ - _display_frame.reset (); - break; - } + if (!_queue.empty ()) { + process_video (_queue.back().first, EYES_BOTH, _queue.back().second); + _queue.pop_back (); + } else { + 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 (DecodeError& e) { - error_dialog (this, String::compose ("Could not decode video for view (%1)", e.what())); } + + _panel->Refresh (); + _panel->Update (); } void @@ -376,3 +372,43 @@ FilmViewer::active_jobs_changed (bool a) _play_button->Enable (!a); } +void +FilmViewer::back_clicked () +{ + if (!_player) { + return; + } + + /* 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 (); +} + +void +FilmViewer::forward_clicked () +{ + if (!_player) { + return; + } + + fetch_next_frame (); +} + +void +FilmViewer::player_changed (bool frequent) +{ + if (frequent) { + return; + } + + calculate_sizes (); + fetch_current_frame_again (); +} diff --git a/src/wx/film_viewer.h b/src/wx/film_viewer.h index 6029c04f3..7bda9617e 100644 --- a/src/wx/film_viewer.h +++ b/src/wx/film_viewer.h @@ -23,16 +23,27 @@ #include <wx/wx.h> #include "lib/film.h" -#include "lib/decoder_factory.h" class wxToggleButton; class FFmpegPlayer; class Image; class RGBPlusAlphaImage; -class Subtitle; /** @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 { @@ -42,41 +53,42 @@ public: void set_film (boost::shared_ptr<Film>); private: - void film_changed (Film::Property); - void paint_panel (wxPaintEvent &); + void paint_panel (); void panel_sized (wxSizeEvent &); - void slider_moved (wxScrollEvent &); - void play_clicked (wxCommandEvent &); - void timer (wxTimerEvent &); - void process_video (boost::shared_ptr<Image>, bool, boost::shared_ptr<Subtitle>); + void slider_moved (); + void play_clicked (); + void timer (); + void process_video (boost::shared_ptr<const Image>, Eyes, Time); void calculate_sizes (); void check_play_state (); - void update_from_raw (); - void decoder_changed (); - void raw_to_display (); - void get_frame (); + 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); boost::shared_ptr<Film> _film; + boost::shared_ptr<Player> _player; - wxBoxSizer* _v_sizer; + wxSizer* _v_sizer; wxPanel* _panel; wxSlider* _slider; + wxButton* _back_button; + wxButton* _forward_button; + wxStaticText* _frame_number; + wxStaticText* _timecode; wxToggleButton* _play_button; wxTimer _timer; - Decoders _decoders; - boost::shared_ptr<Image> _raw_frame; - boost::shared_ptr<Subtitle> _raw_sub; - boost::shared_ptr<Image> _display_frame; - boost::shared_ptr<RGBPlusAlphaImage> _display_sub; - Position _display_sub_position; + boost::shared_ptr<const Image> _frame; bool _got_frame; - int _out_width; - int _out_height; - int _panel_width; - int _panel_height; + /** Size of our output (including padding if we have any) */ + libdcp::Size _out_size; + /** Size of the panel that we have available */ + libdcp::Size _panel_size; - bool _clear_required; + std::list<std::pair<boost::shared_ptr<const Image>, Time> > _queue; }; diff --git a/src/wx/filter_dialog.cc b/src/wx/filter_dialog.cc index 2abe53026..13907ae0c 100644 --- a/src/wx/filter_dialog.cc +++ b/src/wx/filter_dialog.cc @@ -23,14 +23,14 @@ #include "lib/film.h" #include "filter_dialog.h" -#include "filter_view.h" +#include "filter_editor.h" using namespace std; using boost::bind; FilterDialog::FilterDialog (wxWindow* parent, vector<Filter const *> const & f) : wxDialog (parent, wxID_ANY, wxString (_("Filters"))) - , _filters (new FilterView (this, f)) + , _filters (new FilterEditor (this, f)) { wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); sizer->Add (_filters, 1, wxEXPAND | wxALL, 6); diff --git a/src/wx/filter_dialog.h b/src/wx/filter_dialog.h index e76f8536b..d54e6f2e4 100644 --- a/src/wx/filter_dialog.h +++ b/src/wx/filter_dialog.h @@ -25,7 +25,8 @@ #include <boost/signals2.hpp> class Film; -class FilterView; +class FilterEditor; +class Filter; /** @class FilterDialog * @brief A dialog to select FFmpeg filters. @@ -40,5 +41,5 @@ public: private: void active_changed (); - FilterView* _filters; + FilterEditor* _filters; }; diff --git a/src/wx/filter_view.cc b/src/wx/filter_editor.cc index 8d9535d81..4dd18004b 100644 --- a/src/wx/filter_view.cc +++ b/src/wx/filter_editor.cc @@ -17,19 +17,19 @@ */ -/** @file src/filter_view.cc +/** @file src/filter_editor.cc * @brief A panel to select FFmpeg filters. */ #include <iostream> #include <algorithm> #include "lib/filter.h" -#include "filter_view.h" +#include "filter_editor.h" #include "wx_util.h" using namespace std; -FilterView::FilterView (wxWindow* parent, vector<Filter const *> const & active) +FilterEditor::FilterEditor (wxWindow* parent, vector<Filter const *> const & active) : wxPanel (parent) { wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); @@ -37,24 +37,49 @@ FilterView::FilterView (wxWindow* parent, vector<Filter const *> const & active) vector<Filter const *> filters = Filter::all (); + typedef map<string, list<Filter const *> > CategoryMap; + CategoryMap categories; + for (vector<Filter const *>::iterator i = filters.begin(); i != filters.end(); ++i) { - wxCheckBox* b = new wxCheckBox (this, wxID_ANY, std_to_wx ((*i)->name ())); - bool const a = find (active.begin(), active.end(), *i) != active.end (); - b->SetValue (a); - _filters[*i] = b; - b->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (FilterView::filter_toggled), 0, this); - sizer->Add (b); + CategoryMap::iterator j = categories.find ((*i)->category ()); + if (j == categories.end ()) { + list<Filter const *> c; + c.push_back (*i); + categories[(*i)->category()] = c; + } else { + j->second.push_back (*i); + } + } + + for (CategoryMap::iterator i = categories.begin(); i != categories.end(); ++i) { + + wxStaticText* c = new wxStaticText (this, wxID_ANY, std_to_wx (i->first)); + wxFont font = c->GetFont(); + font.SetWeight(wxFONTWEIGHT_BOLD); + c->SetFont(font); + sizer->Add (c); + + for (list<Filter const *>::iterator j = i->second.begin(); j != i->second.end(); ++j) { + wxCheckBox* b = new wxCheckBox (this, wxID_ANY, std_to_wx ((*j)->name ())); + bool const a = find (active.begin(), active.end(), *j) != active.end (); + b->SetValue (a); + _filters[*j] = b; + b->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&FilterEditor::filter_toggled, this)); + sizer->Add (b); + } + + sizer->AddSpacer (6); } } void -FilterView::filter_toggled (wxCommandEvent &) +FilterEditor::filter_toggled () { ActiveChanged (); } vector<Filter const*> -FilterView::active () const +FilterEditor::active () const { vector<Filter const *> active; for (map<Filter const *, wxCheckBox*>::const_iterator i = _filters.begin(); i != _filters.end(); ++i) { diff --git a/src/wx/filter_view.h b/src/wx/filter_editor.h index b8d5f644f..4e1d682d5 100644 --- a/src/wx/filter_view.h +++ b/src/wx/filter_editor.h @@ -17,7 +17,7 @@ */ -/** @file src/filter_view.h +/** @file src/filter_editor.h * @brief A panel to select FFmpeg filters. */ @@ -28,20 +28,20 @@ class Filter; -/** @class FilterView +/** @class FilterEditor * @brief A panel to select FFmpeg filters. */ -class FilterView : public wxPanel +class FilterEditor : public wxPanel { public: - FilterView (wxWindow *, std::vector<Filter const *> const &); + FilterEditor (wxWindow *, std::vector<Filter const *> const &); std::vector<Filter const *> active () const; boost::signals2::signal<void()> ActiveChanged; private: - void filter_toggled (wxCommandEvent &); + void filter_toggled (); std::map<Filter const *, wxCheckBox *> _filters; }; diff --git a/src/wx/gain_calculator_dialog.cc b/src/wx/gain_calculator_dialog.cc index 3f07faf06..f9880c044 100644 --- a/src/wx/gain_calculator_dialog.cc +++ b/src/wx/gain_calculator_dialog.cc @@ -24,21 +24,21 @@ using namespace boost; GainCalculatorDialog::GainCalculatorDialog (wxWindow* parent) - : wxDialog (parent, wxID_ANY, wxString (_("Gain Calculator"))) + : wxDialog (parent, wxID_ANY, _("Gain Calculator")) { - wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); table->AddGrowableCol (1, 1); - add_label_to_sizer (table, this, "I want to play this back at fader"); + add_label_to_sizer (table, this, _("I want to play this back at fader"), true); _wanted = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator (wxFILTER_NUMERIC)); table->Add (_wanted, 1, wxEXPAND); - add_label_to_sizer (table, this, "But I have to use fader"); + add_label_to_sizer (table, this, _("But I have to use fader"), true); _actual = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, wxDefaultSize, 0, wxTextValidator (wxFILTER_NUMERIC)); table->Add (_actual, 1, wxEXPAND); wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); - overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); if (buttons) { diff --git a/src/wx/job_manager_view.cc b/src/wx/job_manager_view.cc index 9c7040584..a95087b02 100644 --- a/src/wx/job_manager_view.cc +++ b/src/wx/job_manager_view.cc @@ -30,85 +30,173 @@ using std::string; using std::list; +using std::map; +using std::cout; using boost::shared_ptr; +using boost::weak_ptr; + +class JobRecord +{ +public: + JobRecord (shared_ptr<Job> job, wxScrolledWindow* window, wxPanel* panel, wxFlexGridSizer* table, bool pause) + : _job (job) + , _window (window) + , _panel (panel) + , _table (table) + { + int n = 0; + + wxStaticText* m = new wxStaticText (panel, wxID_ANY, std_to_wx (_job->name ())); + table->Insert (n, m, 0, wxALIGN_CENTER_VERTICAL | wxALL, 6); + ++n; + + _gauge = new wxGauge (panel, wxID_ANY, 100); + /* This seems to be required to allow the gauge to shrink under OS X */ + _gauge->SetMinSize (wxSize (0, -1)); + table->Insert (n, _gauge, 1, wxEXPAND | wxLEFT | wxRIGHT); + ++n; + + _message = new wxStaticText (panel, wxID_ANY, std_to_wx ("")); + table->Insert (n, _message, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6); + ++n; + + _cancel = new wxButton (panel, wxID_ANY, _("Cancel")); + _cancel->Bind (wxEVT_COMMAND_BUTTON_CLICKED, &JobRecord::cancel_clicked, this); + table->Insert (n, _cancel, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6); + ++n; + + if (pause) { + _pause = new wxButton (_panel, wxID_ANY, _("Pause")); + _pause->Bind (wxEVT_COMMAND_BUTTON_CLICKED, &JobRecord::pause_clicked, this); + table->Insert (n, _pause, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6); + ++n; + } + + _details = new wxButton (_panel, wxID_ANY, _("Details...")); + _details->Bind (wxEVT_COMMAND_BUTTON_CLICKED, &JobRecord::details_clicked, this); + _details->Enable (false); + table->Insert (n, _details, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6); + ++n; + + job->Progress.connect (boost::bind (&JobRecord::progress, this)); + job->Finished.connect (boost::bind (&JobRecord::finished, this)); + + table->Layout (); + panel->FitInside (); + } + + void maybe_pulse () + { + if (_job->running() && _job->progress_unknown ()) { + _gauge->Pulse (); + } + } + +private: + + void progress () + { + float const p = _job->overall_progress (); + if (p >= 0) { + checked_set (_message, _job->status ()); + _gauge->SetValue (p * 100); + } + + _table->Layout (); + _window->FitInside (); + } + + void finished () + { + checked_set (_message, _job->status ()); + if (!_job->finished_cancelled ()) { + _gauge->SetValue (100); + } + + _cancel->Enable (false); + if (!_job->error_details().empty ()) { + _details->Enable (true); + } + + _table->Layout (); + _window->FitInside (); + } + + void details_clicked (wxCommandEvent &) + { + string s = _job->error_summary(); + s[0] = toupper (s[0]); + error_dialog (_window, std_to_wx (String::compose ("%1.\n\n%2", s, _job->error_details()))); + } + + void cancel_clicked (wxCommandEvent &) + { + _job->cancel (); + } + + void pause_clicked (wxCommandEvent &) + { + if (_job->paused()) { + _job->resume (); + _pause->SetLabel (_("Pause")); + } else { + _job->pause (); + _pause->SetLabel (_("Resume")); + } + } + + boost::shared_ptr<Job> _job; + wxScrolledWindow* _window; + wxPanel* _panel; + wxFlexGridSizer* _table; + wxGauge* _gauge; + wxStaticText* _message; + wxButton* _cancel; + wxButton* _pause; + wxButton* _details; +}; /** Must be called in the GUI thread */ -JobManagerView::JobManagerView (wxWindow* parent) +JobManagerView::JobManagerView (wxWindow* parent, Buttons buttons) : wxScrolledWindow (parent) + , _buttons (buttons) { _panel = new wxPanel (this); wxSizer* sizer = new wxBoxSizer (wxVERTICAL); sizer->Add (_panel, 1, wxEXPAND); SetSizer (sizer); + + int N = 5; + if (buttons & PAUSE) { + ++N; + } - _table = new wxFlexGridSizer (3, 6, 6); + _table = new wxFlexGridSizer (N, 6, 6); _table->AddGrowableCol (1, 1); _panel->SetSizer (_table); SetScrollRate (0, 32); - Connect (wxID_ANY, wxEVT_TIMER, wxTimerEventHandler (JobManagerView::periodic), 0, this); + Bind (wxEVT_TIMER, boost::bind (&JobManagerView::periodic, this)); _timer.reset (new wxTimer (this)); _timer->Start (1000); - - update (); + + JobManager::instance()->JobAdded.connect (bind (&JobManagerView::job_added, this, _1)); } void -JobManagerView::periodic (wxTimerEvent &) +JobManagerView::job_added (weak_ptr<Job> j) { - update (); + shared_ptr<Job> job = j.lock (); + if (job) { + _job_records.push_back (shared_ptr<JobRecord> (new JobRecord (job, this, _panel, _table, _buttons & PAUSE))); + } } -/** Update the view by examining the state of each job. - * Must be called in the GUI thread. - */ void -JobManagerView::update () +JobManagerView::periodic () { - list<shared_ptr<Job> > jobs = JobManager::instance()->get (); - - int index = 0; - - for (list<shared_ptr<Job> >::iterator i = jobs.begin(); i != jobs.end(); ++i) { - - if (_job_records.find (*i) == _job_records.end ()) { - wxStaticText* m = new wxStaticText (_panel, wxID_ANY, std_to_wx ((*i)->name ())); - _table->Insert (index, m, 0, wxALIGN_CENTER_VERTICAL | wxALL, 6); - - JobRecord r; - r.finalised = false; - r.gauge = new wxGauge (_panel, wxID_ANY, 100); - _table->Insert (index + 1, r.gauge, 1, wxEXPAND | wxLEFT | wxRIGHT); - - r.message = new wxStaticText (_panel, wxID_ANY, std_to_wx ("")); - _table->Insert (index + 2, r.message, 1, wxALIGN_CENTER_VERTICAL | wxALL, 6); - - _job_records[*i] = r; - } - - string const st = (*i)->status (); - - if (!(*i)->finished ()) { - float const p = (*i)->overall_progress (); - if (p >= 0) { - _job_records[*i].message->SetLabel (std_to_wx (st)); - _job_records[*i].gauge->SetValue (p * 100); - } else { - _job_records[*i].message->SetLabel (wxT ("Running")); - _job_records[*i].gauge->Pulse (); - } - } - - if ((*i)->finished() && !_job_records[*i].finalised) { - _job_records[*i].gauge->SetValue (100); - _job_records[*i].message->SetLabel (std_to_wx (st)); - _job_records[*i].finalised = true; - } - - index += 3; + for (list<shared_ptr<JobRecord> >::iterator i = _job_records.begin(); i != _job_records.end(); ++i) { + (*i)->maybe_pulse (); } - - _table->Layout (); - FitInside (); } diff --git a/src/wx/job_manager_view.h b/src/wx/job_manager_view.h index 5c10890ef..c4bb1e218 100644 --- a/src/wx/job_manager_view.h +++ b/src/wx/job_manager_view.h @@ -26,6 +26,7 @@ #include <wx/wx.h> class Job; +class JobRecord; /** @class JobManagerView * @brief Class which is a wxPanel for showing the progress of jobs. @@ -33,21 +34,20 @@ class Job; class JobManagerView : public wxScrolledWindow { public: - JobManagerView (wxWindow *); - - void update (); + enum Buttons { + PAUSE = 0x1, + }; + + JobManagerView (wxWindow *, Buttons); private: - void periodic (wxTimerEvent &); + void job_added (boost::weak_ptr<Job>); + void periodic (); - boost::shared_ptr<wxTimer> _timer; wxPanel* _panel; wxFlexGridSizer* _table; - struct JobRecord { - wxGauge* gauge; - wxStaticText* message; - bool finalised; - }; + boost::shared_ptr<wxTimer> _timer; - std::map<boost::shared_ptr<Job>, JobRecord> _job_records; + std::list<boost::shared_ptr<JobRecord> > _job_records; + Buttons _buttons; }; diff --git a/src/wx/job_wrapper.cc b/src/wx/job_wrapper.cc index f2056cf49..df4aa7d2e 100644 --- a/src/wx/job_wrapper.cc +++ b/src/wx/job_wrapper.cc @@ -26,17 +26,17 @@ using boost::shared_ptr; void -JobWrapper::make_dcp (wxWindow* parent, shared_ptr<Film> film, bool transcode) +JobWrapper::make_dcp (wxWindow* parent, shared_ptr<Film> film) { if (!film) { return; } try { - film->make_dcp (transcode); + film->make_dcp (); } catch (BadSettingError& e) { - error_dialog (parent, String::compose ("Bad setting for %1 (%2)", e.setting(), e.what ())); + error_dialog (parent, wxString::Format (_("Bad setting for %s (%s)"), std_to_wx(e.setting()).data(), std_to_wx(e.what()).data())); } catch (std::exception& e) { - error_dialog (parent, String::compose ("Could not make DCP: %1", e.what ())); + error_dialog (parent, wxString::Format (_("Could not make DCP: %s"), std_to_wx(e.what()).data())); } } diff --git a/src/wx/job_wrapper.h b/src/wx/job_wrapper.h index 7120e9f10..b0a4693dd 100644 --- a/src/wx/job_wrapper.h +++ b/src/wx/job_wrapper.h @@ -24,6 +24,6 @@ class Film; namespace JobWrapper { -void make_dcp (wxWindow *, boost::shared_ptr<Film>, bool); +void make_dcp (wxWindow *, boost::shared_ptr<Film>); } diff --git a/src/wx/kdm_dialog.cc b/src/wx/kdm_dialog.cc index d94c13057..a9f63cffc 100644 --- a/src/wx/kdm_dialog.cc +++ b/src/wx/kdm_dialog.cc @@ -43,9 +43,6 @@ KDMDialog::KDMDialog (wxWindow* parent) : wxDialog (parent, wxID_ANY, _("Make KDMs")) { wxBoxSizer* vertical = new wxBoxSizer (wxVERTICAL); - - add_label_to_sizer (vertical, this, "Make KDMs for"); - wxBoxSizer* targets = new wxBoxSizer (wxHORIZONTAL); _targets = new wxTreeCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTR_HIDE_ROOT | wxTR_MULTIPLE | wxTR_HAS_BUTTONS); @@ -63,37 +60,37 @@ KDMDialog::KDMDialog (wxWindow* parent) wxBoxSizer* target_buttons = new wxBoxSizer (wxVERTICAL); _add_cinema = new wxButton (this, wxID_ANY, _("Add Cinema...")); - target_buttons->Add (_add_cinema, 1, 0, 6); + target_buttons->Add (_add_cinema, 1, wxEXPAND, 6); _edit_cinema = new wxButton (this, wxID_ANY, _("Edit Cinema...")); - target_buttons->Add (_edit_cinema, 1, 0, 6); + target_buttons->Add (_edit_cinema, 1, wxEXPAND, 6); _remove_cinema = new wxButton (this, wxID_ANY, _("Remove Cinema")); - target_buttons->Add (_remove_cinema, 1, 0, 6); + target_buttons->Add (_remove_cinema, 1, wxEXPAND, 6); _add_screen = new wxButton (this, wxID_ANY, _("Add Screen...")); - target_buttons->Add (_add_screen, 1, 0, 6); + target_buttons->Add (_add_screen, 1, wxEXPAND, 6); _edit_screen = new wxButton (this, wxID_ANY, _("Edit Screen...")); - target_buttons->Add (_edit_screen, 1, 0, 6); + target_buttons->Add (_edit_screen, 1, wxEXPAND, 6); _remove_screen = new wxButton (this, wxID_ANY, _("Remove Screen")); - target_buttons->Add (_remove_screen, 1, 0, 6); + target_buttons->Add (_remove_screen, 1, wxEXPAND, 6); targets->Add (target_buttons, 0, 0, 6); vertical->Add (targets, 1, wxEXPAND | wxALL, 6); wxFlexGridSizer* table = new wxFlexGridSizer (3, 2, 6); - add_label_to_sizer (table, this, "From"); + add_label_to_sizer (table, this, "From", true); _from_date = new wxDatePickerCtrl (this, wxID_ANY); table->Add (_from_date, 1, wxEXPAND); _from_time = new wxTimePickerCtrl (this, wxID_ANY); table->Add (_from_time, 1, wxEXPAND); - add_label_to_sizer (table, this, "Until"); + add_label_to_sizer (table, this, "Until", true); _until_date = new wxDatePickerCtrl (this, wxID_ANY); table->Add (_until_date, 1, wxEXPAND); _until_time = new wxTimePickerCtrl (this, wxID_ANY); table->Add (_until_time, 1, wxEXPAND); - add_label_to_sizer (table, this, "Write to"); + add_label_to_sizer (table, this, "Write to", true); #ifdef __WXMSW__ _folder = new DirPickerCtrl (this); @@ -105,7 +102,7 @@ KDMDialog::KDMDialog (wxWindow* parent) vertical->Add (table, 0, wxEXPAND | wxALL, 6); - wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL); if (buttons) { vertical->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); } diff --git a/src/wx/new_film_dialog.cc b/src/wx/new_film_dialog.cc index eb6f2849b..6a8935232 100644 --- a/src/wx/new_film_dialog.cc +++ b/src/wx/new_film_dialog.cc @@ -21,35 +21,43 @@ #include <wx/stdpaths.h> #include "lib/config.h" #include "new_film_dialog.h" -#ifdef __WXMSW__ +#include "wx_util.h" +#ifdef DCPOMATIC_USE_OWN_DIR_PICKER #include "dir_picker_ctrl.h" #endif -#include "wx_util.h" using namespace std; using namespace boost; +boost::optional<string> NewFilmDialog::_directory; + NewFilmDialog::NewFilmDialog (wxWindow* parent) - : wxDialog (parent, wxID_ANY, wxString (_("New Film"))) + : wxDialog (parent, wxID_ANY, _("New Film")) { wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); SetSizer (overall_sizer); - wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); table->AddGrowableCol (1, 1); - overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); - add_label_to_sizer (table, this, "Film name"); + add_label_to_sizer (table, this, _("Film name"), true); _name = new wxTextCtrl (this, wxID_ANY); - table->Add (_name, 1, wxEXPAND); + table->Add (_name, 0, wxEXPAND); - add_label_to_sizer (table, this, "Create in folder"); -#ifdef __WXMSW__ - _folder = new DirPickerCtrl (this); + add_label_to_sizer (table, this, _("Create in folder"), true); + +#ifdef DCPOMATIC_USE_OWN_DIR_PICKER + _folder = new DirPickerCtrl (this); #else - _folder = new wxDirPickerCtrl (this, wxDD_DIR_MUST_EXIST); + _folder = new wxDirPickerCtrl (this, wxID_ANY); #endif - _folder->SetPath (std_to_wx (Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())))); + + if (!_directory) { + _directory = Config::instance()->default_directory_or (wx_to_std (wxStandardPaths::Get().GetDocumentsDir())); + } + + _folder->SetPath (std_to_wx (_directory.get())); table->Add (_folder, 1, wxEXPAND); wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL); @@ -61,6 +69,11 @@ NewFilmDialog::NewFilmDialog (wxWindow* parent) overall_sizer->SetSizeHints (this); } +NewFilmDialog::~NewFilmDialog () +{ + _directory = wx_to_std (_folder->GetPath ()); +} + string NewFilmDialog::get_path () const { diff --git a/src/wx/new_film_dialog.h b/src/wx/new_film_dialog.h index 3d1253ecc..f8f3aa08d 100644 --- a/src/wx/new_film_dialog.h +++ b/src/wx/new_film_dialog.h @@ -19,6 +19,7 @@ #include <wx/wx.h> #include <wx/filepicker.h> +#include "wx_util.h" class DirPickerCtrl; @@ -26,14 +27,16 @@ class NewFilmDialog : public wxDialog { public: NewFilmDialog (wxWindow *); + ~NewFilmDialog (); std::string get_path () const; private: wxTextCtrl* _name; -#ifdef __WXMSW__ +#ifdef DCPOMATIC_USE_OWN_DIR_PICKER DirPickerCtrl* _folder; -#else +#else wxDirPickerCtrl* _folder; #endif + static boost::optional<std::string> _directory; }; diff --git a/src/wx/po/es_ES.po b/src/wx/po/es_ES.po new file mode 100644 index 000000000..08a1fde08 --- /dev/null +++ b/src/wx/po/es_ES.po @@ -0,0 +1,673 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: libdcpomatic-wx\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-02 19:08-0500\n" +"Last-Translator: Manuel AC <manuel.acevedo@civantos.>\n" +"Language-Team: Manuel AC <manuel.acevedo@civantos.com>\n" +"Language: es-ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/wx/film_editor.cc:426 src/wx/film_editor.cc:435 +msgid "%" +msgstr "%" + +#: src/wx/about_dialog.cc:77 +msgid "" +"(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen" +msgstr "" + +#: src/wx/config_dialog.cc:96 +msgid "(restart DCP-o-matic to see language changes)" +msgstr "" + +#: src/wx/film_editor.cc:1423 +msgid "1 channel" +msgstr "1 canal" + +#: src/wx/about_dialog.cc:30 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:299 +msgid "Add" +msgstr "Añadir" + +#: src/wx/film_editor.cc:317 +msgid "Add..." +msgstr "" + +#: src/wx/audio_dialog.cc:32 src/wx/film_editor.cc:343 +msgid "Audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:379 +msgid "Audio Delay" +msgstr "Retardo del audio" + +#: src/wx/film_editor.cc:367 +msgid "Audio Gain" +msgstr "Ganancia del audio" + +#: src/wx/dci_metadata_dialog.cc:33 +msgid "Audio Language (e.g. EN)" +msgstr "Idioma del audio (ej. ES)" + +#: src/wx/film_editor.cc:391 +#, fuzzy +msgid "Audio Stream" +msgstr "Retardo del audio" + +#: src/wx/job_wrapper.cc:38 +#, c-format +msgid "Bad setting for %s (%s)" +msgstr "Configuración erronea para %s (%s)" + +#: src/wx/film_editor.cc:264 +msgid "Bottom crop" +msgstr "Recortar abajo" + +#: src/wx/dir_picker_ctrl.cc:38 +msgid "Browse..." +msgstr "Explorar..." + +#: src/wx/gain_calculator_dialog.cc:36 +msgid "But I have to use fader" +msgstr "pero tengo que usar el fader a" + +#: src/wx/audio_mapping_view.cc:192 +msgid "C" +msgstr "" + +#: src/wx/film_editor.cc:376 +msgid "Calculate..." +msgstr "Calcular..." + +#: src/wx/job_manager_view.cc:98 +msgid "Cancel" +msgstr "" + +#: src/wx/audio_dialog.cc:43 +msgid "Channels" +msgstr "Canales" + +#: src/wx/film_editor.cc:1163 +msgid "Choose a file or files" +msgstr "" + +#: src/wx/film_editor.cc:131 +#, fuzzy +msgid "Container" +msgstr "Contenido" + +#: src/wx/film_editor.cc:82 +msgid "Content" +msgstr "Contenido" + +#: src/wx/film_editor.cc:136 +msgid "Content Type" +msgstr "Tipo de contenido" + +#: src/wx/audio_mapping_view.cc:181 +#, fuzzy +msgid "Content channel" +msgstr "1 canal" + +#: src/wx/film_viewer.cc:326 +#, c-format +msgid "Could not decode video for view (%s)" +msgstr "No se pudo decodificar el vídeo para mostrarlo (%s)" + +#: src/wx/job_wrapper.cc:40 +#, c-format +msgid "Could not make DCP: %s" +msgstr "No se pudo crear el DCP: %s" + +#: src/wx/new_film_dialog.cc:48 +msgid "Create in folder" +msgstr "Crear en carpeta" + +#: src/wx/config_dialog.cc:260 +#, fuzzy +msgid "Creator" +msgstr "Crear en carpeta" + +#: src/wx/film_editor.cc:1322 +#, c-format +msgid "Cropped to %dx%d (%.2f:1)\n" +msgstr "" + +#: src/wx/dci_metadata_dialog.cc:28 +msgid "DCI name" +msgstr "Nombre DCI" + +#: src/wx/film_editor.cc:84 +msgid "DCP" +msgstr "" + +#: src/wx/film_editor.cc:142 +msgid "DCP Frame Rate" +msgstr "Velocidad DCP" + +#: src/wx/film_editor.cc:115 +msgid "DCP Name" +msgstr "Nombre DCP" + +#: src/wx/film_editor.cc:152 +#, fuzzy +msgid "DCP audio channels" +msgstr "canales" + +#: src/wx/about_dialog.cc:44 src/wx/wx_util.cc:87 src/wx/wx_util.cc:95 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:46 +msgid "DCP-o-matic Preferences" +msgstr "Preferencias DCP-o-matic" + +#: src/wx/audio_dialog.cc:97 +#, fuzzy, c-format +msgid "DCP-o-matic audio - %s" +msgstr "Audio DCP-o-matic - %1" + +#: src/wx/config_dialog.cc:123 +msgid "Default DCI name details" +msgstr "Detalles por defecto del nombre DCI" + +#: src/wx/config_dialog.cc:138 +#, fuzzy +msgid "Default JPEG2000 bandwidth" +msgstr "Ancho de banda JPEG2000" + +#: src/wx/config_dialog.cc:128 +#, fuzzy +msgid "Default container" +msgstr "Tipo de contenido" + +#: src/wx/config_dialog.cc:133 +#, fuzzy +msgid "Default content type" +msgstr "Tipo de contenido" + +#: src/wx/config_dialog.cc:114 +msgid "Default directory for new films" +msgstr "Carpeta por defecto para nuevas películas" + +#: src/wx/config_dialog.cc:109 +#, fuzzy +msgid "Default duration of still images" +msgstr "Carpeta por defecto para nuevas películas" + +#: src/wx/film_editor.cc:127 src/wx/job_manager_view.cc:110 +msgid "Details..." +msgstr "Detalles..." + +#: src/wx/properties_dialog.cc:45 +msgid "Disk space required" +msgstr "Espacio requerido en disco" + +#: src/wx/imagemagick_content_dialog.cc:36 +msgid "Duration" +msgstr "Duración" + +#: src/wx/config_dialog.cc:301 +msgid "Edit" +msgstr "Editar" + +#: src/wx/config_dialog.cc:124 src/wx/film_editor.cc:288 +msgid "Edit..." +msgstr "Editar..." + +#: src/wx/config_dialog.cc:55 +#, fuzzy +msgid "Encoding servers" +msgstr "Servidores de codificación" + +#: src/wx/dci_metadata_dialog.cc:53 +msgid "Facility (e.g. DLA)" +msgstr "Compañía (ej. DLA)" + +#: src/wx/properties_dialog.cc:36 +msgid "Film Properties" +msgstr "Propiedades de la película" + +#: src/wx/new_film_dialog.cc:44 +msgid "Film name" +msgstr "Nombre de la película" + +#: src/wx/film_editor.cc:284 src/wx/filter_dialog.cc:32 +msgid "Filters" +msgstr "Filtros" + +#: src/wx/properties_dialog.cc:41 +msgid "Frames" +msgstr "Fotogramas" + +#: src/wx/properties_dialog.cc:49 +msgid "Frames already encoded" +msgstr "Fotogramas ya codificados" + +#: src/wx/about_dialog.cc:60 +msgid "Free, open-source DCP generation from almost anything." +msgstr "" + +#: src/wx/gain_calculator_dialog.cc:27 +msgid "Gain Calculator" +msgstr "Calculadora de ganancia" + +#: src/wx/properties_dialog.cc:56 +msgid "Gb" +msgstr "Gb" + +#: src/wx/server_dialog.cc:36 +msgid "Host name or IP address" +msgstr "Nombre o dirección IP" + +#: src/wx/film_editor.cc:1427 +msgid "Hz" +msgstr "Hz" + +#: src/wx/gain_calculator_dialog.cc:32 +msgid "I want to play this back at fader" +msgstr "Quiero reproducir con el fader a" + +#: src/wx/config_dialog.cc:217 src/wx/config_dialog.cc:288 +msgid "IP address" +msgstr "Dirección IP" + +#: src/wx/imagemagick_content_dialog.cc:29 +msgid "Image" +msgstr "" + +#: src/wx/config_dialog.cc:256 +msgid "Issuer" +msgstr "" + +#: src/wx/film_editor.cc:158 +msgid "JPEG2000 bandwidth" +msgstr "Ancho de banda JPEG2000" + +#: src/wx/audio_mapping_view.cc:184 +msgid "L" +msgstr "" + +#: src/wx/film_editor.cc:249 +msgid "Left crop" +msgstr "Recorte izquierda" + +#: src/wx/film_editor.cc:460 +msgid "Length" +msgstr "Longitud" + +#: src/wx/audio_mapping_view.cc:196 +msgid "Lfe" +msgstr "" + +#: src/wx/film_editor.cc:330 +msgid "Loop everything" +msgstr "" + +#: src/wx/audio_mapping_view.cc:200 +#, fuzzy +msgid "Ls" +msgstr "s" + +#: src/wx/config_dialog.cc:141 src/wx/film_editor.cc:162 +msgid "MBps" +msgstr "MBps" + +#: src/wx/config_dialog.cc:57 +msgid "Metadata" +msgstr "" + +#: src/wx/config_dialog.cc:53 +msgid "Miscellaneous" +msgstr "" + +#: src/wx/dir_picker_ctrl.cc:52 +msgid "My Documents" +msgstr "Mis documentos" + +#: src/wx/film_editor.cc:110 +msgid "Name" +msgstr "Nombre" + +#: src/wx/new_film_dialog.cc:35 +msgid "New Film" +msgstr "Nueva película" + +#: src/wx/film_editor.cc:286 src/wx/film_editor.cc:769 +msgid "None" +msgstr "Ninguno" + +#: src/wx/film_editor.cc:1309 +#, c-format +msgid "Original video is %dx%d (%.2f:1)\n" +msgstr "" + +#: src/wx/dci_metadata_dialog.cc:57 +msgid "Package Type (e.g. OV)" +msgstr "Tipo de paquete (ej. OV)" + +#: src/wx/film_editor.cc:1343 +#, c-format +msgid "Padded with black to %dx%d (%.2f:1)\n" +msgstr "" + +#: src/wx/config_dialog.cc:229 +#, fuzzy +msgid "Password" +msgstr "Clave del TMS" + +#: src/wx/job_manager_view.cc:104 src/wx/job_manager_view.cc:206 +msgid "Pause" +msgstr "" + +#: src/wx/audio_dialog.cc:59 +msgid "Peak" +msgstr "Pico" + +#: src/wx/film_viewer.cc:64 +msgid "Play" +msgstr "Reproducir" + +#: src/wx/audio_plot.cc:110 +msgid "Please wait; audio is being analysed..." +msgstr "Por favor espere, el audio está siendo analizado..." + +#: src/wx/audio_mapping_view.cc:188 +msgid "R" +msgstr "" + +#: src/wx/audio_dialog.cc:60 +msgid "RMS" +msgstr "RMS" + +#: src/wx/dci_metadata_dialog.cc:45 +msgid "Rating (e.g. 15)" +msgstr "Clasificación (ej. 16)" + +#: src/wx/config_dialog.cc:303 src/wx/film_editor.cc:319 +msgid "Remove" +msgstr "Quitar" + +#: src/wx/job_manager_view.cc:209 +msgid "Resume" +msgstr "" + +#: src/wx/film_editor.cc:254 +msgid "Right crop" +msgstr "Recorte derecha" + +#: src/wx/audio_mapping_view.cc:204 +#, fuzzy +msgid "Rs" +msgstr "s" + +#: src/wx/job_manager_view.cc:128 +msgid "Running" +msgstr "Ejecutando" + +#: src/wx/film_editor.cc:269 +#, fuzzy +msgid "Scale to" +msgstr "Escalador" + +#: src/wx/film_editor.cc:1335 +#, c-format +msgid "Scaled to %dx%d (%.2f:1)\n" +msgstr "" + +#: src/wx/film_editor.cc:167 +msgid "Scaler" +msgstr "Escalador" + +#: src/wx/server_dialog.cc:25 +msgid "Server" +msgstr "Servidor" + +#: src/wx/config_dialog.cc:85 +msgid "Set language" +msgstr "" + +#: src/wx/film_editor.cc:362 +msgid "Show Audio..." +msgstr "Mostrar audio..." + +#: src/wx/audio_dialog.cc:70 +msgid "Smoothing" +msgstr "Suavizado" + +#: src/wx/film_editor.cc:457 +#, fuzzy +msgid "Start time" +msgstr "Inicio" + +#: src/wx/dci_metadata_dialog.cc:49 +msgid "Studio (e.g. TCF)" +msgstr "Estudio (ej. TCF)" + +#: src/wx/dci_metadata_dialog.cc:37 +msgid "Subtitle Language (e.g. FR)" +msgstr "Idioma del subtítulo (ej. EN)" + +#: src/wx/film_editor.cc:422 +msgid "Subtitle Offset" +msgstr "Desplazamiento del subtítulo" + +#: src/wx/film_editor.cc:431 +msgid "Subtitle Scale" +msgstr "Escala del subtítulo" + +#: src/wx/film_editor.cc:439 +#, fuzzy +msgid "Subtitle Stream" +msgstr "Escala del subtítulo" + +#: src/wx/film_editor.cc:345 +msgid "Subtitles" +msgstr "Subtítulos" + +#: src/wx/about_dialog.cc:120 +msgid "Supported by" +msgstr "" + +#: src/wx/config_dialog.cc:59 +#, fuzzy +msgid "TMS" +msgstr "RMS" + +#: src/wx/config_dialog.cc:221 +#, fuzzy +msgid "Target path" +msgstr "Ruta en el TMS" + +#: src/wx/dci_metadata_dialog.cc:41 +msgid "Territory (e.g. UK)" +msgstr "Territorio (ej. ES)" + +#: src/wx/config_dialog.cc:292 +msgid "Threads" +msgstr "Hilos" + +#: src/wx/server_dialog.cc:40 +msgid "Threads to use" +msgstr "Hilos a utilizar" + +#: src/wx/config_dialog.cc:104 +msgid "Threads to use for encoding on this host" +msgstr "Hilos a utilizar para la codificación en esta máquina" + +#: src/wx/audio_plot.cc:140 +msgid "Time" +msgstr "Tiempo" + +#: src/wx/timeline_dialog.cc:32 +#, fuzzy +msgid "Timeline" +msgstr "Tiempo" + +#: src/wx/film_editor.cc:321 +msgid "Timeline..." +msgstr "" + +#: src/wx/film_editor.cc:347 +msgid "Timing" +msgstr "" + +#: src/wx/film_editor.cc:259 +msgid "Top crop" +msgstr "Recortar arriba" + +#: src/wx/about_dialog.cc:99 +msgid "Translated by" +msgstr "" + +#: src/wx/audio_dialog.cc:54 +msgid "Type" +msgstr "Tipo" + +#: src/wx/film_editor.cc:125 +msgid "Use DCI name" +msgstr "Usar el nombre DCI" + +#: src/wx/film_editor.cc:146 +msgid "Use best" +msgstr "Usar la mejor" + +#: src/wx/config_dialog.cc:225 +#, fuzzy +msgid "User name" +msgstr "Usar el nombre DCI" + +#: src/wx/film_editor.cc:341 +msgid "Video" +msgstr "Vídeo" + +#: src/wx/film_editor.cc:417 +msgid "With Subtitles" +msgstr "Con subtítulos" + +#: src/wx/about_dialog.cc:90 +msgid "Written by" +msgstr "" + +#: src/wx/timeline.cc:200 +#, fuzzy +msgid "audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:1425 +msgid "channels" +msgstr "canales" + +#: src/wx/properties_dialog.cc:50 +msgid "counting..." +msgstr "contando..." + +#: src/wx/film_editor.cc:372 +msgid "dB" +msgstr "dB" + +#. / TRANSLATORS: this is an abbreviation for milliseconds, the unit of time +#: src/wx/film_editor.cc:385 +msgid "ms" +msgstr "ms" + +#. / TRANSLATORS: this is an abbreviation for seconds, the unit of time +#: src/wx/config_dialog.cc:112 src/wx/imagemagick_content_dialog.cc:41 +msgid "s" +msgstr "s" + +#: src/wx/film_editor.cc:334 +msgid "times" +msgstr "" + +#: src/wx/timeline.cc:220 +#, fuzzy +msgid "video" +msgstr "Vídeo" + +#~ msgid "A/B" +#~ msgstr "A/B" + +#~ msgid "Colour look-up table" +#~ msgstr "Tabla de referencia de colores" + +#~ msgid "Could not open content file (%s)" +#~ msgstr "No se pudo abrir el fichero (%s)" + +#~ msgid "Could not set content: %s" +#~ msgstr "No se pudo establecer el contenido: %s" + +#, fuzzy +#~ msgid "DVD-o-matic Preferences" +#~ msgstr "Preferencias DVD-o-matic" + +#~ msgid "End" +#~ msgstr "Fin" + +#~ msgid "Film" +#~ msgstr "Película" + +#~ msgid "Format" +#~ msgstr "Formato" + +#~ msgid "Original Frame Rate" +#~ msgstr "Velocidad original" + +#, fuzzy +#~ msgid "Reference filters" +#~ msgstr "Filtros de referencia para A/B" + +#, fuzzy +#~ msgid "Reference scaler" +#~ msgstr "Escalador de referencia para A/B" + +#~ msgid "Select Audio File" +#~ msgstr "Seleccionar fichero de audio" + +#~ msgid "Select Content File" +#~ msgstr "Seleccionar fichero de contenido" + +#~ msgid "Trim frames" +#~ msgstr "Recortar fotogramas" + +#, fuzzy +#~ msgid "Trim method" +#~ msgstr "Recortar fotogramas" + +#~ msgid "Trust content's header" +#~ msgstr "Confiar en la cabecera del contenido" + +#~ msgid "Use content's audio" +#~ msgstr "Usar el audio del contenido" + +#~ msgid "Use external audio" +#~ msgstr "Usar audio externo" + +#~ msgid "frames" +#~ msgstr "fotogramas" + +#~ msgid "unknown" +#~ msgstr "desconocido" + +#~ msgid "TMS IP address" +#~ msgstr "Dirección IP del TMS" + +#~ msgid "TMS user name" +#~ msgstr "Usuario del TMS" + +#~ msgid "Original Size" +#~ msgstr "Tamaño original" diff --git a/src/wx/po/fr_FR.po b/src/wx/po/fr_FR.po new file mode 100644 index 000000000..1c0a02d4f --- /dev/null +++ b/src/wx/po/fr_FR.po @@ -0,0 +1,676 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic FRENCH\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-07-16 23:21+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/wx/film_editor.cc:426 +#: src/wx/film_editor.cc:435 +msgid "%" +msgstr "%" + +#: src/wx/about_dialog.cc:77 +msgid "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen" +msgstr "(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen" + +#: src/wx/config_dialog.cc:96 +msgid "(restart DCP-o-matic to see language changes)" +msgstr "(redémarrez DCP-o-matic pour que les changements de langue prennent effet)" + +#: src/wx/film_editor.cc:1423 +msgid "1 channel" +msgstr "1 canal" + +#: src/wx/about_dialog.cc:30 +msgid "About DCP-o-matic" +msgstr "À propos de DCP-o-matic" + +#: src/wx/config_dialog.cc:299 +msgid "Add" +msgstr "Ajouter" + +#: src/wx/film_editor.cc:317 +msgid "Add..." +msgstr "Ajouter..." + +#: src/wx/audio_dialog.cc:32 +#: src/wx/film_editor.cc:343 +msgid "Audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:379 +msgid "Audio Delay" +msgstr "Délai audio" + +#: src/wx/film_editor.cc:367 +msgid "Audio Gain" +msgstr "Gain audio" + +#: src/wx/dci_metadata_dialog.cc:33 +msgid "Audio Language (e.g. EN)" +msgstr "Langue audio (ex. FR)" + +#: src/wx/film_editor.cc:391 +msgid "Audio Stream" +msgstr "Flux audio" + +#: src/wx/job_wrapper.cc:38 +#, c-format +msgid "Bad setting for %s (%s)" +msgstr "Mauvais paramètre pour %s (%s)" + +#: src/wx/film_editor.cc:264 +msgid "Bottom crop" +msgstr "Découpe bas" + +#: src/wx/dir_picker_ctrl.cc:38 +msgid "Browse..." +msgstr "Parcourir..." + +#: src/wx/gain_calculator_dialog.cc:36 +msgid "But I have to use fader" +msgstr "Je souhaite utiliser ce volume" + +#: src/wx/audio_mapping_view.cc:192 +msgid "C" +msgstr "C" + +#: src/wx/film_editor.cc:376 +msgid "Calculate..." +msgstr "Calcul..." + +#: src/wx/job_manager_view.cc:98 +msgid "Cancel" +msgstr "Annuler" + +#: src/wx/audio_dialog.cc:43 +msgid "Channels" +msgstr "Canaux" + +#: src/wx/film_editor.cc:1163 +msgid "Choose a file or files" +msgstr "Choisissez un ou plusieurs fichiers" + +#: src/wx/film_editor.cc:131 +msgid "Container" +msgstr "Contenu" + +#: src/wx/film_editor.cc:82 +msgid "Content" +msgstr "Contenu" + +#: src/wx/film_editor.cc:136 +msgid "Content Type" +msgstr "Type de Contenu" + +#: src/wx/audio_mapping_view.cc:181 +msgid "Content channel" +msgstr "Contenu audio" + +#: src/wx/film_viewer.cc:326 +#, c-format +msgid "Could not decode video for view (%s)" +msgstr "Décodage de la vidéo pour visualisation impossible (%s)" + +#: src/wx/job_wrapper.cc:40 +#, c-format +msgid "Could not make DCP: %s" +msgstr "Impossible de créer le DCP : %s" + +#: src/wx/new_film_dialog.cc:48 +msgid "Create in folder" +msgstr "Créer dans le dossier" + +#: src/wx/config_dialog.cc:260 +msgid "Creator" +msgstr "Créateur" + +#: src/wx/film_editor.cc:1322 +#, c-format +msgid "Cropped to %dx%d (%.2f:1)\n" +msgstr "Découpe de %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:28 +msgid "DCI name" +msgstr "Nom DCI" + +#: src/wx/film_editor.cc:84 +msgid "DCP" +msgstr "DCP" + +#: src/wx/film_editor.cc:142 +msgid "DCP Frame Rate" +msgstr "Cadence image du DCP" + +#: src/wx/film_editor.cc:115 +msgid "DCP Name" +msgstr "Nom du DCP" + +#: src/wx/film_editor.cc:152 +msgid "DCP audio channels" +msgstr "canaux audios du DCP" + +#: src/wx/about_dialog.cc:44 +#: src/wx/wx_util.cc:87 +#: src/wx/wx_util.cc:95 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:46 +msgid "DCP-o-matic Preferences" +msgstr "Préférences DCP-o-matic" + +#: src/wx/audio_dialog.cc:97 +#, c-format +msgid "DCP-o-matic audio - %s" +msgstr "Son DCP-o-matic - %s" + +#: src/wx/config_dialog.cc:123 +msgid "Default DCI name details" +msgstr "Détails du nom DCI par défaut" + +#: src/wx/config_dialog.cc:138 +msgid "Default JPEG2000 bandwidth" +msgstr "Qualité JPEG2000 par défaut" + +#: src/wx/config_dialog.cc:128 +msgid "Default container" +msgstr "Type de contenu par défaut" + +#: src/wx/config_dialog.cc:133 +msgid "Default content type" +msgstr "Type de contenu par défaut" + +#: src/wx/config_dialog.cc:114 +msgid "Default directory for new films" +msgstr "Dossier par défaut des nouveaux films" + +#: src/wx/config_dialog.cc:109 +msgid "Default duration of still images" +msgstr "Durée par défaut des images fixes" + +#: src/wx/film_editor.cc:127 +#: src/wx/job_manager_view.cc:110 +msgid "Details..." +msgstr "Détails..." + +#: src/wx/properties_dialog.cc:45 +msgid "Disk space required" +msgstr "Espace disque requis" + +#: src/wx/imagemagick_content_dialog.cc:36 +msgid "Duration" +msgstr "Durée" + +#: src/wx/config_dialog.cc:301 +msgid "Edit" +msgstr "Édition" + +#: src/wx/config_dialog.cc:124 +#: src/wx/film_editor.cc:288 +msgid "Edit..." +msgstr "Éditer..." + +#: src/wx/config_dialog.cc:55 +msgid "Encoding servers" +msgstr "Serveurs d'encodage" + +#: src/wx/dci_metadata_dialog.cc:53 +msgid "Facility (e.g. DLA)" +msgstr "Laboratoire (ex. DLA)" + +#: src/wx/properties_dialog.cc:36 +msgid "Film Properties" +msgstr "Propriétés du film" + +#: src/wx/new_film_dialog.cc:44 +msgid "Film name" +msgstr "Nom du Film" + +#: src/wx/film_editor.cc:284 +#: src/wx/filter_dialog.cc:32 +msgid "Filters" +msgstr "Filtres" + +#: src/wx/properties_dialog.cc:41 +msgid "Frames" +msgstr "Images" + +#: src/wx/properties_dialog.cc:49 +msgid "Frames already encoded" +msgstr "Images déjà encodées" + +#: src/wx/about_dialog.cc:60 +msgid "Free, open-source DCP generation from almost anything." +msgstr "Création de DCP libre et gratuit depuis presque tout." + +#: src/wx/gain_calculator_dialog.cc:27 +msgid "Gain Calculator" +msgstr "Calculateur de gain" + +#: src/wx/properties_dialog.cc:56 +msgid "Gb" +msgstr "Gb" + +#: src/wx/server_dialog.cc:36 +msgid "Host name or IP address" +msgstr "Nom de l'hôte ou adresse IP" + +#: src/wx/film_editor.cc:1427 +msgid "Hz" +msgstr "Hz" + +#: src/wx/gain_calculator_dialog.cc:32 +msgid "I want to play this back at fader" +msgstr "Je veux le jouer à ce volume" + +#: src/wx/config_dialog.cc:217 +#: src/wx/config_dialog.cc:288 +msgid "IP address" +msgstr "Adresse IP" + +#: src/wx/imagemagick_content_dialog.cc:29 +msgid "Image" +msgstr "Image" + +#: src/wx/config_dialog.cc:256 +msgid "Issuer" +msgstr "Emetteur" + +#: src/wx/film_editor.cc:158 +msgid "JPEG2000 bandwidth" +msgstr "Qualité JPEG2000" + +#: src/wx/audio_mapping_view.cc:184 +msgid "L" +msgstr "L" + +#: src/wx/film_editor.cc:249 +msgid "Left crop" +msgstr "Découpe gauche" + +#: src/wx/film_editor.cc:460 +msgid "Length" +msgstr "Longueur / durée" + +#: src/wx/audio_mapping_view.cc:196 +msgid "Lfe" +msgstr "Lfe" + +#: src/wx/film_editor.cc:330 +msgid "Loop everything" +msgstr "Tout mettre en boucle" + +#: src/wx/audio_mapping_view.cc:200 +msgid "Ls" +msgstr "Ls" + +#: src/wx/config_dialog.cc:141 +#: src/wx/film_editor.cc:162 +msgid "MBps" +msgstr "MBps" + +#: src/wx/config_dialog.cc:57 +msgid "Metadata" +msgstr "Métadonnées" + +#: src/wx/config_dialog.cc:53 +msgid "Miscellaneous" +msgstr "Divers" + +#: src/wx/dir_picker_ctrl.cc:52 +msgid "My Documents" +msgstr "Mes Documents" + +#: src/wx/film_editor.cc:110 +msgid "Name" +msgstr "Nom" + +#: src/wx/new_film_dialog.cc:35 +msgid "New Film" +msgstr "Nouveau Film" + +#: src/wx/film_editor.cc:286 +#: src/wx/film_editor.cc:769 +msgid "None" +msgstr "Aucun" + +#: src/wx/film_editor.cc:1309 +#, c-format +msgid "Original video is %dx%d (%.2f:1)\n" +msgstr "La vidéo originale est %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:57 +msgid "Package Type (e.g. OV)" +msgstr "Type de paquet (ex. OV)" + +#: src/wx/film_editor.cc:1343 +#, c-format +msgid "Padded with black to %dx%d (%.2f:1)\n" +msgstr "Enveloppe noire de %dx%d (%.2f:1)\n" + +#: src/wx/config_dialog.cc:229 +msgid "Password" +msgstr "Mot de passe" + +#: src/wx/job_manager_view.cc:104 +#: src/wx/job_manager_view.cc:206 +msgid "Pause" +msgstr "Pause" + +#: src/wx/audio_dialog.cc:59 +msgid "Peak" +msgstr "Crête" + +#: src/wx/film_viewer.cc:64 +msgid "Play" +msgstr "Lecture" + +#: src/wx/audio_plot.cc:110 +msgid "Please wait; audio is being analysed..." +msgstr "Merci de patienter ; analyse de la piste son..." + +#: src/wx/audio_mapping_view.cc:188 +msgid "R" +msgstr "R" + +#: src/wx/audio_dialog.cc:60 +msgid "RMS" +msgstr "RMS" + +#: src/wx/dci_metadata_dialog.cc:45 +msgid "Rating (e.g. 15)" +msgstr "Rating (ex. 15)" + +#: src/wx/config_dialog.cc:303 +#: src/wx/film_editor.cc:319 +msgid "Remove" +msgstr "Supprimer" + +#: src/wx/job_manager_view.cc:209 +msgid "Resume" +msgstr "Reprendre" + +#: src/wx/film_editor.cc:254 +msgid "Right crop" +msgstr "Découpe droite" + +#: src/wx/audio_mapping_view.cc:204 +msgid "Rs" +msgstr "Rs" + +#: src/wx/job_manager_view.cc:128 +msgid "Running" +msgstr "Progression" + +#: src/wx/film_editor.cc:269 +msgid "Scale to" +msgstr "Mise à l'échelle" + +#: src/wx/film_editor.cc:1335 +#, c-format +msgid "Scaled to %dx%d (%.2f:1)\n" +msgstr "Mis à l'échelle de %dx%d (%.2f:1)\n" + +#: src/wx/film_editor.cc:167 +msgid "Scaler" +msgstr "Mise à l'échelle" + +#: src/wx/server_dialog.cc:25 +msgid "Server" +msgstr "Serveur" + +#: src/wx/config_dialog.cc:85 +msgid "Set language" +msgstr "Selectionnez la langue" + +#: src/wx/film_editor.cc:362 +msgid "Show Audio..." +msgstr "Analyser le son..." + +#: src/wx/audio_dialog.cc:70 +msgid "Smoothing" +msgstr "Lissage" + +#: src/wx/film_editor.cc:457 +msgid "Start time" +msgstr "Début" + +#: src/wx/dci_metadata_dialog.cc:49 +msgid "Studio (e.g. TCF)" +msgstr "Studio (ex. TCF)" + +#: src/wx/dci_metadata_dialog.cc:37 +msgid "Subtitle Language (e.g. FR)" +msgstr "Langue de sous-titres (ex. FR)" + +#: src/wx/film_editor.cc:422 +msgid "Subtitle Offset" +msgstr "Décalage du sous-titre" + +#: src/wx/film_editor.cc:431 +msgid "Subtitle Scale" +msgstr "Taille du sous-titre" + +#: src/wx/film_editor.cc:439 +msgid "Subtitle Stream" +msgstr "Flux de sous-titre" + +#: src/wx/film_editor.cc:345 +msgid "Subtitles" +msgstr "Sous-titres" + +#: src/wx/about_dialog.cc:120 +msgid "Supported by" +msgstr "Soutenu par" + +#: src/wx/config_dialog.cc:59 +msgid "TMS" +msgstr "TMS" + +#: src/wx/config_dialog.cc:221 +msgid "Target path" +msgstr "Chemin d'accès" + +#: src/wx/dci_metadata_dialog.cc:41 +msgid "Territory (e.g. UK)" +msgstr "Territoire (ex. FR)" + +#: src/wx/config_dialog.cc:292 +msgid "Threads" +msgstr "Processus" + +#: src/wx/server_dialog.cc:40 +msgid "Threads to use" +msgstr "Nombre de processus à utiliser" + +#: src/wx/config_dialog.cc:104 +msgid "Threads to use for encoding on this host" +msgstr "Nombre de processus à utiliser sur cet hôte" + +#: src/wx/audio_plot.cc:140 +msgid "Time" +msgstr "Durée" + +#: src/wx/timeline_dialog.cc:32 +msgid "Timeline" +msgstr "Timeline" + +#: src/wx/film_editor.cc:321 +msgid "Timeline..." +msgstr "Timeline..." + +#: src/wx/film_editor.cc:347 +msgid "Timing" +msgstr "" + +#: src/wx/film_editor.cc:259 +msgid "Top crop" +msgstr "Découpe haut" + +#: src/wx/about_dialog.cc:99 +msgid "Translated by" +msgstr "Traduit par" + +#: src/wx/audio_dialog.cc:54 +msgid "Type" +msgstr "Type" + +#: src/wx/film_editor.cc:125 +msgid "Use DCI name" +msgstr "Utiliser le nom DCI" + +#: src/wx/film_editor.cc:146 +msgid "Use best" +msgstr "Automatique" + +#: src/wx/config_dialog.cc:225 +msgid "User name" +msgstr "Nom d'utilisateur" + +#: src/wx/film_editor.cc:341 +msgid "Video" +msgstr "Vidéo" + +#: src/wx/film_editor.cc:417 +msgid "With Subtitles" +msgstr "Avec sous-titres" + +#: src/wx/about_dialog.cc:90 +msgid "Written by" +msgstr "Développé par" + +#: src/wx/timeline.cc:200 +msgid "audio" +msgstr "audio" + +#: src/wx/film_editor.cc:1425 +msgid "channels" +msgstr "canaux" + +#: src/wx/properties_dialog.cc:50 +msgid "counting..." +msgstr "calcul..." + +#: src/wx/film_editor.cc:372 +msgid "dB" +msgstr "dB" + +#. / TRANSLATORS: this is an abbreviation for milliseconds, the unit of time +#: src/wx/film_editor.cc:385 +msgid "ms" +msgstr "ms" + +#. / TRANSLATORS: this is an abbreviation for seconds, the unit of time +#: src/wx/config_dialog.cc:112 +#: src/wx/imagemagick_content_dialog.cc:41 +msgid "s" +msgstr "s" + +#: src/wx/film_editor.cc:334 +msgid "times" +msgstr "" + +#: src/wx/timeline.cc:220 +msgid "video" +msgstr "vidéo" + +#~ msgid "A/B" +#~ msgstr "A/B" + +#~ msgid "A/B mode" +#~ msgstr "A/B mode" + +#~ msgid "Audio will be resampled from %dHz to %dHz\n" +#~ msgstr "L'audio sera rééchantillonné de %dHz à %dHz\n" + +#~ msgid "Colour look-up table" +#~ msgstr "Espace colorimétrique" + +#~ msgid "Could not open content file (%s)" +#~ msgstr "Ouverture du contenu impossible (%s)" + +#~ msgid "Could not set content: %s" +#~ msgstr "Sélectionner du contenu impossible : %s" + +#, fuzzy +#~ msgid "DVD-o-matic Preferences" +#~ msgstr "Préférences de DCP-o-matic" + +#~ msgid "Default format" +#~ msgstr "Format par défaut" + +#~ msgid "End" +#~ msgstr "Fin" + +#~ msgid "Film" +#~ msgstr "Film" + +#~ msgid "Format" +#~ msgstr "Format" + +#~ msgid "Original Frame Rate" +#~ msgstr "Cadence d'images originale" + +#~ msgid "Reference filters" +#~ msgstr "Filtres de référence" + +#~ msgid "Reference scaler" +#~ msgstr "Échelle de référence" + +#~ msgid "Select Audio File" +#~ msgstr "Sélectionner le fichier son" + +#~ msgid "Select Content File" +#~ msgstr "Sélectionner le fichier vidéo" + +#~ msgid "Trim frames" +#~ msgstr "Images coupées" + +#~ msgid "Trim method" +#~ msgstr "Méthod de découpage" + +#~ msgid "Trust content's header" +#~ msgstr "Faire confiance à l'en-tête" + +#~ msgid "Use content's audio" +#~ msgstr "Utiliser le son intégré" + +#~ msgid "Use external audio" +#~ msgstr "Utiliser une source audio externe" + +#~ msgid "encode all frames and play the subset" +#~ msgstr "encoder toutes les images mais lire seulement la sélection" + +#~ msgid "encode only the subset" +#~ msgstr "encoder seulement la sélection" + +#~ msgid "frames" +#~ msgstr "images" + +#~ msgid "pixels" +#~ msgstr "pixels" + +#~ msgid "unknown" +#~ msgstr "inconnu" + +#~ msgid "TMS IP address" +#~ msgstr "Adresse IP du TMS" + +#~ msgid "TMS user name" +#~ msgstr "Nom d'utilisateur du TMS" + +#~ msgid "Original Size" +#~ msgstr "Taille Originale" diff --git a/src/wx/po/it_IT.po b/src/wx/po/it_IT.po new file mode 100644 index 000000000..fd5b6502c --- /dev/null +++ b/src/wx/po/it_IT.po @@ -0,0 +1,684 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: IT VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-28 10:27+0100\n" +"Last-Translator: Maci <macibro@gmail.com>\n" +"Language-Team: \n" +"Language: Italiano\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/wx/film_editor.cc:426 src/wx/film_editor.cc:435 +msgid "%" +msgstr "%" + +#: src/wx/about_dialog.cc:77 +msgid "" +"(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen" +msgstr "" + +#: src/wx/config_dialog.cc:96 +msgid "(restart DCP-o-matic to see language changes)" +msgstr "(riavviare DCP-o-matic per vedere i cambiamenti di lingua)" + +#: src/wx/film_editor.cc:1423 +msgid "1 channel" +msgstr "1 canale" + +#: src/wx/about_dialog.cc:30 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:299 +msgid "Add" +msgstr "Aggiungi" + +#: src/wx/film_editor.cc:317 +msgid "Add..." +msgstr "" + +#: src/wx/audio_dialog.cc:32 src/wx/film_editor.cc:343 +msgid "Audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:379 +msgid "Audio Delay" +msgstr "Ritardo dell'audio" + +#: src/wx/film_editor.cc:367 +msgid "Audio Gain" +msgstr "Guadagno dell'audio" + +#: src/wx/dci_metadata_dialog.cc:33 +msgid "Audio Language (e.g. EN)" +msgstr "Lingua dell'audio (es. EN)" + +#: src/wx/film_editor.cc:391 +#, fuzzy +msgid "Audio Stream" +msgstr "Ritardo dell'audio" + +#: src/wx/job_wrapper.cc:38 +#, c-format +msgid "Bad setting for %s (%s)" +msgstr "Valore sbagliato per %s (%s)" + +#: src/wx/film_editor.cc:264 +msgid "Bottom crop" +msgstr "Taglio in basso" + +#: src/wx/dir_picker_ctrl.cc:38 +msgid "Browse..." +msgstr "Sfoglia..." + +#: src/wx/gain_calculator_dialog.cc:36 +msgid "But I have to use fader" +msgstr "Ma dovrò riprodurre con il fader a" + +#: src/wx/audio_mapping_view.cc:192 +msgid "C" +msgstr "" + +#: src/wx/film_editor.cc:376 +msgid "Calculate..." +msgstr "Calcola..." + +#: src/wx/job_manager_view.cc:98 +msgid "Cancel" +msgstr "Annulla" + +#: src/wx/audio_dialog.cc:43 +msgid "Channels" +msgstr "Canali" + +#: src/wx/film_editor.cc:1163 +msgid "Choose a file or files" +msgstr "" + +#: src/wx/film_editor.cc:131 +#, fuzzy +msgid "Container" +msgstr "Contenuto" + +#: src/wx/film_editor.cc:82 +msgid "Content" +msgstr "Contenuto" + +#: src/wx/film_editor.cc:136 +msgid "Content Type" +msgstr "Tipo di contenuto" + +#: src/wx/audio_mapping_view.cc:181 +#, fuzzy +msgid "Content channel" +msgstr "1 canale" + +#: src/wx/film_viewer.cc:326 +#, c-format +msgid "Could not decode video for view (%s)" +msgstr "Non posso decodificare il video per guardarlo (%s)" + +#: src/wx/job_wrapper.cc:40 +#, c-format +msgid "Could not make DCP: %s" +msgstr "Non posso creare il DCP: %s" + +#: src/wx/new_film_dialog.cc:48 +msgid "Create in folder" +msgstr "Crea nella cartella" + +#: src/wx/config_dialog.cc:260 +#, fuzzy +msgid "Creator" +msgstr "Crea nella cartella" + +#: src/wx/film_editor.cc:1322 +#, c-format +msgid "Cropped to %dx%d (%.2f:1)\n" +msgstr "Tagliato da %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:28 +msgid "DCI name" +msgstr "Nome del DCP" + +#: src/wx/film_editor.cc:84 +msgid "DCP" +msgstr "" + +#: src/wx/film_editor.cc:142 +msgid "DCP Frame Rate" +msgstr "Frequenza fotogrammi del DCP" + +#: src/wx/film_editor.cc:115 +msgid "DCP Name" +msgstr "Nome del DCP" + +#: src/wx/film_editor.cc:152 +#, fuzzy +msgid "DCP audio channels" +msgstr "canali" + +#: src/wx/about_dialog.cc:44 src/wx/wx_util.cc:87 src/wx/wx_util.cc:95 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:46 +msgid "DCP-o-matic Preferences" +msgstr "Preferenze DCP-o-matic" + +#: src/wx/audio_dialog.cc:97 +#, c-format +msgid "DCP-o-matic audio - %s" +msgstr "Audio DCP-o-matic - %s" + +#: src/wx/config_dialog.cc:123 +msgid "Default DCI name details" +msgstr "Dettagli del nome di default DCI" + +#: src/wx/config_dialog.cc:138 +#, fuzzy +msgid "Default JPEG2000 bandwidth" +msgstr "Banda passante JPEG2000" + +#: src/wx/config_dialog.cc:128 +#, fuzzy +msgid "Default container" +msgstr "Tipo di contenuto" + +#: src/wx/config_dialog.cc:133 +#, fuzzy +msgid "Default content type" +msgstr "Tipo di contenuto" + +#: src/wx/config_dialog.cc:114 +msgid "Default directory for new films" +msgstr "Directory di default per i nuovi films" + +#: src/wx/config_dialog.cc:109 +#, fuzzy +msgid "Default duration of still images" +msgstr "Directory di default per i nuovi films" + +#: src/wx/film_editor.cc:127 src/wx/job_manager_view.cc:110 +msgid "Details..." +msgstr "Dettagli" + +#: src/wx/properties_dialog.cc:45 +msgid "Disk space required" +msgstr "Spazio su disco rischiesto" + +#: src/wx/imagemagick_content_dialog.cc:36 +msgid "Duration" +msgstr "Durata" + +#: src/wx/config_dialog.cc:301 +msgid "Edit" +msgstr "Modifica" + +#: src/wx/config_dialog.cc:124 src/wx/film_editor.cc:288 +msgid "Edit..." +msgstr "Modifica..." + +#: src/wx/config_dialog.cc:55 +#, fuzzy +msgid "Encoding servers" +msgstr "Servers di codifica" + +#: src/wx/dci_metadata_dialog.cc:53 +msgid "Facility (e.g. DLA)" +msgstr "Facility (es. DLA)" + +#: src/wx/properties_dialog.cc:36 +msgid "Film Properties" +msgstr "Proprietà del film" + +#: src/wx/new_film_dialog.cc:44 +msgid "Film name" +msgstr "Nome del film" + +#: src/wx/film_editor.cc:284 src/wx/filter_dialog.cc:32 +msgid "Filters" +msgstr "Filtri" + +#: src/wx/properties_dialog.cc:41 +msgid "Frames" +msgstr "Fotogrammi" + +#: src/wx/properties_dialog.cc:49 +msgid "Frames already encoded" +msgstr "Fotogrammi già codificati" + +#: src/wx/about_dialog.cc:60 +msgid "Free, open-source DCP generation from almost anything." +msgstr "" + +#: src/wx/gain_calculator_dialog.cc:27 +msgid "Gain Calculator" +msgstr "Calcolatore del guadagno audio" + +#: src/wx/properties_dialog.cc:56 +msgid "Gb" +msgstr "Gb" + +#: src/wx/server_dialog.cc:36 +msgid "Host name or IP address" +msgstr "Nome dell'Host o indirizzo IP" + +#: src/wx/film_editor.cc:1427 +msgid "Hz" +msgstr "Hz" + +#: src/wx/gain_calculator_dialog.cc:32 +msgid "I want to play this back at fader" +msgstr "Sto usando il fader a" + +#: src/wx/config_dialog.cc:217 src/wx/config_dialog.cc:288 +msgid "IP address" +msgstr "Indirizzo IP" + +#: src/wx/imagemagick_content_dialog.cc:29 +msgid "Image" +msgstr "" + +#: src/wx/config_dialog.cc:256 +msgid "Issuer" +msgstr "" + +#: src/wx/film_editor.cc:158 +msgid "JPEG2000 bandwidth" +msgstr "Banda passante JPEG2000" + +#: src/wx/audio_mapping_view.cc:184 +msgid "L" +msgstr "" + +#: src/wx/film_editor.cc:249 +msgid "Left crop" +msgstr "Taglio a sinistra" + +#: src/wx/film_editor.cc:460 +msgid "Length" +msgstr "Lunghezza" + +#: src/wx/audio_mapping_view.cc:196 +msgid "Lfe" +msgstr "" + +#: src/wx/film_editor.cc:330 +msgid "Loop everything" +msgstr "" + +#: src/wx/audio_mapping_view.cc:200 +#, fuzzy +msgid "Ls" +msgstr "s" + +#: src/wx/config_dialog.cc:141 src/wx/film_editor.cc:162 +msgid "MBps" +msgstr "MBps" + +#: src/wx/config_dialog.cc:57 +msgid "Metadata" +msgstr "" + +#: src/wx/config_dialog.cc:53 +msgid "Miscellaneous" +msgstr "" + +#: src/wx/dir_picker_ctrl.cc:52 +msgid "My Documents" +msgstr "Documenti" + +#: src/wx/film_editor.cc:110 +msgid "Name" +msgstr "Nome" + +#: src/wx/new_film_dialog.cc:35 +msgid "New Film" +msgstr "Nuovo Film" + +#: src/wx/film_editor.cc:286 src/wx/film_editor.cc:769 +msgid "None" +msgstr "Nessuno" + +#: src/wx/film_editor.cc:1309 +#, c-format +msgid "Original video is %dx%d (%.2f:1)\n" +msgstr "Il video originale è %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:57 +msgid "Package Type (e.g. OV)" +msgstr "Tipo di Package (es. OV)" + +#: src/wx/film_editor.cc:1343 +#, c-format +msgid "Padded with black to %dx%d (%.2f:1)\n" +msgstr "Riempito con nero a %dx%d (%.2f:1)\n" + +#: src/wx/config_dialog.cc:229 +#, fuzzy +msgid "Password" +msgstr "Password del TMS" + +#: src/wx/job_manager_view.cc:104 src/wx/job_manager_view.cc:206 +msgid "Pause" +msgstr "" + +#: src/wx/audio_dialog.cc:59 +msgid "Peak" +msgstr "Picco" + +#: src/wx/film_viewer.cc:64 +msgid "Play" +msgstr "Riproduci" + +#: src/wx/audio_plot.cc:110 +msgid "Please wait; audio is being analysed..." +msgstr "Attendere prego; sto analizzando l'audio..." + +#: src/wx/audio_mapping_view.cc:188 +msgid "R" +msgstr "" + +#: src/wx/audio_dialog.cc:60 +msgid "RMS" +msgstr "RMS" + +#: src/wx/dci_metadata_dialog.cc:45 +msgid "Rating (e.g. 15)" +msgstr "Classificazione (es. 15)" + +#: src/wx/config_dialog.cc:303 src/wx/film_editor.cc:319 +msgid "Remove" +msgstr "Rimuovi" + +#: src/wx/job_manager_view.cc:209 +msgid "Resume" +msgstr "" + +#: src/wx/film_editor.cc:254 +msgid "Right crop" +msgstr "Taglio a destra" + +#: src/wx/audio_mapping_view.cc:204 +#, fuzzy +msgid "Rs" +msgstr "s" + +#: src/wx/job_manager_view.cc:128 +msgid "Running" +msgstr "In corso" + +#: src/wx/film_editor.cc:269 +#, fuzzy +msgid "Scale to" +msgstr "Scaler" + +#: src/wx/film_editor.cc:1335 +#, c-format +msgid "Scaled to %dx%d (%.2f:1)\n" +msgstr "Scalato a %dx%d (%.2f:1)\n" + +#: src/wx/film_editor.cc:167 +msgid "Scaler" +msgstr "Scaler" + +#: src/wx/server_dialog.cc:25 +msgid "Server" +msgstr "Server" + +#: src/wx/config_dialog.cc:85 +msgid "Set language" +msgstr "Seleziona la lingua" + +#: src/wx/film_editor.cc:362 +msgid "Show Audio..." +msgstr "Mostra Audio..." + +#: src/wx/audio_dialog.cc:70 +msgid "Smoothing" +msgstr "Levigatura" + +#: src/wx/film_editor.cc:457 +#, fuzzy +msgid "Start time" +msgstr "Inizio" + +#: src/wx/dci_metadata_dialog.cc:49 +msgid "Studio (e.g. TCF)" +msgstr "Studio (es. TCF)" + +#: src/wx/dci_metadata_dialog.cc:37 +msgid "Subtitle Language (e.g. FR)" +msgstr "Lingua dei Sottotitoli (es. FR)" + +#: src/wx/film_editor.cc:422 +msgid "Subtitle Offset" +msgstr "Sfalsamento dei Sottotitoli" + +#: src/wx/film_editor.cc:431 +msgid "Subtitle Scale" +msgstr "Scala dei Sottotitoli" + +#: src/wx/film_editor.cc:439 +#, fuzzy +msgid "Subtitle Stream" +msgstr "Scala dei Sottotitoli" + +#: src/wx/film_editor.cc:345 +msgid "Subtitles" +msgstr "Sottotitoli" + +#: src/wx/about_dialog.cc:120 +msgid "Supported by" +msgstr "" + +#: src/wx/config_dialog.cc:59 +#, fuzzy +msgid "TMS" +msgstr "RMS" + +#: src/wx/config_dialog.cc:221 +#, fuzzy +msgid "Target path" +msgstr "Percorso di destinazione del TMS" + +#: src/wx/dci_metadata_dialog.cc:41 +msgid "Territory (e.g. UK)" +msgstr "Nazione (es. UK)" + +#: src/wx/config_dialog.cc:292 +msgid "Threads" +msgstr "Threads" + +#: src/wx/server_dialog.cc:40 +msgid "Threads to use" +msgstr "Threads da usare" + +#: src/wx/config_dialog.cc:104 +msgid "Threads to use for encoding on this host" +msgstr "Threads da usare per codificare su questo host" + +#: src/wx/audio_plot.cc:140 +msgid "Time" +msgstr "Tempo" + +#: src/wx/timeline_dialog.cc:32 +#, fuzzy +msgid "Timeline" +msgstr "Tempo" + +#: src/wx/film_editor.cc:321 +msgid "Timeline..." +msgstr "" + +#: src/wx/film_editor.cc:347 +msgid "Timing" +msgstr "" + +#: src/wx/film_editor.cc:259 +msgid "Top crop" +msgstr "Taglio in alto" + +#: src/wx/about_dialog.cc:99 +msgid "Translated by" +msgstr "" + +#: src/wx/audio_dialog.cc:54 +msgid "Type" +msgstr "Tipo" + +#: src/wx/film_editor.cc:125 +msgid "Use DCI name" +msgstr "Usa nome DCI" + +#: src/wx/film_editor.cc:146 +msgid "Use best" +msgstr "Usa la migliore" + +#: src/wx/config_dialog.cc:225 +#, fuzzy +msgid "User name" +msgstr "Usa nome DCI" + +#: src/wx/film_editor.cc:341 +msgid "Video" +msgstr "Video" + +#: src/wx/film_editor.cc:417 +msgid "With Subtitles" +msgstr "Con sottotitoli" + +#: src/wx/about_dialog.cc:90 +msgid "Written by" +msgstr "" + +#: src/wx/timeline.cc:200 +#, fuzzy +msgid "audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:1425 +msgid "channels" +msgstr "canali" + +#: src/wx/properties_dialog.cc:50 +msgid "counting..." +msgstr "conteggio..." + +#: src/wx/film_editor.cc:372 +msgid "dB" +msgstr "dB" + +#. / TRANSLATORS: this is an abbreviation for milliseconds, the unit of time +#: src/wx/film_editor.cc:385 +msgid "ms" +msgstr "ms" + +#. / TRANSLATORS: this is an abbreviation for seconds, the unit of time +#: src/wx/config_dialog.cc:112 src/wx/imagemagick_content_dialog.cc:41 +msgid "s" +msgstr "s" + +#: src/wx/film_editor.cc:334 +msgid "times" +msgstr "" + +#: src/wx/timeline.cc:220 +#, fuzzy +msgid "video" +msgstr "Video" + +#~ msgid "A/B" +#~ msgstr "A/B" + +#~ msgid "Audio will be resampled from %dHz to %dHz\n" +#~ msgstr "L'Audio sarà ricampionato da %dHz a %dHz\n" + +#~ msgid "Colour look-up table" +#~ msgstr "Tabella per ricerca del colore" + +#~ msgid "Could not open content file (%s)" +#~ msgstr "Non posso aprire il file del contenuto (%s)" + +#~ msgid "Could not set content: %s" +#~ msgstr "Non posso regolare il contenuto: %s" + +#, fuzzy +#~ msgid "DVD-o-matic Preferences" +#~ msgstr "Preferenze DVD-o-matic" + +#~ msgid "End" +#~ msgstr "Fine" + +#~ msgid "Film" +#~ msgstr "Film" + +#~ msgid "Format" +#~ msgstr "Formato" + +#~ msgid "Original Frame Rate" +#~ msgstr "Frequenza fotogrammi originale" + +#, fuzzy +#~ msgid "Reference filters" +#~ msgstr "Filtri di riferimento A/B" + +#, fuzzy +#~ msgid "Reference scaler" +#~ msgstr "Scalatura di riferimento A/B" + +#~ msgid "Select Audio File" +#~ msgstr "Seleziona file audio" + +#~ msgid "Select Content File" +#~ msgstr "Seleziona il file con il contenuto" + +#~ msgid "Trim frames" +#~ msgstr "Taglia fotogrammi" + +#~ msgid "Trim method" +#~ msgstr "Metodo di taglio" + +#~ msgid "Trust content's header" +#~ msgstr "Conferma l'intestazione del contenuto" + +#~ msgid "Use content's audio" +#~ msgstr "Usa l'audio del contenuto" + +#~ msgid "Use external audio" +#~ msgstr "Usa l'audio esterno" + +#~ msgid "encode all frames and play the subset" +#~ msgstr "Codifica tutti i fotogrammi e riproduci la selezione" + +#~ msgid "encode only the subset" +#~ msgstr "codifica solo la selezione" + +#~ msgid "frames" +#~ msgstr "fotogrammi" + +#~ msgid "pixels" +#~ msgstr "pizels" + +#~ msgid "unknown" +#~ msgstr "sconosciuto" + +#~ msgid "TMS IP address" +#~ msgstr "Indirizzo IP del TMS" + +#~ msgid "TMS user name" +#~ msgstr "Nome utente del TMS" + +#~ msgid "Original Size" +#~ msgstr "Dimensione Originale" diff --git a/src/wx/po/sv_SE.po b/src/wx/po/sv_SE.po new file mode 100644 index 000000000..c86fcc8f8 --- /dev/null +++ b/src/wx/po/sv_SE.po @@ -0,0 +1,679 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: DCP-o-matic\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-07-15 22:07+0100\n" +"PO-Revision-Date: 2013-04-09 10:13+0100\n" +"Last-Translator: Adam Klotblixt <adam.klotblixt@gmail.com>\n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +#: src/wx/film_editor.cc:426 src/wx/film_editor.cc:435 +msgid "%" +msgstr "%" + +#: src/wx/about_dialog.cc:77 +msgid "" +"(C) 2012-2013 Carl Hetherington, Terrence Meiczinger, Paul Davis, Ole Laursen" +msgstr "" + +#: src/wx/config_dialog.cc:96 +msgid "(restart DCP-o-matic to see language changes)" +msgstr "(starta om DCP-o-matic för att se språkändringar)" + +#: src/wx/film_editor.cc:1423 +msgid "1 channel" +msgstr "1 kanal" + +#: src/wx/about_dialog.cc:30 +#, fuzzy +msgid "About DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:299 +msgid "Add" +msgstr "Lägg till" + +#: src/wx/film_editor.cc:317 +msgid "Add..." +msgstr "" + +#: src/wx/audio_dialog.cc:32 src/wx/film_editor.cc:343 +msgid "Audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:379 +msgid "Audio Delay" +msgstr "Audio Fördröjning" + +#: src/wx/film_editor.cc:367 +msgid "Audio Gain" +msgstr "Audio Förstärkning" + +#: src/wx/dci_metadata_dialog.cc:33 +msgid "Audio Language (e.g. EN)" +msgstr "Audio Språk (ex. SV)" + +#: src/wx/film_editor.cc:391 +#, fuzzy +msgid "Audio Stream" +msgstr "Audio Fördröjning" + +#: src/wx/job_wrapper.cc:38 +#, c-format +msgid "Bad setting for %s (%s)" +msgstr "Felaktig inställning för %s (%s)" + +#: src/wx/film_editor.cc:264 +msgid "Bottom crop" +msgstr "Nedre beskärning" + +#: src/wx/dir_picker_ctrl.cc:38 +msgid "Browse..." +msgstr "Bläddra..." + +#: src/wx/gain_calculator_dialog.cc:36 +msgid "But I have to use fader" +msgstr "Men jag måste använda mixervolym" + +#: src/wx/audio_mapping_view.cc:192 +msgid "C" +msgstr "" + +#: src/wx/film_editor.cc:376 +msgid "Calculate..." +msgstr "Beräkna..." + +#: src/wx/job_manager_view.cc:98 +msgid "Cancel" +msgstr "Avbryt" + +#: src/wx/audio_dialog.cc:43 +msgid "Channels" +msgstr "Kanaler" + +#: src/wx/film_editor.cc:1163 +msgid "Choose a file or files" +msgstr "" + +#: src/wx/film_editor.cc:131 +#, fuzzy +msgid "Container" +msgstr "Innehåll" + +#: src/wx/film_editor.cc:82 +msgid "Content" +msgstr "Innehåll" + +#: src/wx/film_editor.cc:136 +msgid "Content Type" +msgstr "Innehållstyp" + +#: src/wx/audio_mapping_view.cc:181 +#, fuzzy +msgid "Content channel" +msgstr "1 kanal" + +#: src/wx/film_viewer.cc:326 +#, c-format +msgid "Could not decode video for view (%s)" +msgstr "Kunde inte avkoda video för visning (%s)" + +#: src/wx/job_wrapper.cc:40 +#, c-format +msgid "Could not make DCP: %s" +msgstr "Kunde inte skapa DCP: %s" + +#: src/wx/new_film_dialog.cc:48 +msgid "Create in folder" +msgstr "Skapa i katalog" + +#: src/wx/config_dialog.cc:260 +#, fuzzy +msgid "Creator" +msgstr "Skapa i katalog" + +#: src/wx/film_editor.cc:1322 +#, c-format +msgid "Cropped to %dx%d (%.2f:1)\n" +msgstr "Beskuren till %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:28 +msgid "DCI name" +msgstr "DCI namn" + +#: src/wx/film_editor.cc:84 +msgid "DCP" +msgstr "" + +#: src/wx/film_editor.cc:142 +msgid "DCP Frame Rate" +msgstr "DCP bildhastighet" + +#: src/wx/film_editor.cc:115 +msgid "DCP Name" +msgstr "DCP Namn" + +#: src/wx/film_editor.cc:152 +#, fuzzy +msgid "DCP audio channels" +msgstr "kanaler" + +#: src/wx/about_dialog.cc:44 src/wx/wx_util.cc:87 src/wx/wx_util.cc:95 +msgid "DCP-o-matic" +msgstr "DCP-o-matic" + +#: src/wx/config_dialog.cc:46 +msgid "DCP-o-matic Preferences" +msgstr "DCP-o-matic Inställningar" + +#: src/wx/audio_dialog.cc:97 +#, c-format +msgid "DCP-o-matic audio - %s" +msgstr "DCP-o-matic audio - %s" + +#: src/wx/config_dialog.cc:123 +msgid "Default DCI name details" +msgstr "Detaljer om förvalda DCI-namn" + +#: src/wx/config_dialog.cc:138 +#, fuzzy +msgid "Default JPEG2000 bandwidth" +msgstr "JPEG2000 bandbredd" + +#: src/wx/config_dialog.cc:128 +#, fuzzy +msgid "Default container" +msgstr "Innehållstyp" + +#: src/wx/config_dialog.cc:133 +#, fuzzy +msgid "Default content type" +msgstr "Innehållstyp" + +#: src/wx/config_dialog.cc:114 +msgid "Default directory for new films" +msgstr "Förvald katalog för nya filmer" + +#: src/wx/config_dialog.cc:109 +#, fuzzy +msgid "Default duration of still images" +msgstr "Förvald katalog för nya filmer" + +#: src/wx/film_editor.cc:127 src/wx/job_manager_view.cc:110 +msgid "Details..." +msgstr "Detaljer..." + +#: src/wx/properties_dialog.cc:45 +msgid "Disk space required" +msgstr "Diskutrymme som krävs" + +#: src/wx/imagemagick_content_dialog.cc:36 +msgid "Duration" +msgstr "Längd" + +#: src/wx/config_dialog.cc:301 +msgid "Edit" +msgstr "Redigera" + +#: src/wx/config_dialog.cc:124 src/wx/film_editor.cc:288 +msgid "Edit..." +msgstr "Redigera..." + +#: src/wx/config_dialog.cc:55 +#, fuzzy +msgid "Encoding servers" +msgstr "Kodningsservrar" + +#: src/wx/dci_metadata_dialog.cc:53 +msgid "Facility (e.g. DLA)" +msgstr "Företag (ex. DLA)" + +#: src/wx/properties_dialog.cc:36 +msgid "Film Properties" +msgstr "Film Egenskaper" + +#: src/wx/new_film_dialog.cc:44 +msgid "Film name" +msgstr "film namn" + +#: src/wx/film_editor.cc:284 src/wx/filter_dialog.cc:32 +msgid "Filters" +msgstr "Filter" + +#: src/wx/properties_dialog.cc:41 +msgid "Frames" +msgstr "Bildrutor" + +#: src/wx/properties_dialog.cc:49 +msgid "Frames already encoded" +msgstr "Bildrutor redan kodade" + +#: src/wx/about_dialog.cc:60 +msgid "Free, open-source DCP generation from almost anything." +msgstr "" + +#: src/wx/gain_calculator_dialog.cc:27 +msgid "Gain Calculator" +msgstr "Volym Kalkylator" + +#: src/wx/properties_dialog.cc:56 +msgid "Gb" +msgstr "Gb" + +#: src/wx/server_dialog.cc:36 +msgid "Host name or IP address" +msgstr "Värd-namn eller IP-adress" + +#: src/wx/film_editor.cc:1427 +msgid "Hz" +msgstr "Hz" + +#: src/wx/gain_calculator_dialog.cc:32 +msgid "I want to play this back at fader" +msgstr "Jag vill spela upp detta med mixervolym" + +#: src/wx/config_dialog.cc:217 src/wx/config_dialog.cc:288 +msgid "IP address" +msgstr "IP-adress" + +#: src/wx/imagemagick_content_dialog.cc:29 +msgid "Image" +msgstr "" + +#: src/wx/config_dialog.cc:256 +msgid "Issuer" +msgstr "" + +#: src/wx/film_editor.cc:158 +msgid "JPEG2000 bandwidth" +msgstr "JPEG2000 bandbredd" + +#: src/wx/audio_mapping_view.cc:184 +msgid "L" +msgstr "" + +#: src/wx/film_editor.cc:249 +msgid "Left crop" +msgstr "Vänster beskärning" + +#: src/wx/film_editor.cc:460 +msgid "Length" +msgstr "Längd" + +#: src/wx/audio_mapping_view.cc:196 +msgid "Lfe" +msgstr "" + +#: src/wx/film_editor.cc:330 +msgid "Loop everything" +msgstr "" + +#: src/wx/audio_mapping_view.cc:200 +#, fuzzy +msgid "Ls" +msgstr "s" + +#: src/wx/config_dialog.cc:141 src/wx/film_editor.cc:162 +msgid "MBps" +msgstr "MBps" + +#: src/wx/config_dialog.cc:57 +msgid "Metadata" +msgstr "" + +#: src/wx/config_dialog.cc:53 +msgid "Miscellaneous" +msgstr "" + +#: src/wx/dir_picker_ctrl.cc:52 +msgid "My Documents" +msgstr "Mina Dokument" + +#: src/wx/film_editor.cc:110 +msgid "Name" +msgstr "Namn" + +#: src/wx/new_film_dialog.cc:35 +msgid "New Film" +msgstr "Ny Film" + +#: src/wx/film_editor.cc:286 src/wx/film_editor.cc:769 +msgid "None" +msgstr "Inget" + +#: src/wx/film_editor.cc:1309 +#, c-format +msgid "Original video is %dx%d (%.2f:1)\n" +msgstr "Original-videon är %dx%d (%.2f:1)\n" + +#: src/wx/dci_metadata_dialog.cc:57 +msgid "Package Type (e.g. OV)" +msgstr "Förpackningstyp (ex. OV)" + +#: src/wx/film_editor.cc:1343 +#, c-format +msgid "Padded with black to %dx%d (%.2f:1)\n" +msgstr "Svarta kanter tillagda för %dx%d (%.2f:1)\n" + +#: src/wx/config_dialog.cc:229 +#, fuzzy +msgid "Password" +msgstr "TMS lösenord" + +#: src/wx/job_manager_view.cc:104 src/wx/job_manager_view.cc:206 +msgid "Pause" +msgstr "" + +#: src/wx/audio_dialog.cc:59 +msgid "Peak" +msgstr "Topp" + +#: src/wx/film_viewer.cc:64 +msgid "Play" +msgstr "Spela" + +#: src/wx/audio_plot.cc:110 +msgid "Please wait; audio is being analysed..." +msgstr "Vänligen vänta; audio analyseras..." + +#: src/wx/audio_mapping_view.cc:188 +msgid "R" +msgstr "" + +#: src/wx/audio_dialog.cc:60 +msgid "RMS" +msgstr "RMS" + +#: src/wx/dci_metadata_dialog.cc:45 +msgid "Rating (e.g. 15)" +msgstr "Klassificering (ex. 15)" + +#: src/wx/config_dialog.cc:303 src/wx/film_editor.cc:319 +msgid "Remove" +msgstr "Ta bort" + +#: src/wx/job_manager_view.cc:209 +msgid "Resume" +msgstr "" + +#: src/wx/film_editor.cc:254 +msgid "Right crop" +msgstr "Höger beskärning" + +#: src/wx/audio_mapping_view.cc:204 +#, fuzzy +msgid "Rs" +msgstr "s" + +#: src/wx/job_manager_view.cc:128 +msgid "Running" +msgstr "Körs" + +#: src/wx/film_editor.cc:269 +#, fuzzy +msgid "Scale to" +msgstr "Omskalare" + +#: src/wx/film_editor.cc:1335 +#, c-format +msgid "Scaled to %dx%d (%.2f:1)\n" +msgstr "Skalad till %dx%d (%.2f:1)\n" + +#: src/wx/film_editor.cc:167 +msgid "Scaler" +msgstr "Omskalare" + +#: src/wx/server_dialog.cc:25 +msgid "Server" +msgstr "Server" + +#: src/wx/config_dialog.cc:85 +msgid "Set language" +msgstr "Välj språk" + +#: src/wx/film_editor.cc:362 +msgid "Show Audio..." +msgstr "Visa Audio..." + +#: src/wx/audio_dialog.cc:70 +msgid "Smoothing" +msgstr "Utjämning" + +#: src/wx/film_editor.cc:457 +#, fuzzy +msgid "Start time" +msgstr "Start" + +#: src/wx/dci_metadata_dialog.cc:49 +msgid "Studio (e.g. TCF)" +msgstr "Studio (ex. TCF)" + +#: src/wx/dci_metadata_dialog.cc:37 +msgid "Subtitle Language (e.g. FR)" +msgstr "Undertextspråk (ex. SV)" + +#: src/wx/film_editor.cc:422 +msgid "Subtitle Offset" +msgstr "Undertext Förskjutning" + +#: src/wx/film_editor.cc:431 +msgid "Subtitle Scale" +msgstr "Undertext Skalning" + +#: src/wx/film_editor.cc:439 +#, fuzzy +msgid "Subtitle Stream" +msgstr "Undertext Skalning" + +#: src/wx/film_editor.cc:345 +msgid "Subtitles" +msgstr "Undertexter" + +#: src/wx/about_dialog.cc:120 +msgid "Supported by" +msgstr "" + +#: src/wx/config_dialog.cc:59 +#, fuzzy +msgid "TMS" +msgstr "RMS" + +#: src/wx/config_dialog.cc:221 +#, fuzzy +msgid "Target path" +msgstr "TMS målsökväg" + +#: src/wx/dci_metadata_dialog.cc:41 +msgid "Territory (e.g. UK)" +msgstr "Område (ex. SV)" + +#: src/wx/config_dialog.cc:292 +msgid "Threads" +msgstr "Trådar" + +#: src/wx/server_dialog.cc:40 +msgid "Threads to use" +msgstr "Antal trådar att använda" + +#: src/wx/config_dialog.cc:104 +msgid "Threads to use for encoding on this host" +msgstr "Antal trådar att använda vid kodning på denna maskin" + +#: src/wx/audio_plot.cc:140 +msgid "Time" +msgstr "Tid" + +#: src/wx/timeline_dialog.cc:32 +#, fuzzy +msgid "Timeline" +msgstr "Tid" + +#: src/wx/film_editor.cc:321 +msgid "Timeline..." +msgstr "" + +#: src/wx/film_editor.cc:347 +msgid "Timing" +msgstr "" + +#: src/wx/film_editor.cc:259 +msgid "Top crop" +msgstr "Övre beskärning" + +#: src/wx/about_dialog.cc:99 +msgid "Translated by" +msgstr "" + +#: src/wx/audio_dialog.cc:54 +msgid "Type" +msgstr "Typ" + +#: src/wx/film_editor.cc:125 +msgid "Use DCI name" +msgstr "Använd DCI-namnet" + +#: src/wx/film_editor.cc:146 +msgid "Use best" +msgstr "Använd bästa" + +#: src/wx/config_dialog.cc:225 +#, fuzzy +msgid "User name" +msgstr "Använd DCI-namnet" + +#: src/wx/film_editor.cc:341 +msgid "Video" +msgstr "Video" + +#: src/wx/film_editor.cc:417 +msgid "With Subtitles" +msgstr "Med Undertexter" + +#: src/wx/about_dialog.cc:90 +msgid "Written by" +msgstr "" + +#: src/wx/timeline.cc:200 +#, fuzzy +msgid "audio" +msgstr "Audio" + +#: src/wx/film_editor.cc:1425 +msgid "channels" +msgstr "kanaler" + +#: src/wx/properties_dialog.cc:50 +msgid "counting..." +msgstr "räknar..." + +#: src/wx/film_editor.cc:372 +msgid "dB" +msgstr "dB" + +#. / TRANSLATORS: this is an abbreviation for milliseconds, the unit of time +#: src/wx/film_editor.cc:385 +msgid "ms" +msgstr "ms" + +#. / TRANSLATORS: this is an abbreviation for seconds, the unit of time +#: src/wx/config_dialog.cc:112 src/wx/imagemagick_content_dialog.cc:41 +msgid "s" +msgstr "s" + +#: src/wx/film_editor.cc:334 +msgid "times" +msgstr "" + +#: src/wx/timeline.cc:220 +#, fuzzy +msgid "video" +msgstr "Video" + +#~ msgid "A/B" +#~ msgstr "A/B" + +#~ msgid "Audio will be resampled from %dHz to %dHz\n" +#~ msgstr "Audio kommer att samplas om från %dHz till %dHz\n" + +#~ msgid "Colour look-up table" +#~ msgstr "Färguppslagningstabell" + +#~ msgid "Could not open content file (%s)" +#~ msgstr "Kunde inte öppna innehållsfilen (%s)" + +#~ msgid "Could not set content: %s" +#~ msgstr "Kunde inte fastställa innehåll: %s" + +#, fuzzy +#~ msgid "DVD-o-matic Preferences" +#~ msgstr "DVD-o-matic Inställningar" + +#~ msgid "End" +#~ msgstr "Slut" + +#~ msgid "Film" +#~ msgstr "Film" + +#~ msgid "Format" +#~ msgstr "Format" + +#~ msgid "Original Frame Rate" +#~ msgstr "Ursprunglig bildhastighet" + +#, fuzzy +#~ msgid "Reference filters" +#~ msgstr "Referensfilter för A/B" + +#, fuzzy +#~ msgid "Reference scaler" +#~ msgstr "Referensomskalare för A/B" + +#~ msgid "Select Audio File" +#~ msgstr "Välj audiofil" + +#~ msgid "Select Content File" +#~ msgstr "Välj innehållsfil" + +#~ msgid "Trim frames" +#~ msgstr "Skippa bilder" + +#, fuzzy +#~ msgid "Trim method" +#~ msgstr "Skippa bilder" + +#~ msgid "Trust content's header" +#~ msgstr "Lita på källans information" + +#~ msgid "Use content's audio" +#~ msgstr "Använd innehållets audio" + +#~ msgid "Use external audio" +#~ msgstr "Använd extern audio" + +#~ msgid "frames" +#~ msgstr "bilder" + +#~ msgid "pixels" +#~ msgstr "pixlar" + +#~ msgid "unknown" +#~ msgstr "okänt" + +#~ msgid "TMS IP address" +#~ msgstr "TMS IP-adress" + +#~ msgid "TMS user name" +#~ msgstr "TMS användarnamn" + +#~ msgid "Original Size" +#~ msgstr "Ursprunglig Storlek" diff --git a/src/wx/preset_colour_conversion_dialog.cc b/src/wx/preset_colour_conversion_dialog.cc new file mode 100644 index 000000000..ce6897ecd --- /dev/null +++ b/src/wx/preset_colour_conversion_dialog.cc @@ -0,0 +1,69 @@ +/* + 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 <wx/statline.h> +#include "lib/colour_conversion.h" +#include "wx_util.h" +#include "preset_colour_conversion_dialog.h" +#include "colour_conversion_editor.h" + +using std::string; +using std::cout; + +PresetColourConversionDialog::PresetColourConversionDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("Colour conversion")) + , _editor (new ColourConversionEditor (this)) +{ + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + SetSizer (overall_sizer); + + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + add_label_to_sizer (table, this, _("Name"), true); + _name = new wxTextCtrl (this, wxID_ANY, wxT ("")); + table->Add (_name, 1, wxEXPAND | wxALL); + + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + overall_sizer->Add (new wxStaticLine (this, wxID_ANY), 0, wxEXPAND); + overall_sizer->Add (_editor); + + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); + if (buttons) { + overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + overall_sizer->Layout (); + overall_sizer->SetSizeHints (this); +} + +PresetColourConversion +PresetColourConversionDialog::get () const +{ + PresetColourConversion pc; + pc.name = wx_to_std (_name->GetValue ()); + pc.conversion = _editor->get (); + return pc; +} + +void +PresetColourConversionDialog::set (PresetColourConversion c) +{ + _name->SetValue (std_to_wx (c.name)); + _editor->set (c.conversion); +} diff --git a/src/wx/preset_colour_conversion_dialog.h b/src/wx/preset_colour_conversion_dialog.h new file mode 100644 index 000000000..4e612398c --- /dev/null +++ b/src/wx/preset_colour_conversion_dialog.h @@ -0,0 +1,35 @@ +/* + 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 <wx/wx.h> + +class ColourConversionEditor; + +class PresetColourConversionDialog : public wxDialog +{ +public: + PresetColourConversionDialog (wxWindow *); + + void set (PresetColourConversion); + PresetColourConversion get () const; + +private: + wxTextCtrl* _name; + ColourConversionEditor* _editor; +}; diff --git a/src/wx/properties_dialog.cc b/src/wx/properties_dialog.cc index b03c6b32c..a1ba81b3b 100644 --- a/src/wx/properties_dialog.cc +++ b/src/wx/properties_dialog.cc @@ -36,41 +36,28 @@ PropertiesDialog::PropertiesDialog (wxWindow* parent, shared_ptr<Film> film) : wxDialog (parent, wxID_ANY, _("Film Properties"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE) , _film (film) { - wxFlexGridSizer* table = new wxFlexGridSizer (2, 3, 6); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); - add_label_to_sizer (table, this, "Frames"); - _frames = new wxStaticText (this, wxID_ANY, std_to_wx ("")); + add_label_to_sizer (table, this, _("Frames"), true); + _frames = new wxStaticText (this, wxID_ANY, wxT ("")); table->Add (_frames, 1, wxALIGN_CENTER_VERTICAL); - add_label_to_sizer (table, this, "Disk space required for frames"); - _disk_for_frames = new wxStaticText (this, wxID_ANY, std_to_wx ("")); - table->Add (_disk_for_frames, 1, wxALIGN_CENTER_VERTICAL); - - add_label_to_sizer (table, this, "Total disk space required"); - _total_disk = new wxStaticText (this, wxID_ANY, std_to_wx ("")); - table->Add (_total_disk, 1, wxALIGN_CENTER_VERTICAL); + add_label_to_sizer (table, this, _("Disk space required"), true); + _disk = new wxStaticText (this, wxID_ANY, wxT ("")); + table->Add (_disk, 1, wxALIGN_CENTER_VERTICAL); - add_label_to_sizer (table, this, "Frames already encoded"); - _encoded = new ThreadedStaticText (this, "counting...", boost::bind (&PropertiesDialog::frames_already_encoded, this)); + add_label_to_sizer (table, this, _("Frames already encoded"), true); + _encoded = new ThreadedStaticText (this, _("counting..."), boost::bind (&PropertiesDialog::frames_already_encoded, this)); table->Add (_encoded, 1, wxALIGN_CENTER_VERTICAL); - if (_film->length()) { - _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->length().get()))); - double const disk = ((double) _film->j2k_bandwidth() / 8) * _film->length().get() / (_film->frames_per_second () * 1073741824); - stringstream s; - s << fixed << setprecision (1) << disk << "Gb"; - _disk_for_frames->SetLabel (std_to_wx (s.str ())); - stringstream t; - t << fixed << setprecision (1) << (disk * 2) << "Gb"; - _total_disk->SetLabel (std_to_wx (t.str ())); - } else { - _frames->SetLabel (_("unknown")); - _disk_for_frames->SetLabel (_("unknown")); - _total_disk->SetLabel (_("unknown")); - } + _frames->SetLabel (std_to_wx (lexical_cast<string> (_film->time_to_video_frames (_film->length())))); + double const disk = ((double) _film->j2k_bandwidth() / 8) * _film->length() / (TIME_HZ * 1073741824.0f); + stringstream s; + s << fixed << setprecision (1) << disk << wx_to_std (_("Gb")); + _disk->SetLabel (std_to_wx (s.str ())); wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); - overall_sizer->Add (table, 0, wxALL, 6); + overall_sizer->Add (table, 0, wxALL, DCPOMATIC_DIALOG_BORDER); wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); if (buttons) { @@ -91,9 +78,9 @@ PropertiesDialog::frames_already_encoded () const return ""; } - if (_film->dcp_length()) { + if (_film->length()) { /* XXX: encoded_frames() should check which frames have been encoded */ - u << " (" << ((_film->encoded_frames() - _film->dcp_trim_start()) * 100 / _film->dcp_length().get()) << "%)"; + u << " (" << (_film->encoded_frames() * 100 / _film->time_to_video_frames (_film->length())) << "%)"; } return u.str (); } diff --git a/src/wx/properties_dialog.h b/src/wx/properties_dialog.h index 308c0f7b3..cae929e18 100644 --- a/src/wx/properties_dialog.h +++ b/src/wx/properties_dialog.h @@ -32,8 +32,7 @@ private: boost::shared_ptr<Film> _film; wxStaticText* _frames; - wxStaticText* _disk_for_frames; - wxStaticText* _total_disk; + wxStaticText* _disk; ThreadedStaticText* _encoded; }; diff --git a/src/wx/repeat_dialog.cc b/src/wx/repeat_dialog.cc new file mode 100644 index 000000000..3721c61b9 --- /dev/null +++ b/src/wx/repeat_dialog.cc @@ -0,0 +1,54 @@ +/* + Copyright (C) 2013 Carl Hetherington <cth@carlh.net> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include "repeat_dialog.h" +#include "wx_util.h" + +RepeatDialog::RepeatDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("Repeat Content")) +{ + wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); + SetSizer (overall_sizer); + + wxFlexGridSizer* table = new wxFlexGridSizer (3, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + table->AddGrowableCol (1, 1); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); + + add_label_to_sizer (table, this, _("Repeat"), true); + _number = new wxSpinCtrl (this, wxID_ANY); + table->Add (_number, 1); + + add_label_to_sizer (table, this, _("times"), false); + + _number->SetRange (1, 1024); + + wxSizer* buttons = CreateSeparatedButtonSizer (wxOK | wxCANCEL); + if (buttons) { + overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder()); + } + + overall_sizer->Layout (); + overall_sizer->SetSizeHints (this); +} + +int +RepeatDialog::number () const +{ + return _number->GetValue (); +} diff --git a/src/lib/gain.h b/src/wx/repeat_dialog.h index 716ee9b51..cbcc6bb7a 100644 --- a/src/lib/gain.h +++ b/src/wx/repeat_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,15 +17,16 @@ */ -#include "processor.h" +#include <wx/wx.h> +#include <wx/spinctrl.h> -class Gain : public AudioProcessor +class RepeatDialog : public wxDialog { public: - Gain (Log* log, float gain); + RepeatDialog (wxWindow *); - void process_audio (boost::shared_ptr<AudioBuffers>); + int number () const; private: - float _gain; + wxSpinCtrl* _number; }; diff --git a/src/wx/screen_dialog.cc b/src/wx/screen_dialog.cc index 910dece9d..7ff519713 100644 --- a/src/wx/screen_dialog.cc +++ b/src/wx/screen_dialog.cc @@ -35,11 +35,11 @@ ScreenDialog::ScreenDialog (wxWindow* parent, string title, string name, shared_ wxFlexGridSizer* table = new wxFlexGridSizer (2, 6, 6); table->AddGrowableCol (1, 1); - add_label_to_sizer (table, this, "Name"); + add_label_to_sizer (table, this, "Name", true); _name = new wxTextCtrl (this, wxID_ANY, std_to_wx (name), wxDefaultPosition, wxSize (320, -1)); table->Add (_name, 1, wxEXPAND); - add_label_to_sizer (table, this, "Certificate"); + add_label_to_sizer (table, this, "Certificate", true); _certificate_load = new wxButton (this, wxID_ANY, wxT ("Load from file...")); table->Add (_certificate_load, 1, wxEXPAND); diff --git a/src/wx/server_dialog.cc b/src/wx/server_dialog.cc index 7b394a484..c1dbc4bca 100644 --- a/src/wx/server_dialog.cc +++ b/src/wx/server_dialog.cc @@ -21,35 +21,34 @@ #include "server_dialog.h" #include "wx_util.h" -ServerDialog::ServerDialog (wxWindow* parent, ServerDescription* server) - : wxDialog (parent, wxID_ANY, wxString (_("Server"))) +using boost::shared_ptr; + +ServerDialog::ServerDialog (wxWindow* parent) + : wxDialog (parent, wxID_ANY, _("Server")) { - if (server) { - _server = server; - } else { - _server = new ServerDescription ("localhost", 1); - } - - wxFlexGridSizer* table = new wxFlexGridSizer (2, 4, 4); + wxFlexGridSizer* table = new wxFlexGridSizer (2, DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); table->AddGrowableCol (1, 1); - add_label_to_sizer (table, this, "Host name or IP address"); - _host = new wxTextCtrl (this, wxID_ANY); - table->Add (_host, 1, wxEXPAND); + wxClientDC dc (parent); + /* XXX: bit of a mystery why we need such a long string here */ + wxSize size = dc.GetTextExtent (wxT ("255.255.255.255.255.255.255.255")); + size.SetHeight (-1); + + wxTextValidator validator (wxFILTER_INCLUDE_CHAR_LIST); + wxArrayString list; - add_label_to_sizer (table, this, "Threads to use"); + add_label_to_sizer (table, this, _("Host name or IP address"), true); + _host = new wxTextCtrl (this, wxID_ANY, wxT (""), wxDefaultPosition, size); + table->Add (_host, 1, wxEXPAND | wxALL); + + add_label_to_sizer (table, this, _("Threads to use"), true); _threads = new wxSpinCtrl (this, wxID_ANY); table->Add (_threads, 1, wxEXPAND); - _host->Connect (wxID_ANY, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ServerDialog::host_changed), 0, this); _threads->SetRange (0, 256); - _threads->Connect (wxID_ANY, wxEVT_COMMAND_SPINCTRL_UPDATED, wxCommandEventHandler (ServerDialog::threads_changed), 0, this); - - _host->SetValue (std_to_wx (_server->host_name ())); - _threads->SetValue (_server->threads ()); wxBoxSizer* overall_sizer = new wxBoxSizer (wxVERTICAL); - overall_sizer->Add (table, 1, wxEXPAND | wxALL, 6); + overall_sizer->Add (table, 1, wxEXPAND | wxALL, DCPOMATIC_DIALOG_BORDER); wxSizer* buttons = CreateSeparatedButtonSizer (wxOK); if (buttons) { @@ -62,20 +61,18 @@ ServerDialog::ServerDialog (wxWindow* parent, ServerDescription* server) } void -ServerDialog::host_changed (wxCommandEvent &) -{ - _server->set_host_name (wx_to_std (_host->GetValue ())); -} - -void -ServerDialog::threads_changed (wxCommandEvent &) +ServerDialog::set (ServerDescription server) { - _server->set_threads (_threads->GetValue ()); + _host->SetValue (std_to_wx (server.host_name ())); + _threads->SetValue (server.threads ()); } -ServerDescription * -ServerDialog::server () const +ServerDescription +ServerDialog::get () const { - return _server; + ServerDescription server; + server.set_host_name (wx_to_std (_host->GetValue ())); + server.set_threads (_threads->GetValue ()); + return server; } diff --git a/src/wx/server_dialog.h b/src/wx/server_dialog.h index 0912fd60f..a6f48fe7b 100644 --- a/src/wx/server_dialog.h +++ b/src/wx/server_dialog.h @@ -25,15 +25,12 @@ class ServerDescription; class ServerDialog : public wxDialog { public: - ServerDialog (wxWindow *, ServerDescription *); + ServerDialog (wxWindow *); - ServerDescription* server () const; + void set (ServerDescription); + ServerDescription get () const; private: - void host_changed (wxCommandEvent &); - void threads_changed (wxCommandEvent &); - - ServerDescription* _server; wxTextCtrl* _host; wxSpinCtrl* _threads; }; diff --git a/src/wx/subtitle_panel.cc b/src/wx/subtitle_panel.cc new file mode 100644 index 000000000..8f2b08af5 --- /dev/null +++ b/src/wx/subtitle_panel.cc @@ -0,0 +1,192 @@ +/* + Copyright (C) 2012-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 <boost/lexical_cast.hpp> +#include <wx/spinctrl.h> +#include "lib/ffmpeg_content.h" +#include "subtitle_panel.h" +#include "film_editor.h" +#include "wx_util.h" + +using std::vector; +using std::string; +using boost::shared_ptr; +using boost::lexical_cast; +using boost::dynamic_pointer_cast; + +SubtitlePanel::SubtitlePanel (FilmEditor* e) + : FilmEditorPanel (e, _("Subtitles")) +{ + 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); + grid->AddSpacer (0); + + { + add_label_to_sizer (grid, this, _("Subtitle Offset"), true); + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _offset = new wxSpinCtrl (this); + s->Add (_offset); + add_label_to_sizer (s, this, _("%"), false); + grid->Add (s); + } + + { + add_label_to_sizer (grid, this, _("Subtitle Scale"), true); + wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL); + _scale = new wxSpinCtrl (this); + s->Add (_scale); + add_label_to_sizer (s, this, _("%"), false); + grid->Add (s); + } + + add_label_to_sizer (grid, this, _("Subtitle Stream"), true); + _stream = new wxChoice (this, wxID_ANY); + grid->Add (_stream, 1, wxEXPAND); + + _offset->SetRange (-100, 100); + _scale->SetRange (1, 1000); + _scale->SetValue (100); + + _with_subtitles->Bind (wxEVT_COMMAND_CHECKBOX_CLICKED, boost::bind (&SubtitlePanel::with_subtitles_toggled, this)); + _offset->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::offset_changed, this)); + _scale->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&SubtitlePanel::scale_changed, this)); + _stream->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&SubtitlePanel::stream_changed, 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 ()); + setup_sensitivity (); + break; + default: + break; + } +} + +void +SubtitlePanel::film_content_changed (shared_ptr<Content> c, int property) +{ + shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (c); + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + + if (property == FFmpegContentProperty::SUBTITLE_STREAMS) { + _stream->Clear (); + if (fc) { + vector<shared_ptr<FFmpegSubtitleStream> > s = fc->subtitle_streams (); + for (vector<shared_ptr<FFmpegSubtitleStream> >::iterator i = s.begin(); i != s.end(); ++i) { + _stream->Append (std_to_wx ((*i)->name), new wxStringClientData (std_to_wx (lexical_cast<string> ((*i)->id)))); + } + + if (fc->subtitle_stream()) { + checked_set (_stream, lexical_cast<string> (fc->subtitle_stream()->id)); + } else { + _stream->SetSelection (wxNOT_FOUND); + } + } + setup_sensitivity (); + } else if (property == SubtitleContentProperty::SUBTITLE_OFFSET) { + checked_set (_offset, sc ? (sc->subtitle_offset() * 100) : 0); + } else if (property == SubtitleContentProperty::SUBTITLE_SCALE) { + checked_set (_scale, sc ? (sc->subtitle_scale() * 100) : 100); + } + +} + +void +SubtitlePanel::with_subtitles_toggled () +{ + if (!_editor->film()) { + return; + } + + _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 (); + } + + _with_subtitles->Enable (h); + _offset->Enable (j); + _scale->Enable (j); + _stream->Enable (j); +} + +void +SubtitlePanel::stream_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + if (!fc) { + return; + } + + vector<shared_ptr<FFmpegSubtitleStream> > a = fc->subtitle_streams (); + vector<shared_ptr<FFmpegSubtitleStream> >::iterator i = a.begin (); + string const s = string_client_data (_stream->GetClientObject (_stream->GetSelection ())); + while (i != a.end() && lexical_cast<string> ((*i)->id) != s) { + ++i; + } + + if (i != a.end ()) { + fc->set_subtitle_stream (*i); + } +} + +void +SubtitlePanel::offset_changed () +{ + shared_ptr<SubtitleContent> c = _editor->selected_subtitle_content (); + if (!c) { + return; + } + + c->set_subtitle_offset (_offset->GetValue() / 100.0); +} + +void +SubtitlePanel::scale_changed () +{ + shared_ptr<SubtitleContent> c = _editor->selected_subtitle_content (); + if (!c) { + return; + } + + c->set_subtitle_scale (_scale->GetValue() / 100.0); +} + diff --git a/src/wx/subtitle_panel.h b/src/wx/subtitle_panel.h new file mode 100644 index 000000000..3f7951895 --- /dev/null +++ b/src/wx/subtitle_panel.h @@ -0,0 +1,46 @@ +/* + Copyright (C) 2012-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 "film_editor_panel.h" + +class wxCheckBox; +class wxSpinCtrl; + +class SubtitlePanel : public FilmEditorPanel +{ +public: + SubtitlePanel (FilmEditor *); + + void film_changed (Film::Property); + void film_content_changed (boost::shared_ptr<Content>, int); + + +private: + void with_subtitles_toggled (); + void offset_changed (); + void scale_changed (); + void stream_changed (); + + void setup_sensitivity (); + + wxCheckBox* _with_subtitles; + wxSpinCtrl* _offset; + wxSpinCtrl* _scale; + wxChoice* _stream; +}; diff --git a/src/wx/timecode.cc b/src/wx/timecode.cc new file mode 100644 index 000000000..033bd2bd0 --- /dev/null +++ b/src/wx/timecode.cc @@ -0,0 +1,139 @@ +/* + 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 <boost/lexical_cast.hpp> +#include "lib/util.h" +#include "timecode.h" +#include "wx_util.h" + +using std::string; +using std::cout; +using boost::lexical_cast; + +Timecode::Timecode (wxWindow* parent) + : wxPanel (parent) +{ + wxClientDC dc (parent); + wxSize size = dc.GetTextExtent (wxT ("9999")); + size.SetHeight (-1); + + wxTextValidator validator (wxFILTER_INCLUDE_CHAR_LIST); + wxArrayString list; + + wxString n (wxT ("0123456789")); + for (size_t i = 0; i < n.Length(); ++i) { + list.Add (n[i]); + } + + validator.SetIncludes (list); + + _sizer = new wxBoxSizer (wxHORIZONTAL); + + _editable = new wxPanel (this); + wxSizer* editable_sizer = new wxBoxSizer (wxHORIZONTAL); + _hours = new wxTextCtrl (_editable, wxID_ANY, wxT(""), wxDefaultPosition, size, 0, validator); + _hours->SetMaxLength (2); + editable_sizer->Add (_hours); + add_label_to_sizer (editable_sizer, _editable, wxT (":"), false); + _minutes = new wxTextCtrl (_editable, wxID_ANY, wxT(""), wxDefaultPosition, size, 0, validator); + _minutes->SetMaxLength (2); + editable_sizer->Add (_minutes); + add_label_to_sizer (editable_sizer, _editable, wxT (":"), false); + _seconds = new wxTextCtrl (_editable, wxID_ANY, wxT(""), wxDefaultPosition, size, 0, validator); + _seconds->SetMaxLength (2); + editable_sizer->Add (_seconds); + add_label_to_sizer (editable_sizer, _editable, wxT ("."), false); + _frames = new wxTextCtrl (_editable, wxID_ANY, wxT(""), wxDefaultPosition, size, 0, validator); + _frames->SetMaxLength (2); + editable_sizer->Add (_frames); + _set_button = new wxButton (_editable, wxID_ANY, _("Set")); + editable_sizer->Add (_set_button, 0, wxLEFT | wxRIGHT, 8); + _editable->SetSizerAndFit (editable_sizer); + _sizer->Add (_editable); + + _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)); + + _set_button->Enable (false); + + set_editable (true); + + SetSizerAndFit (_sizer); +} + +void +Timecode::set (Time t, int fps) +{ + int const h = t / (3600 * TIME_HZ); + t -= h * 3600 * TIME_HZ; + int const m = t / (60 * TIME_HZ); + t -= m * 60 * TIME_HZ; + int const s = t / TIME_HZ; + t -= s * TIME_HZ; + int const f = t * fps / TIME_HZ; + + 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.%02d", 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::changed () +{ + _set_button->Enable (true); +} + +void +Timecode::set_clicked () +{ + Changed (); + _set_button->Enable (false); +} + +void +Timecode::set_editable (bool e) +{ + _editable->Show (e); + _fixed->Show (!e); + _sizer->Layout (); +} diff --git a/src/wx/timecode.h b/src/wx/timecode.h new file mode 100644 index 000000000..5b094e39f --- /dev/null +++ b/src/wx/timecode.h @@ -0,0 +1,50 @@ +/* + 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 <boost/signals2.hpp> +#include <wx/wx.h> +#include "lib/types.h" + +class Timecode : public wxPanel +{ +public: + Timecode (wxWindow *); + + void set (Time, int); + Time get (int) const; + + void set_editable (bool); + + boost::signals2::signal<void ()> Changed; + +private: + void changed (); + void set_clicked (); + + wxSizer* _sizer; + wxPanel* _editable; + wxTextCtrl* _hours; + wxStaticText* _hours_label; + wxTextCtrl* _minutes; + wxTextCtrl* _seconds; + wxTextCtrl* _frames; + wxButton* _set_button; + wxStaticText* _fixed; +}; + diff --git a/src/wx/timeline.cc b/src/wx/timeline.cc new file mode 100644 index 000000000..87070b35e --- /dev/null +++ b/src/wx/timeline.cc @@ -0,0 +1,652 @@ +/* + 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 <list> +#include <wx/graphics.h> +#include <boost/weak_ptr.hpp> +#include "lib/film.h" +#include "lib/playlist.h" +#include "film_editor.h" +#include "timeline.h" +#include "wx_util.h" + +using std::list; +using std::cout; +using std::max; +using boost::shared_ptr; +using boost::weak_ptr; +using boost::dynamic_pointer_cast; +using boost::bind; + +class View : public boost::noncopyable +{ +public: + View (Timeline& t) + : _timeline (t) + { + + } + + virtual ~View () {} + + void paint (wxGraphicsContext* g) + { + _last_paint_bbox = bbox (); + do_paint (g); + } + + void force_redraw () + { + _timeline.force_redraw (_last_paint_bbox); + _timeline.force_redraw (bbox ()); + } + + virtual dcpomatic::Rect<int> bbox () const = 0; + +protected: + virtual void do_paint (wxGraphicsContext *) = 0; + + int time_x (Time t) const + { + return _timeline.tracks_position().x + t * _timeline.pixels_per_time_unit(); + } + + Timeline& _timeline; + +private: + dcpomatic::Rect<int> _last_paint_bbox; +}; + +class ContentView : public View +{ +public: + ContentView (Timeline& tl, shared_ptr<Content> c) + : View (tl) + , _content (c) + , _track (0) + , _selected (false) + { + _content_connection = c->Changed.connect (bind (&ContentView::content_changed, this, _2, _3)); + } + + dcpomatic::Rect<int> bbox () const + { + shared_ptr<const Film> film = _timeline.film (); + shared_ptr<const Content> content = _content.lock (); + if (!film || !content) { + return dcpomatic::Rect<int> (); + } + + return dcpomatic::Rect<int> ( + time_x (content->position ()) - 8, + y_pos (_track) - 8, + content->length_after_trim () * _timeline.pixels_per_time_unit() + 16, + _timeline.track_height() + 16 + ); + } + + void set_selected (bool s) { + _selected = s; + force_redraw (); + } + + bool selected () const { + return _selected; + } + + shared_ptr<Content> content () const { + return _content.lock (); + } + + void set_track (int t) { + _track = t; + } + + int track () const { + return _track; + } + + virtual wxString type () const = 0; + virtual wxColour colour () const = 0; + +private: + + void do_paint (wxGraphicsContext* gc) + { + shared_ptr<const Film> film = _timeline.film (); + shared_ptr<const Content> cont = content (); + if (!film || !cont) { + return; + } + + Time const position = cont->position (); + Time const len = cont->length_after_trim (); + + wxColour selected (colour().Red() / 2, colour().Green() / 2, colour().Blue() / 2); + + gc->SetPen (*wxBLACK_PEN); + + gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 4, wxPENSTYLE_SOLID)); + if (_selected) { + gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (selected, wxBRUSHSTYLE_SOLID)); + } else { + gc->SetBrush (*wxTheBrushList->FindOrCreateBrush (colour(), wxBRUSHSTYLE_SOLID)); + } + + wxGraphicsPath path = gc->CreatePath (); + path.MoveToPoint (time_x (position), y_pos (_track) + 4); + path.AddLineToPoint (time_x (position + len), y_pos (_track) + 4); + path.AddLineToPoint (time_x (position + len), y_pos (_track + 1) - 4); + path.AddLineToPoint (time_x (position), y_pos (_track + 1) - 4); + path.AddLineToPoint (time_x (position), y_pos (_track) + 4); + gc->StrokePath (path); + gc->FillPath (path); + + wxString name = wxString::Format (wxT ("%s [%s]"), std_to_wx (cont->path().filename().string()).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), len * _timeline.pixels_per_time_unit(), _timeline.track_height())); + gc->DrawText (name, time_x (position) + 12, y_pos (_track + 1) - name_height - 4); + gc->ResetClip (); + } + + int y_pos (int t) const + { + return _timeline.tracks_position().y + t * _timeline.track_height(); + } + + void content_changed (int p, bool frequent) + { + ensure_ui_thread (); + + if (p == ContentProperty::POSITION || p == ContentProperty::LENGTH) { + force_redraw (); + } + + if (!frequent) { + _timeline.setup_pixels_per_time_unit (); + _timeline.Refresh (); + } + } + + boost::weak_ptr<Content> _content; + int _track; + bool _selected; + + boost::signals2::scoped_connection _content_connection; +}; + +class AudioContentView : public ContentView +{ +public: + AudioContentView (Timeline& tl, shared_ptr<Content> c) + : ContentView (tl, c) + {} + +private: + wxString type () const + { + return _("audio"); + } + + wxColour colour () const + { + return wxColour (149, 121, 232, 255); + } +}; + +class VideoContentView : public ContentView +{ +public: + VideoContentView (Timeline& tl, shared_ptr<Content> c) + : ContentView (tl, c) + {} + +private: + + wxString type () const + { + if (dynamic_pointer_cast<FFmpegContent> (content ())) { + return _("video"); + } else { + return _("still"); + } + } + + wxColour colour () const + { + return wxColour (242, 92, 120, 255); + } +}; + +class TimeAxisView : public View +{ +public: + TimeAxisView (Timeline& tl, int y) + : View (tl) + , _y (y) + {} + + dcpomatic::Rect<int> bbox () const + { + return dcpomatic::Rect<int> (0, _y - 4, _timeline.width(), 24); + } + + void set_y (int y) + { + _y = y; + force_redraw (); + } + +private: + + void do_paint (wxGraphicsContext* gc) + { + gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (0, 0, 0), 1, wxPENSTYLE_SOLID)); + + int mark_interval = rint (128 / (TIME_HZ * _timeline.pixels_per_time_unit ())); + if (mark_interval > 5) { + mark_interval -= mark_interval % 5; + } + if (mark_interval > 10) { + mark_interval -= mark_interval % 10; + } + if (mark_interval > 60) { + mark_interval -= mark_interval % 60; + } + if (mark_interval > 3600) { + mark_interval -= mark_interval % 3600; + } + + if (mark_interval < 1) { + mark_interval = 1; + } + + wxGraphicsPath path = gc->CreatePath (); + path.MoveToPoint (_timeline.x_offset(), _y); + path.AddLineToPoint (_timeline.width(), _y); + gc->StrokePath (path); + + Time t = 0; + while ((t * _timeline.pixels_per_time_unit()) < _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; + int const h = tc / 3600; + tc -= h * 3600; + int const m = tc / 60; + tc -= m * 60; + int const s = tc; + + wxString str = wxString::Format (wxT ("%02d:%02d:%02d"), h, m, s); + wxDouble str_width; + wxDouble str_height; + wxDouble str_descent; + wxDouble str_leading; + gc->GetTextExtent (str, &str_width, &str_height, &str_descent, &str_leading); + + int const tx = _timeline.x_offset() + t * _timeline.pixels_per_time_unit(); + if ((tx + str_width) < _timeline.width()) { + gc->DrawText (str, time_x (t), _y + 16); + } + + t += mark_interval * TIME_HZ; + } + } + +private: + int _y; +}; + +Timeline::Timeline (wxWindow* parent, FilmEditor* ed, shared_ptr<Film> film) + : wxPanel (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxFULL_REPAINT_ON_RESIZE) + , _film_editor (ed) + , _film (film) + , _time_axis_view (new TimeAxisView (*this, 32)) + , _tracks (0) + , _pixels_per_time_unit (0) + , _left_down (false) + , _down_view_position (0) + , _first_move (false) + , _menu (film, this) +{ +#ifndef __WXOSX__ + SetDoubleBuffered (true); +#endif + + Bind (wxEVT_PAINT, boost::bind (&Timeline::paint, this)); + Bind (wxEVT_LEFT_DOWN, boost::bind (&Timeline::left_down, this, _1)); + Bind (wxEVT_LEFT_UP, boost::bind (&Timeline::left_up, this, _1)); + Bind (wxEVT_RIGHT_DOWN, boost::bind (&Timeline::right_down, this, _1)); + Bind (wxEVT_MOTION, boost::bind (&Timeline::mouse_moved, this, _1)); + Bind (wxEVT_SIZE, boost::bind (&Timeline::resized, this)); + + playlist_changed (); + + SetMinSize (wxSize (640, tracks() * track_height() + 96)); + + _playlist_connection = film->playlist()->Changed.connect (bind (&Timeline::playlist_changed, this)); +} + +void +Timeline::paint () +{ + wxPaintDC dc (this); + + wxGraphicsContext* gc = wxGraphicsContext::Create (dc); + if (!gc) { + return; + } + + gc->SetFont (gc->CreateFont (*wxNORMAL_FONT)); + + for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { + (*i)->paint (gc); + } + + delete gc; +} + +void +Timeline::playlist_changed () +{ + ensure_ui_thread (); + + shared_ptr<const Film> fl = _film.lock (); + if (!fl) { + return; + } + + _views.clear (); + _views.push_back (_time_axis_view); + + ContentList content = fl->playlist()->content (); + + for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { + if (dynamic_pointer_cast<VideoContent> (*i)) { + _views.push_back (shared_ptr<View> (new VideoContentView (*this, *i))); + } + if (dynamic_pointer_cast<AudioContent> (*i)) { + _views.push_back (shared_ptr<View> (new AudioContentView (*this, *i))); + } + } + + assign_tracks (); + setup_pixels_per_time_unit (); + Refresh (); +} + +void +Timeline::assign_tracks () +{ + for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { + shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i); + if (cv) { + cv->set_track (0); + _tracks = 1; + } + } + + for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { + shared_ptr<AudioContentView> acv = dynamic_pointer_cast<AudioContentView> (*i); + if (!acv) { + continue; + } + + shared_ptr<Content> acv_content = acv->content(); + + int t = 1; + while (1) { + ViewList::iterator j = _views.begin(); + while (j != _views.end()) { + shared_ptr<AudioContentView> test = dynamic_pointer_cast<AudioContentView> (*j); + if (!test) { + ++j; + continue; + } + + shared_ptr<Content> test_content = test->content(); + + if (test && test->track() == t) { + bool const no_overlap = + (acv_content->position() < test_content->position() && acv_content->end() < test_content->position()) || + (acv_content->position() > test_content->end() && acv_content->end() > test_content->end()); + + if (!no_overlap) { + /* we have an overlap on track `t' */ + ++t; + break; + } + } + + ++j; + } + + if (j == _views.end ()) { + /* no overlap on `t' */ + break; + } + } + + acv->set_track (t); + _tracks = max (_tracks, t + 1); + } + + _time_axis_view->set_y (tracks() * track_height() + 32); +} + +int +Timeline::tracks () const +{ + return _tracks; +} + +void +Timeline::setup_pixels_per_time_unit () +{ + shared_ptr<const Film> film = _film.lock (); + if (!film) { + return; + } + + _pixels_per_time_unit = static_cast<double>(width() - x_offset() * 2) / film->length (); +} + +shared_ptr<View> +Timeline::event_to_view (wxMouseEvent& ev) +{ + ViewList::iterator i = _views.begin(); + Position<int> const p (ev.GetX(), ev.GetY()); + while (i != _views.end() && !(*i)->bbox().contains (p)) { + ++i; + } + + if (i == _views.end ()) { + return shared_ptr<View> (); + } + + return *i; +} + +void +Timeline::left_down (wxMouseEvent& ev) +{ + shared_ptr<View> view = event_to_view (ev); + shared_ptr<ContentView> content_view = dynamic_pointer_cast<ContentView> (view); + + _down_view.reset (); + + if (content_view) { + _down_view = content_view; + _down_view_position = content_view->content()->position (); + } + + for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { + shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i); + if (!cv) { + continue; + } + + if (!ev.ShiftDown ()) { + cv->set_selected (view == *i); + } + + if (view == *i) { + _film_editor->set_selection (cv->content ()); + } + } + + if (content_view && ev.ShiftDown ()) { + content_view->set_selected (!content_view->selected ()); + } + + _left_down = true; + _down_point = ev.GetPosition (); + _first_move = false; + + if (_down_view) { + _down_view->content()->set_change_signals_frequent (true); + } +} + +void +Timeline::left_up (wxMouseEvent& ev) +{ + _left_down = false; + + if (_down_view) { + _down_view->content()->set_change_signals_frequent (false); + } + + set_position_from_event (ev); +} + +void +Timeline::mouse_moved (wxMouseEvent& ev) +{ + if (!_left_down) { + return; + } + + set_position_from_event (ev); +} + +void +Timeline::right_down (wxMouseEvent& ev) +{ + shared_ptr<View> view = event_to_view (ev); + shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (view); + if (!cv) { + return; + } + + if (!cv->selected ()) { + clear_selection (); + cv->set_selected (true); + } + + _menu.popup (selected_content (), ev.GetPosition ()); +} + +void +Timeline::set_position_from_event (wxMouseEvent& ev) +{ + wxPoint const p = ev.GetPosition(); + + if (!_first_move) { + int const dist = sqrt (pow (p.x - _down_point.x, 2) + pow (p.y - _down_point.y, 2)); + if (dist < 8) { + return; + } + _first_move = true; + } + + Time const time_diff = (p.x - _down_point.x) / _pixels_per_time_unit; + if (_down_view) { + _down_view->content()->set_position (max (static_cast<Time> (0), _down_view_position + time_diff)); + + shared_ptr<Film> film = _film.lock (); + assert (film); + film->set_sequence_video (false); + } +} + +void +Timeline::force_redraw (dcpomatic::Rect<int> const & r) +{ + RefreshRect (wxRect (r.x, r.y, r.width, r.height), false); +} + +shared_ptr<const Film> +Timeline::film () const +{ + return _film.lock (); +} + +void +Timeline::resized () +{ + setup_pixels_per_time_unit (); +} + +void +Timeline::clear_selection () +{ + for (ViewList::iterator i = _views.begin(); i != _views.end(); ++i) { + shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i); + if (cv) { + cv->set_selected (false); + } + } +} + +Timeline::ContentViewList +Timeline::selected_views () const +{ + ContentViewList sel; + + for (ViewList::const_iterator i = _views.begin(); i != _views.end(); ++i) { + shared_ptr<ContentView> cv = dynamic_pointer_cast<ContentView> (*i); + if (cv && cv->selected()) { + sel.push_back (cv); + } + } + + return sel; +} + +ContentList +Timeline::selected_content () const +{ + ContentList sel; + ContentViewList views = selected_views (); + + for (ContentViewList::const_iterator i = views.begin(); i != views.end(); ++i) { + sel.push_back ((*i)->content ()); + } + + return sel; +} diff --git a/src/wx/timeline.h b/src/wx/timeline.h new file mode 100644 index 000000000..0217373b9 --- /dev/null +++ b/src/wx/timeline.h @@ -0,0 +1,100 @@ +/* + 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 <boost/shared_ptr.hpp> +#include <boost/weak_ptr.hpp> +#include <boost/signals2.hpp> +#include <wx/wx.h> +#include "lib/util.h" +#include "lib/rect.h" +#include "content_menu.h" + +class Film; +class View; +class ContentView; +class FilmEditor; +class TimeAxisView; + +class Timeline : public wxPanel +{ +public: + Timeline (wxWindow *, FilmEditor *, boost::shared_ptr<Film>); + + boost::shared_ptr<const Film> film () const; + + void force_redraw (dcpomatic::Rect<int> const &); + + int x_offset () const { + return 8; + } + + int width () const { + return GetSize().GetWidth (); + } + + int track_height () const { + return 48; + } + + double pixels_per_time_unit () const { + return _pixels_per_time_unit; + } + + Position<int> tracks_position () const { + return Position<int> (8, 8); + } + + int tracks () const; + + void setup_pixels_per_time_unit (); + +private: + void paint (); + void left_down (wxMouseEvent &); + void left_up (wxMouseEvent &); + void right_down (wxMouseEvent &); + void mouse_moved (wxMouseEvent &); + void playlist_changed (); + void resized (); + void assign_tracks (); + void set_position_from_event (wxMouseEvent &); + void clear_selection (); + + typedef std::vector<boost::shared_ptr<View> > ViewList; + typedef std::vector<boost::shared_ptr<ContentView> > ContentViewList; + + boost::shared_ptr<View> event_to_view (wxMouseEvent &); + ContentViewList selected_views () const; + ContentList selected_content () const; + + FilmEditor* _film_editor; + boost::weak_ptr<Film> _film; + ViewList _views; + boost::shared_ptr<TimeAxisView> _time_axis_view; + int _tracks; + double _pixels_per_time_unit; + bool _left_down; + wxPoint _down_point; + boost::shared_ptr<ContentView> _down_view; + Time _down_view_position; + bool _first_move; + ContentMenu _menu; + + boost::signals2::scoped_connection _playlist_connection; +}; diff --git a/src/wx/timeline_dialog.cc b/src/wx/timeline_dialog.cc new file mode 100644 index 000000000..9493d0acb --- /dev/null +++ b/src/wx/timeline_dialog.cc @@ -0,0 +1,42 @@ +/* + 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 <list> +#include <wx/graphics.h> +#include "lib/playlist.h" +#include "film_editor.h" +#include "timeline_dialog.h" +#include "wx_util.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) +{ + wxBoxSizer* sizer = new wxBoxSizer (wxVERTICAL); + + sizer->Add (&_timeline, 1, wxEXPAND | wxALL, 12); + + SetSizer (sizer); + sizer->Layout (); + sizer->SetSizeHints (this); +} diff --git a/src/tools/test.cc b/src/wx/timeline_dialog.h index 4baaeb73f..17ca22c49 100644 --- a/src/tools/test.cc +++ b/src/wx/timeline_dialog.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2012 Carl Hetherington <cth@carlh.net> + 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 @@ -17,18 +17,18 @@ */ -#include <stdint.h> #include <boost/shared_ptr.hpp> -#include "image.h" -#include "server.h" +#include <boost/weak_ptr.hpp> +#include <wx/wx.h> +#include "timeline.h" -using namespace boost; +class Playlist; -int main () +class TimelineDialog : public wxDialog { - uint8_t* rgb = new uint8_t[256]; - shared_ptr<Image> image (new Image (rgb, 0, 32, 32, 24)); - Server* s = new Server ("localhost", 2); - image->encode_remotely (s); - return 0; -} +public: + TimelineDialog (FilmEditor *, boost::shared_ptr<Film>); + +private: + Timeline _timeline; +}; diff --git a/src/wx/timing_panel.cc b/src/wx/timing_panel.cc new file mode 100644 index 000000000..ba645cf32 --- /dev/null +++ b/src/wx/timing_panel.cc @@ -0,0 +1,134 @@ +/* + Copyright (C) 2012-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 "lib/content.h" +#include "lib/still_image_content.h" +#include "timing_panel.h" +#include "wx_util.h" +#include "timecode.h" +#include "film_editor.h" + +using std::cout; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; + +TimingPanel::TimingPanel (FilmEditor* e) + : FilmEditorPanel (e, _("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); + grid->Add (_position); + add_label_to_sizer (grid, this, _("Length"), true); + _length = new Timecode (this); + grid->Add (_length); + add_label_to_sizer (grid, this, _("Trim from start"), true); + _trim_start = new Timecode (this); + grid->Add (_trim_start); + add_label_to_sizer (grid, this, _("Trim from end"), true); + _trim_end = new Timecode (this); + grid->Add (_trim_end); + + _position->Changed.connect (boost::bind (&TimingPanel::position_changed, this)); + _length->Changed.connect (boost::bind (&TimingPanel::length_changed, this)); + _trim_start->Changed.connect (boost::bind (&TimingPanel::trim_start_changed, this)); + _trim_end->Changed.connect (boost::bind (&TimingPanel::trim_end_changed, this)); +} + +void +TimingPanel::film_content_changed (shared_ptr<Content> content, int property) +{ + if (property == ContentProperty::POSITION) { + if (content) { + _position->set (content->position (), _editor->film()->video_frame_rate ()); + } else { + _position->set (0, 24); + } + } else if (property == ContentProperty::LENGTH) { + if (content) { + _length->set (content->full_length (), _editor->film()->video_frame_rate ()); + } else { + _length->set (0, 24); + } + } else if (property == ContentProperty::TRIM_START) { + if (content) { + _trim_start->set (content->trim_start (), _editor->film()->video_frame_rate ()); + } else { + _trim_start->set (0, 24); + } + } else if (property == ContentProperty::TRIM_END) { + if (content) { + _trim_end->set (content->trim_end (), _editor->film()->video_frame_rate ()); + } else { + _trim_end->set (0, 24); + } + } + + _length->set_editable (dynamic_pointer_cast<StillImageContent> (content)); +} + +void +TimingPanel::position_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + c->set_position (_position->get (_editor->film()->video_frame_rate ())); +} + +void +TimingPanel::length_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<StillImageContent> ic = dynamic_pointer_cast<StillImageContent> (c); + if (ic) { + ic->set_video_length (rint (_length->get (_editor->film()->video_frame_rate()) * ic->video_frame_rate() / TIME_HZ)); + } +} + +void +TimingPanel::trim_start_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + c->set_trim_start (_trim_start->get (_editor->film()->video_frame_rate ())); +} + + +void +TimingPanel::trim_end_changed () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + c->set_trim_end (_trim_end->get (_editor->film()->video_frame_rate ())); +} diff --git a/src/wx/timing_panel.h b/src/wx/timing_panel.h new file mode 100644 index 000000000..b84ea52be --- /dev/null +++ b/src/wx/timing_panel.h @@ -0,0 +1,41 @@ +/* + Copyright (C) 2012-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 "film_editor_panel.h" + +class Timecode; + +class TimingPanel : public FilmEditorPanel +{ +public: + TimingPanel (FilmEditor *); + + void film_content_changed (boost::shared_ptr<Content>, int); + +private: + void position_changed (); + void length_changed (); + void trim_start_changed (); + void trim_end_changed (); + + Timecode* _position; + Timecode* _length; + Timecode* _trim_start; + Timecode* _trim_end; +}; diff --git a/src/wx/video_panel.cc b/src/wx/video_panel.cc new file mode 100644 index 000000000..bb8476d63 --- /dev/null +++ b/src/wx/video_panel.cc @@ -0,0 +1,393 @@ +/* + Copyright (C) 2012-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 <wx/spinctrl.h> +#include "lib/ratio.h" +#include "lib/filter.h" +#include "lib/ffmpeg_content.h" +#include "lib/colour_conversion.h" +#include "lib/config.h" +#include "filter_dialog.h" +#include "video_panel.h" +#include "wx_util.h" +#include "film_editor.h" +#include "content_colour_conversion_dialog.h" + +using std::vector; +using std::string; +using std::pair; +using std::cout; +using std::list; +using boost::shared_ptr; +using boost::dynamic_pointer_cast; +using boost::bind; +using boost::optional; + +VideoPanel::VideoPanel (FilmEditor* e) + : FilmEditorPanel (e, _("Video")) +{ + wxGridBagSizer* grid = new wxGridBagSizer (DCPOMATIC_SIZER_X_GAP, DCPOMATIC_SIZER_Y_GAP); + _sizer->Add (grid, 0, wxALL, 8); + + int r = 0; + + add_label_to_grid_bag_sizer (grid, this, _("Type"), true, wxGBPosition (r, 0)); + _frame_type = new wxChoice (this, wxID_ANY); + grid->Add (_frame_type, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Left crop"), true, wxGBPosition (r, 0)); + _left_crop = new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); + grid->Add (_left_crop, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Right crop"), true, wxGBPosition (r, 0)); + _right_crop = new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); + grid->Add (_right_crop, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Top crop"), true, wxGBPosition (r, 0)); + _top_crop = new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); + grid->Add (_top_crop, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Bottom crop"), true, wxGBPosition (r, 0)); + _bottom_crop = new wxSpinCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize (64, -1)); + grid->Add (_bottom_crop, wxGBPosition (r, 1)); + ++r; + + add_label_to_grid_bag_sizer (grid, this, _("Scale to"), true, wxGBPosition (r, 0)); + _ratio = new wxChoice (this, wxID_ANY); + grid->Add (_ratio, wxGBPosition (r, 1)); + ++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); + } + ++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); + } + ++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); + wxFont font = _description->GetFont(); + font.SetStyle(wxFONTSTYLE_ITALIC); + font.SetPointSize(font.GetPointSize() - 1); + _description->SetFont(font); + ++r; + + _left_crop->SetRange (0, 1024); + _top_crop->SetRange (0, 1024); + _right_crop->SetRange (0, 1024); + _bottom_crop->SetRange (0, 1024); + + vector<Ratio const *> ratios = Ratio::all (); + _ratio->Clear (); + for (vector<Ratio const *>::iterator i = ratios.begin(); i != ratios.end(); ++i) { + _ratio->Append (std_to_wx ((*i)->nickname ())); + } + + _frame_type->Append (_("2D")); + _frame_type->Append (_("3D left/right")); + + _frame_type->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&VideoPanel::frame_type_changed, this)); + _left_crop->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&VideoPanel::left_crop_changed, this)); + _right_crop->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&VideoPanel::right_crop_changed, this)); + _top_crop->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&VideoPanel::top_crop_changed, this)); + _bottom_crop->Bind (wxEVT_COMMAND_SPINCTRL_UPDATED, boost::bind (&VideoPanel::bottom_crop_changed, this)); + _ratio->Bind (wxEVT_COMMAND_CHOICE_SELECTED, boost::bind (&VideoPanel::ratio_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)); +} + + +/** Called when the left crop widget has been changed */ +void +VideoPanel::left_crop_changed () +{ + shared_ptr<VideoContent> c = _editor->selected_video_content (); + if (!c) { + return; + } + + c->set_left_crop (_left_crop->GetValue ()); +} + +/** Called when the right crop widget has been changed */ +void +VideoPanel::right_crop_changed () +{ + shared_ptr<VideoContent> c = _editor->selected_video_content (); + if (!c) { + return; + } + + c->set_right_crop (_right_crop->GetValue ()); +} + +/** Called when the top crop widget has been changed */ +void +VideoPanel::top_crop_changed () +{ + shared_ptr<VideoContent> c = _editor->selected_video_content (); + if (!c) { + return; + } + + c->set_top_crop (_top_crop->GetValue ()); +} + +/** Called when the bottom crop value has been changed */ +void +VideoPanel::bottom_crop_changed () +{ + shared_ptr<VideoContent> c = _editor->selected_video_content (); + if (!c) { + return; + } + + c->set_bottom_crop (_bottom_crop->GetValue ()); +} + +void +VideoPanel::film_changed (Film::Property property) +{ + switch (property) { + case Film::CONTAINER: + case Film::VIDEO_FRAME_RATE: + setup_description (); + break; + default: + break; + } +} + +void +VideoPanel::film_content_changed (shared_ptr<Content> c, int property) +{ + shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (c); + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + + if (property == VideoContentProperty::VIDEO_FRAME_TYPE) { + checked_set (_frame_type, vc ? vc->video_frame_type () : VIDEO_FRAME_TYPE_2D); + setup_description (); + } else if (property == VideoContentProperty::VIDEO_CROP) { + checked_set (_left_crop, vc ? vc->crop().left : 0); + checked_set (_right_crop, vc ? vc->crop().right : 0); + checked_set (_top_crop, vc ? vc->crop().top : 0); + checked_set (_bottom_crop, vc ? vc->crop().bottom : 0); + setup_description (); + } else if (property == VideoContentProperty::VIDEO_RATIO) { + if (vc) { + int n = 0; + vector<Ratio const *> ratios = Ratio::all (); + vector<Ratio const *>::iterator i = ratios.begin (); + while (i != ratios.end() && *i != vc->ratio()) { + ++i; + ++n; + } + + if (i == ratios.end()) { + checked_set (_ratio, -1); + } else { + checked_set (_ratio, n); + } + } else { + checked_set (_ratio, -1); + } + setup_description (); + } else if (property == VideoContentProperty::VIDEO_FRAME_RATE) { + setup_description (); + } else if (property == VideoContentProperty::COLOUR_CONVERSION) { + optional<size_t> preset = vc ? vc->colour_conversion().preset () : optional<size_t> (); + vector<PresetColourConversion> cc = Config::instance()->colour_conversions (); + _colour_conversion->SetLabel (preset ? std_to_wx (cc[preset.get()].name) : _("Custom")); + } else if (property == FFmpegContentProperty::FILTERS) { + if (fc) { + pair<string, string> p = Filter::ffmpeg_strings (fc->filters ()); + if (p.first.empty () && p.second.empty ()) { + _filters->SetLabel (_("None")); + } else { + string const b = p.first + " " + p.second; + _filters->SetLabel (std_to_wx (b)); + } + } + } +} + +/** Called when the `Edit filters' button has been clicked */ +void +VideoPanel::edit_filters_clicked () +{ + shared_ptr<Content> c = _editor->selected_content (); + if (!c) { + return; + } + + shared_ptr<FFmpegContent> fc = dynamic_pointer_cast<FFmpegContent> (c); + if (!fc) { + return; + } + + FilterDialog* d = new FilterDialog (this, fc->filters()); + d->ActiveChanged.connect (bind (&FFmpegContent::set_filters, fc, _1)); + d->ShowModal (); + d->Destroy (); +} + +void +VideoPanel::setup_description () +{ + shared_ptr<VideoContent> vc = _editor->selected_video_content (); + if (!vc) { + _description->SetLabel (""); + return; + } + + wxString d; + + int lines = 0; + + if (vc->video_size().width && vc->video_size().height) { + d << wxString::Format ( + _("Content video is %dx%d (%.2f:1)\n"), + vc->video_size_after_3d_split().width, vc->video_size_after_3d_split().height, + float (vc->video_size_after_3d_split().width) / vc->video_size_after_3d_split().height + ); + ++lines; + } + + Crop const crop = vc->crop (); + if ((crop.left || crop.right || crop.top || crop.bottom) && vc->video_size() != libdcp::Size (0, 0)) { + libdcp::Size cropped = vc->video_size_after_3d_split (); + cropped.width -= crop.left + crop.right; + cropped.height -= crop.top + crop.bottom; + d << wxString::Format ( + _("Cropped to %dx%d (%.2f:1)\n"), + cropped.width, cropped.height, + float (cropped.width) / cropped.height + ); + ++lines; + } + + Ratio const * ratio = vc->ratio (); + if (ratio) { + libdcp::Size container_size = _editor->film()->container()->size (_editor->film()->full_frame ()); + + libdcp::Size const scaled = ratio->size (container_size); + d << wxString::Format ( + _("Scaled to %dx%d (%.2f:1)\n"), + scaled.width, scaled.height, + float (scaled.width) / scaled.height + ); + ++lines; + + if (scaled != container_size) { + d << wxString::Format ( + _("Padded with black to %dx%d (%.2f:1)\n"), + container_size.width, container_size.height, + float (container_size.width) / container_size.height + ); + ++lines; + } + } + + d << wxString::Format (_("Content frame rate %.4f\n"), vc->video_frame_rate ()); + ++lines; + FrameRateConversion frc (vc->video_frame_rate(), _editor->film()->video_frame_rate ()); + d << frc.description << "\n"; + ++lines; + + for (int i = lines; i < 6; ++i) { + d << wxT ("\n "); + } + + _description->SetLabel (d); + _sizer->Layout (); +} + + +void +VideoPanel::ratio_changed () +{ + if (!_editor->film ()) { + return; + } + + shared_ptr<VideoContent> vc = _editor->selected_video_content (); + + int const n = _ratio->GetSelection (); + if (n >= 0) { + vector<Ratio const *> ratios = Ratio::all (); + assert (n < int (ratios.size())); + vc->set_ratio (ratios[n]); + } +} + +void +VideoPanel::frame_type_changed () +{ + shared_ptr<VideoContent> vc = _editor->selected_video_content (); + if (vc) { + vc->set_video_frame_type (static_cast<VideoFrameType> (_frame_type->GetSelection ())); + } +} + +void +VideoPanel::edit_colour_conversion_clicked () +{ + shared_ptr<VideoContent> vc = _editor->selected_video_content (); + if (!vc) { + return; + } + + ColourConversion conversion = vc->colour_conversion (); + ContentColourConversionDialog* d = new ContentColourConversionDialog (this); + d->set (conversion); + d->ShowModal (); + + vc->set_colour_conversion (d->get ()); + d->Destroy (); +} diff --git a/src/wx/video_panel.h b/src/wx/video_panel.h new file mode 100644 index 000000000..2ecf3c87f --- /dev/null +++ b/src/wx/video_panel.h @@ -0,0 +1,60 @@ +/* + Copyright (C) 2012-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 "lib/film.h" +#include "film_editor_panel.h" + +class wxChoice; +class wxStaticText; +class wxSpinCtrl; +class wxButton; + +class VideoPanel : public FilmEditorPanel +{ +public: + VideoPanel (FilmEditor *); + + void film_changed (Film::Property); + void film_content_changed (boost::shared_ptr<Content>, int); + +private: + void left_crop_changed (); + void right_crop_changed (); + void top_crop_changed (); + void bottom_crop_changed (); + void edit_filters_clicked (); + void ratio_changed (); + void frame_type_changed (); + void edit_colour_conversion_clicked (); + + void setup_description (); + + wxChoice* _frame_type; + wxSpinCtrl* _left_crop; + wxSpinCtrl* _right_crop; + wxSpinCtrl* _top_crop; + wxSpinCtrl* _bottom_crop; + wxChoice* _ratio; + wxStaticText* _ratio_description; + wxStaticText* _description; + wxStaticText* _filters; + wxButton* _filters_button; + wxStaticText* _colour_conversion; + wxButton* _colour_conversion_button; +}; diff --git a/src/wx/wscript b/src/wx/wscript index 82d9d3738..8f35e2fac 100644 --- a/src/wx/wscript +++ b/src/wx/wscript @@ -1,5 +1,66 @@ +import os +import glob +from waflib import Logs +import i18n + +sources = """ + about_dialog.cc + audio_dialog.cc + audio_mapping_view.cc + audio_panel.cc + audio_plot.cc + cinema_dialog.cc + colour_conversion_editor.cc + config_dialog.cc + content_colour_conversion_dialog.cc + content_menu.cc + dci_metadata_dialog.cc + dir_picker_ctrl.cc + film_editor.cc + film_editor_panel.cc + film_viewer.cc + filter_dialog.cc + filter_editor.cc + gain_calculator_dialog.cc + job_manager_view.cc + job_wrapper.cc + kdm_dialog.cc + new_film_dialog.cc + preset_colour_conversion_dialog.cc + properties_dialog.cc + repeat_dialog.cc + screen_dialog.cc + server_dialog.cc + subtitle_panel.cc + timecode.cc + timeline.cc + timeline_dialog.cc + timing_panel.cc + video_panel.cc + wx_util.cc + wx_ui_signaller.cc + """ + def configure(conf): - conf.check_cfg(package = '', path = 'wx-config', args = '--cppflags --cxxflags --libs', uselib_store = 'WXWIDGETS', mandatory = True) + args = '--cppflags --cxxflags' + if not conf.env.STATIC: + args += ' --libs' + + conf.check_cfg(msg='Checking for wxWidgets', package='', path=conf.options.wx_config, args=args, + uselib_store='WXWIDGETS', mandatory=True) + + if conf.env.STATIC: + # wx-config returns its static libraries as full paths, without -l prefixes, which confuses + # check_cfg(), so just hard-code it all. + conf.env.STLIB_WXWIDGETS = ['wx_gtk2u_xrc-2.9', 'wx_gtk2u_qa-2.9', 'wx_baseu_net-2.9', 'wx_gtk2u_html-2.9', + 'wx_gtk2u_adv-2.9', 'wx_gtk2u_core-2.9', 'wx_baseu_xml-2.9', 'wx_baseu-2.9'] + conf.env.LIB_WXWIDGETS = ['tiff', 'SM', 'dl', 'jpeg', 'png', 'X11'] + + conf.in_msg = 1 + wx_version = conf.check_cfg(package='', path=conf.options.wx_config, args='--version').strip() + conf.im_msg = 0 + if wx_version != '2.9.4' and wx_version != '2.9.5': + conf.fatal('wxwidgets version 2.9.4 or 2.9.5 is required; %s found' % wx_version) def build(bld): if bld.env.STATIC: @@ -7,30 +68,20 @@ def build(bld): else: obj = bld(features = 'cxx cxxshlib') - obj.name = 'libdvdomatic-wx' - obj.includes = [ '..' ] - obj.export_includes = ['.'] + obj.name = 'libdcpomatic-wx' +# obj.includes = [ '..' ] + obj.export_includes = ['..'] obj.uselib = 'WXWIDGETS' - obj.use = 'libdvdomatic' - obj.source = """ - config_dialog.cc - dci_name_dialog.cc - dir_picker_ctrl.cc - film_editor.cc - film_viewer.cc - filter_dialog.cc - filter_view.cc - gain_calculator_dialog.cc - job_manager_view.cc - job_wrapper.cc - kdm_dialog.cc - cinema_dialog.cc - new_film_dialog.cc - screen_dialog.cc - properties_dialog.cc - server_dialog.cc - wx_util.cc - wx_ui_signaller.cc - """ - - obj.target = 'dvdomatic-wx' + if bld.env.TARGET_LINUX: + obj.uselib += ' GTK' + obj.use = 'libdcpomatic' + obj.source = sources + obj.target = 'dcpomatic-wx' + + i18n.po_to_mo(os.path.join('src', 'wx'), 'libdcpomatic-wx', bld) + +def pot(bld): + i18n.pot(os.path.join('src', 'wx'), sources, 'libdcpomatic-wx') + +def pot_merge(bld): + i18n.pot_merge(os.path.join('src', 'wx'), 'libdcpomatic-wx') diff --git a/src/wx/wx_ui_signaller.cc b/src/wx/wx_ui_signaller.cc index 2e926edc6..f30631960 100644 --- a/src/wx/wx_ui_signaller.cc +++ b/src/wx/wx_ui_signaller.cc @@ -29,6 +29,6 @@ wxUISignaller::wxUISignaller (wxEvtHandler* h) void wxUISignaller::wake_ui () { - wxCommandEvent event (-1, -1); - _handler->AddPendingEvent (event); + wxCommandEvent event (-1, -1); + _handler->AddPendingEvent (event); } diff --git a/src/wx/wx_ui_signaller.h b/src/wx/wx_ui_signaller.h index d134d2b6d..f7df6fca4 100644 --- a/src/wx/wx_ui_signaller.h +++ b/src/wx/wx_ui_signaller.h @@ -17,7 +17,7 @@ */ -#include "ui_signaller.h" +#include "lib/ui_signaller.h" class wxEvtHandler; diff --git a/src/wx/wx_util.cc b/src/wx/wx_util.cc index bc444e4bc..20fd2df75 100644 --- a/src/wx/wx_util.cc +++ b/src/wx/wx_util.cc @@ -24,6 +24,8 @@ #include <boost/thread.hpp> #include <wx/filepicker.h> #include <wx/spinctrl.h> +#include "lib/config.h" +#include "lib/util.h" #include "wx_util.h" using namespace std; @@ -33,13 +35,45 @@ using namespace boost; * @param s Sizer to add to. * @param p Parent window for the wxStaticText. * @param t Text for the wxStaticText. + * @param left true if this label is a `left label'; ie the sort + * of label which should be right-aligned on OS X. * @param prop Proportion to pass when calling Add() on the wxSizer. */ wxStaticText * -add_label_to_sizer (wxSizer* s, wxWindow* p, string t, int prop) +#ifdef __WXOSX__ +add_label_to_sizer (wxSizer* s, wxWindow* p, wxString t, bool left, int prop) +#else +add_label_to_sizer (wxSizer* s, wxWindow* p, wxString t, bool, int prop) +#endif { - wxStaticText* m = new wxStaticText (p, wxID_ANY, std_to_wx (t)); - s->Add (m, prop, wxALIGN_CENTER_VERTICAL | wxALL, 6); + int flags = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT; +#ifdef __WXOSX__ + if (left) { + flags |= wxALIGN_RIGHT; + t += wxT (":"); + } +#endif + wxStaticText* m = new wxStaticText (p, wxID_ANY, t); + s->Add (m, prop, flags, 6); + return m; +} + +wxStaticText * +#ifdef __WXOSX__ +add_label_to_grid_bag_sizer (wxGridBagSizer* s, wxWindow* p, wxString t, bool left, wxGBPosition pos, wxGBSpan span) +#else +add_label_to_grid_bag_sizer (wxGridBagSizer* s, wxWindow* p, wxString t, bool, wxGBPosition pos, wxGBSpan span) +#endif +{ + int flags = wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT; +#ifdef __WXOSX__ + if (left) { + flags |= wxALIGN_RIGHT; + t += wxT (":"); + } +#endif + wxStaticText* m = new wxStaticText (p, wxID_ANY, t); + s->Add (m, pos, span, flags); return m; } @@ -48,13 +82,23 @@ add_label_to_sizer (wxSizer* s, wxWindow* p, string t, int prop) * @param m Message. */ void -error_dialog (wxWindow* parent, string m) +error_dialog (wxWindow* parent, wxString m) { - wxMessageDialog* d = new wxMessageDialog (parent, std_to_wx (m), wxT ("DVD-o-matic"), wxOK); + wxMessageDialog* d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxOK); d->ShowModal (); d->Destroy (); } +bool +confirm_dialog (wxWindow* parent, wxString m) +{ + wxMessageDialog* d = new wxMessageDialog (parent, m, _("DCP-o-matic"), wxYES_NO | wxICON_QUESTION); + int const r = d->ShowModal (); + d->Destroy (); + return r == wxID_YES; +} + + /** @param s wxWidgets string. * @return Corresponding STL string. */ @@ -79,10 +123,10 @@ 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, string initial, function<string ()> fn) - : wxStaticText (parent, wxID_ANY, std_to_wx (initial)) +ThreadedStaticText::ThreadedStaticText (wxWindow* parent, wxString initial, function<string ()> fn) + : wxStaticText (parent, wxID_ANY, initial) { - Connect (_update_event_id, wxEVT_COMMAND_TEXT_UPDATED, wxCommandEventHandler (ThreadedStaticText::thread_finished), 0, this); + Bind (wxEVT_COMMAND_TEXT_UPDATED, boost::bind (&ThreadedStaticText::thread_finished, this, _1), _update_event_id); _thread = new thread (bind (&ThreadedStaticText::run, this, fn)); } @@ -138,22 +182,15 @@ checked_set (wxSpinCtrl* widget, int value) } void -checked_set (wxComboBox* widget, int value) +checked_set (wxChoice* widget, int value) { if (widget->GetSelection() != value) { - if (value == wxNOT_FOUND) { - /* Work around an apparent wxWidgets bug; SetSelection (wxNOT_FOUND) - appears not to work sometimes. - */ - widget->SetValue (wxT ("")); - } else { - widget->SetSelection (value); - } + widget->SetSelection (value); } } void -checked_set (wxComboBox* widget, string value) +checked_set (wxChoice* widget, string value) { wxClientData* o = 0; if (widget->GetSelection() != -1) { @@ -178,6 +215,14 @@ checked_set (wxTextCtrl* widget, string value) } void +checked_set (wxStaticText* widget, string value) +{ + if (widget->GetLabel() != std_to_wx (value)) { + widget->SetLabel (std_to_wx (value)); + } +} + +void checked_set (wxCheckBox* widget, bool value) { if (widget->GetValue() != value) { @@ -192,3 +237,42 @@ checked_set (wxRadioButton* widget, bool value) widget->SetValue (value); } } + +void +dcpomatic_setup_i18n () +{ + int language = wxLANGUAGE_DEFAULT; + + boost::optional<string> config_lang = Config::instance()->language (); + if (config_lang && !config_lang->empty ()) { + wxLanguageInfo const * li = wxLocale::FindLanguageInfo (std_to_wx (config_lang.get ())); + if (li) { + language = li->Language; + } + } + + wxLocale* locale = 0; + if (wxLocale::IsAvailable (language)) { + locale = new wxLocale (language, wxLOCALE_LOAD_DEFAULT); + +#ifdef DCPOMATIC_WINDOWS + locale->AddCatalogLookupPathPrefix (std_to_wx (mo_path().string())); +#endif + +#ifdef DCPOMATIC_POSIX + locale->AddCatalogLookupPathPrefix (POSIX_LOCALE_PREFIX); +#endif + + locale->AddCatalog (wxT ("libdcpomatic-wx")); + locale->AddCatalog (wxT ("dcpomatic")); + + if (!locale->IsOk()) { + delete locale; + locale = new wxLocale (wxLANGUAGE_ENGLISH); + } + } + + if (locale) { + dcpomatic_setup_gettext_i18n (wx_to_std (locale->GetCanonicalName ())); + } +} diff --git a/src/wx/wx_util.h b/src/wx/wx_util.h index 6cb7fd002..d942d8fa8 100644 --- a/src/wx/wx_util.h +++ b/src/wx/wx_util.h @@ -17,21 +17,36 @@ */ +#ifndef DCPOMATIC_WX_UTIL_H +#define DCPOMATIC_WX_UTIL_H + #include <wx/wx.h> +#include <wx/gbsizer.h> #include <boost/function.hpp> #include <boost/thread.hpp> +#ifdef __WXGTK__ +#include <gtk/gtk.h> +#endif class wxFilePickerCtrl; class wxSpinCtrl; +class wxGridBagSizer; + +#define DCPOMATIC_SIZER_X_GAP 8 +#define DCPOMATIC_SIZER_Y_GAP 8 +#define DCPOMATIC_DIALOG_BORDER 12 /** @file src/wx/wx_util.h * @brief Some utility functions and classes. */ -extern void error_dialog (wxWindow *, std::string); -extern wxStaticText* add_label_to_sizer (wxSizer *, wxWindow *, std::string, int prop = 0); +extern void error_dialog (wxWindow *, wxString); +extern bool confirm_dialog (wxWindow *, wxString); +extern wxStaticText* add_label_to_sizer (wxSizer *, wxWindow *, wxString, bool left, int prop = 0); +extern wxStaticText* add_label_to_grid_bag_sizer (wxGridBagSizer *, wxWindow *, wxString, bool, wxGBPosition, wxGBSpan span = wxDefaultSpan); extern std::string wx_to_std (wxString); extern wxString std_to_wx (std::string); +extern void dcpomatic_setup_i18n (); /** @class ThreadedStaticText * @@ -41,7 +56,7 @@ extern wxString std_to_wx (std::string); class ThreadedStaticText : public wxStaticText { public: - ThreadedStaticText (wxWindow* parent, std::string initial, boost::function<std::string ()> fn); + ThreadedStaticText (wxWindow* parent, wxString initial, boost::function<std::string ()> fn); ~ThreadedStaticText (); private: @@ -58,8 +73,18 @@ extern std::string string_client_data (wxClientData* o); extern void checked_set (wxFilePickerCtrl* widget, std::string value); extern void checked_set (wxSpinCtrl* widget, int value); -extern void checked_set (wxComboBox* widget, int value); -extern void checked_set (wxComboBox* widget, std::string value); +extern void checked_set (wxChoice* widget, int value); +extern void checked_set (wxChoice* widget, std::string value); extern void checked_set (wxTextCtrl* widget, std::string value); extern void checked_set (wxCheckBox* widget, bool value); extern void checked_set (wxRadioButton* widget, bool value); +extern void checked_set (wxStaticText* widget, std::string value); + +/* GTK 2.24.17 has a buggy GtkFileChooserButton and it was put in Ubuntu 13.04. + Use our own dir picker as this is the least bad option I can think of. +*/ +#if defined(__WXMSW__) || (GTK_MAJOR_VERSION == 2 && GTK_MINOR_VERSION == 24 && GTK_MICRO_VERSION == 17) +#define DCPOMATIC_USE_OWN_DIR_PICKER +#endif + +#endif |
