1#!/usr/bin/env python
2# -*- coding: UTF-8 -*-
3#
4osLibFound = False
5sysLibFound = False
6shutilLibFound = False
7waveLibFound = False
8ctypesLibFound = False
9structLibFound = False
10
11try:
12	import os
13except ImportError:
14	print "[Error] os python library is required to be installed!"
15else:
16	osLibFound = True
17
18try:
19	import sys
20except ImportError:
21	print "[Error] sys python library is required to be installed!"
22else:
23	sysLibFound = True
24
25try:
26	import wave
27except ImportError:
28	print "[Error] Wave python library is required to be installed!"
29else:
30	waveLibFound = True
31
32try:
33	import struct
34except ImportError:
35	print "[Error] struct python library is required to be installed!"
36else:
37	structLibFound = True
38
39if 	(not osLibFound) \
40	or (not sysLibFound) \
41	or (not waveLibFound) \
42	or (not structLibFound):
43	sys.stdout.write("[Error] Errors were found when trying to import required python libraries\n")
44	sys.exit(1)
45
46from struct import *
47from audFileDecode import *
48
49MY_MODULE_VERSION = "0.80"
50MY_MODULE_NAME = "audFileLib"
51
52#constants
53aud_chunk_id = 0x0000deaf
54SIZE_OF_AUD_HEADER_IN_BYTES = 12
55SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES = 8
56
57class AudHeader(object):
58	m_samplerate = 0    # Frequency                     // int16_t // TODO should be unsigned (?)
59	m_size_in = 0       # Size of file (without header) // int32_t // TODO should be unsigned (?)
60	m_size_out = 0      # Size of output data           // int32_t // TODO should be unsigned (?)
61	m_flags = 0         # bit 0=stereo, bit 1=16bit     // int8_t
62	m_compression = 0   # 1=WW compressed, 99=IMA ADPCM (0x63) // int8_t
63	m_populated = False
64
65	def __init__(self):
66		return
67
68
69# The rest of the AUD files is divided in chunks.
70# These are usually 512 bytes long, except for the last one.
71class AudChunkHeader(object):
72	m_ch_size_in = 0     # Size of compressed data // int16_t // TODO should be unsigned (?)
73	m_ch_size_out = 0    # Size of output data     // int16_t // TODO should be unsigned (?)
74	m_ch_id = 0x0000FFFF # Always 0x0000DEAF       // int32_t
75
76	def __init__(self):
77		return
78
79#
80#
81#
82class audFile(object):
83	m_header = AudHeader()
84	m_traceModeEnabled = False
85	m_simpleAudioFileName = 'GENERIC.AUD'
86
87	# traceModeEnabled is bool to enable more printed debug messages
88	def __init__(self, traceModeEnabled = True):
89		self.m_simpleAudioFileName = 'GENERIC.AUD'
90		self.m_traceModeEnabled = traceModeEnabled
91		return
92
93	# std::fstream& fs, AudFileNS::pos_type startAudFilepos, AudFileNS::pos_type endAudFilepos, const std::string& filename
94	def export_as_wav(self, audBytesBuff, filename):
95		if (not self.header().m_populated):
96			print "[Error] file was not loaded properly (header info missing): " + filename
97			return 1
98
99		print "[Info] Exporting to wav: " + filename
100
101		cvirtualBinaryD = None
102		if self.header().m_compression > 0:
103			cvirtualBinaryD = self.decode(self.header().m_compression, audBytesBuff)
104		elif self.header().m_flags == 2: # compression 0, 16bit stereo
105			cbinaryDataOutLst = []
106			offsInAudFile = SIZE_OF_AUD_HEADER_IN_BYTES
107			for i in range(0, (len(audBytesBuff) - SIZE_OF_AUD_HEADER_IN_BYTES) / 2):
108				if (self.m_traceModeEnabled):
109					print "[Trace] Reading bytes %d, %d" % (2*i, 2*i + 1)
110				tmpTupleL = struct.unpack_from('B', audBytesBuff, offsInAudFile)
111				offsInAudFile += 1
112				tmpTupleH = struct.unpack_from('B', audBytesBuff, offsInAudFile)
113				offsInAudFile += 1
114				cbinaryDataOutLst.append(tmpTupleL[0])
115				cbinaryDataOutLst.append(tmpTupleH[0])
116			cvirtualBinaryD = struct.pack('B'*len(cbinaryDataOutLst), *cbinaryDataOutLst)
117
118		if (not cvirtualBinaryD and (len(audBytesBuff) - SIZE_OF_AUD_HEADER_IN_BYTES) > 0):
119			print "[Error] audio file could not be exported properly (0 data read): %s" % (filename)
120			return 1
121		elif (len(audBytesBuff) - SIZE_OF_AUD_HEADER_IN_BYTES) == 0:
122			print "[Warning] Creating empty wav file: %s" % (filename)
123
124		cb_sample = self.get_cb_sample()
125		cs_remaining = self.get_c_samples()
126
127		waveWritFile = wave.open(filename, 'wb')
128		waveWritFile.setnchannels(self.get_c_channels())
129		waveWritFile.setsampwidth(cb_sample)
130		waveWritFile.setframerate(self.get_samplerate())
131		waveWritFile.setnframes(cs_remaining)
132		#waveWritFile.setcomptype(None, '')
133		waveWritFile.writeframesraw(cvirtualBinaryD)
134		waveWritFile.close()
135
136#		t_wav_header header;
137#		memset(&header, 0, sizeof(t_wav_header));
138#		header.file_header.id = wav_file_id; // # "RIFF"
139#		header.file_header.size = sizeof(header) - sizeof(header.file_header) + (cs_remaining << 1);
140#		header.form_type = wav_form_id;		// # "WAVE"
141#		header.format_chunk.header.id = wav_format_id;	// #"fmt "
142#		header.format_chunk.header.size = sizeof(header.format_chunk) - sizeof(header.format_chunk.header);
143#		header.format_chunk.formattag = 1;
144#		header.format_chunk.c_channels = 1;
145#		header.format_chunk.samplerate = get_samplerate();
146#		header.format_chunk.byterate =	cb_sample * get_samplerate();
147#		header.format_chunk.blockalign = cb_sample;
148#		header.format_chunk.cbits_sample = cb_sample << 3;
149#		header.data_chunk_header.id = wav_data_id;		# "data"
150#		header.data_chunk_header.size = cb_sample * cs_remaining;
151#		error = f.write(&header, sizeof(t_wav_header));
152#		return error ? error : f.write(d);
153		return 0	# TODO fix
154
155
156	def loadAudFile(self, audBytesBuff, maxLength, audFileName):
157		self.m_simpleAudioFileName = audFileName
158		offsInAudFile = 0
159		tmpTuple = struct.unpack_from('H', audBytesBuff, offsInAudFile)
160		self.header().m_samplerate = tmpTuple[0]
161		offsInAudFile += 2
162		tmpTuple = struct.unpack_from('I', audBytesBuff, offsInAudFile)
163		self.header().m_size_in = tmpTuple[0]
164		offsInAudFile += 4
165		tmpTuple = struct.unpack_from('I', audBytesBuff, offsInAudFile)
166		self.header().m_size_out = tmpTuple[0]
167		offsInAudFile += 4
168		tmpTuple = struct.unpack_from('B', audBytesBuff, offsInAudFile)
169		self.header().m_flags = tmpTuple[0]
170		offsInAudFile += 1
171		tmpTuple = struct.unpack_from('B', audBytesBuff, offsInAudFile)
172		self.header().m_compression = tmpTuple[0]
173		offsInAudFile += 1
174
175		if self.m_traceModeEnabled:
176			print "[Debug] Sample rate: %d\tsizeIn: %d\tsizeOut: %d\tflags: %d\tcompression: %d" % (self.get_samplerate(), self.header().m_size_in, self.header().m_size_out, self.header().m_flags, self.header().m_compression)
177
178		if self.get_samplerate() < 8000 or self.get_samplerate() > 48000 or self.header().m_size_in > (maxLength - SIZE_OF_AUD_HEADER_IN_BYTES ):
179			print "[Warning] Bad AUD Header info in file %s, size_in: %d, maxLen: %d" % (self.m_simpleAudioFileName, self.header().m_size_in, (maxLength - SIZE_OF_AUD_HEADER_IN_BYTES))
180			if self.header().m_size_in == 0:
181				# handle special case where only the header of the AUD file is present and the size_in is 0.
182				# fill the header with "valid" info for an empty wav file
183				self.header().m_size_out = 0
184				self.header().m_samplerate = 22050
185				self.header().m_compression = 0
186				self.header().m_flags = 2
187				self.header().m_populated = True
188				return True
189			else:
190				return False
191		else:
192			if self.header().m_compression == 1:
193				if (self.header().m_flags != 0):
194					return False
195			elif self.header().m_compression == 0x63:
196				if (self.header().m_flags != 2):
197					return False
198			elif self.header().m_compression == 0: # no compression. At least some AUD files in SFX.MIX have this
199				if (self.header().m_flags != 2):
200					return False
201		self.header().m_populated = True
202		return True
203
204	# int AudFile::get_chunk_header(int i, std::fstream& fs, AudFileNS::pos_type startAudFilepos, AudFileNS::pos_type endAudFilepos, AudChunkHeader& outAudChunkHeader)
205	def get_chunk_header(self, chunkIdx, inAudFileBytesBuffer, inAudFileSize):
206		#fs.seekg(int(startAudFilepos) + int(SIZE_OF_AUD_HEADER_IN_BYTES), fs.beg);
207		#AudFileNS::pos_type rAudPos;
208		#rAudPos = fs.tellg();
209		outAudChunkHeader =  AudChunkHeader()
210		rAudPos = SIZE_OF_AUD_HEADER_IN_BYTES
211
212		if (self.m_traceModeEnabled):
213			print "[Trace] Getting chunk header at %d" % (rAudPos)
214		#AudChunkHeader  tmpInremediateChunkheader;
215		tmpInremediateChunkheader = AudChunkHeader()
216		#while (i--) # value of i is decreased after checked by while loop
217		while(chunkIdx > 0):
218			chunkIdx -= 1
219			if (rAudPos + SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES > inAudFileSize):
220				return (-1, rAudPos, None)
221
222			tmpAudFileOffset = rAudPos
223			tmpTuple = struct.unpack_from('H', inAudFileBytesBuffer, tmpAudFileOffset)
224			tmpInremediateChunkheader.m_ch_size_in = tmpTuple[0]
225			tmpAudFileOffset += 2
226			tmpTuple = struct.unpack_from('H', inAudFileBytesBuffer, tmpAudFileOffset)
227			tmpInremediateChunkheader.m_ch_size_out = tmpTuple[0]
228			tmpAudFileOffset += 2
229			tmpTuple = struct.unpack_from('I', inAudFileBytesBuffer, tmpAudFileOffset)
230			tmpInremediateChunkheader.m_ch_id  = tmpTuple[0]
231			tmpAudFileOffset += 4
232			#fs.read((char*)&tmpInremediateChunkheader, SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES);
233			rAudPos += SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES + tmpInremediateChunkheader.m_ch_size_in
234			#fs.seekg(int(rAudPos), fs.beg);
235
236		if (rAudPos + SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES > inAudFileSize ):
237			return (-1, rAudPos, None)
238		# write to FINAL output chunk header
239		tmpAudFileOffset = rAudPos
240		tmpTuple = struct.unpack_from('H', inAudFileBytesBuffer, tmpAudFileOffset)
241		outAudChunkHeader.m_ch_size_in = tmpTuple[0]
242		tmpAudFileOffset += 2
243		tmpTuple = struct.unpack_from('H', inAudFileBytesBuffer, tmpAudFileOffset)
244		outAudChunkHeader.m_ch_size_out = tmpTuple[0]
245		tmpAudFileOffset += 2
246		tmpTuple = struct.unpack_from('I', inAudFileBytesBuffer, tmpAudFileOffset)
247		outAudChunkHeader.m_ch_id  = tmpTuple[0]
248		tmpAudFileOffset += 4
249		#fs.read((char*)&outAudChunkHeader, SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES);
250		if (rAudPos + SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES + outAudChunkHeader.m_ch_size_in > inAudFileSize):
251			return (-1, rAudPos, None)
252		rAudPos += SIZE_OF_AUD_CHUNK_HEADER_IN_BYTES
253		return (0, rAudPos, outAudChunkHeader) # //reinterpret_cast<const AudChunkHeader*>(r);
254
255	# int AudFile::get_chunk_data(int i, std::fstream& fs, int sizeToRead, AudFileNS::byte* byteChunkDataPtr)
256	def get_chunk_data(self, inAudFileBytesBuffer, startOffs, sizeToRead):
257		#fs.read((char*)byteChunkDataPtr, sizeToRead)
258		outChunkDataLst = []
259		if (self.m_traceModeEnabled):
260			print "[Trace] Getting chunk data"
261			print "[Trace] startOffs: %d, sizeToRead: %d" % (startOffs, sizeToRead)
262		for i in range(startOffs, startOffs + sizeToRead):
263			#outChunkDataLst.append(ctypes.c_char(inAudFileBytesBuffer[i]).value)
264			#outChunkDataLst.append(ctypes.c_byte(inAudFileBytesBuffer[i]).value)
265			tmpTuple = struct.unpack_from('B', inAudFileBytesBuffer, i)
266			outChunkDataLst.append(tmpTuple[0])
267		#byteChunkDataOut = struct.pack('b'*len(outChunkDataLst), *outChunkDataLst)
268		#return (0, byteChunkDataOut)
269		return (0, outChunkDataLst)
270
271
272	# std::fstream& fs, AudFileNS::pos_type startAudFilepos, AudFileNS::pos_type endAudFilepos
273	# returned Cvirtual_binary
274	def decode(self, speccompression, audBytesBuff):
275		# The * operator unpacks an argument list. It allows you to call a function with the list items as individual arguments.
276		# binDataOut = struct.pack('i'*len(data), *data)
277		if self.m_traceModeEnabled:
278			print "[Debug] Decoding AUD file format..."
279#		Cvirtual_binary d;
280		binaryDataOutLst = []
281		binaryDataOutBuff = None
282		cb_audio = self.get_cb_sample() * self.get_c_samples() # int cb_audio - basically this should be the size_out
283		if speccompression == 1:
284			# write_start allocates space for virtualBinary
285			# AudFileNS::byte* w = d.write_start(cb_audio);
286			errGetChunk = 0 # int errGetChunk
287			#for (int chunk_i = 0; w != d.data_end(); chunk_i++)
288			chunk_i = 0
289			wIndex = 0
290			while (wIndex < cb_audio):
291				#AudChunkHeader out_chunk_header;
292				#out_chunk_header = AudChunkHeader()
293				(errGetChunk, bufferDataPos, out_chunk_header) = self.get_chunk_header(chunk_i, audBytesBuff, len(audBytesBuff))
294				if errGetChunk != 0:
295					if self.m_traceModeEnabled:
296						print "[Trace] Error OR End file case while getting uncompressed chunk header!"
297					break
298
299				if self.m_traceModeEnabled:
300					print "[Trace] Get uncompressed chunk header returned: %d " % (out_chunk_header.m_ch_id)
301				#Cvirtual_binary out_chunk_data;
302				#AudFileNS::byte* byteChunkDataPtr = out_chunk_data.write_start(out_chunk_header.m_ch_size_in);
303				(errorGCD, byteChunkDataLst) = self.get_chunk_data(audBytesBuff, bufferDataPos, out_chunk_header.m_ch_size_in)
304				# export decoded chunk to w (output) buffer (of CHARS) at the point where we're currently at (so append there)
305				decodedAudioChunkAsLst = aud_decode_ws_chunk(byteChunkDataLst, out_chunk_header.m_ch_size_in, out_chunk_header.m_ch_size_out)
306				binaryDataOutLst.extend(decodedAudioChunkAsLst)
307				wIndex += out_chunk_header.m_ch_size_out
308				chunk_i += 1
309			binaryDataOutBuff = struct.pack('b'*len(binaryDataOutLst), *binaryDataOutLst)
310		elif speccompression == 0x63:
311			decodeInstance = audFileDecode(self.m_traceModeEnabled);
312			#decodeInstance.init();
313			#AudFileNS::byte* w = d.write_start(cb_audio);
314			errGetChunk = 0 # int errGetChunk
315			# for (int chunk_i = 0; w != d.data_end(); chunk_i++)
316			chunk_i = 0
317			wIndex = 0
318			while (wIndex < cb_audio):
319				if self.m_traceModeEnabled:
320					print("[Trace] chunkI: %d\t Windex: %d\t cb_audio: %d") % (chunk_i,wIndex,cb_audio)
321				#AudChunkHeader out_chunk_header;
322				#out_chunk_header = AudChunkHeader()
323				#errGetChunk = self.get_chunk_header(chunk_i, fs, startAudFilepos, endAudFilepos, out_chunk_header);
324				(errGetChunk, bufferDataPos, out_chunk_header) = self.get_chunk_header(chunk_i, audBytesBuff, len(audBytesBuff))
325				if errGetChunk != 0:
326					print "[Warning] Error OR End file case while getting COMPRESSED chunk header!"
327					break
328				if self.m_traceModeEnabled:
329					print "[Trace] Get COMPRESSED chunk header returned:: headerInSize: %d headerOutSize: %d id: %d" % (out_chunk_header.m_ch_size_in, out_chunk_header.m_ch_size_out,  out_chunk_header.m_ch_id)
330				#Cvirtual_binary out_chunk_data;
331				#AudFileNS::byte* byteChunkDataPtr = out_chunk_data.write_start(out_chunk_header.m_ch_size_in);
332				(errorGCD, byteChunkDataLst) = self.get_chunk_data(audBytesBuff, bufferDataPos, out_chunk_header.m_ch_size_in)
333				# export decoded chunk to w (output) buffer (of SHORTS) at the point where we're currently at (so append there)
334				if self.m_traceModeEnabled:
335					print "[Trace] byteChunkDataLst len: %d, m_ch_size_in was: %d" % (len(byteChunkDataLst), out_chunk_header.m_ch_size_in)
336				decodedAudioChunkAsLst = decodeInstance.decode_chunk(byteChunkDataLst, out_chunk_header.m_ch_size_out / self.get_cb_sample());
337				binaryDataOutLst.extend(decodedAudioChunkAsLst)
338				wIndex += out_chunk_header.m_ch_size_out
339				if self.m_traceModeEnabled:
340					print("[Trace] New Windex: %d\t cb_audio: %d") % (wIndex,cb_audio)
341				chunk_i += 1
342			binaryDataOutBuff = struct.pack('h'*len(binaryDataOutLst), *binaryDataOutLst)
343		if self.m_traceModeEnabled:
344			if binaryDataOutBuff is not None:
345				if self.m_traceModeEnabled:
346					print "[Trace] Decoding Done."
347			else: #if binaryDataOutBuff is None:
348				print "[Error] Decoding yielded errors (data out buffer is null)."
349		return binaryDataOutBuff
350
351	def header(self):
352		return self.m_header
353
354	def get_c_samples(self):
355		return self.m_header.m_size_out / self.get_cb_sample()
356
357	def get_samplerate(self):
358		return self.m_header.m_samplerate;
359
360	# flag bit 0 is stereo(set) mono(clear)
361	def get_c_channels(self):
362		return 2 if (self.m_header.m_flags & 0x01) else 1;
363
364	# flag bit 1 is 16bit(set) 8bit (clear)
365	def get_cb_sample(self):
366		return 2 if (self.m_header.m_flags & 0x02) else 1
367#
368#
369#
370if __name__ == '__main__':
371	# main()
372	errorFound = False
373	# By default assumes a file of name 000000.AUD in same directory
374	# otherwise tries to use the first command line argument as input file
375	inAUDFile = None
376	inAUDFileName = '00000000.AUD'
377
378	if len(sys.argv[1:])  > 0 \
379		and os.path.isfile(os.path.join('.', sys.argv[1])) \
380		and len(sys.argv[1]) >= 5 \
381		and sys.argv[1][-3:].upper() == 'AUD':
382		inAUDFileName = sys.argv[1]
383		print "[Info] Attempting to use %s as input AUD file..." % (inAUDFileName)
384	elif os.path.isfile(os.path.join('.', inAUDFileName)):
385		print "[Info] Using default %s as input AUD file..." % (inAUDFileName)
386	else:
387		print "[Error] No valid input file argument was specified and default input file %s is missing." % (inAUDFileName)
388		errorFound = True
389
390	if not errorFound:
391		try:
392			print "[Info] Opening %s" % (inAUDFileName)
393			inAUDFile = open(os.path.join('.', inAUDFileName), 'rb')
394		except:
395			errorFound = True
396			print "[Error] Unexpected event:", sys.exc_info()[0]
397			raise
398		if not errorFound:
399			allOfAudFileInBuffer = inAUDFile.read()
400			audFileInstance = audFile(True)
401			if audFileInstance.m_traceModeEnabled:
402				print "[Debug] Running %s (%s) as main module" % (MY_MODULE_NAME, MY_MODULE_VERSION)
403			if audFileInstance.loadAudFile(allOfAudFileInBuffer, len(allOfAudFileInBuffer), inAUDFileName):
404				print "[Info] Audio file (AUD) loaded successfully!"
405				audFileInstance.export_as_wav(allOfAudFileInBuffer, './tmp.wav')
406			else:
407				print "[Error] Error while loading Audio file (AUD)!"
408			inAUDFile.close()
409else:
410	#debug
411	#print "[Debug] Running %s (%s) imported from another module" % (MY_MODULE_NAME, MY_MODULE_VERSION)
412	pass
413