More player debugging for butler video-full states.
[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 <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 = 3;
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         _samples_per_point = f.number_child<int64_t> ("SamplesPerPoint");
94         _sample_rate = f.number_child<int64_t> ("SampleRate");
95 }
96
97 void
98 AudioAnalysis::add_point (int c, AudioPoint const & p)
99 {
100         DCPOMATIC_ASSERT (c < channels ());
101         _data[c].push_back (p);
102 }
103
104 AudioPoint
105 AudioAnalysis::get_point (int c, int p) const
106 {
107         DCPOMATIC_ASSERT (p < points (c));
108         return _data[c][p];
109 }
110
111 int
112 AudioAnalysis::channels () const
113 {
114         return _data.size ();
115 }
116
117 int
118 AudioAnalysis::points (int c) const
119 {
120         DCPOMATIC_ASSERT (c < channels ());
121         return _data[c].size ();
122 }
123
124 void
125 AudioAnalysis::write (boost::filesystem::path filename)
126 {
127         shared_ptr<xmlpp::Document> doc (new xmlpp::Document);
128         xmlpp::Element* root = doc->create_root_node ("AudioAnalysis");
129
130         root->add_child("Version")->add_child_text (raw_convert<string> (_current_state_version));
131
132         BOOST_FOREACH (vector<AudioPoint>& i, _data) {
133                 xmlpp::Element* channel = root->add_child ("Channel");
134                 BOOST_FOREACH (AudioPoint& j, i) {
135                         j.as_xml (channel->add_child ("Point"));
136                 }
137         }
138
139         for (size_t i = 0; i < _sample_peak.size(); ++i) {
140                 xmlpp::Element* n = root->add_child("SamplePeak");
141                 n->add_child_text (raw_convert<string> (_sample_peak[i].peak));
142                 n->set_attribute ("Time", raw_convert<string> (_sample_peak[i].time.get()));
143         }
144
145         BOOST_FOREACH (float i, _true_peak) {
146                 root->add_child("TruePeak")->add_child_text (raw_convert<string> (i));
147         }
148
149         if (_integrated_loudness) {
150                 root->add_child("IntegratedLoudness")->add_child_text (raw_convert<string> (_integrated_loudness.get ()));
151         }
152
153         if (_loudness_range) {
154                 root->add_child("LoudnessRange")->add_child_text (raw_convert<string> (_loudness_range.get ()));
155         }
156
157         if (_analysis_gain) {
158                 root->add_child("AnalysisGain")->add_child_text (raw_convert<string> (_analysis_gain.get ()));
159         }
160
161         root->add_child("SamplesPerPoint")->add_child_text (raw_convert<string> (_samples_per_point));
162         root->add_child("SampleRate")->add_child_text (raw_convert<string> (_sample_rate));
163
164         doc->write_to_file_formatted (filename.string ());
165 }
166
167 float
168 AudioAnalysis::gain_correction (shared_ptr<const Playlist> playlist)
169 {
170         if (playlist->content().size() == 1 && analysis_gain ()) {
171                 /* In this case we know that the analysis was of a single piece of content and
172                    we know that content's gain when the analysis was run.  Hence we can work out
173                    what correction is now needed to make it look `right'.
174                 */
175                 DCPOMATIC_ASSERT (playlist->content().front()->audio);
176                 return playlist->content().front()->audio->gain() - analysis_gain().get ();
177         }
178
179         return 0.0f;
180 }
181
182 /** @return Peak across all channels, and the channel number it is on */
183 pair<AudioAnalysis::PeakTime, int>
184 AudioAnalysis::overall_sample_peak () const
185 {
186         DCPOMATIC_ASSERT (!_sample_peak.empty ());
187
188         optional<PeakTime> pt;
189         int c = 0;
190
191         for (size_t i = 0; i < _sample_peak.size(); ++i) {
192                 if (!pt || _sample_peak[i].peak > pt->peak) {
193                         pt = _sample_peak[i];
194                         c = i;
195                 }
196         }
197
198         return make_pair (pt.get(), c);
199 }
200
201 optional<float>
202 AudioAnalysis::overall_true_peak () const
203 {
204         optional<float> p;
205
206         BOOST_FOREACH (float i, _true_peak) {
207                 if (!p || i > *p) {
208                         p = i;
209                 }
210         }
211
212         return p;
213 }