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