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