Fix the build for older macOS.
[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 "cross.h"
24 #include "util.h"
25 #include "playlist.h"
26 #include "audio_content.h"
27 #include "warnings.h"
28 #include <dcp/raw_convert.h>
29 DCPOMATIC_DISABLE_WARNINGS
30 #include <libxml++/libxml++.h>
31 DCPOMATIC_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 (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                 _sample_peak.push_back (
88                         PeakTime(
89                                 dcp::raw_convert<float>(i->content()), DCPTime(i->number_attribute<Frame>("Time"))
90                                 )
91                         );
92         }
93
94         for (auto i: f.node_children("TruePeak")) {
95                 _true_peak.push_back (dcp::raw_convert<float>(i->content()));
96         }
97
98         _integrated_loudness = f.optional_number_child<float>("IntegratedLoudness");
99         _loudness_range = f.optional_number_child<float>("LoudnessRange");
100
101         _analysis_gain = f.optional_number_child<double>("AnalysisGain");
102         _samples_per_point = f.number_child<int64_t>("SamplesPerPoint");
103         _sample_rate = f.number_child<int64_t>("SampleRate");
104
105         _leqm = f.optional_number_child<double>("Leqm");
106 }
107
108
109 void
110 AudioAnalysis::add_point (int c, AudioPoint const & p)
111 {
112         DCPOMATIC_ASSERT (c < channels ());
113         _data[c].push_back (p);
114 }
115
116
117 AudioPoint
118 AudioAnalysis::get_point (int c, int p) const
119 {
120         DCPOMATIC_ASSERT (p < points(c));
121         return _data[c][p];
122 }
123
124
125 int
126 AudioAnalysis::channels () const
127 {
128         return _data.size ();
129 }
130
131
132 int
133 AudioAnalysis::points (int c) const
134 {
135         DCPOMATIC_ASSERT (c < channels());
136         return _data[c].size ();
137 }
138
139
140 void
141 AudioAnalysis::write (boost::filesystem::path filename)
142 {
143         auto doc = make_shared<xmlpp::Document>();
144         xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
145
146         root->add_child("Version")->add_child_text(raw_convert<string>(_current_state_version));
147
148         for (auto& i: _data) {
149                 auto channel = root->add_child ("Channel");
150                 for (auto& j: i) {
151                         j.as_xml (channel->add_child ("Point"));
152                 }
153         }
154
155         for (size_t i = 0; i < _sample_peak.size(); ++i) {
156                 auto n = root->add_child("SamplePeak");
157                 n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
158                 n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
159         }
160
161         for (auto i: _true_peak) {
162                 root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
163         }
164
165         if (_integrated_loudness) {
166                 root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
167         }
168
169         if (_loudness_range) {
170                 root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
171         }
172
173         if (_analysis_gain) {
174                 root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
175         }
176
177         root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
178         root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
179
180         if (_leqm) {
181                 root->add_child("Leqm")->add_child_text(raw_convert<string>(*_leqm));
182         }
183
184         doc->write_to_file_formatted (filename.string ());
185 }
186
187
188 float
189 AudioAnalysis::gain_correction (shared_ptr<const Playlist> playlist)
190 {
191         if (playlist->content().size() == 1 && analysis_gain ()) {
192                 /* In this case we know that the analysis was of a single piece of content and
193                    we know that content's gain when the analysis was run.  Hence we can work out
194                    what correction is now needed to make it look `right'.
195                 */
196                 DCPOMATIC_ASSERT (playlist->content().front()->audio);
197                 return playlist->content().front()->audio->gain() - analysis_gain().get();
198         }
199
200         return 0.0f;
201 }
202
203
204 /** @return Peak across all channels, and the channel number it is on */
205 pair<AudioAnalysis::PeakTime, int>
206 AudioAnalysis::overall_sample_peak () const
207 {
208         DCPOMATIC_ASSERT (!_sample_peak.empty());
209
210         optional<PeakTime> pt;
211         int c = 0;
212
213         for (size_t i = 0; i < _sample_peak.size(); ++i) {
214                 if (!pt || _sample_peak[i].peak > pt->peak) {
215                         pt = _sample_peak[i];
216                         c = i;
217                 }
218         }
219
220         return make_pair (pt.get(), c);
221 }
222
223
224 optional<float>
225 AudioAnalysis::overall_true_peak () const
226 {
227         optional<float> p;
228
229         for (auto i: _true_peak) {
230                 if (!p || i > *p) {
231                         p = i;
232                 }
233         }
234
235         return p;
236 }
237