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 interpret 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