2013-03-01 Carl Hetherington <cth@carlh.net>
+ * Add primitive feature to plot graphs
+ of the soundtrack (#67).
+
* Version 0.76beta1 released.
2013-02-27 Carl Hetherington <cth@carlh.net>
--- /dev/null
+/*
+ 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 "options.h"
+#include "decoder_factory.h"
+#include "audio_decoder.h"
+
+#include "i18n.h"
+
+using std::string;
+using std::max;
+using std::cout;
+using boost::shared_ptr;
+
+int const AnalyseAudioJob::_num_points = 1024;
+
+AnalyseAudioJob::AnalyseAudioJob (shared_ptr<Film> f)
+ : Job (f)
+ , _done (0)
+ , _samples_per_point (1)
+{
+
+}
+
+string
+AnalyseAudioJob::name () const
+{
+ return String::compose (_("Analyse audio of %1"), _film->name());
+}
+
+void
+AnalyseAudioJob::run ()
+{
+ if (!_film->audio_stream () || !_film->length()) {
+ set_progress (1);
+ set_state (FINISHED_ERROR);
+ return;
+ }
+
+ DecodeOptions options;
+ options.decode_video = false;
+
+ Decoders decoders = decoder_factory (_film, options);
+ assert (decoders.audio);
+
+ decoders.audio->set_audio_stream (_film->audio_stream ());
+ decoders.audio->Audio.connect (bind (&AnalyseAudioJob::audio, this, _1));
+
+ int64_t total_audio_frames = video_frames_to_audio_frames (_film->length().get(), _film->audio_stream()->sample_rate(), _film->frames_per_second());
+ _samples_per_point = total_audio_frames / _num_points;
+
+ _current.resize (_film->audio_stream()->channels ());
+ _analysis.reset (new AudioAnalysis (_film->audio_stream()->channels()));
+
+ while (!decoders.audio->pass()) {
+ set_progress (float (_done) / total_audio_frames);
+ }
+
+ _analysis->write (_film->audio_analysis_path ());
+
+ set_progress (1);
+ set_state (FINISHED_OK);
+}
+
+void
+AnalyseAudioJob::audio (shared_ptr<AudioBuffers> b)
+{
+ 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;
+ }
+}
+
--- /dev/null
+/*
+ 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 "job.h"
+#include "audio_analysis.h"
+
+class AudioBuffers;
+
+class AnalyseAudioJob : public Job
+{
+public:
+ AnalyseAudioJob (boost::shared_ptr<Film> f);
+
+ std::string name () const;
+ void run ();
+
+private:
+ void audio (boost::shared_ptr<AudioBuffers>);
+
+ int64_t _done;
+ int64_t _samples_per_point;
+ std::vector<AudioPoint> _current;
+
+ boost::shared_ptr<AudioAnalysis> _analysis;
+
+ static const int _num_points;
+};
+
--- /dev/null
+/*
+ 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];
+ }
+}
+
+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 (string filename)
+{
+ ifstream f (filename.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 (string filename)
+{
+ string tmp = filename + ".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);
+}
--- /dev/null
+/*
+ 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_AUDIO_ANALYSIS_H
+#define DVDOMATIC_AUDIO_ANALYSIS_H
+
+#include <iostream>
+#include <vector>
+#include <list>
+
+class AudioPoint
+{
+public:
+ enum Type {
+ PEAK,
+ RMS,
+ COUNT
+ };
+
+ AudioPoint ();
+ AudioPoint (std::istream &);
+
+ void write (std::ostream &) const;
+
+ float& operator[] (int t) {
+ return _data[t];
+ }
+
+private:
+ float _data[COUNT];
+};
+
+class AudioAnalysis
+{
+public:
+ AudioAnalysis (int c);
+ AudioAnalysis (std::string);
+
+ 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 (std::string);
+
+private:
+ std::vector<std::vector<AudioPoint> > _data;
+};
+
+#endif
void
Encoder::write_audio (shared_ptr<const AudioBuffers> data)
{
- 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);
+ AudioMapping m (_film->audio_channels ());
+ if (m.dcp_channels() != _film->audio_channels()) {
+
+ /* Remap (currently just for mono -> 5.1) */
+
+ shared_ptr<AudioBuffers> b (new AudioBuffers (m.dcp_channels(), data->frames ()));
+ for (int i = 0; i < m.dcp_channels(); ++i) {
+ optional<int> s = m.dcp_to_source (static_cast<libdcp::Channel> (i));
+ if (!s) {
+ b->make_silent (i);
+ } else {
+ memcpy (b->data()[i], data->data()[s.get()], data->frames() * sizeof(float));
+ }
+ }
data = b;
}
void
FFmpegDecoder::setup_subtitle ()
{
- if (!_subtitle_stream) {
+ if (!_subtitle_stream || _subtitle_stream->id() >= int (_format_context->nb_streams)) {
return;
}
int frame_finished;
- while (avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
- filter_and_emit_video (_frame);
+ if (_opt.decode_video) {
+ while (avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet) >= 0 && frame_finished) {
+ filter_and_emit_video (_frame);
+ }
}
if (_audio_stream && _opt.decode_audio) {
shared_ptr<FFmpegAudioStream> ffa = dynamic_pointer_cast<FFmpegAudioStream> (_audio_stream);
- if (_packet.stream_index == _video_stream) {
+ if (_packet.stream_index == _video_stream && _opt.decode_video) {
int frame_finished;
int const r = avcodec_decode_video2 (_video_codec_context, _frame, &frame_finished, &_packet);
was before this packet. Until then audio is thrown away.
*/
- if (_first_video && _first_video.get() <= source_pts_seconds) {
+ if ((_first_video && _first_video.get() <= source_pts_seconds) || !_opt.decode_video) {
- if (!_first_audio) {
+ if (!_first_audio && _opt.decode_video) {
_first_audio = source_pts_seconds;
/* This is our first audio frame, and if we've arrived here we must have had our
#include "video_decoder.h"
#include "audio_decoder.h"
#include "external_audio_decoder.h"
+#include "analyse_audio_job.h"
#include "i18n.h"
return video_state_identifier() + N_(".mxf");
}
+string
+Film::audio_analysis_path () const
+{
+ boost::filesystem::path p;
+ p /= "analysis";
+ p /= content_digest();
+ return file (p.string ());
+}
+
/** Add suitable Jobs to the JobManager to create a DCP for this Film */
void
Film::make_dcp ()
}
}
+/** Start a job to analyse the audio of our content file */
+void
+Film::analyse_audio ()
+{
+ if (_analyse_audio_job) {
+ return;
+ }
+
+ _analyse_audio_job.reset (new AnalyseAudioJob (shared_from_this()));
+ _analyse_audio_job->Finished.connect (bind (&Film::analyse_audio_finished, this));
+ JobManager::instance()->add (_analyse_audio_job);
+}
+
/** Start a job to examine our content file */
void
Film::examine_content ()
JobManager::instance()->add (_examine_content_job);
}
+void
+Film::analyse_audio_finished ()
+{
+ ensure_ui_thread ();
+ _analyse_audio_job.reset ();
+
+ AudioAnalysisFinished ();
+}
+
void
Film::examine_content_finished ()
{
set_content_audio_streams (d.audio->audio_streams ());
}
+ {
+ boost::mutex::scoped_lock lm (_state_mutex);
+ _content = c;
+ }
+
+ signal_changed (CONTENT);
+
/* 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());
set_subtitle_stream (d.video->subtitle_streams().front());
}
- {
- boost::mutex::scoped_lock lm (_state_mutex);
- _content = c;
- }
-
- signal_changed (CONTENT);
-
examine_content ();
} catch (...) {
class Filter;
class Log;
class ExamineContentJob;
+class AnalyseAudioJob;
class ExternalAudioStream;
/** @class Film
std::string info_path (int f) const;
std::string video_mxf_dir () const;
std::string video_mxf_filename () const;
+ std::string audio_analysis_path () const;
void examine_content ();
+ void analyse_audio ();
void send_dcp_to_tms ();
void make_dcp ();
/** Emitted when some property has changed */
mutable boost::signals2::signal<void (Property)> Changed;
+ boost::signals2::signal<void ()> AudioAnalysisFinished;
+
/** Current version number of the state file */
static int const state_version;
/** Any running ExamineContentJob, or 0 */
boost::shared_ptr<ExamineContentJob> _examine_content_job;
+ /** Any running AnalyseAudioJob, or 0 */
+ boost::shared_ptr<AnalyseAudioJob> _analyse_audio_job;
void signal_changed (Property);
void examine_content_finished ();
+ void analyse_audio_finished ();
std::string video_state_identifier () const;
/** Complete path to directory containing the film metadata;
if (_state == FINISHED_OK || _state == FINISHED_ERROR) {
_ran_for = elapsed_time ();
- Finished ();
}
}
void descend (float);
float overall_progress () const;
+ /** Emitted by the JobManagerView from the UI thread */
boost::signals2::signal<void()> Finished;
protected:
{
public:
DecodeOptions ()
- : decode_audio (true)
+ : decode_video (true)
+ , decode_audio (true)
, decode_subtitles (false)
, video_sync (true)
{}
-
+
+ bool decode_video;
bool decode_audio;
bool decode_subtitles;
bool video_sync;
return 96000;
}
-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;
- }
-
- return f;
-}
-
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);
return info;
}
+
+string
+audio_channel_name (int c)
+{
+ assert (MAX_AUDIO_CHANNELS == 6);
+
+ /* 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];
+}
+
+AudioMapping::AudioMapping (int c)
+ : _source_channels (c)
+{
+
+}
+
+optional<libdcp::Channel>
+AudioMapping::source_to_dcp (int c) const
+{
+ if (c >= _source_channels) {
+ return optional<libdcp::Channel> ();
+ }
+
+ if (_source_channels == 1) {
+ /* mono sources to centre */
+ return libdcp::CENTRE;
+ }
+
+ return static_cast<libdcp::Channel> (c);
+}
+
+optional<int>
+AudioMapping::dcp_to_source (libdcp::Channel c) const
+{
+ if (_source_channels == 1) {
+ if (c == libdcp::CENTRE) {
+ return 0;
+ } else {
+ return optional<int> ();
+ }
+ }
+
+ if (static_cast<int> (c) >= _source_channels) {
+ return optional<int> ();
+ }
+
+ return static_cast<int> (c);
+}
+
+int
+AudioMapping::dcp_channels () const
+{
+ if (_source_channels == 1) {
+ /* The source is mono, so to put the mono channel into
+ the centre we need to generate a 5.1 soundtrack.
+ */
+ return 6;
+ }
+
+ return _source_channels;
+}
#include <vector>
#include <boost/shared_ptr.hpp>
#include <boost/asio.hpp>
+#include <boost/optional.hpp>
#include <libdcp/util.h>
extern "C" {
#include <libavcodec/avcodec.h>
extern std::string md5_digest (std::string);
extern std::string md5_digest (void const *, int);
extern void ensure_ui_thread ();
+extern std::string audio_channel_name (int);
typedef int SourceFrame;
extern std::string crop_string (Position, libdcp::Size);
extern int dcp_audio_sample_rate (int);
-extern int dcp_audio_channels (int);
extern std::string colour_lut_index_to_name (int index);
extern int stride_round_up (int, int const *, int);
extern int stride_lookup (int c, int const * stride);
float** _data;
};
+class AudioMapping
+{
+public:
+ AudioMapping (int);
+
+ boost::optional<libdcp::Channel> source_to_dcp (int c) const;
+ boost::optional<int> dcp_to_source (libdcp::Channel c) const;
+ int dcp_channels () const;
+
+private:
+ int _source_channels;
+};
+
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 ();
_picture_asset_writer = _picture_asset->start_write (_first_nonexistant_frame > 0);
- if (dcp_audio_channels (_film->audio_channels()) > 0) {
+ AudioMapping m (_film->audio_channels ());
+
+ if (m.dcp_channels() > 0) {
_sound_asset.reset (
new libdcp::SoundAsset (
_film->dir (_film->dcp_name()),
N_("audio.mxf"),
DCPFrameRate (_film->frames_per_second()).frames_per_second,
- dcp_audio_channels (_film->audio_channels()),
+ m.dcp_channels (),
dcp_audio_sample_rate (_film->audio_stream()->sample_rate())
)
);
sources = """
ab_transcode_job.cc
ab_transcoder.cc
+ analyse_audio_job.cc
+ audio_analysis.cc
audio_decoder.cc
audio_source.cc
config.cc
ID_jobs_make_dcp,
ID_jobs_send_dcp_to_tms,
ID_jobs_show_dcp,
- ID_jobs_examine_content,
+ ID_jobs_analyse_audio,
ID_help_about
};
add_item (jobs_menu, _("&Send DCP to TMS"), ID_jobs_send_dcp_to_tms, NEEDS_FILM);
add_item (jobs_menu, _("S&how DCP"), ID_jobs_show_dcp, NEEDS_FILM);
jobs_menu->AppendSeparator ();
- add_item (jobs_menu, _("&Examine content"), ID_jobs_examine_content, NEEDS_FILM);
+ add_item (jobs_menu, _("&Analyse audio"), ID_jobs_analyse_audio, NEEDS_FILM);
wxMenu* help = new wxMenu;
add_item (help, _("About"), ID_help_about, ALWAYS);
Connect (ID_jobs_make_dcp, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_make_dcp));
Connect (ID_jobs_send_dcp_to_tms, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_send_dcp_to_tms));
Connect (ID_jobs_show_dcp, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_show_dcp));
- Connect (ID_jobs_examine_content, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_examine_content));
+ Connect (ID_jobs_analyse_audio, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::jobs_analyse_audio));
Connect (ID_help_about, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler (Frame::help_about));
Connect (wxID_ANY, wxEVT_MENU_OPEN, wxMenuEventHandler (Frame::menu_opened));
}
#endif
}
-
- void jobs_examine_content (wxCommandEvent &)
+
+ void jobs_analyse_audio (wxCommandEvent &)
{
- film->examine_content ();
+ film->analyse_audio ();
}
void help_about (wxCommandEvent &)
--- /dev/null
+/*
+ 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 "audio_dialog.h"
+#include "audio_plot.h"
+#include "audio_analysis.h"
+#include "film.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)
+ , _plot (0)
+{
+ wxBoxSizer* sizer = new wxBoxSizer (wxHORIZONTAL);
+
+ _plot = new AudioPlot (this);
+ sizer->Add (_plot, 1, wxALL, 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]->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (AudioDialog::channel_clicked), 0, this);
+ }
+
+ {
+ 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]->Connect (wxID_ANY, wxEVT_COMMAND_CHECKBOX_CLICKED, wxCommandEventHandler (AudioDialog::type_clicked), 0, this);
+ }
+
+ {
+ 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->Connect (wxID_ANY, wxEVT_SCROLL_THUMBTRACK, wxScrollEventHandler (AudioDialog::smoothing_changed), 0, this);
+ side->Add (_smoothing, 1, wxEXPAND);
+
+ sizer->Add (side, 0, wxALL, 12);
+
+ SetSizer (sizer);
+ sizer->Layout ();
+ sizer->SetSizeHints (this);
+}
+
+void
+AudioDialog::set_film (boost::shared_ptr<Film> f)
+{
+ _film_changed_connection.disconnect ();
+ _film_audio_analysis_finished_connection.disconnect ();
+
+ _film = f;
+
+ try_to_load_analysis ();
+ setup_channels ();
+ _plot->set_gain (_film->audio_gain ());
+
+ _film_changed_connection = _film->Changed.connect (bind (&AudioDialog::film_changed, this, _1));
+ _film_audio_analysis_finished_connection = _film->AudioAnalysisFinished.connect (bind (&AudioDialog::try_to_load_analysis, this));
+
+ SetTitle (std_to_wx (String::compose (wx_to_std (_("DVD-o-matic audio - %1")), _film->name())));
+}
+
+void
+AudioDialog::setup_channels ()
+{
+ if (!_film->audio_stream()) {
+ return;
+ }
+
+ AudioMapping m (_film->audio_stream()->channels ());
+
+ for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
+ if (m.dcp_to_source(static_cast<libdcp::Channel>(i))) {
+ _channel_checkbox[i]->Show ();
+ } else {
+ _channel_checkbox[i]->Hide ();
+ }
+ }
+}
+
+void
+AudioDialog::try_to_load_analysis ()
+{
+ shared_ptr<AudioAnalysis> a;
+
+ if (boost::filesystem::exists (_film->audio_analysis_path())) {
+ a.reset (new AudioAnalysis (_film->audio_analysis_path ()));
+ } else {
+ if (IsShown ()) {
+ _film->analyse_audio ();
+ }
+ }
+
+ _plot->set_analysis (a);
+
+ AudioMapping m (_film->audio_stream()->channels ());
+ optional<libdcp::Channel> c = m.source_to_dcp (0);
+ if (c) {
+ _channel_checkbox[c.get()]->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::channel_clicked (wxCommandEvent& ev)
+{
+ int c = 0;
+ while (c < MAX_AUDIO_CHANNELS && ev.GetEventObject() != _channel_checkbox[c]) {
+ ++c;
+ }
+
+ assert (c < MAX_AUDIO_CHANNELS);
+
+ AudioMapping m (_film->audio_stream()->channels ());
+ optional<int> s = m.dcp_to_source (static_cast<libdcp::Channel> (c));
+ if (s) {
+ _plot->set_channel_visible (s.get(), _channel_checkbox[c]->GetValue ());
+ }
+}
+
+void
+AudioDialog::film_changed (Film::Property p)
+{
+ switch (p) {
+ case Film::AUDIO_GAIN:
+ _plot->set_gain (_film->audio_gain ());
+ break;
+ case Film::CONTENT_AUDIO_STREAM:
+ case Film::EXTERNAL_AUDIO:
+ case Film::USE_CONTENT_AUDIO:
+ setup_channels ();
+ break;
+ default:
+ break;
+ }
+}
+
+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 (wxScrollEvent &)
+{
+ _plot->set_smoothing (_smoothing->GetValue ());
+}
--- /dev/null
+/*
+ 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/signals2.hpp>
+#include <wx/wx.h>
+#include "lib/film.h"
+#include "lib/audio_analysis.h"
+
+class AudioPlot;
+class Film;
+
+class AudioDialog : public wxDialog
+{
+public:
+ AudioDialog (wxWindow *);
+
+ void set_film (boost::shared_ptr<Film>);
+
+private:
+ void film_changed (Film::Property);
+ void channel_clicked (wxCommandEvent &);
+ void type_clicked (wxCommandEvent &);
+ void smoothing_changed (wxScrollEvent &);
+ void try_to_load_analysis ();
+ void setup_channels ();
+
+ boost::shared_ptr<Film> _film;
+ AudioPlot* _plot;
+ wxCheckBox* _channel_checkbox[MAX_AUDIO_CHANNELS];
+ wxCheckBox* _type_checkbox[AudioPoint::COUNT];
+ wxSlider* _smoothing;
+ boost::signals2::scoped_connection _film_changed_connection;
+ boost::signals2::scoped_connection _film_audio_analysis_finished_connection;
+};
--- /dev/null
+/*
+ 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/decoder_factory.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)
+ , _gain (0)
+ , _smoothing (max_smoothing / 2)
+{
+ SetDoubleBuffered (true);
+
+ 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));
+
+ Connect (wxID_ANY, wxEVT_PAINT, wxPaintEventHandler (AudioPlot::paint), 0, 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::paint (wxPaintEvent &)
+{
+ wxPaintDC dc (this);
+
+ wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
+ if (!gc) {
+ return;
+ }
+
+ if (!_analysis || _analysis->channels() == 0) {
+ gc->SetFont (gc->CreateFont (*wxNORMAL_FONT));
+ gc->DrawText (_("Please wait; audio is being analysed..."), 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 ("-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];
+#if wxMAJOR_VERSION == 2 && wxMINOR_VERSION >= 9
+ gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (col.Red(), col.Green(), col.Blue(), col.Alpha() / 2), 1, wxPENSTYLE_SOLID));
+#else
+ gc->SetPen (*wxThePenList->FindOrCreatePen (wxColour (col.Red(), col.Green(), col.Blue(), col.Alpha() / 2), 1, wxSOLID));
+#endif
+ 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];
+#if wxMAJOR_VERSION == 2 && wxMINOR_VERSION >= 9
+ gc->SetPen (*wxThePenList->FindOrCreatePen (col, 1, wxPENSTYLE_SOLID));
+#else
+ gc->SetPen (*wxThePenList->FindOrCreatePen (col, 1, wxSOLID));
+#endif
+ 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
+{
+ 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
+{
+ 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 ();
+}
--- /dev/null
+/*
+ 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 "util.h"
+#include "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);
+
+ static const int max_smoothing;
+
+private:
+ void paint (wxPaintEvent &);
+
+ 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;
+
+ static const int _minimum;
+};
#include "sound_processor.h"
#include "dci_metadata_dialog.h"
#include "scaler.h"
+#include "audio_dialog.h"
using std::string;
using std::cout;
: wxPanel (parent)
, _film (f)
, _generally_sensitive (true)
+ , _audio_dialog (0)
{
wxBoxSizer* s = new wxBoxSizer (wxVERTICAL);
SetSizer (s);
_audio_gain_calculate_button->Connect (
wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::audio_gain_calculate_button_clicked), 0, this
);
+ _show_audio->Connect (wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler (FilmEditor::show_audio_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);
wxFlexGridSizer* grid = new wxFlexGridSizer (2, 4, 4);
_audio_sizer->Add (grid, 0, wxALL, 8);
+ _show_audio = new wxButton (_audio_panel, wxID_ANY, _("Show Audio..."));
+ grid->Add (_show_audio, 1);
+ grid->AddSpacer (0);
+
{
video_control (add_label_to_sizer (grid, _audio_panel, _("Audio Gain")));
wxBoxSizer* s = new wxBoxSizer (wxHORIZONTAL);
grid->Add (_use_external_audio);
grid->AddSpacer (0);
- assert (MAX_AUDIO_CHANNELS == 6);
-
- /// TRANSLATORS: these are the names of audio channels; Lfe (sub) is the low-frequency
- /// enhancement channel (sub-woofer).
- wxString const channels[] = {
- _("Left"),
- _("Right"),
- _("Centre"),
- _("Lfe (sub)"),
- _("Left surround"),
- _("Right surround"),
- };
-
for (int i = 0; i < MAX_AUDIO_CHANNELS; ++i) {
- add_label_to_sizer (grid, _audio_panel, channels[i]);
+ add_label_to_sizer (grid, _audio_panel, std_to_wx (audio_channel_name (i)));
_external_audio[i] = new wxFilePickerCtrl (_audio_panel, wxID_ANY, wxT (""), _("Select Audio File"), wxT ("*.wav"));
grid->Add (_external_audio[i], 1, wxEXPAND);
}
} else {
FileChanged (wx_to_std (N_("")));
}
+
+ if (_audio_dialog) {
+ _audio_dialog->set_film (_film);
+ }
film_changed (Film::NAME);
film_changed (Film::USE_DCI_NAME);
_j2k_bandwidth->Enable (s);
_audio_gain->Enable (s);
_audio_gain_calculate_button->Enable (s);
+ _show_audio->Enable (s);
_audio_delay->Enable (s);
_still_duration->Enable (s);
_dcp_name->SetLabel (std_to_wx (s));
}
}
+
+void
+FilmEditor::show_audio_clicked (wxCommandEvent &)
+{
+ if (_audio_dialog) {
+ _audio_dialog->Destroy ();
+ _audio_dialog = 0;
+ }
+
+ _audio_dialog = new AudioDialog (this);
+ _audio_dialog->Show ();
+ _audio_dialog->set_film (_film);
+}
#include "lib/film.h"
class wxNotebook;
-
class Film;
+class AudioDialog;
/** @class FilmEditor
* @brief A wx widget to edit a film's metadata, and perform various functions.
void scaler_changed (wxCommandEvent &);
void audio_gain_changed (wxCommandEvent &);
void audio_gain_calculate_button_clicked (wxCommandEvent &);
+ void show_audio_clicked (wxCommandEvent &);
void audio_delay_changed (wxCommandEvent &);
void with_subtitles_toggled (wxCommandEvent &);
void subtitle_offset_changed (wxCommandEvent &);
wxSpinCtrl* _audio_gain;
/** A button to open the gain calculation dialogue */
wxButton* _audio_gain_calculate_button;
+ wxButton* _show_audio;
/** The Film's audio delay */
wxSpinCtrl* _audio_delay;
wxCheckBox* _with_subtitles;
std::vector<Format const *> _formats;
bool _generally_sensitive;
+ AudioDialog* _audio_dialog;
};
o.decode_audio = false;
o.decode_subtitles = true;
o.video_sync = false;
- _decoders = decoder_factory (_film, o);
+
+ try {
+ _decoders = decoder_factory (_film, o);
+ } catch (StringError& e) {
+ error_dialog (this, wxString::Format (_("Could not open content file (%s)"), e.what()));
+ return;
+ }
+
if (_decoders.video == 0) {
break;
}
if ((*i)->finished() && !_job_records[*i].finalised) {
_job_records[*i].gauge->SetValue (100);
checked_set (_job_records[*i].message, st);
+ (*i)->Finished ();
_job_records[*i].finalised = true;
if (!(*i)->error_details().empty ()) {
_job_records[*i].details->Enable (true);
import i18n
sources = """
+ audio_dialog.cc
+ audio_plot.cc
config_dialog.cc
dci_metadata_dialog.cc
dir_picker_ctrl.cc