1 #include "sources/soundsourcemodplug.h"
2 
3 #include "audio/streaminfo.h"
4 #include "track/trackmetadata.h"
5 #include "util/logger.h"
6 #include "util/sample.h"
7 #include "util/timer.h"
8 
9 #include <QFile>
10 
11 #include <stdlib.h>
12 
13 namespace mixxx {
14 
15 namespace {
16 
17 const Logger kLogger("SoundSourceModPlug");
18 
19 const QStringList kSupportedFileExtensions = {
20         // ModPlug supports more formats but file name
21         // extensions are not always present with modules.
22         QStringLiteral("mod"),
23         QStringLiteral("med"),
24         QStringLiteral("okt"),
25         QStringLiteral("s3m"),
26         QStringLiteral("stm"),
27         QStringLiteral("xm"),
28         QStringLiteral("it"),
29 };
30 
31 /* read files in 512k chunks */
32 constexpr SINT kChunkSizeInBytes = SINT(1) << 19;
33 
getModPlugTypeFromUrl(const QUrl & url)34 QString getModPlugTypeFromUrl(const QUrl& url) {
35     const QString fileExtension(SoundSource::getFileExtensionFromUrl(url));
36     if (fileExtension == "mod") {
37         return "Protracker";
38     } else if (fileExtension == "med") {
39         return "OctaMed";
40     } else if (fileExtension == "okt") {
41         return "Oktalyzer";
42     } else if (fileExtension == "s3m") {
43         return "Scream Tracker 3";
44     } else if (fileExtension == "stm") {
45         return "Scream Tracker";
46     } else if (fileExtension == "xm") {
47         return "FastTracker2";
48     } else if (fileExtension == "it") {
49         return "Impulse Tracker";
50     } else {
51         return "Module";
52     }
53 }
54 
55 } // anonymous namespace
56 
57 //static
58 unsigned int SoundSourceModPlug::s_bufferSizeLimit = 0;
59 
60 //static
configure(unsigned int bufferSizeLimit,const ModPlug::ModPlug_Settings & settings)61 void SoundSourceModPlug::configure(unsigned int bufferSizeLimit,
62         const ModPlug::ModPlug_Settings& settings) {
63     s_bufferSizeLimit = bufferSizeLimit;
64     ModPlug::ModPlug_SetSettings(&settings);
65 }
66 
67 //static
68 const QString SoundSourceProviderModPlug::kDisplayName = QStringLiteral("MODPlug");
69 
getSupportedFileExtensions() const70 QStringList SoundSourceProviderModPlug::getSupportedFileExtensions() const {
71     return kSupportedFileExtensions;
72 }
73 
SoundSourceModPlug(const QUrl & url)74 SoundSourceModPlug::SoundSourceModPlug(const QUrl& url)
75         : SoundSource(url, getModPlugTypeFromUrl(url)),
76           m_pModFile(nullptr) {
77 }
78 
~SoundSourceModPlug()79 SoundSourceModPlug::~SoundSourceModPlug() {
80     close();
81 }
82 
83 std::pair<MetadataSource::ImportResult, QDateTime>
importTrackMetadataAndCoverImage(TrackMetadata * pTrackMetadata,QImage * pCoverArt) const84 SoundSourceModPlug::importTrackMetadataAndCoverImage(
85         TrackMetadata* pTrackMetadata,
86         QImage* pCoverArt) const {
87     if (pTrackMetadata != nullptr) {
88         QFile modFile(getLocalFileName());
89         modFile.open(QIODevice::ReadOnly);
90         const QByteArray fileBuf(modFile.readAll());
91         modFile.close();
92 
93         ModPlug::ModPlugFile* pModFile = ModPlug::ModPlug_Load(fileBuf.constData(),
94                 fileBuf.length());
95         if (pModFile == nullptr) {
96             return std::make_pair(ImportResult::Failed, QDateTime());
97         }
98 
99         pTrackMetadata->refTrackInfo().setComment(QString(ModPlug::ModPlug_GetMessage(pModFile)));
100         pTrackMetadata->refTrackInfo().setTitle(QString(ModPlug::ModPlug_GetName(pModFile)));
101         pTrackMetadata->setStreamInfo(audio::StreamInfo{
102                 audio::SignalInfo{
103                         audio::ChannelCount(kChannelCount),
104                         audio::SampleRate(kSampleRate),
105                 },
106                 audio::Bitrate(8),
107                 Duration::fromMillis(ModPlug::ModPlug_GetLength(pModFile)),
108         });
109 
110         return std::make_pair(ImportResult::Succeeded, QFileInfo(modFile).lastModified());
111     }
112 
113     // The modplug library currently does not support reading cover-art from
114     // modplug files -- kain88 (Oct 2014)
115     return MetadataSourceTagLib::importTrackMetadataAndCoverImage(nullptr, pCoverArt);
116 }
117 
tryOpen(OpenMode,const OpenParams &)118 SoundSource::OpenResult SoundSourceModPlug::tryOpen(
119         OpenMode /*mode*/,
120         const OpenParams& /*config*/) {
121     ScopedTimer t("SoundSourceModPlug::open()");
122 
123     // read module file to byte array
124     const QString fileName(getLocalFileName());
125     QFile modFile(fileName);
126     kLogger.debug() << "Loading ModPlug module " << modFile.fileName();
127     modFile.open(QIODevice::ReadOnly);
128     m_fileBuf = modFile.readAll();
129     modFile.close();
130 
131     // get ModPlugFile descriptor for later access
132     m_pModFile = ModPlug::ModPlug_Load(m_fileBuf.constData(),
133             m_fileBuf.length());
134     if (m_pModFile == nullptr) {
135         // an error occurred
136         t.cancel();
137         kLogger.debug() << "Could not load module file: " << fileName;
138         return OpenResult::Failed;
139     }
140 
141     DEBUG_ASSERT(0 == (kChunkSizeInBytes % sizeof(m_sampleBuf[0])));
142     const SINT chunkSizeInSamples = kChunkSizeInBytes / sizeof(m_sampleBuf[0]);
143 
144     const ModSampleBuffer::size_type bufferSizeLimitInSamples = s_bufferSizeLimit / sizeof(m_sampleBuf[0]);
145 
146     // Estimate size of sample buffer (for better performance) aligned
147     // with the chunk size. Beware: Module length estimation is unreliable
148     // due to loops!
149     const ModSampleBuffer::size_type estimateMilliseconds =
150             ModPlug::ModPlug_GetLength(m_pModFile);
151     const ModSampleBuffer::size_type estimateSamples =
152             estimateMilliseconds * kChannelCount * kSampleRate;
153     const ModSampleBuffer::size_type estimateChunks =
154             (estimateSamples + (chunkSizeInSamples - 1)) / chunkSizeInSamples;
155     const ModSampleBuffer::size_type sampleBufferCapacity = math_min(
156             estimateChunks * chunkSizeInSamples, bufferSizeLimitInSamples);
157     m_sampleBuf.reserve(sampleBufferCapacity);
158     kLogger.debug() << "Reserved " << m_sampleBuf.capacity() << " #samples";
159 
160     // decode samples into sample buffer
161     while (m_sampleBuf.size() < bufferSizeLimitInSamples) {
162         // reserve enough space in sample buffer
163         const ModSampleBuffer::size_type currentSize = m_sampleBuf.size();
164         m_sampleBuf.resize(currentSize + chunkSizeInSamples);
165         const int bytesRead = ModPlug::ModPlug_Read(m_pModFile,
166                 &m_sampleBuf[currentSize],
167                 kChunkSizeInBytes);
168         // adjust size of sample buffer after reading
169         if (0 < bytesRead) {
170             DEBUG_ASSERT(0 == (bytesRead % sizeof(m_sampleBuf[0])));
171             const ModSampleBuffer::size_type samplesRead = bytesRead / sizeof(m_sampleBuf[0]);
172             m_sampleBuf.resize(currentSize + samplesRead);
173         } else {
174             // nothing read -> EOF
175             m_sampleBuf.resize(currentSize);
176             break; // exit loop
177         }
178     }
179     kLogger.debug() << "Filled Sample buffer with " << m_sampleBuf.size()
180                     << " samples.";
181     kLogger.debug() << "Sample buffer has "
182                     << m_sampleBuf.capacity() - m_sampleBuf.size()
183                     << " samples unused capacity.";
184 
185     initChannelCountOnce(kChannelCount);
186     initSampleRateOnce(kSampleRate);
187     initFrameIndexRangeOnce(
188             IndexRange::forward(
189                     0,
190                     getSignalInfo().samples2frames(m_sampleBuf.size())));
191 
192     return OpenResult::Succeeded;
193 }
194 
close()195 void SoundSourceModPlug::close() {
196     if (m_pModFile) {
197         ModPlug::ModPlug_Unload(m_pModFile);
198         m_pModFile = nullptr;
199     }
200 }
201 
readSampleFramesClamped(const WritableSampleFrames & writableSampleFrames)202 ReadableSampleFrames SoundSourceModPlug::readSampleFramesClamped(
203         const WritableSampleFrames& writableSampleFrames) {
204     const SINT readOffset = getSignalInfo().frames2samples(writableSampleFrames.frameIndexRange().start());
205     const SINT readSamples = getSignalInfo().frames2samples(writableSampleFrames.frameLength());
206     SampleUtil::convertS16ToFloat32(
207             writableSampleFrames.writableData(),
208             &m_sampleBuf[readOffset],
209             readSamples);
210 
211     return ReadableSampleFrames(
212             writableSampleFrames.frameIndexRange(),
213             SampleBuffer::ReadableSlice(
214                     writableSampleFrames.writableData(),
215                     writableSampleFrames.writableLength()));
216 }
217 
218 } // namespace mixxx
219