Add channel details to high-audio-level hints (#822).
[dcpomatic.git] / src / lib / audio_analysis.cc
1 /*
2     Copyright (C) 2012-2016 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 #include "audio_analysis.h"
22 #include "cross.h"
23 #include "util.h"
24 #include "playlist.h"
25 #include "audio_content.h"
26 #include <dcp/raw_convert.h>
27 #include <libxml++/libxml++.h>
28 #include <boost/filesystem.hpp>
29 #include <boost/foreach.hpp>
30 #include <stdint.h>
31 #include <cmath>
32 #include <cstdio>
33 #include <iostream>
34 #include <inttypes.h>
35
36 using std::ostream;
37 using std::istream;
38 using std::string;
39 using std::vector;
40 using std::cout;
41 using std::max;
42 using std::pair;
43 using std::make_pair;
44 using std::list;
45 using boost::shared_ptr;
46 using boost::optional;
47 using boost::dynamic_pointer_cast;
48 using dcp::raw_convert;
49
50 int const AudioAnalysis::_current_state_version = 2;
51
52 AudioAnalysis::AudioAnalysis (int channels)
53 {
54         _data.resize (channels);
55 }
56
57 AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
58 {
59         cxml::Document f ("AudioAnalysis");
60         f.read_file (filename);
61
62         if (f.optional_number_child<int>("Version").get_value_or(1) < _current_state_version) {
63                 /* Too old.  Throw an exception so that this analysis is re-run. */
64                 throw OldFormatError ("Audio analysis file is too old");
65         }
66
67         BOOST_FOREACH (cxml::NodePtr i, f.node_children ("Channel")) {
68                 vector<AudioPoint> channel;
69
70                 BOOST_FOREACH (cxml::NodePtr j, i->node_children ("Point")) {
71                         channel.push_back (AudioPoint (j));
72                 }
73
74                 _data.push_back (channel);
75         }
76
77         BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("SamplePeak")) {
78                 _sample_peak.push_back (
79                         PeakTime (
80                                 dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
81                                 )
82                         );
83         }
84
85         BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("TruePeak")) {
86                 _true_peak.push_back (dcp::raw_convert<float> (i->content ()));
87         }
88
89         _integrated_loudness = f.optional_number_child<float> ("IntegratedLoudness");
90         _loudness_range = f.optional_number_child<float> ("LoudnessRange");
91
92         _analysis_gain = f.optional_number_child<double> ("AnalysisGain");
93 }
94
95 void
96 AudioAnalysis::add_point (int c, AudioPoint const & p)
97 {
98         DCPOMATIC_ASSERT (c < channels ());
99         _data[c].push_back (p);
100 }
101
102 AudioPoint
103 AudioAnalysis::get_point (int c, int p) const
104 {
105         DCPOMATIC_ASSERT (p < points (c));
106         return _data[c][p];
107 }
108
109 int
110 AudioAnalysis::channels () const
111 {
112         return _data.size ();
113 }
114
115 int
116 AudioAnalysis::points (int c) const
117 {
118         DCPOMATIC_ASSERT (c < channels ());
119         return _data[c].size ();
120 }
121
122 void
123 AudioAnalysis::write (boost::filesystem::path filename)
124 {
125         shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
126         xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
127
128         root->add_child("Version")->add_child_text (raw_convert<string> (_current_state_version));
129
130         BOOST_FOREACH (vector<AudioPoint>& i, _data) {
131                 xmlpp::Element* channel = root->add_child ("Channel");
132                 BOOST_FOREACH (AudioPoint& j, i) {
133                         j.as_xml (channel->add_child ("Point"));
134                 }
135         }
136
137         for (size_t i = 0; i < _sample_peak.size(); ++i) {
138                 xmlpp::Element* n = root->add_child("SamplePeak");
139                 n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
140                 n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
141         }
142
143         BOOST_FOREACH (float i, _true_peak) {
144                 root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
145         }
146
147         if (_integrated_loudness) {
148                 root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
149         }
150
151         if (_loudness_range) {
152                 root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
153         }
154
155         if (_analysis_gain) {
156                 root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
157         }
158
159         doc->write_to_file_formatted (filename.string ());
160 }
161
162 float
163 AudioAnalysis::gain_correction (shared_ptr<const Playlist> playlist)
164 {
165         if (playlist->content().size() == 1 && analysis_gain ()) {
166                 /* In this case we know that the analysis was of a single piece of content and
167                    we know that content's gain when the analysis was run.  Hence we can work out
168                    what correction is now needed to make it look `right'.
169                 */
170                 DCPOMATIC_ASSERT (playlist->content().front()->audio);
171                 return playlist->content().front()->audio->gain() - analysis_gain().get ();
172         }
173
174         return 0.0f;
175 }
176
177 /** @return Peak across all channels, and the channel number it is on */
178 pair<AudioAnalysis::PeakTime, int>
179 AudioAnalysis::overall_sample_peak () const
180 {
181         optional<PeakTime> pt;
182         int c;
183
184         for (size_t i = 0; i < _sample_peak.size(); ++i) {
185                 if (!pt || _sample_peak[i].peak > pt->peak) {
186                         pt = _sample_peak[i];
187                         c = i;
188                 }
189         }
190
191         return make_pair (pt.get(), c);
192 }
193
194 optional<float>
195 AudioAnalysis::overall_true_peak () const
196 {
197         optional<float> p;
198
199         BOOST_FOREACH (float i, _true_peak) {
200                 if (!p || i > *p) {
201                         p = i;
202                 }
203         }
204
205         return p;
206 }