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