#=====================================================================================================
# Sega Dreamcast MLT file creator (ver 0.01 2008-10-14) by kingshriek
# Inspired by MLT Creator.exe tool by manakoAT.
# Creates an MLT file from given Manatee sound data files (FOB, FPB, OSB, MSB, MDB, MPB).
# The resultant MLT file can then be used with dsfmake to produce a DSF rip.
# Version history located near end of file
#=====================================================================================================

#=====================================================================================================
import os
import sys
from array import *
from glob import *
from struct import *
#=====================================================================================================

#=====================================================================================================
DRIVER_SIZE = 0x18000    # Size of Manatee driver (upper bound)
DEFAULT_FPW_SIZE = 0x20040    # Use default of 0x20040 bytes for DSP work RAM, the maximum allowed
fpw_size_table = [0x4040, 0x8040, 0x10040, 0x20040, 0]    # Valid sizes for DSP work RAM
#=====================================================================================================

#=====================================================================================================
# Check allocated DSP program work RAM against DSP program requirements
def check_fpw_ram(mlt_filename, fpw_size, data):
	nfpw = unpack('<I', data[0xC:0x10])[0]    # Number of DSP programs
	for k in range(nfpw):    # Check all DSP programs in bank
		offset = unpack('<I', data[0x10+4*k:0x10+4*k+4])[0]    # DSP program offset
		req_index = data[offset+0x20]    # Required DSP work RAM index
		req_fpw_size = fpw_size_table[req_index]    # Required DSP work RAM
		if fpw_size < req_fpw_size:    # Warn user if not enough DSP work RAM allocated
			print 'Warning: In %s, DSP program %d requires 0x%X bytes of RAM, but only 0x%X bytes are allocated.' % (mlt_filename, k, req_fpw_size, fpw_size)
			print '         Try increasing DSP work RAM size.' 
	
#=====================================================================================================

#=====================================================================================================
# Check total allocated bytes in MLT file
def check_mlt_alloc(mlt_filename, mlt_hdr):
	total_alloc = 0    # Total number of bytes allocated
	nhdr = unpack('<I', mlt_hdr[0x8:0xC])[0]    # Number of MLT header entries
	hdr_offset = 0x20    # Skip the MLT top header (no allocation info here)
	for k in range(nhdr):    # Check all MLT header entries
		offset = unpack('<I', mlt_hdr[hdr_offset+0x8:hdr_offset+0xC])[0]    # Data offset
		size = unpack('<I', mlt_hdr[hdr_offset+0xC:hdr_offset+0x10])[0]    # Data size
		alloc = offset + size    # Sum data offset and size to get allocation amount
		total_alloc = max(alloc, total_alloc)    # Update maximum allocation
		hdr_offset += 0x20
	if total_alloc > 0x200000:   # Warn user if total allocation exceeds total Dreamcast sound RAM
		print 'Warning: In %s, total MLT data allocation exceeds 2 MB Dreamcast sound RAM  (%d bytes allocated).' % (mlt_filename, total_alloc)
		print '         Try decreasing DSP work RAM allocation or removing some input files (note: Naomi/Hikaru boards have 8 MB of sound RAM)'
#=====================================================================================================

#=====================================================================================================
# Create base MLT header entry
def create_hdr(hdr_str):
	hdr = array('B', '\x00'*0x20)    # MLT header entries are 32 bytes
	hdr[0x0:0x4] = array('B', hdr_str)    # First 4 bytes are a header string indicating the type of data
	hdr[0x10:0x18] = array('B', '\xFF'*8)   # Fill MLT offset and size fields with 0xFF (here 0xFF means unused)
	return hdr
#=====================================================================================================

#=====================================================================================================
# Initialize MLT header
def init_hdr(fpw_size=DEFAULT_FPW_SIZE):
	mlt_hdr = create_hdr('SMLT')    # Create base MLT header   
	if fpw_size:    # If DSP work RAM is to be allocated...
		mlt_hdr += create_hdr('SFPW')    # Create DSP work RAM header entry
		mlt_hdr[0x8:0xC] = array('B', pack('<I', 1))    # Set number of MLT entries to 1
		mlt_hdr[0x28:0x2C] = array('B', pack('<I', DRIVER_SIZE))    # Place data offset immediately after sound driver
		mlt_hdr[0x2C:0x30] = array('B', pack('<I', fpw_size))    # Write in DSP work RAM size
	return mlt_hdr
#=====================================================================================================

#=====================================================================================================
# Append a single header entry to MLT header
def append_hdr(mlt_hdr, data, hdr_str, bank, include):
	size = len(data)
	hdr = create_hdr(hdr_str)    # Create base header entry with the given header string
	hdr[0x4:0x8] = array('B', pack('<I', bank))    # Write in bank number
	mlt_hdr[0x4:0x8] = data[0x4:0x8]    # Copy version number from sound data to MLT header
	hdr[0xC:0x10] = array('B', pack('<I', size))    # Write in number of bytes to be allocated in sound RAM
	if include:    # If the data itself is to be included in the MLT file...
		hdr[0x14:0x18] = array('B', pack('<I', size))    # Write in data size
	mlt_hdr[0x8:0xC] = array('B', pack('<I', unpack('<I', mlt_hdr[0x8:0xC])[0] + 1))    # Increment number of MLT header entries
	mlt_hdr += hdr    # Append header entry
	return mlt_hdr
#=====================================================================================================
	
#=====================================================================================================
# Apply offsets to MLT header
def apply_offsets(mlt_hdr, offsets, sizes, includes, fpw_size=DEFAULT_FPW_SIZE):
	if fpw_size:    # If we have allocated DSP work RAM...
		hdr_offset = 0x40
	else:           # Otherwise...
		hdr_offset = 0x20
	hdr_size = len(mlt_hdr)
	void_size = 0    # Void size is the number of bytes allocated in sound RAM that are not included in the MLT file
	for offset, size, include in zip(offsets, sizes, includes):    # Loop through data allocations
		#print 'DEBUG: offset 0x%06X size 0x%06X include %s' % (offset, size, include) 
		mlt_hdr[hdr_offset+0x8:hdr_offset+0xC] = array('B', pack('<I', offset + DRIVER_SIZE + fpw_size + void_size))    # Write sound RAM offset
		if include:   # If we are including the data in the MLT file...
			mlt_hdr[hdr_offset+0x10:hdr_offset+0x14] = array('B', pack('<I', offset + hdr_size))    # Write offset into MLT file
		else:    # Otherwise...
			void_size += size    # Update the void size
		hdr_offset += 0x20
	return mlt_hdr
#=====================================================================================================

#=====================================================================================================
# Read in data and zero-fill to 32-bit boundary
def read_data(filename):
	data = open(filename, 'rb').read()
	size = len(data)
	if size % 4:    # If size not multiple of 4
		data += '\x00' * (4 - (size % 4))    # Zero-fill to the next 32-bit boundary
	data = array('B', data)
	return data
#=====================================================================================================

#=====================================================================================================
# Get data and related information, deal with input syntax
def get_data(filename, banks):
	if filename.find(':::') >= 0:    # Check first if not including in MLT file
		include = False
		filename, bank = filename.split(':::')
		data = read_data(filename)
		hdr_str = data[0x0:0x4].tostring()    # Get header-string from data
		if bank:
			bank = int(bank)
		else:
			bank = banks[hdr_str]    # If bank not specified, use the next available bank value
	elif filename.find('::') >= 0:    # Check second if including in MLT file
		include = True
		filename, bank = filename.split('::')
		data = read_data(filename)
		hdr_str = data[0x0:0x4].tostring()
		if bank:
			bank = int(bank)
		else:
			bank = banks[hdr_str]    # If bank not specified, use the next available bank value
	else:    # Default case, include in MLT file, use next available bank
		include = True
		data = read_data(filename)
		hdr_str = data[0x0:0x4].tostring()
		bank = banks[hdr_str]    # Use next available bank value
	size = len(data)
	#print 'DEBUG: hdr_str %s bank %d size 0x%06X include %s' % (hdr_str, bank, size, str(include))
	return (data, size, hdr_str, bank, include)
#=====================================================================================================

#=====================================================================================================
# Create MLT file from input files
def mltmake(mlt_filename, filenames, fpw_size=DEFAULT_FPW_SIZE):
	mlt_hdr = init_hdr(fpw_size)    # Initialize MLT header, taking into account DSP work RAM size
	mlt_data = array('B', '')
	offset = 0
	offsets = []; sizes = []; includes = []
	banks = {'SFPB': 0, 'SFOB': 0, 'SOSB': 0, 'SMPB': 0, 'SMSB': 0, 'SMDB': 0}    # Next available bank for each header string
	for filename in filenames:
		(data, size, hdr_str, bank, include) = get_data(filename, banks)    # Get sound data and other useful info
		if hdr_str == 'SFPB':    # If data is a DSP program bank...
			check_fpw_ram(mlt_filename, fpw_size, data)    # Check DSP work RAM allocation
		mlt_hdr = append_hdr(mlt_hdr, data, hdr_str, bank, include)    # Append header entry to MLT header
		# Update offsets, sizes, include list
		offsets.append(offset)
		sizes.append(size)
		includes.append(include)
		if include:    # If we are including the data in the MLT file...
			mlt_data += data    # Append to total MLT data
			offset += len(data)    # Update the MLT file offset
		banks[hdr_str] = bank + 1    # Set next available bank
	mlt_hdr = apply_offsets(mlt_hdr, offsets, sizes, includes, fpw_size)    # Update sound RAM and MLT file offsets in MLT header
	check_mlt_alloc(mlt_filename, mlt_hdr)    # Check total MLT data allocation
	open(mlt_filename, 'wb').write(mlt_hdr + mlt_data)    # Write out MLT file
	return
#=====================================================================================================

#=====================================================================================================
# mltmake script driver
if __name__ == '__main__':
	argv = sys.argv
	argc = len(argv)
	if argc < 2:
		print 'MLT creator v0.01 by kingshriek'
		print 'Usage: python %s [options] <mlt file> <sound files[::[:]bank] ...>' % os.path.basename(argv[0])
		print ''
		print 'Options:'
		print '    -d <n>               Set DSP work RAM size. If not specified, the MLT will be created with'
		print '                         0x20040 bytes allocated for DSP work RAM, the maximum amount. Thus, you'
		print '                         normally do not have to worry about this. You will only need to consider'
		print '                         this if the script detects and warns you about an MLT data overflow.'
		print '                         Here, <n> is an index (0-4) that gives the following sizes:'
		print '                             Index                   DSP work RAM size'
		print '                             ----------------------------------------------'
		print '                             0                       0x4040 bytes'
		print '                             1                       0x8040 bytes'
		print '                             2                       0x10040 bytes'
		print '                             3                       0x20040 bytes'
		print '                             4                       0 bytes (no DSP work RAM allocated)'
		print ''
		print 'Sound file syntax (examples):'
		print '    EXAMPLE.MSB          Use the next available bank, write both header and data.'
		print '    EXAMPLE.MSB::        Same as above.'
		print '    EXAMPLE.MSB::2       Use bank 2, write both header and data.'
		print '    EXAMPLE.MSB:::       Use the next available bank, write header only.'
		print '    EXAMPLE.MSB:::2      Use bank 2, write header only.'
		print ''
		sys.exit(0)
	fpw_size = DEFAULT_FPW_SIZE    
	filenames = []
	nfiles = 0
	# Parse command-line arguments
	flags = {'d': 0}
	for arg in argv[1:]:
		if flags['d']:
			fpw_size = fpw_size_table[int(arg)]
			flags['d'] = 0
		elif arg[0] == '-':
			if arg[1:] == 'd':
				flags['d'] = 1
		else:
			if nfiles:
				filenames.append(arg)
			else:
				mlt_filename = arg    # If first file in argument list, set as MLT file
			nfiles += 1
	mltmake(mlt_filename, filenames, fpw_size)
#=====================================================================================================

#=====================================================================================================
# 08-10-14 (v0.01) - Changed the input file syntax, as the previous one interfered with full pathnames
#                    on Windows. Added capability to not include sound data in MLT files but still 
#                    populate the header (can be useful for dsflib/minidsf creation). Made the DSP work 
#                    RAM size user-settable. Added lots of comments.
# 08-10-12 (v0.00) - Initial release.
#=====================================================================================================