# =====================================================================================================
# Preliminary MLT--->DSF converter (ver 0.03 2008-10-14) by kingshriek
# This script only supports the Manatee sound driver and not necessarily all the different versions of
# it. For sound data not in .MLT files, convert to .MLT first before using.
# Requires bin2psf & psfpoint (http://www.neillcorlett.com/psf/utilities.html)
# Update history located near end of file
# =====================================================================================================

# =====================================================================================================
# This script produces .dsf/.minidsf files from a Manatee sound driver image and a multi-unit sound 
# data file (.MLT extension) and has similar functionality to the ssfmake script (for .ssf/.minissf). 
# Not all games pack their sound data into .MLT files, and for those that don't, it will require 
# combining the various sound files into an .MLT file before using this script. The individual sound
# formats should have extentions similar to their header strings, only without the leading "S" (e.g. a
# sequence data file will have extention .msb and header string SMSB). See comments below inside
# dsfmake() for information on the various sound formats and header strings.
#
# This script in its current form may not work with some versions of the manatee driver. This is the 
# case since the driver needs to be patched in a few locations so that it doesn't wipe out ripper-provided
# sound commands upon initialization and these patch locations may vary with different driver versions.
# Also, some of these patches disable loading of various default sound data (the driver contains itself
# contains a default DSP program and mixer for instance) so that it uses data in the MLT file instead.
# If a track needs any of this default data, you will need to comment out the corresponding patches in
# dsfmake() for a proper rip.
#
# The bulk of the script is defined as a function, which is called at the very bottom. Feel free to customize 
# this bottom section as you wish. Once good use for it is that you can write some code to loop over a bunch 
# of files and perform the dsf creation in batch mode.
# =====================================================================================================

# =====================================================================================================

# =====================================================================================================
from struct import *    # pack, unpack
from array import *
import getopt
import sys
import os    # system
# =====================================================================================================

# =====================================================================================================
# Converts list of bytes into a sound command array (zero-padded out to 16 bytes)
def sndcmd(x):
	if len(x) > 0x10:
		x = x[:0x10]
	return array('B', x + [0x00] * (0x10 - len(x)))
# =====================================================================================================

# =====================================================================================================
# Get base file name with and without extension
def fnoext(fname):
	fnameb = os.path.basename(fname)
	idot  = -fname[::-1].find('.')-1
	if idot:
		fnamex = fnameb[:idot]
	else:
		fnamex = fnameb
	return (fnameb, fnamex)
# =====================================================================================================

# =====================================================================================================
# Get sound RAM offset and byte allocation (useful for custom dsflib/minidsf creation)
def get_offset(mlt_filename, hdr_str, bank):
	mlt_hdr_top = array('B', open(mlt_filename, 'rb').read(0x20))    
	nhdr = unpack('<I', mlt_hdr_top[0x8:0xC])[0]    # Number of MLT header entries
	hdr_offset = 0x20    # Skip the MLT top header
	mlt_hdr = array('B', open(mlt_filename, 'rb').read(0x20 * (nhdr + 1)))    # Get the full MLT header
	for k in range(nhdr):    # Search for header string and bank
		mlt_hdr_str = mlt_hdr[hdr_offset:hdr_offset + 4].tostring()    # Get header string from MLT header entry
		mlt_bank = unpack('<I', mlt_hdr[hdr_offset + 4:hdr_offset + 8])[0]    # Get bank number from MLT header entry
		if mlt_hdr_str == hdr_str and mlt_bank == bank:    # Compare to inputs
			offset = unpack('<I', mlt_hdr[hdr_offset + 0x8:hdr_offset + 0xC])[0]    # Get sound RAM offset from MLT header entry
			alloc = unpack('<I', mlt_hdr[hdr_offset + 0xC:hdr_offset + 0x10])[0]    # Get sound RAM allocation from MLT header entry
			return (offset, alloc)
		hdr_offset += 0x20
	return (None, None)	
# =====================================================================================================	

# =====================================================================================================
# Creates ssf file from user-specified parameters.
# Inputs are defined in paramter section above.
def dsfmake(nout, driver, nmlt, bank, track, volume, mixer, effect):
	# Initialization
	szdrv = 0
	szmlt = 0
	datadrv = array('B', [])
	datamlt = array('B', [])
	ntracks = 0
# -----------------------------------------------------------------------------------------------------
	if driver != '':
		fdrv = open(driver, 'rb')    # sound driver
		szdrv = os.path.getsize(driver)
		datadrv = array('B', fdrv.read(szdrv))
		fdrv.close()
	if nmlt != '':
		fmlt = open(nmlt, 'rb')    # sound data
		szmlt = os.path.getsize(nmlt)
		datamlt = array('B', fmlt.read(szmlt))
		fmlt.close()
# -----------------------------------------------------------------------------------------------------
	if naomi:
		dsfbin = array('B', '\x00' * 0x800000)
	else:
		dsfbin = array('B', '\x00' * 0x200000)
# -----------------------------------------------------------------------------------------------------
	# Set driver
	# Identified by header string 'SDRV'
	if datadrv[:4].tostring() == 'SDRV':    # header string for sound driver
		dsfbin[:szdrv-0x20] = datadrv[0x20:szdrv]
		vdrv = datadrv[0x04]    # driver version
# -----------------------------------------------------------------------------------------------------
	# Set sound data
	# MANATEE driver sound data formats identified by header strings:
	#     SMLT - multi-unit archive 
	#     SMSB - MIDI sequence bank 
	#     SMPB - MIDI program bank
	#     SMDB - MIDI drum bank
	#     SOSB - one-shot bank (sound effects)
	#     SFPB - DSP program bank
	#     SFPW - DSP work RAM
	#     SFOB - DSP output bank
	#     SPSR - PCM stream ring-buffer
	sbank = ('SMSB', 'SOSB')    # DSF can either target MIDI sequences or one-shots
	areamap = {'SMSB':0x14000, 'SMPB':0x14080, 'SOSB':0x14100, 'SFPB':0x14200, 'SFOB':0x14280, 'SFPW':0x14288, 'SMDB':0x14290, 'SPSR':0x14180} # canonical area map offsets
	#fcon = open('mlt_contents.txt', 'w') # DEBUG
	if datamlt[:4].tostring() == 'SMLT':
		nfiles = unpack('<I', datamlt[0x8:0xC].tostring())[0]
		offset = 0x20
		for ifile in range(0, nfiles): 
			fhdr = datamlt[offset + 0x0:offset + 0x4].tostring()
			foff = unpack('<i', datamlt[offset + 0x10:offset + 0x14].tostring())[0]
			fsz = unpack('<i', datamlt[offset + 0x14:offset + 0x18].tostring())[0]
			mbank = unpack('<i', datamlt[offset + 0x4:offset + 0x8].tostring())[0]
			moff = unpack('<i', datamlt[offset + 0x8:offset + 0xC].tostring())[0]
			msz = unpack('<i', datamlt[offset + 0xC:offset + 0x10].tostring())[0]
			#if fhdr in areamap and (fhdr == 'SFPW' or foff >=0):    # write area map data if offset is known
			dsfbin[areamap[fhdr] + 8 * mbank + 0x0:areamap[fhdr] + 8 * mbank + 0x4] = datamlt[offset + 0x8:offset + 0xC]
			dsfbin[areamap[fhdr] + 8 * mbank + 0x4:areamap[fhdr] + 8 * mbank + 0x8] = datamlt[offset + 0xC:offset + 0x10]
			#if foff >= 0 or fhdr == 'SFPW' or fhdr == 'SPSR':    # DEBUG
				#fcon.write('%s: bank=0x%02X offset=0x%06X size=0x%06X\n' % (fhdr, mbank, moff, msz))    # DEBUG
			if fhdr != 'SFPW' and fhdr != 'SPSR' and foff >= 0:    # if DSP work ram or invalid offset, don't write out anything
				if fhdr == 'SMPB' and use_osb:
					offset += 0x20
					continue
				if fhdr == 'SOSB' and not use_osb:
					offset += 0x20
					continue
				dsfbin[moff:moff + fsz] = datamlt[foff:foff + fsz]    # write sound data
				if fhdr == sbank[use_osb] and mbank == bank:    # check specifically for sequence data and get number of tracks
					ntracks = dsfbin[moff + 0xC]
					print '%s data contains %d track(s).' % (sbank[use_osb], ntracks)
			offset += 0x20
	#fcon.close()    # DEBUG
# -----------------------------------------------------------------------------------------------------
	# Set sound commands
	if track != None:
		ctrack = track
	else:
		ctrack = 0
	scbase = {0x01: 0x13200, 0x02: 0x12200}[vdrv]
	ssbase = scbase + 0x1F0   # SEQUENCE_START track number offset (for minidsf)
	dsfbin[scbase + 0x00:scbase + 0x10] = sndcmd([0x87, 0x00, 0x00, 0x00, 0x00, 0x00])    # not sure, but 3rd and 5th bytes have various effects
	dsfbin[scbase + 0x10:scbase + 0x20] = sndcmd([0x82, 0x00, mixer])    # MIXER_CHANGE
	dsfbin[scbase + 0x20:scbase + 0x30] = sndcmd([0x84, 0x00, effect])    # EFFECT_CHANGE
	dsfbin[scbase + 0x30:scbase + 0x40] = sndcmd([0x81, 0x00, 0xFF])    # TOTAL_VOLUME
	dsfbin[scbase + 0x40:scbase + 0x50] = sndcmd([0x05 + 0x10 * use_osb, 0x00, 0x00, volume])    # SEQUENCE_VOLUME (or OSB_VOLUME)
	dsfbin[ssbase + 0x00:ssbase + 0x10] = sndcmd([0x01 + 0x10 * use_osb, 0x00, 0x00, bank, ctrack])    # SEQUENCE_START (or OSB_START)
# -----------------------------------------------------------------------------------------------------
	# Necessary for sound command processing
	dsfbin[scbase + 0x200] = 0x01
# -----------------------------------------------------------------------------------------------------
	# Driver patch for MANATEE.DRV - should cover most versions of the driver
	# Comment any of these out as needed
	NOPtable = {
	0x01: [0x448, 0x470, 0x488, 0x4A4, 0x4AC, 0x4C4, 0x4DC],
	0x02: [0x434, 0x4CC, 0x4E4, 0x500, 0x508, 0x520, 0x538],
	}[vdrv]
	NOP = array('B', [0x00, 0x00, 0xA0, 0xE3])
	dsfbin[NOPtable[0]:NOPtable[0] + 4] = NOP      # don't clear command area
	dsfbin[NOPtable[1]:NOPtable[1] + 4] = NOP      # don't clear MSB banks
	dsfbin[NOPtable[2]:NOPtable[2] + 4] = NOP      # don't clear MPB banks
	dsfbin[NOPtable[3]:NOPtable[3] + 8] = NOP * 2    # don't use default MDB bank 0
	dsfbin[NOPtable[4]:NOPtable[4] + 8] = NOP * 2    # don't use default MDB bank 1
	dsfbin[NOPtable[5]:NOPtable[5] + 8] = NOP * 2    # don't use default FPB
	dsfbin[NOPtable[6]:NOPtable[6] + 8] = NOP * 2    # don't use default FOB
# -----------------------------------------------------------------------------------------------------
	# Write output file
	(bout, xout) = fnoext(nout)
	if use_osb:
		xout += '_OSB'
	fo = open(xout + '.bin', 'wb')
	fo.write('\x00' * 4)    # load address
	fo.write(dsfbin.tostring())
	fo.close()
# -----------------------------------------------------------------------------------------------------
    # If track number is explicity given, generate a single dsf file
	if track != None:
		ext = bout.replace(xout, '')[1:]
		os.system('bin2psf %s 18 %s.bin' % (ext, xout))
	# Otherwise, automatically create minidsfs based on number of tracks found in bank
	else:
		# If only 1 track, create dsf file
		if ntracks == 1:
			os.system('bin2psf dsf 18 %s.bin' % xout)
		# Otherwise, create the dsflib file
		elif ntracks > 1:
			os.system('bin2psf dsflib 18 %s.bin' % xout)
# -----------------------------------------------------------------------------------------------------
			# Create minidsfs
			for itrack in range(0, ntracks):
				fo = open('temp.bin', 'wb')
				fo.write(pack('<I', ssbase + 3))
				fo.write(pack('B', bank))
				fo.write(pack('B', itrack))
				fo.close()
				os.system('bin2psf minidsf 18 ' + 'temp.bin')
				os.system('psfpoint "-_lib=' + xout + '.dsflib" ' + 'temp.minidsf')
				strfmt = ('_%02d', '_%03d')[use_osb]
				fname = xout +  '_%02d' % bank + strfmt % itrack + '.minidsf'
				if os.access(fname, os.F_OK):
					os.remove(fname)
				os.rename('temp.minidsf', fname)
# -----------------------------------------------------------------------------------------------------
	# Cleanup
	if os.access(xout + '.bin', os.F_OK):
		os.remove(xout + '.bin')
	if os.access('temp.bin', os.F_OK):
		os.remove('temp.bin')
# =====================================================================================================

# =====================================================================================================
# Generate dsf file(s). Feel free to customize this section as you see fit (looping through a bunch of files, etc.)

# =====================================================================================================
# PARAMETERS
# -----------------------------------------------------------------------------------------------------
bank     = -1
track    = None    # sequence track number (if None, create minidsfs for all tracks)
volume   = 0x7F   # sequence volume (0xFF is max, but it should probably be <= 0x7F in most cases)
mixer    = 0x01    # effect-out mixer select (usually 0x00)
effect   = 0x00    # DSP program select (usually 0x00)
# Not sure about any other paramaters yet, especially for sound command 0x87. For others, try editing
# the sound commands specified in dsfmake() directly for now.
# -----------------------------------------------------------------------------------------------------
use_osb  = 0      # 0: use MIDI sequence data, 1: use one-shot data (sound effects)
naomi    = 0       # 0: assume 2 MB sound RAM, 1: assume 8 MB sound RAM (Naomi/Hikaru boards)
# -----------------------------------------------------------------------------------------------------
# filenames
driver = 'MANATEE.DRV'    # sound driver (usually MANATEE.DRV)

options, args = getopt.getopt(sys.argv[1:], "b:t:v:m:e:d:",
	[ "bank=", "track=", "volume=", "mixer=", "effect=", "driver=" ])

for o, a in options:
	if o in ("-b", "--bank"):
		bank = int(a)
	elif o in ("-t", "--track"):
		track = int(a)
	elif o in ("-v", "--volume"):
		volume = int(a)
	elif o in ("-m", "--mixer"):
		mixer = int(a)
	elif o in ("-e", "--effect"):
		effect = int(a)
	elif o in ("-d", "--driver"):
		driver = a

for file in args:
	output = file.upper().replace('.MLT', '.DSF')
	if bank < 0:
		for i in xrange(100):
			try:
				dsfmake(output, driver, file, i, track, volume, mixer, effect)
			except:
				pass
	else:
		dsfmake(output, driver, file, bank, track, volume, mixer, effect)
	
# =====================================================================================================

# =====================================================================================================
# Update history:
# 08-10-14 (0.03) - Changed the naming behavior of the output file when automatic dsflib/minidsf creation
#                   is turned off so that it doesn't force a .dsf extention (useful for custom dsflibs). 
#                   Added a simple utility function that gets the sound RAM offset and allocation for
#                   a given sound data type and bank (useful for custom dsflib/minidsf creation).
# 08-10-12 (0.02) - Very minor update. Made it possible to turn off automatic dsflib/minidsf creation.
# 08-02-16 (0.01) - Filled in much of the missing information from the initial version - sound data types, 
#                   area map locations, sound commands, driver patches, etc. Added ability to rip one-shot 
#                   data in addition to sequence data.
# 08-01-12 (0.00) - Initial version.
# =====================================================================================================