#!/usr/bin/env python """ it2fss.py, version 0.4.1 ---------------------- Python 3 only. This script converts single-channel Impulse Tracker module into a FSS text file for use with fsound.exe. Tempo Effect command is supported. Kick and Snare Samples are supported! To make white noise, use note value C-0 or everything else except C-1 to B-9. Kick Sample is triggered with F-1 and Snare with F-9. Make sure to use the Volume Column for every note. Volumes 0 to 15 are mapped to 0 to F and everything above 15 is also mapped to F. White Noise and Square Wave need to have a specific volume. That means, if you leave the column empty for notes in this octaves, they are converted to "None" and might cause errors. Kick and Snare Samples don't have volume control, so feel free to leave the volume column empty on that rows, it won't have any effects. The only supported effect is Txx. Every other effect will be ignored! Tempo Slides are not supported, all values <20 will just be interpreted as BPM<32. Only use the effect along with a note, otherwise it will be ignored. Version history --------------- * 0.4.1: Fixed a bug where consecutive drum notes would be combined into one * 0.4: Added Tempo Change Support * 0.3: Added Sample and Volume Support * 0.2: Fixed a bug that occurred when translating a single IT note into multiple FSS notes. * 0.1: Initial creation of program. """ from sys import argv, stderr, version_info def die(msg): if isinstance(msg, BaseException): msg = str(msg) stderr.write(str(msg) + '\n') exit(1) if version_info.major < 3: die('python3 only!') from collections import namedtuple from math import floor, log2 from struct import unpack_from if len(argv) == 2: MODULE = argv[1] else: die('Usage: {} MODULE'.format(argv[0])) Module = namedtuple('Module', ('speed', 'tempo', 'orders', 'patterns', 'patternsVol', 'patternsCmdVal')) NOTE_NAMES = ['a', 'A', 'b', 'c', 'C', 'd', 'D', 'e', 'f', 'F', 'g', 'G'] VALUE_NAMES = ['f', '8', '4', '2', '1'] def note_format(note, rows, vol, cmdVal, tempo): if vol != None: if vol == 10: vol = "a" elif vol == 11: vol = "b" elif vol == 12: vol = "c" elif vol == 13: vol = "d" elif vol == 14: vol = "e" elif vol >= 15: vol = "f" lengths = [] while rows > 0: power = min(4, floor(log2(rows))) lengths.append(VALUE_NAMES[power]) rows -= 2 ** power strings = [] for length in lengths: if cmdVal != None: temp = '{}\n\n'.format(2500 // tempo * cmdVal) strings.append('t' + temp) if note is not None: if note < 120: fs_note = note - 9 octave = fs_note // 12 if 1 <= octave <= 7: name = NOTE_NAMES[fs_note % 12] strings.append(name + str(octave) + length + str(vol)) elif octave == 0: strings.append('K-' + length) elif octave == 8: strings.append('S-' + length) else: strings.append('x-' + length + str(vol)) else: strings.append('r-' + length) else: strings.append('r-' + length) if strings: return '\n'.join(strings) + '\n' return '' def read_orders(data): ordnum = unpack_from('H', data, 0x20)[0] return unpack_from('B' * ordnum, data, 0xC0) def pattern_offsets(data): ordnum, insnum, smpnum, patnum = unpack_from('HHHH', data, 0x20) offset = 0xC0 + ordnum + insnum * 4 + smpnum * 4 return unpack_from('I' * patnum, data, offset) def read_pattern(data, offset): _, rows = unpack_from('HH', data, offset) offset += 8 prev_maskvar, prev_note, prev_ins = ([0] * 64 for i in range(3)) prev_vol, prev_cmd, prev_cmdval = ([0] * 64 for i in range(3)) items = [[None for y in range(rows)] for x in range(4)] itemsVol = [[None for y in range(rows)] for x in range(4)] itemsCmdVal = [[None for y in range(rows)] for x in range(4)] for row in range(rows): while True: channelvariable = unpack_from('B', data, offset)[0] offset += 1 if channelvariable == 0: break # end of row channel = (channelvariable - 1) & 63 if channelvariable & 128: maskvar = unpack_from('B', data, offset)[0] offset += 1 else: maskvar = prev_maskvar[channel] prev_maskvar[channel] = maskvar if maskvar & 1: note = unpack_from('B', data, offset)[0] prev_note[channel] = note offset += 1 else: note = None if maskvar & 2: ins = unpack_from('B', data, offset)[0] prev_ins[channel] = ins offset += 1 else: ins = None if maskvar & 4: vol = unpack_from('B', data, offset)[0] prev_vol[channel] = vol offset += 1 else: vol = None if maskvar & 8: cmd, cmdval = unpack_from('BB', data, offset) prev_cmd[channel], prev_cmdval[channel] = cmd, cmdval offset += 2 else: cmd, cmdval = None, None if maskvar & 16: note = prev_note[channel] if maskvar & 32: ins = prev_ins[channel] if maskvar & 64: vol = prev_vol[channel] if maskvar & 128: cmd = prev_cmd[channel] cmdval = prev_cmdval[channel] if channel < 4: items[channel][row] = note itemsVol[channel][row] = vol if cmd == 20 or cmd == 1: itemsCmdVal[channel][row] = cmdval else: itemsCmdVal[channel][row] = None return items, itemsVol, itemsCmdVal def read_patterns(data): offsets = pattern_offsets(data) patterns = [] patternsVol = [] patternsCmdVal = [] for offset in offsets: pattern, patternVol, patternCmdVal = read_pattern(data, offset) patterns.append(pattern) patternsVol.append(patternVol) patternsCmdVal.append(patternCmdVal) return tuple(patterns), tuple(patternsVol), tuple(patternsCmdVal) def read_module(filename): try: with open(filename, 'rb') as f: data = f.read() except BaseException as ex: die(ex) if data[:4].decode('ascii') != 'IMPM': die("Invalid IT module: '{}'".format(filename)) speed, tempo = unpack_from('BB', data, 0x32) orders = read_orders(data) patterns, patternsVol, patternsCmdVal = read_patterns(data) return Module(speed, tempo, orders, patterns, patternsVol, patternsCmdVal) def convert(module, filename): try: outfile = open(filename, 'w') except BaseException as ex: die(ex) outfile.write('{}\n\n'.format(2500 // module.tempo * module.speed)) outfile.write('> generated by it2fss.py Ver 0.4.1\n\n') item = 255 length = 0 vol = 0 cmdVal = module.tempo for order in (x for x in module.orders if x != 255): pattern = module.patterns[order] patternVol = module.patternsVol[order] patternCmdVal = module.patternsCmdVal[order] outfile.write('> pattern {}\n'.format(order)) for row in range(len(pattern[0])): cur_item = pattern[0][row] cur_vol = patternVol[0][row] cur_cmdVal = patternCmdVal[0][row] if cur_item is not None and (cur_item != item or cur_vol != vol or cur_item <= 17 or cur_item >= 113): outfile.write(note_format(item, length, vol, cmdVal, module.tempo)) length = 0 item = cur_item vol = cur_vol cmdVal = cur_cmdVal length += 1 outfile.write('\n') if item: outfile.write(note_format(item, length, vol, cmdVal, module.tempo)) outfile.close() module = read_module(MODULE) convert(module, MODULE + '.fss')