libdcp
sound_asset.cc
Go to the documentation of this file.
1 /*
2  Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3 
4  This file is part of libdcp.
5 
6  libdcp 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  libdcp 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 libdcp. If not, see <http://www.gnu.org/licenses/>.
18 
19  In addition, as a special exception, the copyright holders give
20  permission to link the code of portions of this program with the
21  OpenSSL library under certain conditions as described in each
22  individual source file, and distribute linked combinations
23  including the two.
24 
25  You must obey the GNU General Public License in all respects
26  for all of the code used other than OpenSSL. If you modify
27  file(s) with this exception, you may extend this exception to your
28  version of the file(s), but you are not obligated to do so. If you
29  do not wish to do so, delete this exception statement from your
30  version. If you delete this exception statement from all source
31  files in the program, then also delete it here.
32 */
33 
34 
40 #include "compose.hpp"
41 #include "dcp_assert.h"
42 #include "equality_options.h"
43 #include "exceptions.h"
44 #include "filesystem.h"
45 #include "sound_asset.h"
46 #include "sound_asset_reader.h"
47 #include "sound_asset_writer.h"
48 #include "sound_frame.h"
49 #include "util.h"
50 #include "warnings.h"
51 LIBDCP_DISABLE_WARNINGS
52 #include <asdcp/AS_DCP.h>
53 #include <asdcp/KM_fileio.h>
54 #include <asdcp/Metadata.h>
55 LIBDCP_ENABLE_WARNINGS
56 #include <libxml++/nodes/element.h>
57 #include <boost/filesystem.hpp>
58 #include <stdexcept>
59 
60 
61 using std::dynamic_pointer_cast;
62 using std::list;
63 using std::shared_ptr;
64 using std::string;
65 using std::vector;
66 using boost::optional;
67 using namespace dcp;
68 
69 
70 SoundAsset::SoundAsset (boost::filesystem::path file)
71  : Asset (file)
72 {
73  Kumu::FileReaderFactory factory;
74  ASDCP::PCM::MXFReader reader(factory);
75  auto r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
76  if (ASDCP_FAILURE(r)) {
77  boost::throw_exception (MXFFileError("could not open MXF file for reading", file.string(), r));
78  }
79 
80  ASDCP::PCM::AudioDescriptor desc;
81  if (ASDCP_FAILURE (reader.FillAudioDescriptor(desc))) {
82  boost::throw_exception (ReadError("could not read audio MXF information"));
83  }
84 
85  _sampling_rate = desc.AudioSamplingRate.Denominator ? (desc.AudioSamplingRate.Numerator / desc.AudioSamplingRate.Denominator) : 0;
86  _bit_depth = desc.QuantizationBits;
87  _channels = desc.ChannelCount;
88  _edit_rate = Fraction (desc.EditRate.Numerator, desc.EditRate.Denominator);
89 
90  _intrinsic_duration = desc.ContainerDuration;
91 
92  ASDCP::WriterInfo info;
93  if (ASDCP_FAILURE (reader.FillWriterInfo(info))) {
94  boost::throw_exception (ReadError("could not read audio MXF information"));
95  }
96 
97  ASDCP::MXF::SoundfieldGroupLabelSubDescriptor* soundfield;
98  auto rr = reader.OP1aHeader().GetMDObjectByType(
99  asdcp_smpte_dict->ul(ASDCP::MDD_SoundfieldGroupLabelSubDescriptor),
100  reinterpret_cast<ASDCP::MXF::InterchangeObject**>(&soundfield)
101  );
102 
103  if (KM_SUCCESS(rr)) {
104  if (!soundfield->RFC5646SpokenLanguage.empty()) {
105  char buffer[64];
106  soundfield->RFC5646SpokenLanguage.get().EncodeString(buffer, sizeof(buffer));
107  _language = buffer;
108  }
109  }
110 
111  list<ASDCP::MXF::InterchangeObject*> channel_labels;
112  rr = reader.OP1aHeader().GetMDObjectsByType(
113  asdcp_smpte_dict->ul(ASDCP::MDD_AudioChannelLabelSubDescriptor),
114  channel_labels
115  );
116 
117  if (KM_SUCCESS(rr)) {
118  _active_channels = channel_labels.size();
119  }
120 
121  _id = read_writer_info (info);
122 }
123 
124 
125 SoundAsset::SoundAsset (Fraction edit_rate, int sampling_rate, int channels, LanguageTag language, Standard standard)
126  : MXF (standard)
127  , _edit_rate (edit_rate)
128  , _channels (channels)
129  , _sampling_rate (sampling_rate)
130  , _language (language.to_string())
131 {
132 
133 }
134 
135 
136 bool
137 SoundAsset::equals(shared_ptr<const Asset> other, EqualityOptions const& opt, NoteHandler note) const
138 {
139  if (opt.sound_assets_can_differ) {
140  return true;
141  }
142 
143  Kumu::FileReaderFactory factory;
144  ASDCP::PCM::MXFReader reader_A(factory);
145  DCP_ASSERT (file());
146  auto r = reader_A.OpenRead(dcp::filesystem::fix_long_path(*file()).string().c_str());
147  if (ASDCP_FAILURE(r)) {
148  boost::throw_exception (MXFFileError("could not open MXF file for reading", file()->string(), r));
149  }
150 
151  ASDCP::PCM::MXFReader reader_B(factory);
152  r = reader_B.OpenRead(dcp::filesystem::fix_long_path(*other->file()).string().c_str());
153  if (ASDCP_FAILURE (r)) {
154  boost::throw_exception (MXFFileError("could not open MXF file for reading", other->file()->string(), r));
155  }
156 
157  ASDCP::PCM::AudioDescriptor desc_A;
158  if (ASDCP_FAILURE (reader_A.FillAudioDescriptor(desc_A))) {
159  boost::throw_exception (ReadError ("could not read audio MXF information"));
160  }
161  ASDCP::PCM::AudioDescriptor desc_B;
162  if (ASDCP_FAILURE (reader_B.FillAudioDescriptor(desc_B))) {
163  boost::throw_exception (ReadError ("could not read audio MXF information"));
164  }
165 
166  if (desc_A.EditRate != desc_B.EditRate) {
167  note (
168  NoteType::ERROR,
169  String::compose (
170  "audio edit rates differ: %1/%2 cf %3/%4",
171  desc_A.EditRate.Numerator, desc_A.EditRate.Denominator, desc_B.EditRate.Numerator, desc_B.EditRate.Denominator
172  )
173  );
174  return false;
175  } else if (desc_A.AudioSamplingRate != desc_B.AudioSamplingRate) {
176  note (
177  NoteType::ERROR,
178  String::compose (
179  "audio sampling rates differ: %1 cf %2",
180  desc_A.AudioSamplingRate.Numerator, desc_A.AudioSamplingRate.Denominator,
181  desc_B.AudioSamplingRate.Numerator, desc_B.AudioSamplingRate.Numerator
182  )
183  );
184  return false;
185  } else if (desc_A.Locked != desc_B.Locked) {
186  note (NoteType::ERROR, String::compose ("audio locked flags differ: %1 cf %2", desc_A.Locked, desc_B.Locked));
187  return false;
188  } else if (desc_A.ChannelCount != desc_B.ChannelCount) {
189  note (NoteType::ERROR, String::compose ("audio channel counts differ: %1 cf %2", desc_A.ChannelCount, desc_B.ChannelCount));
190  return false;
191  } else if (desc_A.QuantizationBits != desc_B.QuantizationBits) {
192  note (NoteType::ERROR, String::compose ("audio bits per sample differ: %1 cf %2", desc_A.QuantizationBits, desc_B.QuantizationBits));
193  return false;
194  } else if (desc_A.BlockAlign != desc_B.BlockAlign) {
195  note (NoteType::ERROR, String::compose ("audio bytes per sample differ: %1 cf %2", desc_A.BlockAlign, desc_B.BlockAlign));
196  return false;
197  } else if (desc_A.AvgBps != desc_B.AvgBps) {
198  note (NoteType::ERROR, String::compose ("audio average bps differ: %1 cf %2", desc_A.AvgBps, desc_B.AvgBps));
199  return false;
200  } else if (desc_A.LinkedTrackID != desc_B.LinkedTrackID) {
201  note (NoteType::ERROR, String::compose ("audio linked track IDs differ: %1 cf %2", desc_A.LinkedTrackID, desc_B.LinkedTrackID));
202  return false;
203  } else if (desc_A.ContainerDuration != desc_B.ContainerDuration) {
204  note (NoteType::ERROR, String::compose ("audio container durations differ: %1 cf %2", desc_A.ContainerDuration, desc_B.ContainerDuration));
205  return false;
206  } else if (desc_A.ChannelFormat != desc_B.ChannelFormat) {
207  /* XXX */
208  }
209 
210  auto other_sound = dynamic_pointer_cast<const SoundAsset> (other);
211 
212  auto reader = start_read ();
213  auto other_reader = other_sound->start_read ();
214 
215  for (int i = 0; i < _intrinsic_duration; ++i) {
216 
217  auto frame_A = reader->get_frame (i);
218  auto frame_B = other_reader->get_frame (i);
219 
220  if (frame_A->size() != frame_B->size()) {
221  note (NoteType::ERROR, String::compose ("sizes of audio data for frame %1 differ", i));
222  return false;
223  }
224 
225  if (memcmp (frame_A->data(), frame_B->data(), frame_A->size()) != 0) {
226  for (int sample = 0; sample < frame_A->samples(); ++sample) {
227  for (int channel = 0; channel < frame_A->channels(); ++channel) {
228  int32_t const d = abs(frame_A->get(channel, sample) - frame_B->get(channel, sample));
229  if (d > opt.max_audio_sample_error) {
230  note (NoteType::ERROR, String::compose("PCM data difference of %1 in frame %2, channel %3, sample %4", d, i, channel, sample));
231  return false;
232  }
233  }
234  }
235  }
236  }
237 
238  return true;
239 }
240 
241 
242 shared_ptr<SoundAssetWriter>
244  boost::filesystem::path file,
245  vector<dcp::Channel> extra_active_channels,
246  AtmosSync atmos_sync,
247  MCASubDescriptors include_mca_subdescriptors
248  )
249 {
250  if (atmos_sync == AtmosSync::ENABLED && _channels < 14) {
251  throw MiscError ("Insufficient channels to write ATMOS sync (there must be at least 14)");
252  }
253 
254  return shared_ptr<SoundAssetWriter>(
255  new SoundAssetWriter(this, file, extra_active_channels, atmos_sync == AtmosSync::ENABLED, include_mca_subdescriptors == MCASubDescriptors::ENABLED)
256  );
257 }
258 
259 
260 shared_ptr<SoundAssetReader>
261 SoundAsset::start_read () const
262 {
263  return shared_ptr<SoundAssetReader> (new SoundAssetReader(this, key(), standard()));
264 }
265 
266 
267 string
268 SoundAsset::static_pkl_type (Standard standard)
269 {
270  switch (standard) {
271  case Standard::INTEROP:
272  return "application/x-smpte-mxf;asdcpKind=Sound";
273  case Standard::SMPTE:
274  return "application/mxf";
275  default:
276  DCP_ASSERT (false);
277  }
278 }
279 
280 
281 bool
282 SoundAsset::valid_mxf (boost::filesystem::path file)
283 {
284  Kumu::FileReaderFactory factory;
285  ASDCP::PCM::MXFReader reader(factory);
286  Kumu::Result_t r = reader.OpenRead(dcp::filesystem::fix_long_path(file).string().c_str());
287  return !ASDCP_FAILURE (r);
288 }
289 
290 
291 int
293 {
294  return _active_channels.get_value_or(_channels);
295 }
296 
Parent class for DCP assets, i.e. picture, sound, subtitles, closed captions, CPLs,...
Definition: asset.h:73
boost::optional< boost::filesystem::path > file() const
Definition: asset.h:100
A class to describe what "equality" means for a particular test.
A fraction (i.e. a thing with an integer numerator and an integer denominator).
Definition: types.h:168
An exception related to an MXF file.
Definition: exceptions.h:82
Parent for classes which represent MXF files.
Definition: mxf.h:74
boost::optional< Key > key() const
Definition: mxf.h:104
A miscellaneous exception.
Definition: exceptions.h:94
Any error that occurs when reading data from a DCP.
Definition: exceptions.h:106
A helper class for writing to SoundAssets.
int active_channels() const
Definition: sound_asset.cc:292
int _channels
number of channels in the MXF
Definition: sound_asset.h:155
boost::optional< int > _active_channels
estimate of the number of active channels
Definition: sound_asset.h:156
std::shared_ptr< SoundAssetWriter > start_write(boost::filesystem::path file, std::vector< dcp::Channel > extra_active_channels, AtmosSync atmos_sync, MCASubDescriptors mca_subdescriptors)
Definition: sound_asset.cc:243
int64_t _intrinsic_duration
Definition: sound_asset.h:154
DCP_ASSERT macro.
Class to describe what equality means when calling Asset::equals().
Exceptions thrown by libdcp.
Namespace for everything in libdcp.
Definition: array_data.h:50
SoundAsset class.
SoundAssetReader typedef.
SoundAssetWriter class.
SoundFrame class.
Utility methods and classes.