41591b062c0f610ef5aa1e0c45db90fa54b620e4
[dcpomatic.git] / src / lib / audio_analysis.cc
1 /*
2     Copyright (C) 2012-2018 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 "warnings.h"
27 #include <dcp/raw_convert.h>
28 DCPOMATIC_DISABLE_WARNINGS
29 #include <libxml++/libxml++.h>
30 DCPOMATIC_ENABLE_WARNINGS
31 #include <boost/filesystem.hpp>
32 #include <boost/foreach.hpp>
33 #include <stdint.h>
34 #include <cmath>
35 #include <cstdio>
36 #include <iostream>
37 #include <inttypes.h>
38
39 using std::ostream;
40 using std::istream;
41 using std::string;
42 using std::vector;
43 using std::cout;
44 using std::max;
45 using std::pair;
46 using std::make_pair;
47 using std::list;
48 using std::shared_ptr;
49 using boost::optional;
50 using std::dynamic_pointer_cast;
51 using dcp::raw_convert;
52 using namespace dcpomatic;
53
54 int const AudioAnalysis::_current_state_version = 3;
55
56 AudioAnalysis::AudioAnalysis (int channels)
57 {
58         _data.resize (channels);
59 }
60
61 AudioAnalysis::AudioAnalysis (boost::filesystem::path filename)
62 {
63         cxml::Document f ("AudioAnalysis");
64         f.read_file (filename);
65
66         if (f.optional_number_child<int>("Version").get_value_or(1) < _current_state_version) {
67                 /* Too old.  Throw an exception so that this analysis is re-run. */
68                 throw OldFormatError ("Audio analysis file is too old");
69         }
70
71         BOOST_FOREACH (cxml::NodePtr i, f.node_children ("Channel")) {
72                 vector<AudioPoint> channel;
73
74                 BOOST_FOREACH (cxml::NodePtr j, i->node_children ("Point")) {
75                         channel.push_back (AudioPoint (j));
76                 }
77
78                 _data.push_back (channel);
79         }
80
81         BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("SamplePeak")) {
82                 _sample_peak.push_back (
83                         PeakTime (
84                                 dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
85                                 )
86                         );
87         }
88
89         BOOST_FOREACH (cxml::ConstNodePtr i, f.node_children ("TruePeak")) {
90                 _true_peak.push_back (dcp::raw_convert<float> (i->content ()));
91         }
92
93         _integrated_loudness = f.optional_number_child<float> ("IntegratedLoudness");
94         _loudness_range = f.optional_number_child<float> ("LoudnessRange");
95
96         _analysis_gain = f.optional_number_child<double> ("AnalysisGain");
97         _samples_per_point = f.number_child<int64_t> ("SamplesPerPoint");
98         _sample_rate = f.number_child<int64_t> ("SampleRate");
99
100         _leqm = f.optional_number_child<double>("Leqm");
101 }
102
103 void
104 AudioAnalysis::add_point (int c, AudioPoint const & p)
105 {
106         DCPOMATIC_ASSERT (c < channels ());
107         _data[c].push_back (p);
108 }
109
110 AudioPoint
111 AudioAnalysis::get_point (int c, int p) const
112 {
113         DCPOMATIC_ASSERT (p < points (c));
114         return _data[c][p];
115 }
116
117 int
118 AudioAnalysis::channels () const
119 {
120         return _data.size ();
121 }
122
123 int
124 AudioAnalysis::points (int c) const
125 {
126         DCPOMATIC_ASSERT (c < channels ());
127         return _data[c].size ();
128 }
129
130 void
131 AudioAnalysis::write (boost::filesystem::path filename)
132 {
133         shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
134         xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
135
136         root->add_child("Version")->add_child_text (raw_convert<string> (_current_state_version));
137
138         BOOST_FOREACH (vector<AudioPoint>& i, _data) {
139                 xmlpp::Element* channel = root->add_child ("Channel");
140                 BOOST_FOREACH (AudioPoint& j, i) {
141                         j.as_xml (channel->add_child ("Point"));
142                 }
143         }
144
145         for (size_t i = 0; i < _sample_peak.size(); ++i) {
146                 xmlpp::Element* n = root->add_child("SamplePeak");
147                 n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
148                 n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
149         }
150
151         BOOST_FOREACH (float i, _true_peak) {
152                 root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
153         }
154
155         if (_integrated_loudness) {
156                 root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
157         }
158
159         if (_loudness_range) {
160                 root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
161         }
162
163         if (_analysis_gain) {
164                 root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
165         }
166
167         root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
168         root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
169
170         if (_leqm) {
171                 root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm));
172         }
173
174         doc->write_to_file_formatted (filename.string ());
175 }
176
177 float
178 AudioAnalysis::gain_correction (shared_ptr<const Playlist> playlist)
179 {
180         if (playlist->content().size() == 1 && analysis_gain ()) {
181                 /* In this case we know that the analysis was of a single piece of content and
182                    we know that content's gain when the analysis was run.  Hence we can work out
183                    what correction is now needed to make it look `right'.
184                 */
185                 DCPOMATIC_ASSERT (playlist->content().front()->audio);
186                 return playlist->content().front()->audio->gain() - analysis_gain().get ();
187         }
188
189         return 0.0f;
190 }
191
192 /** @return Peak across all channels, and the channel number it is on */
193 pair<AudioAnalysis::PeakTime, int>
194 AudioAnalysis::overall_sample_peak () const
195 {
196         DCPOMATIC_ASSERT (!_sample_peak.empty ());
197
198         optional<PeakTime> pt;
199         int c = 0;
200
201         for (size_t i = 0; i < _sample_peak.size(); ++i) {
202                 if (!pt || _sample_peak[i].peak > pt->peak) {
203                         pt = _sample_peak[i];
204                         c = i;
205                 }
206         }
207
208         return make_pair (pt.get(), c);
209 }
210
211 optional<float>
212 AudioAnalysis::overall_true_peak () const
213 {
214         optional<float> p;
215
216         BOOST_FOREACH (float i, _true_peak) {
217                 if (!p || i > *p) {
218                         p = i;
219                 }
220         }
221
222         return p;
223 }
224