1 #include "sources/soundsourceopus.h"
2
3 #include <QFileInfo>
4
5 #include "audio/streaminfo.h"
6 #include "util/logger.h"
7
8 namespace mixxx {
9
10 namespace {
11
12 const Logger kLogger("SoundSourceOpus");
13
14 // Decoded output of opusfile has a fixed sample rate of 48 kHz (fullband)
15 constexpr audio::SampleRate kSampleRate = audio::SampleRate(48000);
16
17 // http://opus-codec.org
18 // - Sample rate 48 kHz (fullband)
19 // - Frame sizes from 2.5 ms to 60 ms
20 // => Up to 48000 kHz * 0.06 s = 2880 sample frames per data frame
21 // Prefetching 2 * 2880 sample frames while seeking limits the decoding
22 // errors to kMaxDecodingError during our tests.
23 //
24 // According to the API documentation of op_pcm_seek():
25 // "...decoding after seeking may not return exactly the same
26 // values as would be obtained by decoding the stream straight
27 // through. However, such differences are expected to be smaller
28 // than the loss introduced by Opus's lossy compression."
29 // This implementation internally uses prefetching to compensate
30 // those differences, although not completely. The following
31 // constant indicates the maximum expected difference for
32 // testing purposes.
33 constexpr SINT kNumberOfPrefetchFrames = 2 * 2880;
34
35 // Parameter for op_channel_count()
36 // See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html
37 constexpr int kCurrentStreamLink = -1; // get ... of the current (stream) link
38
39 // Parameter for op_pcm_total() and op_bitrate()
40 // See also: https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/group__stream__info.html
41 constexpr int kEntireStreamLink = -1; // get ... of the whole/entire stream
42
43 class OggOpusFileOwner {
44 public:
OggOpusFileOwner(OggOpusFile * pFile)45 explicit OggOpusFileOwner(OggOpusFile* pFile)
46 : m_pFile(pFile) {
47 }
48 OggOpusFileOwner(OggOpusFileOwner&&) = delete;
49 OggOpusFileOwner(const OggOpusFileOwner&) = delete;
~OggOpusFileOwner()50 ~OggOpusFileOwner() {
51 if (m_pFile) {
52 op_free(m_pFile);
53 }
54 }
operator OggOpusFile*() const55 operator OggOpusFile*() const {
56 return m_pFile;
57 }
release()58 OggOpusFile* release() {
59 OggOpusFile* pFile = m_pFile;
60 m_pFile = nullptr;
61 return pFile;
62 }
63
64 private:
65 OggOpusFile* m_pFile;
66 };
67
68 } // anonymous namespace
69
70 //static
71 const QString SoundSourceProviderOpus::kDisplayName = QStringLiteral("Xiph.org libopusfile");
72
73 //static
74 const QStringList SoundSourceProviderOpus::kSupportedFileExtensions = {
75 QStringLiteral("opus"),
76 };
77
getPriorityHint(const QString & supportedFileExtension) const78 SoundSourceProviderPriority SoundSourceProviderOpus::getPriorityHint(
79 const QString& supportedFileExtension) const {
80 Q_UNUSED(supportedFileExtension)
81 // This reference decoder is supposed to produce more accurate
82 // and reliable results than any other DEFAULT provider.
83 return SoundSourceProviderPriority::Higher;
84 }
85
SoundSourceOpus(const QUrl & url)86 SoundSourceOpus::SoundSourceOpus(const QUrl& url)
87 : SoundSource(url),
88 m_pOggOpusFile(nullptr),
89 m_curFrameIndex(0) {
90 }
91
~SoundSourceOpus()92 SoundSourceOpus::~SoundSourceOpus() {
93 close();
94 }
95
96 std::pair<MetadataSource::ImportResult, QDateTime>
importTrackMetadataAndCoverImage(TrackMetadata * pTrackMetadata,QImage * pCoverArt) const97 SoundSourceOpus::importTrackMetadataAndCoverImage(
98 TrackMetadata* pTrackMetadata,
99 QImage* pCoverArt) const {
100 auto const imported =
101 SoundSource::importTrackMetadataAndCoverImage(
102 pTrackMetadata, pCoverArt);
103 if (imported.first == ImportResult::Succeeded) {
104 // Done if the default implementation in the base class
105 // supports Opus files.
106 return imported;
107 }
108
109 // Beginning with version 1.9.0 TagLib supports the Opus format.
110 // Until this becomes the minimum version required by Mixxx tags
111 // in .opus files must also be parsed using opusfile. The following
112 // code should removed as soon as it is no longer needed!
113 //
114 // NOTE(uklotzde): The following code has been found in SoundSourceOpus
115 // and will not be improved. We are aware of its shortcomings like
116 // the lack of proper error handling.
117
118 // From opus/opusfile.h
119 // On Windows, this string must be UTF-8 (to allow access to
120 // files whose names cannot be represented in the current
121 // MBCS code page).
122 // All other systems use the native character encoding.
123 #ifdef _WIN32
124 QByteArray qBAFilename = getLocalFileName().toUtf8();
125 #else
126 QByteArray qBAFilename = QFile::encodeName(getLocalFileName());
127 #endif
128
129 int errorCode = 0;
130 OggOpusFileOwner pOggOpusFile(
131 op_open_file(qBAFilename.constData(), &errorCode));
132 if (!pOggOpusFile || (errorCode != 0)) {
133 kLogger.warning()
134 << "Opening of OggOpusFile failed with error"
135 << errorCode
136 << ":"
137 << getLocalFileName();
138 // We couldn't do any better , so just return the (unsuccessful)
139 // result from the base class.
140 return imported;
141 }
142
143 // Cast to double is required for duration with sub-second precision
144 const double dTotalFrames = op_pcm_total(pOggOpusFile, -1);
145 const auto duration = Duration::fromMicros(
146 static_cast<qint64>(1000000 * dTotalFrames / kSampleRate));
147 pTrackMetadata->setStreamInfo(audio::StreamInfo{
148 audio::SignalInfo{
149 audio::ChannelCount(op_channel_count(pOggOpusFile, -1)),
150 kSampleRate,
151 },
152 audio::Bitrate(op_bitrate(pOggOpusFile, -1) / 1000),
153 duration,
154 });
155
156 #ifndef TAGLIB_HAS_OPUSFILE
157 const OpusTags* l_ptrOpusTags = op_tags(pOggOpusFile, -1);
158 bool hasDate = false;
159 for (int i = 0; i < l_ptrOpusTags->comments; ++i) {
160 QString l_SWholeTag = QString(l_ptrOpusTags->user_comments[i]);
161 QString l_STag = l_SWholeTag.left(l_SWholeTag.indexOf("="));
162 QString l_SPayload = l_SWholeTag.right((l_SWholeTag.length() - l_SWholeTag.indexOf("=")) - 1);
163
164 if (!l_STag.compare("ARTIST")) {
165 pTrackMetadata->refTrackInfo().setArtist(l_SPayload);
166 } else if (!l_STag.compare("ALBUM")) {
167 pTrackMetadata->refAlbumInfo().setTitle(l_SPayload);
168 } else if (!l_STag.compare("BPM")) {
169 pTrackMetadata->refTrackInfo().setBpm(Bpm(l_SPayload.toDouble()));
170 } else if (!l_STag.compare("DATE")) {
171 // Prefer "DATE" over "YEAR"
172 pTrackMetadata->refTrackInfo().setYear(l_SPayload.trimmed());
173 // Avoid to overwrite "DATE" with "YEAR"
174 hasDate |= !pTrackMetadata->getTrackInfo().getYear().isEmpty();
175 } else if (!hasDate && !l_STag.compare("YEAR")) {
176 pTrackMetadata->refTrackInfo().setYear(l_SPayload.trimmed());
177 } else if (!l_STag.compare("GENRE")) {
178 pTrackMetadata->refTrackInfo().setGenre(l_SPayload);
179 } else if (!l_STag.compare("TRACKNUMBER")) {
180 pTrackMetadata->refTrackInfo().setTrackNumber(l_SPayload);
181 } else if (!l_STag.compare("COMPOSER")) {
182 pTrackMetadata->refTrackInfo().setComposer(l_SPayload);
183 } else if (!l_STag.compare("ALBUMARTIST")) {
184 pTrackMetadata->refAlbumInfo().setArtist(l_SPayload);
185 } else if (!l_STag.compare("TITLE")) {
186 pTrackMetadata->refTrackInfo().setTitle(l_SPayload);
187 } else if (!l_STag.compare("REPLAYGAIN_TRACK_GAIN")) {
188 bool gainRatioValid = false;
189 double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid);
190 if (gainRatioValid) {
191 ReplayGain trackGain(pTrackMetadata->getTrackInfo().getReplayGain());
192 trackGain.setRatio(gainRatio);
193 pTrackMetadata->refTrackInfo().setReplayGain(trackGain);
194 }
195 } else if (!l_STag.compare("REPLAYGAIN_ALBUM_GAIN")) {
196 bool gainRatioValid = false;
197 double gainRatio = ReplayGain::ratioFromString(l_SPayload, &gainRatioValid);
198 if (gainRatioValid) {
199 ReplayGain albumGain(pTrackMetadata->getAlbumInfo().getReplayGain());
200 albumGain.setRatio(gainRatio);
201 pTrackMetadata->refAlbumInfo().setReplayGain(albumGain);
202 }
203 }
204 }
205 #endif // TAGLIB_HAS_OPUSFILE
206
207 return std::make_pair(
208 ImportResult::Succeeded,
209 QFileInfo(getLocalFileName()).lastModified());
210 }
211
tryOpen(OpenMode,const OpenParams & params)212 SoundSource::OpenResult SoundSourceOpus::tryOpen(
213 OpenMode /*mode*/,
214 const OpenParams& params) {
215 // From opus/opusfile.h
216 // On Windows, this string must be UTF-8 (to allow access to
217 // files whose names cannot be represented in the current
218 // MBCS code page).
219 // All other systems use the native character encoding.
220 #ifdef _WIN32
221 QByteArray qBAFilename = getLocalFileName().toUtf8();
222 #else
223 QByteArray qBAFilename = QFile::encodeName(getLocalFileName());
224 #endif
225
226 int errorCode = 0;
227 OggOpusFileOwner pOggOpusFile(
228 op_open_file(qBAFilename.constData(), &errorCode));
229 if (!pOggOpusFile || (errorCode != 0)) {
230 kLogger.warning()
231 << "Opening of OggOpusFile failed with error"
232 << errorCode
233 << ":"
234 << getLocalFileName();
235 return OpenResult::Failed;
236 }
237 if (!op_seekable(pOggOpusFile)) {
238 kLogger.warning()
239 << "Stream in"
240 << getUrlString()
241 << "is not seekable";
242 return OpenResult::Aborted;
243 }
244 DEBUG_ASSERT(!m_pOggOpusFile);
245 m_pOggOpusFile = pOggOpusFile.release();
246
247 const int streamChannelCount = op_channel_count(m_pOggOpusFile, kCurrentStreamLink);
248 if (0 < streamChannelCount) {
249 // opusfile supports to enforce stereo decoding
250 bool enforceStereoDecoding =
251 params.getSignalInfo().getChannelCount().isValid() &&
252 (params.getSignalInfo().getChannelCount() <= 2) &&
253 // preserve mono signals if stereo signal is not requested explicitly
254 ((params.getSignalInfo().getChannelCount() == 2) || (streamChannelCount > 2));
255 if (enforceStereoDecoding) {
256 initChannelCountOnce(2);
257 } else {
258 initChannelCountOnce(streamChannelCount);
259 }
260 } else {
261 kLogger.warning()
262 << "Failed to read channel configuration of OggOpus file:"
263 << getUrlString();
264 return OpenResult::Failed;
265 }
266
267 // Reserve enough capacity for buffering a stereo signal!
268 const auto prefetchChannelCount = std::min(getSignalInfo().getChannelCount(), audio::ChannelCount(2));
269 SampleBuffer(prefetchChannelCount * kNumberOfPrefetchFrames).swap(m_prefetchSampleBuffer);
270
271 const ogg_int64_t pcmTotal = op_pcm_total(m_pOggOpusFile, kEntireStreamLink);
272 if (0 <= pcmTotal) {
273 initFrameIndexRangeOnce(IndexRange::forward(0, pcmTotal));
274 } else {
275 kLogger.warning()
276 << "Failed to read total length of OggOpus file:"
277 << getUrlString();
278 return OpenResult::Failed;
279 }
280
281 const opus_int32 bitrate = op_bitrate(m_pOggOpusFile, kEntireStreamLink);
282 if (0 < bitrate) {
283 initBitrateOnce(bitrate / 1000);
284 } else {
285 kLogger.warning()
286 << "Failed to determine bitrate of OggOpus file:"
287 << getUrlString();
288 return OpenResult::Failed;
289 }
290
291 initSampleRateOnce(kSampleRate);
292
293 m_curFrameIndex = frameIndexMin();
294
295 return OpenResult::Succeeded;
296 }
297
close()298 void SoundSourceOpus::close() {
299 if (m_pOggOpusFile) {
300 op_free(m_pOggOpusFile);
301 m_pOggOpusFile = nullptr;
302 }
303 }
304
readSampleFramesClamped(const WritableSampleFrames & writableSampleFrames)305 ReadableSampleFrames SoundSourceOpus::readSampleFramesClamped(
306 const WritableSampleFrames& writableSampleFrames) {
307 const SINT firstFrameIndex = writableSampleFrames.frameIndexRange().start();
308
309 if (m_curFrameIndex != firstFrameIndex) {
310 // Prefer skipping over seeking if the seek position is up to
311 // 2 * kNumberOfPrefetchFrames in front of the current position
312 if ((m_curFrameIndex > firstFrameIndex) ||
313 ((firstFrameIndex - m_curFrameIndex) > 2 * kNumberOfPrefetchFrames)) {
314 SINT seekIndex = std::max(firstFrameIndex - kNumberOfPrefetchFrames, frameIndexMin());
315 int seekResult = op_pcm_seek(m_pOggOpusFile, seekIndex);
316 if (0 == seekResult) {
317 m_curFrameIndex = seekIndex;
318 } else {
319 kLogger.warning() << "Failed to seek OggOpus file:" << seekResult;
320 const ogg_int64_t pcmOffset = op_pcm_tell(m_pOggOpusFile);
321 if (0 <= pcmOffset) {
322 m_curFrameIndex = pcmOffset;
323 } else {
324 // Reset to EOF
325 m_curFrameIndex = frameIndexMax();
326 }
327 // Abort
328 return ReadableSampleFrames(
329 IndexRange::between(
330 m_curFrameIndex,
331 m_curFrameIndex));
332 }
333 }
334 // Decoding starts before the actual target position
335 // -> skip decoded samples until reaching the target position
336 DEBUG_ASSERT(m_curFrameIndex <= firstFrameIndex);
337 const auto precedingFrames =
338 IndexRange::between(m_curFrameIndex, firstFrameIndex);
339 if (!precedingFrames.empty() && (precedingFrames != readSampleFramesClamped(WritableSampleFrames(precedingFrames)).frameIndexRange())) {
340 kLogger.warning()
341 << "Failed to skip preceding frames"
342 << precedingFrames;
343 return ReadableSampleFrames(IndexRange::between(m_curFrameIndex, m_curFrameIndex));
344 }
345 }
346 DEBUG_ASSERT(m_curFrameIndex == firstFrameIndex);
347
348 const SINT numberOfFramesTotal = writableSampleFrames.frameLength();
349
350 // pSampleBuffer might be null while skipping (see above)
351 CSAMPLE* pSampleBuffer = writableSampleFrames.writableData();
352 SINT numberOfFramesRemaining = numberOfFramesTotal;
353 while (0 < numberOfFramesRemaining) {
354 SINT numberOfSamplesToRead =
355 getSignalInfo().frames2samples(numberOfFramesRemaining);
356 if (!writableSampleFrames.writableData()) {
357 // NOTE(uklotzde): The opusfile API does not provide any
358 // functions for skipping samples in the audio stream. Calling
359 // API functions with a nullptr buffer does not return. Since
360 // seeking in Opus files requires prefetching + skipping we
361 // need to skip sample frames by reading into a temporary
362 // buffer
363 pSampleBuffer = m_prefetchSampleBuffer.data();
364 if (numberOfSamplesToRead > m_prefetchSampleBuffer.size()) {
365 numberOfSamplesToRead = m_prefetchSampleBuffer.size();
366 }
367 }
368 int readResult;
369 if (getSignalInfo().getChannelCount() == 2) {
370 readResult = op_read_float_stereo(
371 m_pOggOpusFile,
372 pSampleBuffer,
373 numberOfSamplesToRead);
374 } else {
375 readResult = op_read_float(
376 m_pOggOpusFile,
377 pSampleBuffer,
378 numberOfSamplesToRead,
379 nullptr);
380 }
381 if (0 < readResult) {
382 m_curFrameIndex += readResult;
383 pSampleBuffer += getSignalInfo().frames2samples(readResult);
384 numberOfFramesRemaining -= readResult;
385 } else {
386 kLogger.warning() << "Failed to read sample data from OggOpus file:"
387 << readResult;
388 break; // abort
389 }
390 }
391
392 DEBUG_ASSERT(isValidFrameIndex(m_curFrameIndex));
393 DEBUG_ASSERT(numberOfFramesTotal >= numberOfFramesRemaining);
394 const SINT numberOfFrames = numberOfFramesTotal - numberOfFramesRemaining;
395 return ReadableSampleFrames(
396 IndexRange::forward(firstFrameIndex, numberOfFrames),
397 SampleBuffer::ReadableSlice(
398 writableSampleFrames.writableData(),
399 std::min(writableSampleFrames.writableLength(), getSignalInfo().frames2samples(numberOfFrames))));
400 }
401
402 } // namespace mixxx
403