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