1 /***************************************************************************
2 copyright : (C) 2011 by Mathias Panzenböck
3 email : grosser.meister.morti@gmx.net
4 ***************************************************************************/
5
6 /***************************************************************************
7 * This library is free software; you can redistribute it and/or modify *
8 * it under the terms of the GNU Lesser General Public License version *
9 * 2.1 as published by the Free Software Foundation. *
10 * *
11 * This library is distributed in the hope that it will be useful, but *
12 * WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
14 * Lesser General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU Lesser General Public *
17 * License along with this library; if not, write to the Free Software *
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA *
19 * 02110-1301 USA *
20 * *
21 * Alternatively, this file is available under the Mozilla Public *
22 * License Version 1.1. You may obtain a copy of the License at *
23 * http://www.mozilla.org/MPL/ *
24 ***************************************************************************/
25
26
27 #include "tstringlist.h"
28 #include "itfile.h"
29 #include "tdebug.h"
30 #include "modfileprivate.h"
31 #include "tpropertymap.h"
32
33 using namespace TagLib;
34 using namespace IT;
35
36 class IT::File::FilePrivate
37 {
38 public:
FilePrivate(AudioProperties::ReadStyle propertiesStyle)39 FilePrivate(AudioProperties::ReadStyle propertiesStyle)
40 : tag(), properties(propertiesStyle)
41 {
42 }
43
44 Mod::Tag tag;
45 IT::Properties properties;
46 };
47
File(FileName file,bool readProperties,AudioProperties::ReadStyle propertiesStyle)48 IT::File::File(FileName file, bool readProperties,
49 AudioProperties::ReadStyle propertiesStyle) :
50 Mod::FileBase(file),
51 d(new FilePrivate(propertiesStyle))
52 {
53 if(isOpen())
54 read(readProperties);
55 }
56
File(IOStream * stream,bool readProperties,AudioProperties::ReadStyle propertiesStyle)57 IT::File::File(IOStream *stream, bool readProperties,
58 AudioProperties::ReadStyle propertiesStyle) :
59 Mod::FileBase(stream),
60 d(new FilePrivate(propertiesStyle))
61 {
62 if(isOpen())
63 read(readProperties);
64 }
65
~File()66 IT::File::~File()
67 {
68 delete d;
69 }
70
tag() const71 Mod::Tag *IT::File::tag() const
72 {
73 return &d->tag;
74 }
75
properties() const76 PropertyMap IT::File::properties() const
77 {
78 return d->tag.properties();
79 }
80
setProperties(const PropertyMap & properties)81 PropertyMap IT::File::setProperties(const PropertyMap &properties)
82 {
83 return d->tag.setProperties(properties);
84 }
85
audioProperties() const86 IT::Properties *IT::File::audioProperties() const
87 {
88 return &d->properties;
89 }
90
save()91 bool IT::File::save()
92 {
93 if(readOnly())
94 {
95 debug("IT::File::save() - Cannot save to a read only file.");
96 return false;
97 }
98 seek(4);
99 writeString(d->tag.title(), 25);
100 writeByte(0);
101
102 seek(2, Current);
103
104 unsigned short length = 0;
105 unsigned short instrumentCount = 0;
106 unsigned short sampleCount = 0;
107
108 if(!readU16L(length) || !readU16L(instrumentCount) || !readU16L(sampleCount))
109 return false;
110
111 seek(15, Current);
112
113 // write comment as instrument and sample names:
114 StringList lines = d->tag.comment().split("\n");
115 for(unsigned short i = 0; i < instrumentCount; ++ i) {
116 seek(192L + length + ((long)i << 2));
117 unsigned long instrumentOffset = 0;
118 if(!readU32L(instrumentOffset))
119 return false;
120
121 seek(instrumentOffset + 32);
122
123 if(i < lines.size())
124 writeString(lines[i], 25);
125 else
126 writeString(String(), 25);
127 writeByte(0);
128 }
129
130 for(unsigned short i = 0; i < sampleCount; ++ i) {
131 seek(192L + length + ((long)instrumentCount << 2) + ((long)i << 2));
132 unsigned long sampleOffset = 0;
133 if(!readU32L(sampleOffset))
134 return false;
135
136 seek(sampleOffset + 20);
137
138 if((unsigned int)(i + instrumentCount) < lines.size())
139 writeString(lines[i + instrumentCount], 25);
140 else
141 writeString(String(), 25);
142 writeByte(0);
143 }
144
145 // write rest as message:
146 StringList messageLines;
147 for(unsigned int i = instrumentCount + sampleCount; i < lines.size(); ++ i)
148 messageLines.append(lines[i]);
149 ByteVector message = messageLines.toString("\r").data(String::Latin1);
150
151 // it's actually not really stated if the message needs a
152 // terminating NUL but it does not hurt to add one:
153 if(message.size() > 7999)
154 message.resize(7999);
155 message.append((char)0);
156
157 unsigned short special = 0;
158 unsigned short messageLength = 0;
159 unsigned long messageOffset = 0;
160
161 seek(46);
162 if(!readU16L(special))
163 return false;
164
165 unsigned long fileSize = File::length();
166 if(special & Properties::MessageAttached) {
167 seek(54);
168 if(!readU16L(messageLength) || !readU32L(messageOffset))
169 return false;
170
171 if(messageLength == 0)
172 messageOffset = fileSize;
173 }
174 else
175 {
176 messageOffset = fileSize;
177 seek(46);
178 writeU16L(special | 0x1);
179 }
180
181 if(messageOffset + messageLength >= fileSize) {
182 // append new message
183 seek(54);
184 writeU16L(message.size());
185 writeU32L(messageOffset);
186 seek(messageOffset);
187 writeBlock(message);
188 truncate(messageOffset + message.size());
189 }
190 else {
191 // Only overwrite existing message.
192 // I'd need to parse (understand!) the whole file for more.
193 // Although I could just move the message to the end of file
194 // and let the existing one be, but that would waste space.
195 message.resize(messageLength, 0);
196 seek(messageOffset);
197 writeBlock(message);
198 }
199 return true;
200 }
201
read(bool)202 void IT::File::read(bool)
203 {
204 if(!isOpen())
205 return;
206
207 seek(0);
208 READ_ASSERT(readBlock(4) == "IMPM");
209 READ_STRING(d->tag.setTitle, 26);
210
211 seek(2, Current);
212
213 READ_U16L_AS(length);
214 READ_U16L_AS(instrumentCount);
215 READ_U16L_AS(sampleCount);
216
217 d->properties.setInstrumentCount(instrumentCount);
218 d->properties.setSampleCount(sampleCount);
219 READ_U16L(d->properties.setPatternCount);
220 READ_U16L(d->properties.setVersion);
221 READ_U16L(d->properties.setCompatibleVersion);
222 READ_U16L(d->properties.setFlags);
223 READ_U16L_AS(special);
224 d->properties.setSpecial(special);
225 READ_BYTE(d->properties.setGlobalVolume);
226 READ_BYTE(d->properties.setMixVolume);
227 READ_BYTE(d->properties.setBpmSpeed);
228 READ_BYTE(d->properties.setTempo);
229 READ_BYTE(d->properties.setPanningSeparation);
230 READ_BYTE(d->properties.setPitchWheelDepth);
231
232 // IT supports some kind of comment tag. Still, the
233 // sample/instrument names are abused as comments so
234 // I just add all together.
235 String message;
236 if(special & Properties::MessageAttached) {
237 READ_U16L_AS(messageLength);
238 READ_U32L_AS(messageOffset);
239 seek(messageOffset);
240 ByteVector messageBytes = readBlock(messageLength);
241 READ_ASSERT(messageBytes.size() == messageLength);
242 int index = messageBytes.find((char) 0);
243 if(index > -1)
244 messageBytes.resize(index, 0);
245 messageBytes.replace('\r', '\n');
246 message = messageBytes;
247 }
248
249 seek(64);
250
251 ByteVector pannings = readBlock(64);
252 ByteVector volumes = readBlock(64);
253 READ_ASSERT(pannings.size() == 64 && volumes.size() == 64);
254 int channels = 0;
255 for(int i = 0; i < 64; ++ i) {
256 // Strictly speaking an IT file has always 64 channels, but
257 // I don't count disabled and muted channels.
258 // But this always gives 64 channels for all my files anyway.
259 // Strangely VLC does report other values. I wonder how VLC
260 // gets it's values.
261 if((unsigned char) pannings[i] < 128 && volumes[i] > 0)
262 ++channels;
263 }
264 d->properties.setChannels(channels);
265
266 // real length might be shorter because of skips and terminator
267 unsigned short realLength = 0;
268 for(unsigned short i = 0; i < length; ++ i) {
269 READ_BYTE_AS(order);
270 if(order == 255) break;
271 if(order != 254) ++ realLength;
272 }
273 d->properties.setLengthInPatterns(realLength);
274
275 StringList comment;
276 // Note: I found files that have nil characters somewhere
277 // in the instrument/sample names and more characters
278 // afterwards. The spec does not mention such a case.
279 // Currently I just discard anything after a nil, but
280 // e.g. VLC seems to interprete a nil as a space. I
281 // don't know what is the proper behaviour.
282 for(unsigned short i = 0; i < instrumentCount; ++ i) {
283 seek(192L + length + ((long)i << 2));
284 READ_U32L_AS(instrumentOffset);
285 seek(instrumentOffset);
286
287 ByteVector instrumentMagic = readBlock(4);
288 READ_ASSERT(instrumentMagic == "IMPI");
289
290 READ_STRING_AS(dosFileName, 13);
291
292 seek(15, Current);
293
294 READ_STRING_AS(instrumentName, 26);
295 comment.append(instrumentName);
296 }
297
298 for(unsigned short i = 0; i < sampleCount; ++ i) {
299 seek(192L + length + ((long)instrumentCount << 2) + ((long)i << 2));
300 READ_U32L_AS(sampleOffset);
301
302 seek(sampleOffset);
303
304 ByteVector sampleMagic = readBlock(4);
305 READ_ASSERT(sampleMagic == "IMPS");
306
307 READ_STRING_AS(dosFileName, 13);
308 READ_BYTE_AS(globalVolume);
309 READ_BYTE_AS(sampleFlags);
310 READ_BYTE_AS(sampleVolume);
311 READ_STRING_AS(sampleName, 26);
312 /*
313 READ_BYTE_AS(sampleCvt);
314 READ_BYTE_AS(samplePanning);
315 READ_U32L_AS(sampleLength);
316 READ_U32L_AS(loopStart);
317 READ_U32L_AS(loopStop);
318 READ_U32L_AS(c5speed);
319 READ_U32L_AS(sustainLoopStart);
320 READ_U32L_AS(sustainLoopEnd);
321 READ_U32L_AS(sampleDataOffset);
322 READ_BYTE_AS(vibratoSpeed);
323 READ_BYTE_AS(vibratoDepth);
324 READ_BYTE_AS(vibratoRate);
325 READ_BYTE_AS(vibratoType);
326 */
327
328 comment.append(sampleName);
329 }
330
331 if(message.size() > 0)
332 comment.append(message);
333 d->tag.setComment(comment.toString("\n"));
334 d->tag.setTrackerName("Impulse Tracker");
335 }
336