diff --git a/bootstrap/bootstrap b/bootstrap/bootstrap index 468270b0..f7902ff6 100755 --- a/bootstrap/bootstrap +++ b/bootstrap/bootstrap @@ -4,29 +4,36 @@ from __future__ import print_function import sys import struct + def read(): try: return sys.stdin.buffer.read() except AttributeError: return sys.stdin.read() + def write(out): try: sys.stdout.buffer.write(out) except AttributeError: sys.stdout.write(out) + def main(): # read PLAYPAL from stdin, write minimal doom2.wad to stdout if sys.stdin.isatty(): - print("Usage: %s < playpal.lmp > doom2.wad" % sys.argv[0], - file=sys.stderr) + print( + "Usage: %s < playpal.lmp > doom2.wad" % sys.argv[0], + file=sys.stderr, + ) sys.exit(1) # three lumps needed - see bootstrap/README.txt - lumps = [(b'PLAYPAL', read()), - (b'TEXTURE1', struct.pack("i", 0)), # empty texture1 - (b'PNAMES', struct.pack("i8s", 1, b''))] # single pname + lumps = [ + (b"PLAYPAL", read()), + (b"TEXTURE1", struct.pack("i", 0)), # empty texture1 + (b"PNAMES", struct.pack("i8s", 1, b"")), + ] # single pname # calculate wad directory (lump offsets etc.) pos = 12 @@ -36,7 +43,7 @@ def main(): pos += len(data) # write wad header - write(struct.pack("4sii", b'IWAD', len(waddir), pos)) + write(struct.pack("4sii", b"IWAD", len(waddir), pos)) # write lump contents for name, data in lumps: @@ -46,4 +53,6 @@ def main(): for i in waddir: write(struct.pack("ii8s", *i)) -if __name__ == "__main__": main() + +if __name__ == "__main__": + main() diff --git a/dist/pillow-compose b/dist/pillow-compose index 6ae25846..6e64b546 100755 --- a/dist/pillow-compose +++ b/dist/pillow-compose @@ -7,7 +7,7 @@ import sys from PIL import Image src = Image.open(sys.argv[1]) -img = Image.new('RGBA', (int(sys.argv[3]), int(sys.argv[4])), (0, 0, 0, 0)) +img = Image.new("RGBA", (int(sys.argv[3]), int(sys.argv[4])), (0, 0, 0, 0)) # Pillow's compositing won't accept negative values. This can happen # if the destination image is smaller on at least on axis than the @@ -22,5 +22,5 @@ if img.size[1] - src.size[1] < 0: else: off_y = (img.size[1] - src.size[1]) // 2 -img.alpha_composite(src.convert('RGBA'), (off_x, off_y)) +img.alpha_composite(src.convert("RGBA"), (off_x, off_y)) img.save(sys.argv[2]) diff --git a/graphics/text/config.py b/graphics/text/config.py index 58fabff5..8acdd5d0 100644 --- a/graphics/text/config.py +++ b/graphics/text/config.py @@ -14,237 +14,233 @@ import re # TODO: Add more rule for lower-case characters. FONT_KERNING_RULES = { - # Right character fits under left character: - r'[TY][07ACOSZ]': -2, - r'[TYty][a]': -2, - r'P[A]': -3, - r'P[7]': -2, - r'P[Z]': -1, - r'7[Z]': -1, - r'[0OQ]A': -1, - r'S[A]': -1, - r'V[0OC]': -2, - - # Left character fits under right character: - r'L[TY]': -4, - r'L[014COQV]': -3, - r'L[9]': -2, - r'[0O][4TY]': -2, - r'[0O][1]': -1, - r'Q[1T]': -2, - r'Q[Y]': -1, - r'A[TYV]': -2, - r'A[GC]': -1, - r'a[TYty]': -2, - r'a[vV]': -2, - r'a[g]': -1, - - # Fits into "hole" in left character: - r'[BCX8][0CGOQ]': -2, - r'Z[0CO]': -2, - r'Z[GQ]': -1, - r'I[0COQ]': -1, - r'K[0CO]': -4, - r'K[GQ]': -3, - r'K[E]': -1, - r'[PR][0COQ]': -1, - - # Fits into "hole" in right character: - r'[O0Q][X]': -3, - r'[O0Q][28]': -2, - r'[O0Q][9IK]': -1, - - # Just because. - r'[O0][O0]': -1, + # Right character fits under left character: + r"[TY][07ACOSZ]": -2, + r"[TYty][a]": -2, + r"P[A]": -3, + r"P[7]": -2, + r"P[Z]": -1, + r"7[Z]": -1, + r"[0OQ]A": -1, + r"S[A]": -1, + r"V[0OC]": -2, + # Left character fits under right character: + r"L[TY]": -4, + r"L[014COQV]": -3, + r"L[9]": -2, + r"[0O][4TY]": -2, + r"[0O][1]": -1, + r"Q[1T]": -2, + r"Q[Y]": -1, + r"A[TYV]": -2, + r"A[GC]": -1, + r"a[TYty]": -2, + r"a[vV]": -2, + r"a[g]": -1, + # Fits into "hole" in left character: + r"[BCX8][0CGOQ]": -2, + r"Z[0CO]": -2, + r"Z[GQ]": -1, + r"I[0COQ]": -1, + r"K[0CO]": -4, + r"K[GQ]": -3, + r"K[E]": -1, + r"[PR][0COQ]": -1, + # Fits into "hole" in right character: + r"[O0Q][X]": -3, + r"[O0Q][28]": -2, + r"[O0Q][9IK]": -1, + # Just because. + r"[O0][O0]": -1, } white_graphics = { - 'wibp1': 'P1', - 'wibp2': 'P2', - 'wibp3': 'P3', - 'wibp4': 'P4', - 'wicolon': ':', - - # These files are for the title screens of Phase 1 and Phase 2 - 't_phase1': 'PHASE 1', - 't_phase2': 'PHASE 2', - - # Note: level names are also included in this dictionary, with - # the data added programatically from the DEHACKED lump, see - # code below. + "wibp1": "P1", + "wibp2": "P2", + "wibp3": "P3", + "wibp4": "P4", + "wicolon": ":", + # These files are for the title screens of Phase 1 and Phase 2 + "t_phase1": "PHASE 1", + "t_phase2": "PHASE 2", + # Note: level names are also included in this dictionary, with + # the data added programatically from the DEHACKED lump, see + # code below. } blue_graphics = { - 'm_disopt': 'DISPLAY OPTIONS', - 'm_episod': 'Choose Chapter:', - 'm_optttl': 'OPTIONS', - 'm_skill': 'Choose Skill Level:', + "m_disopt": "DISPLAY OPTIONS", + "m_episod": "Choose Chapter:", + "m_optttl": "OPTIONS", + "m_skill": "Choose Skill Level:", } red_graphics = { - # Title for the HELP/HELP1 screen: - 'helpttl': 'Help', - # Title for CREDIT - 'freettl': 'Freedoom', - - 'm_ngame': 'New Game', - 'm_option': 'Options', - 'm_loadg': 'Load Game', - 'm_saveg': 'Save Game', - 'm_rdthis': 'Read This!', - 'm_quitg': 'Quit Game', - - 'm_newg': 'NEW GAME', - 'm_epi1': 'Outpost Outbreak', - 'm_epi2': 'Military Labs', - 'm_epi3': 'Event Horizon', - 'm_epi4': 'Double Impact', - - 'm_jkill': 'Please don\'t kill me!', - 'm_rough': 'Will this hurt?', - 'm_hurt': 'Bring on the pain.', - 'm_ultra': 'Extreme Carnage.', - 'm_nmare': 'MAYHEM!', - - 'm_lgttl': 'LOAD GAME', - 'm_sgttl': 'SAVE GAME', - - 'm_endgam': 'End Game', - 'm_messg': 'Messages:', - 'm_msgoff': 'off', - 'm_msgon': 'on', - 'm_msens': 'Mouse Sensitivity', - 'm_detail': 'Graphic Detail:', - 'm_gdhigh': 'high', - 'm_gdlow': 'low', - 'm_scrnsz': 'Screen Size', - - 'm_svol': 'Sound Volume', - 'm_sfxvol': 'Sfx Volume', - 'm_musvol': 'Music Volume', - - 'm_disp': 'Display', - - 'wif': 'finished', - 'wiostk': 'kills', - 'wiosti': 'items', - 'wiscrt2': 'secret', - 'wiosts': 'scrt', - 'wifrgs': 'frgs', - - 'witime': 'Time:', - 'wisucks': 'sucks', - 'wimstt': 'Total:', - 'wipar': 'Par:', - 'wip1': 'P1', 'wip2': 'P2', 'wip3': 'P3', 'wip4': 'P4', - 'wiostf': 'f.', - 'wimstar': 'you', - 'winum0': '0', 'winum1': '1', 'winum2': '2', 'winum3': '3', - 'winum4': '4', 'winum5': '5', 'winum6': '6', 'winum7': '7', - 'winum8': '8', 'winum9': '9', - 'wipcnt': '%', - 'wiminus': '-', - 'wienter': 'ENTERING', - - 'm_pause': 'pause', - - # Extra graphics used in PrBoom's menus. Generate these as well - # so that when we play in PrBoom the menus look consistent. - 'prboom': 'PrBoom', - 'm_generl': 'General', - 'm_setup': 'Setup', - 'm_keybnd': 'Key Bindings', - 'm_weap': 'Weapons', - 'm_stat': 'Status Bar/HUD', - 'm_auto': 'Automap', - 'm_enem': 'Enemies', - 'm_mess': 'Messages', - 'm_chat': 'Chat Strings', - - 'm_horsen': 'horizontal', - 'm_versen': 'vertical', - 'm_loksen': 'mouse look', - 'm_accel': 'acceleration', - - # Extra graphics from SMMU/Eternity Engine: - 'm_about': 'about', - 'm_chatm': 'Chat Strings', - 'm_compat': 'Compatibility', - 'm_demos': 'demos', - 'm_dmflag': 'deathmatch flags', - 'm_etcopt': 'eternity options', - 'm_feat': 'Features', - 'm_gset': 'game settings', - 'm_hud': 'heads up display', - 'm_joyset': 'joysticks', - 'm_ldsv': 'Load/Save', - 'm_menus': 'Menu Options', - 'm_mouse': 'mouse options', - 'm_player': 'player setup', - 'm_serial': 'serial connection', - 'm_sound': 'sound options', - 'm_status': 'status bar', - 'm_tcpip': 'tcp/ip connection', - 'm_video': 'video options', - 'm_wad': 'load wad', - 'm_wadopt': 'wad options', - # This is from SMMU too, and if we follow things to the letter, - # ought to be all lower-case. However, same lump name is used - # by other ports (Zandronum) which expect a taller graphic to - # match the other main menu graphics. Eternity Engine doesn't - # use it any more, and on SMMU there's enough space for it. - 'm_multi': 'Multiplayer', + # Title for the HELP/HELP1 screen: + "helpttl": "Help", + # Title for CREDIT + "freettl": "Freedoom", + "m_ngame": "New Game", + "m_option": "Options", + "m_loadg": "Load Game", + "m_saveg": "Save Game", + "m_rdthis": "Read This!", + "m_quitg": "Quit Game", + "m_newg": "NEW GAME", + "m_epi1": "Outpost Outbreak", + "m_epi2": "Military Labs", + "m_epi3": "Event Horizon", + "m_epi4": "Double Impact", + "m_jkill": "Please don't kill me!", + "m_rough": "Will this hurt?", + "m_hurt": "Bring on the pain.", + "m_ultra": "Extreme Carnage.", + "m_nmare": "MAYHEM!", + "m_lgttl": "LOAD GAME", + "m_sgttl": "SAVE GAME", + "m_endgam": "End Game", + "m_messg": "Messages:", + "m_msgoff": "off", + "m_msgon": "on", + "m_msens": "Mouse Sensitivity", + "m_detail": "Graphic Detail:", + "m_gdhigh": "high", + "m_gdlow": "low", + "m_scrnsz": "Screen Size", + "m_svol": "Sound Volume", + "m_sfxvol": "Sfx Volume", + "m_musvol": "Music Volume", + "m_disp": "Display", + "wif": "finished", + "wiostk": "kills", + "wiosti": "items", + "wiscrt2": "secret", + "wiosts": "scrt", + "wifrgs": "frgs", + "witime": "Time:", + "wisucks": "sucks", + "wimstt": "Total:", + "wipar": "Par:", + "wip1": "P1", + "wip2": "P2", + "wip3": "P3", + "wip4": "P4", + "wiostf": "f.", + "wimstar": "you", + "winum0": "0", + "winum1": "1", + "winum2": "2", + "winum3": "3", + "winum4": "4", + "winum5": "5", + "winum6": "6", + "winum7": "7", + "winum8": "8", + "winum9": "9", + "wipcnt": "%", + "wiminus": "-", + "wienter": "ENTERING", + "m_pause": "pause", + # Extra graphics used in PrBoom's menus. Generate these as well + # so that when we play in PrBoom the menus look consistent. + "prboom": "PrBoom", + "m_generl": "General", + "m_setup": "Setup", + "m_keybnd": "Key Bindings", + "m_weap": "Weapons", + "m_stat": "Status Bar/HUD", + "m_auto": "Automap", + "m_enem": "Enemies", + "m_mess": "Messages", + "m_chat": "Chat Strings", + "m_horsen": "horizontal", + "m_versen": "vertical", + "m_loksen": "mouse look", + "m_accel": "acceleration", + # Extra graphics from SMMU/Eternity Engine: + "m_about": "about", + "m_chatm": "Chat Strings", + "m_compat": "Compatibility", + "m_demos": "demos", + "m_dmflag": "deathmatch flags", + "m_etcopt": "eternity options", + "m_feat": "Features", + "m_gset": "game settings", + "m_hud": "heads up display", + "m_joyset": "joysticks", + "m_ldsv": "Load/Save", + "m_menus": "Menu Options", + "m_mouse": "mouse options", + "m_player": "player setup", + "m_serial": "serial connection", + "m_sound": "sound options", + "m_status": "status bar", + "m_tcpip": "tcp/ip connection", + "m_video": "video options", + "m_wad": "load wad", + "m_wadopt": "wad options", + # This is from SMMU too, and if we follow things to the letter, + # ought to be all lower-case. However, same lump name is used + # by other ports (Zandronum) which expect a taller graphic to + # match the other main menu graphics. Eternity Engine doesn't + # use it any more, and on SMMU there's enough space for it. + "m_multi": "Multiplayer", } -def read_bex_lump(filename): - """Read the BEX (Dehacked) lump from the given filename. - Returns: - Dictionary mapping from name to value. - """ - result = {} - with open(filename) as f: - for line in f: - # Ignore comments: - line = line.strip() - if len(line) == 0 or line[0] in '#;': - continue - # Just split on '=' and interpret that as an - # assignment. This is primitive and doesn't read - # like a full BEX parser should, but it's good - # enough for our purposes here. - assign = line.split('=', 2) - if len(assign) != 2: - continue - result[assign[0].strip()] = assign[1].strip() - return result +def read_bex_lump(filename): + """Read the BEX (Dehacked) lump from the given filename. + + Returns: + Dictionary mapping from name to value. + """ + result = {} + with open(filename) as f: + for line in f: + # Ignore comments: + line = line.strip() + if len(line) == 0 or line[0] in "#;": + continue + # Just split on '=' and interpret that as an + # assignment. This is primitive and doesn't read + # like a full BEX parser should, but it's good + # enough for our purposes here. + assign = line.split("=", 2) + if len(assign) != 2: + continue + result[assign[0].strip()] = assign[1].strip() + return result + def update_level_name(lumpname, bexdata, bexname): - """Set the level name for the given graphic from BEX file. + """Set the level name for the given graphic from BEX file. - Args: - lumpname: Name of output graphic file. - bexdata: Dictionary of data read from BEX file. - bexname: Name of entry in BEX file to use. - """ - if bexname not in bexdata: - raise Exception('Level name %s not defined in ' - 'DEHACKED lump!' % bexname) - # Strip "MAP01: " or "E1M2: " etc. from start, if present: - levelname = re.sub('^\w*\d:\s*', '', bexdata[bexname]) - white_graphics[lumpname] = levelname + Args: + lumpname: Name of output graphic file. + bexdata: Dictionary of data read from BEX file. + bexname: Name of entry in BEX file to use. + """ + if bexname not in bexdata: + raise Exception( + "Level name %s not defined in " "DEHACKED lump!" % bexname + ) + # Strip "MAP01: " or "E1M2: " etc. from start, if present: + levelname = re.sub("^\w*\d:\s*", "", bexdata[bexname]) + white_graphics[lumpname] = levelname -freedoom_bex = read_bex_lump('../../lumps/p2_deh.lmp') -freedm_bex = read_bex_lump('../../lumps/fdm_deh.lmp') + +freedoom_bex = read_bex_lump("../../lumps/p2_deh.lmp") +freedm_bex = read_bex_lump("../../lumps/fdm_deh.lmp") for e in range(4): - for m in range(9): - # HUSTR_E1M1 from BEX => wilv00 - update_level_name('wilv%i%i' % (e, m), freedoom_bex, - 'HUSTR_E%iM%i' % (e + 1, m + 1)) + for m in range(9): + # HUSTR_E1M1 from BEX => wilv00 + update_level_name( + "wilv%i%i" % (e, m), freedoom_bex, "HUSTR_E%iM%i" % (e + 1, m + 1) + ) for m in range(32): - # HUSTR_1 => cwilv00 - update_level_name('cwilv%02i' % m, freedoom_bex, 'HUSTR_%i' % (m + 1)) - # HUSTR_1 => dmwilv00 (from freedm.bex) - update_level_name('dmwilv%02i' % m, freedm_bex, 'HUSTR_%i' % (m + 1)) + # HUSTR_1 => cwilv00 + update_level_name("cwilv%02i" % m, freedoom_bex, "HUSTR_%i" % (m + 1)) + # HUSTR_1 => dmwilv00 (from freedm.bex) + update_level_name("dmwilv%02i" % m, freedm_bex, "HUSTR_%i" % (m + 1)) diff --git a/graphics/text/create_caption b/graphics/text/create_caption index 25b07571..b363d24b 100755 --- a/graphics/text/create_caption +++ b/graphics/text/create_caption @@ -7,13 +7,13 @@ from PIL import Image, ImageFont, ImageDraw import sys import os -#create_caption.py +# create_caption.py font = ImageFont.load_default() -txt1= "© 2001-2019" -txt2= os.environ['VERSION'] +txt1 = "© 2001-2019" +txt2 = os.environ["VERSION"] background_image = Image.open(sys.argv[1]) background_image.load() background_image = background_image.convert("RGBA") @@ -21,17 +21,37 @@ image = Image.new("RGBA", background_image.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(image) txt1_size = draw.textsize(txt1, font=font) txt2_size = draw.textsize(txt2, font=font) -draw.text((5, int(image.height - txt1_size[1] - 5)), txt1, font=font, fill=(255,165,0,255)) -draw.text((int(image.width - txt2_size[0] - 10), int(image.height - txt2_size[1] - 5)), txt2, font=font, fill=(255,165,0,255)) + +draw.text( + (5, int(image.height - txt1_size[1] - 5)), + txt1, + font=font, + fill=(255, 165, 0, 255), +) +draw.text( + ( + int(image.width - txt2_size[0] - 10), + int(image.height - txt2_size[1] - 5), + ), + txt2, + font=font, + fill=(255, 165, 0, 255), +) if len(sys.argv) > 3: - #paste the other stuff onto the thing. + # paste the other stuff onto the thing. logo = Image.open(sys.argv[2]) logo.load() phase = Image.open(sys.argv[3]) phase.load - image.paste(logo, ((int(image.width/2) - int(logo.width/2), 18))) - image.paste(phase, ((int(image.width/2) - int(phase.width/2)), int(image.height - phase.height - 30))) + image.paste(logo, ((int(image.width / 2) - int(logo.width / 2), 18))) + image.paste( + phase, + ( + (int(image.width / 2) - int(phase.width / 2)), + int(image.height - phase.height - 30), + ), + ) outfile_name = sys.argv[4] else: outfile_name = sys.argv[2] diff --git a/graphics/text/image_dimensions.py b/graphics/text/image_dimensions.py index 4e9f78f7..8ce0a947 100755 --- a/graphics/text/image_dimensions.py +++ b/graphics/text/image_dimensions.py @@ -5,20 +5,21 @@ import re from PIL import Image + def get_image_dimensions(filename): - """Get image dimensions w x h + """Get image dimensions w x h Args: filename: filename of the image """ - with Image.open(filename) as img: - width, height = img.size - return (width, height) + with Image.open(filename) as img: + width, height = img.size + return (width, height) -if __name__ == '__main__': - import sys - x,y = get_image_dimensions(sys.argv[1]) - string = "%i %i" % (x, y) - sys.stdout.write(string) +if __name__ == "__main__": + import sys + x, y = get_image_dimensions(sys.argv[1]) + string = "%i %i" % (x, y) + sys.stdout.write(string) diff --git a/graphics/text/rotate b/graphics/text/rotate index 2b890bc5..4ab5f78a 100755 --- a/graphics/text/rotate +++ b/graphics/text/rotate @@ -23,5 +23,5 @@ else: img2 = img2.crop() if os.path.exists(sys.argv[3]): # delete any previous result file - os.remove(sys.argv[3]) + os.remove(sys.argv[3]) img2.save(sys.argv[3]) diff --git a/graphics/text/smtextgen b/graphics/text/smtextgen index 7d52abf9..3b305c29 100755 --- a/graphics/text/smtextgen +++ b/graphics/text/smtextgen @@ -12,143 +12,140 @@ import re from image_dimensions import * from tint import image_tint -DIMENSION_MATCH_RE = re.compile(r'(\d+)[x,](\d+)') +DIMENSION_MATCH_RE = re.compile(r"(\d+)[x,](\d+)") + class SmallTextGenerator(object): - def __init__(self): - self.get_font_widths() - # Width of a space character in pixels. - SPACE_WIDTH = 4 - # Height of the font. - FONT_HEIGHT = 8 - # Regexp to match dimensions/x,y coordinate pair. + def __init__(self): + self.get_font_widths() - def compile_kerning_table(self, kerning_table): - """Given a dictionary of kerning patterns, compile Regexps.""" + # Width of a space character in pixels. + SPACE_WIDTH = 4 + # Height of the font. + FONT_HEIGHT = 8 + # Regexp to match dimensions/x,y coordinate pair. - result = {} - for pattern, adjust in kerning_table.items(): - result[re.compile(pattern)] = adjust - return result + def compile_kerning_table(self, kerning_table): + """Given a dictionary of kerning patterns, compile Regexps.""" - def get_font_widths(self): - charfiles = glob('../stcfn*.png') - self.char_widths = {} - for c in range(128): - filename = self.char_filename(chr(c)) - if filename not in charfiles: - continue - w, _ = get_image_dimensions(filename) - self.char_widths[chr(c)] = w + result = {} + for pattern, adjust in kerning_table.items(): + result[re.compile(pattern)] = adjust + return result - def __contains__(self, c): - return c in self.char_widths + def get_font_widths(self): + charfiles = glob("../stcfn*.png") + self.char_widths = {} + for c in range(128): + filename = self.char_filename(chr(c)) + if filename not in charfiles: + continue + w, _ = get_image_dimensions(filename) + self.char_widths[chr(c)] = w - def char_width(self, c): - return self.char_widths[c] + def __contains__(self, c): + return c in self.char_widths - def char_filename(self, c): - return '../stcfn%03d.png' % (ord(c)) + def char_width(self, c): + return self.char_widths[c] - def draw_for_text(self, image, text, x, y): - text = text.upper() - new_image = image.copy() - x1, y1 = x, y + def char_filename(self, c): + return "../stcfn%03d.png" % (ord(c)) - for c in text: - if c == '\n': - y1 += self.FONT_HEIGHT - x1 = x - elif c == ' ': - x1 += self.SPACE_WIDTH + def draw_for_text(self, image, text, x, y): + text = text.upper() + new_image = image.copy() + x1, y1 = x, y - if c not in self: - continue + for c in text: + if c == "\n": + y1 += self.FONT_HEIGHT + x1 = x + elif c == " ": + x1 += self.SPACE_WIDTH - filename = self.char_filename(c) - char_image = Image.open(filename) - char_image.load() - new_image = self.paste_image(new_image, char_image, x1, y1) - x1 += self.char_width(c) - return new_image + if c not in self: + continue - def paste_image(self, image, src, x, y): - int_image = Image.new("RGBA", image.size, (0, 0, 0, 0)) - int_image.paste(src, (x, y)) - new_image = Image.alpha_composite(image, int_image) - return new_image + filename = self.char_filename(c) + char_image = Image.open(filename) + char_image.load() + new_image = self.paste_image(new_image, char_image, x1, y1) + x1 += self.char_width(c) + return new_image + + def paste_image(self, image, src, x, y): + int_image = Image.new("RGBA", image.size, (0, 0, 0, 0)) + int_image.paste(src, (x, y)) + new_image = Image.alpha_composite(image, int_image) + return new_image def parse_command_line(args): - if len(args) < 4 or (len(args) % 2) != 0: - return None + if len(args) < 4 or (len(args) % 2) != 0: + return None - result = { - 'filename': args[0], - 'background': None, - 'strings': [], - } + result = {"filename": args[0], "background": None, "strings": []} - m = DIMENSION_MATCH_RE.match(args[1]) - if not m: - return None - result['dimensions'] = (int(m.group(1)), int(m.group(2))) + m = DIMENSION_MATCH_RE.match(args[1]) + if not m: + return None + result["dimensions"] = (int(m.group(1)), int(m.group(2))) - i = 2 - while i < len(args): - if args[i] == '-background': - result['background'] = args[i+1] - i += 2 - continue + i = 2 + while i < len(args): + if args[i] == "-background": + result["background"] = args[i + 1] + i += 2 + continue - m = DIMENSION_MATCH_RE.match(args[i]) - if not m: - return None + m = DIMENSION_MATCH_RE.match(args[i]) + if not m: + return None - xy = (int(m.group(1)), int(m.group(2))) + xy = (int(m.group(1)), int(m.group(2))) - result['strings'].append((xy, args[i + 1])) - i += 2 + result["strings"].append((xy, args[i + 1])) + i += 2 - return result + return result -if __name__ == '__main__': +if __name__ == "__main__": - args = parse_command_line(sys.argv[1:]) + args = parse_command_line(sys.argv[1:]) - if not args: - print("Usage: smtextgen [...text commands...]") - print("Where each text command looks like:") - print(" [x,y] [text]") - sys.exit(0) + if not args: + print("Usage: smtextgen [...text commands...]") + print("Where each text command looks like:") + print(" [x,y] [text]") + sys.exit(0) - smallfont = SmallTextGenerator() + smallfont = SmallTextGenerator() - if args['background'] is not None: - background_image = Image.open(args['background']) - background_image.load() - background_image = background_image.convert("RGBA") + if args["background"] is not None: + background_image = Image.open(args["background"]) + background_image.load() + background_image = background_image.convert("RGBA") - image = Image.new("RGBA", args['dimensions'],(0,0,0,0)) + image = Image.new("RGBA", args["dimensions"], (0, 0, 0, 0)) - for xy, string in args['strings']: - # Allow contents of a file to be included with special prefix: - if string.startswith('include:'): - with open(string[8:]) as f: - string = f.read() + for xy, string in args["strings"]: + # Allow contents of a file to be included with special prefix: + if string.startswith("include:"): + with open(string[8:]) as f: + string = f.read() - # Allow special notation to indicate an image file to just draw - # rather than rendering a string. - if string.startswith('file:'): - src_image = Image.open(string[5:]) - src_image.load() - image = smallfont.paste_image(image, src_image, xy[0], xy[1]) - else: - image = smallfont.draw_for_text(image, string, xy[0], xy[1]) + # Allow special notation to indicate an image file to just draw + # rather than rendering a string. + if string.startswith("file:"): + src_image = Image.open(string[5:]) + src_image.load() + image = smallfont.paste_image(image, src_image, xy[0], xy[1]) + else: + image = smallfont.draw_for_text(image, string, xy[0], xy[1]) - if args['background'] is not None: - image = Image.alpha_composite(background_image, image) - - image.save(args['filename']) + if args["background"] is not None: + image = Image.alpha_composite(background_image, image) + image.save(args["filename"]) diff --git a/graphics/text/textgen b/graphics/text/textgen index 99e77d7b..fdb9985a 100755 --- a/graphics/text/textgen +++ b/graphics/text/textgen @@ -15,152 +15,152 @@ from image_dimensions import * from tint import image_tint - class TextGenerator(object): - def __init__(self, fontdir, kerning_table={}): - self.fontdir = fontdir - self.kerning_table = self.compile_kerning_table(kerning_table) - self.get_font_widths() + def __init__(self, fontdir, kerning_table={}): + self.fontdir = fontdir + self.kerning_table = self.compile_kerning_table(kerning_table) + self.get_font_widths() - # Tinting parameters for colorizing text: - COLOR_BLUE = "#000001" - COLOR_RED = "#010000" - COLOR_WHITE = None + # Tinting parameters for colorizing text: + COLOR_BLUE = "#000001" + COLOR_RED = "#010000" + COLOR_WHITE = None - # Height of font in pixels. - FONT_HEIGHT = 15 - FONT_LC_HEIGHT = 15 # 12 + # Height of font in pixels. + FONT_HEIGHT = 15 + FONT_LC_HEIGHT = 15 # 12 - # If true, the font only has uppercase characters. - UPPERCASE_FONT = False + # If true, the font only has uppercase characters. + UPPERCASE_FONT = False - # Width of a space character in pixels. - SPACE_WIDTH = 7 - LOWERCASE_RE = re.compile(r'^[a-z\!\. ]*$') + # Width of a space character in pixels. + SPACE_WIDTH = 7 + LOWERCASE_RE = re.compile(r"^[a-z\!\. ]*$") - def compile_kerning_table(self, kerning_table): - """Given a dictionary of kerning patterns, compile Regexps.""" + def compile_kerning_table(self, kerning_table): + """Given a dictionary of kerning patterns, compile Regexps.""" - result = {} - for pattern, adjust in kerning_table.items(): - result[re.compile(pattern)] = adjust - return result + result = {} + for pattern, adjust in kerning_table.items(): + result[re.compile(pattern)] = adjust + return result - def get_font_widths(self): - charfiles = glob('%s/font*.png' % self.fontdir) - self.char_widths = {} - for c in range(128): - filename = self.char_filename(chr(c)) - if filename not in charfiles: - continue - w, _ = get_image_dimensions(filename) - self.char_widths[chr(c)] = w + def get_font_widths(self): + charfiles = glob("%s/font*.png" % self.fontdir) + self.char_widths = {} + for c in range(128): + filename = self.char_filename(chr(c)) + if filename not in charfiles: + continue + w, _ = get_image_dimensions(filename) + self.char_widths[chr(c)] = w - def __contains__(self, c): - return c in self.char_widths + def __contains__(self, c): + return c in self.char_widths - def char_width(self, c): - return self.char_widths[c] + def char_width(self, c): + return self.char_widths[c] - def char_filename(self, c): - return '%s/font%03d.png' % (self.fontdir, ord(c)) + def char_filename(self, c): + return "%s/font%03d.png" % (self.fontdir, ord(c)) - def kerning_adjust(self, char_1, char_2): - """Get kerning adjustment for pair of characters. + def kerning_adjust(self, char_1, char_2): + """Get kerning adjustment for pair of characters. - Zero means no adjustment. A negative value adjusts to the - left and a positive value adjusts to the right. - """ - for pattern, adjust in self.kerning_table.items(): - if pattern.match(char_1 + char_2): - return adjust - else: - return 0 + Zero means no adjustment. A negative value adjusts to the + left and a positive value adjusts to the right. + """ + for pattern, adjust in self.kerning_table.items(): + if pattern.match(char_1 + char_2): + return adjust + else: + return 0 - def iterate_char_positions(self, text): - """Iterate over characters in string, yielding character with - position it should be placed at in the output file. - """ - x = 0 - last_c = ' ' - for c in text: - if c == ' ': - x += self.SPACE_WIDTH + def iterate_char_positions(self, text): + """Iterate over characters in string, yielding character with + position it should be placed at in the output file. + """ + x = 0 + last_c = " " + for c in text: + if c == " ": + x += self.SPACE_WIDTH - if c in self: - x += self.kerning_adjust(last_c, c) + if c in self: + x += self.kerning_adjust(last_c, c) - yield c, x + yield c, x - # Characters overlap by one pixel. - x += self.char_width(c) - 1 + # Characters overlap by one pixel. + x += self.char_width(c) - 1 - last_c = c + last_c = c - # We need to add back the missing pixel from the right side - # of the last char. - x += 1 - yield None, x + # We need to add back the missing pixel from the right side + # of the last char. + x += 1 + yield None, x - def text_width(self, text): - """Given a string of text, get text width in pixels.""" - for c, x in self.iterate_char_positions(text): - if c is None: - return x + def text_width(self, text): + """Given a string of text, get text width in pixels.""" + for c, x in self.iterate_char_positions(text): + if c is None: + return x - def generate_graphic(self, text, color=None): - """Get command to render text to a file - with the given background color. - """ + def generate_graphic(self, text, color=None): + """Get command to render text to a file + with the given background color. + """ - if self.UPPERCASE_FONT: - text = text.upper() - """Command line construction helper, used in render functions""" - width = self.text_width(text) + if self.UPPERCASE_FONT: + text = text.upper() + """Command line construction helper, used in render functions""" + width = self.text_width(text) - if self.LOWERCASE_RE.match(text): - height = self.FONT_LC_HEIGHT - else: - height = self.FONT_HEIGHT + if self.LOWERCASE_RE.match(text): + height = self.FONT_LC_HEIGHT + else: + height = self.FONT_HEIGHT - txt_image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) - for c, x in self.iterate_char_positions(text): - if c is None: - break + txt_image = Image.new("RGBA", (width, height), (0, 0, 0, 0)) + for c, x in self.iterate_char_positions(text): + if c is None: + break - filename = self.char_filename(c) - char_image = Image.open(filename) - char_image.load() - int_image = Image.new("RGBA", txt_image.size, (0, 0, 0, 0)) - int_image.paste(char_image, (x, height - self.FONT_HEIGHT)) - txt_image = Image.alpha_composite(txt_image, int_image) + filename = self.char_filename(c) + char_image = Image.open(filename) + char_image.load() + int_image = Image.new("RGBA", txt_image.size, (0, 0, 0, 0)) + int_image.paste(char_image, (x, height - self.FONT_HEIGHT)) + txt_image = Image.alpha_composite(txt_image, int_image) - txt_image = image_tint(txt_image, color) - return txt_image + txt_image = image_tint(txt_image, color) + return txt_image def generate_graphics(font, graphics, color=None): - for name, text in sorted(graphics.items()): - # write a makefile fragment - target = '%s.png' % name - image = font.generate_graphic(text, color=color) - image.save(target) + for name, text in sorted(graphics.items()): + # write a makefile fragment + target = "%s.png" % name + image = font.generate_graphic(text, color=color) + image.save(target) def generate_kerning_test(font): - pairs = [] - for c1 in sorted(font.char_widths): - char1 = "%c" % c1 - for c2 in sorted(font.char_widths): - char2 = "%c" % c2 - if font.kerning_adjust(char1, char2) != 0: - pairs.append(char1 + char2) + pairs = [] + for c1 in sorted(font.char_widths): + char1 = "%c" % c1 + for c2 in sorted(font.char_widths): + char2 = "%c" % c2 + if font.kerning_adjust(char1, char2) != 0: + pairs.append(char1 + char2) - cmd = font.generate_graphic(" ".join(pairs), "kerning.png") + cmd = font.generate_graphic(" ".join(pairs), "kerning.png") -if __name__ == '__main__': - font = TextGenerator('fontchars', kerning_table=FONT_KERNING_RULES) - generate_graphics(font, red_graphics, color=font.COLOR_RED) - generate_graphics(font, blue_graphics, color=font.COLOR_BLUE) - generate_graphics(font, white_graphics, color=font.COLOR_WHITE) +if __name__ == "__main__": + + font = TextGenerator("fontchars", kerning_table=FONT_KERNING_RULES) + generate_graphics(font, red_graphics, color=font.COLOR_RED) + generate_graphics(font, blue_graphics, color=font.COLOR_BLUE) + generate_graphics(font, white_graphics, color=font.COLOR_WHITE) diff --git a/graphics/text/tint.py b/graphics/text/tint.py index 72dec66e..5f4ee4aa 100755 --- a/graphics/text/tint.py +++ b/graphics/text/tint.py @@ -6,48 +6,54 @@ from PIL import Image, ImageColor, ImageOps + def image_tint(image, tint=None): - if tint is None: - return image - if image.mode not in ['RGB', 'RGBA']: - image = image.convert('RGBA') + if tint is None: + return image + if image.mode not in ["RGB", "RGBA"]: + image = image.convert("RGBA") - tr, tg, tb = ImageColor.getrgb(tint) - tl = ImageColor.getcolor(tint, "L") # tint color's overall luminosity - if not tl: - tl = 1 # avoid division by zero - tl = float(tl) # compute luminosity preserving tint factors - sr, sg, sb = map(lambda tv: tv / tl, (tr, tg, tb) - ) # per component adjustments + tr, tg, tb = ImageColor.getrgb(tint) + tl = ImageColor.getcolor(tint, "L") # tint color's overall luminosity + if not tl: + tl = 1 # avoid division by zero + tl = float(tl) # compute luminosity preserving tint factors + sr, sg, sb = map( + lambda tv: tv / tl, (tr, tg, tb) + ) # per component adjustments - # create look-up tables to map luminosity to adjusted tint - # (using floating-point math only to compute table) - luts = (tuple(map(lambda lr: int(lr * sr + 0.5), range(256))) + - tuple(map(lambda lg: int(lg * sg + 0.5), range(256))) + - tuple(map(lambda lb: int(lb * sb + 0.5), range(256)))) - l = ImageOps.grayscale(image) # 8-bit luminosity version of whole image - if Image.getmodebands(image.mode) < 4: - merge_args = (image.mode, (l, l, l)) # for RGB verion of grayscale - else: # include copy of image's alpha layer - a = Image.new("L", image.size) - a.putdata(image.getdata(3)) - merge_args = (image.mode, (l, l, l, a)) # for RGBA verion of grayscale - luts += tuple(range(256)) # for 1:1 mapping of copied alpha values + # create look-up tables to map luminosity to adjusted tint + # (using floating-point math only to compute table) + luts = ( + tuple(map(lambda lr: int(lr * sr + 0.5), range(256))) + + tuple(map(lambda lg: int(lg * sg + 0.5), range(256))) + + tuple(map(lambda lb: int(lb * sb + 0.5), range(256))) + ) + l = ImageOps.grayscale(image) # 8-bit luminosity version of whole image + if Image.getmodebands(image.mode) < 4: + merge_args = (image.mode, (l, l, l)) # for RGB verion of grayscale + else: # include copy of image's alpha layer + a = Image.new("L", image.size) + a.putdata(image.getdata(3)) + merge_args = (image.mode, (l, l, l, a)) # for RGBA verion of grayscale + luts += tuple(range(256)) # for 1:1 mapping of copied alpha values + + return Image.merge(*merge_args).point(luts) - return Image.merge(*merge_args).point(luts) def main(input_image_path, tintcolor, result_image_path): - image = Image.open(input_image_path) + image = Image.open(input_image_path) - image.load() + image.load() - result = image_tint(image, tintcolor) - if os.path.exists(result_image_path): # delete any previous result file - os.remove(result_image_path) - result.save(result_image_path) # file name's extension determines format + result = image_tint(image, tintcolor) + if os.path.exists(result_image_path): # delete any previous result file + os.remove(result_image_path) + result.save(result_image_path) # file name's extension determines format -if __name__ == '__main__': - import os - import sys - main(sys.argv[1], sys.argv[2], sys.argv[3]) +if __name__ == "__main__": + import os + import sys + + main(sys.argv[1], sys.argv[2], sys.argv[3]) diff --git a/lumps/colormap/colormap b/lumps/colormap/colormap index 8a46a733..3b57de49 100755 --- a/lumps/colormap/colormap +++ b/lumps/colormap/colormap @@ -38,164 +38,179 @@ tint_frac = 0 # brightness effects. tint_bright = 0.5 + def read_palette(filename): - """Read palette from file and return a list of tuples containing - RGB values.""" - f = open(filename, "rb") + """Read palette from file and return a list of tuples containing + RGB values.""" + f = open(filename, "rb") - colors = [] + colors = [] - for i in range(256): - data = f.read(3) + for i in range(256): + data = f.read(3) - color = struct.unpack("BBB", data) - colors.append(color) + color = struct.unpack("BBB", data) + colors.append(color) + + return colors - return colors # Return closest palette entry to the given RGB triple + def search_palette(palette, target): - """Search the given palette and find the nearest matching - color to the given color, returning an index into the - palette of the color that best matches.""" - best_diff = None - best_index = None + """Search the given palette and find the nearest matching + color to the given color, returning an index into the + palette of the color that best matches.""" + best_diff = None + best_index = None - def square(x): - return x * x + def square(x): + return x * x - for i in range(len(palette)): - color = palette[i] + for i in range(len(palette)): + color = palette[i] - diff = square(target[0] - color[0]) \ - + square(target[1] - color[1]) \ - + square(target[2] - color[2]) + diff = ( + square(target[0] - color[0]) + + square(target[1] - color[1]) + + square(target[2] - color[2]) + ) - if best_index is None or diff < best_diff: - best_diff = diff - best_index = i + if best_index is None or diff < best_diff: + best_diff = diff + best_index = i + + return best_index - return best_index def generate_colormap(colors, palette): - """Given a list of colors, translate these into indexes into - the given palette, finding the nearest color where an exact - match cannot be found.""" - result = [] + """Given a list of colors, translate these into indexes into + the given palette, finding the nearest color where an exact + match cannot be found.""" + result = [] - for color in colors: - index = search_palette(palette, color) - result.append(index) + for color in colors: + index = search_palette(palette, color) + result.append(index) + + return result - return result def tint_colors(colors, tint, bright=0.5): - """Given a list of colors, tint them a particular color.""" + """Given a list of colors, tint them a particular color.""" - result = [] - for c in colors: - # I've experimented with different methods of calculating - # intensity, but this seems to work the best. This is basically - # doing an average of the full channels, but a straight - # average causes the picture to get darker - eg. (0,0,255) - # maps to (87,87,87). So we have a controllable brightness - # factor that allows the brightness to be adjusted. - intensity = min((c[0] + c[1] + c[2]) * bright, 255) / 255.0 - result.append(( - tint[0] * intensity, - tint[1] * intensity, - tint[2] * intensity, - )) + result = [] + for c in colors: + # I've experimented with different methods of calculating + # intensity, but this seems to work the best. This is basically + # doing an average of the full channels, but a straight + # average causes the picture to get darker - eg. (0,0,255) + # maps to (87,87,87). So we have a controllable brightness + # factor that allows the brightness to be adjusted. + intensity = min((c[0] + c[1] + c[2]) * bright, 255) / 255.0 + result.append( + (tint[0] * intensity, tint[1] * intensity, tint[2] * intensity) + ) + + return result - return result def blend_colors(colors1, colors2, factor=0.5): - """Blend the two given lists of colors, with 'factor' controlling - the mix between the two. factor=0 is exactly colors1, while - factor=1 is exactly colors2. Returns a list of blended colors.""" - result = [] + """Blend the two given lists of colors, with 'factor' controlling + the mix between the two. factor=0 is exactly colors1, while + factor=1 is exactly colors2. Returns a list of blended colors.""" + result = [] - for index, c1 in enumerate(colors1): - c2 = colors2[index] + for index, c1 in enumerate(colors1): + c2 = colors2[index] - result.append(( - c2[0] * factor + c1[0] * (1 - factor), - c2[1] * factor + c1[1] * (1 - factor), - c2[2] * factor + c1[2] * (1 - factor), - )) + result.append( + ( + c2[0] * factor + c1[0] * (1 - factor), + c2[1] * factor + c1[1] * (1 - factor), + c2[2] * factor + c1[2] * (1 - factor), + ) + ) + + return result - return result def invert_colors(colors): - """Given a list of colors, translate them to inverted monochrome.""" - result = [] + """Given a list of colors, translate them to inverted monochrome.""" + result = [] - for color in colors: - # Formula comes from ITU-R recommendation BT.709 - gray = round(color[0]*0.2126 + color[1]*0.7152 + color[2]*0.0722) - inverse = 255 - gray + for color in colors: + # Formula comes from ITU-R recommendation BT.709 + gray = round(color[0] * 0.2126 + color[1] * 0.7152 + color[2] * 0.0722) + inverse = 255 - gray - result.append((inverse, inverse, inverse)) + result.append((inverse, inverse, inverse)) + + return result - return result def solid_color_list(color): - """Generate a 256-entry palette where all entries are the - same color.""" - return [color] * 256 + """Generate a 256-entry palette where all entries are the + same color.""" + return [color] * 256 + def output_colormap(colormap): - """Output the given palette to stdout.""" - for c in colormap: - x = struct.pack("B", c) - os.write(sys.stdout.fileno(), x) + """Output the given palette to stdout.""" + for c in colormap: + x = struct.pack("B", c) + os.write(sys.stdout.fileno(), x) + def print_palette(colors): - for y in range(16): - for x in range(16): - color = colors[y * 16 + x] + for y in range(16): + for x in range(16): + color = colors[y * 16 + x] - print("#%02x%02x%02x" % color) + print("#%02x%02x%02x" % color) + + print() - print() def parse_color_code(s): - """Parse a color code in HTML color code format, into an RGB - 3-tuple value.""" - if not s.startswith('#') or len(s) != 7: - raise Exception('Not in HTML color code form: %s' % s) - return (int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16)) + """Parse a color code in HTML color code format, into an RGB + 3-tuple value.""" + if not s.startswith("#") or len(s) != 7: + raise Exception("Not in HTML color code form: %s" % s) + return (int(s[1:3], 16), int(s[3:5], 16), int(s[5:7], 16)) + def set_parameter(name, value): - """Set configuration value, from command line parameters.""" - global dark_color, tint_color, tint_frac, tint_bright + """Set configuration value, from command line parameters.""" + global dark_color, tint_color, tint_frac, tint_bright + + if name == "dark_color": + dark_color = parse_color_code(value) + elif name == "tint_color": + tint_color = parse_color_code(value) + elif name == "tint_pct": + tint_frac = int(value) / 100.0 + elif name == "tint_bright": + tint_bright = float(value) + else: + raise Exception("Unknown parameter: '%s'" % name) - if name == 'dark_color': - dark_color = parse_color_code(value) - elif name == 'tint_color': - tint_color = parse_color_code(value) - elif name == 'tint_pct': - tint_frac = int(value) / 100.0 - elif name == 'tint_bright': - tint_bright = float(value) - else: - raise Exception("Unknown parameter: '%s'" % name) # Parse command line. playpal_filename = None for arg in sys.argv[1:]: - if arg.startswith('--') and '=' in arg: - key, val = arg[2:].split('=', 2) - set_parameter(key, val) - else: - playpal_filename = arg + if arg.startswith("--") and "=" in arg: + key, val = arg[2:].split("=", 2) + set_parameter(key, val) + else: + playpal_filename = arg if playpal_filename is None: - print("Usage: %s playpal.lmp > output-file.lmp" % sys.argv[0]) - sys.exit(1) + print("Usage: %s playpal.lmp > output-file.lmp" % sys.argv[0]) + sys.exit(1) palette = read_palette(playpal_filename) colors = palette @@ -205,18 +220,18 @@ colors = palette # applied. This allows us to darken to a different color than the tint # color, if so desired. if tint_frac > 0: - colors = blend_colors(palette, - tint_colors(colors, tint_color, tint_bright), - tint_frac) + colors = blend_colors( + palette, tint_colors(colors, tint_color, tint_bright), tint_frac + ) # Generate colormaps for different darkness levels, by blending between # the default colors and a palette where every entry is the "dark" color. dark = solid_color_list(dark_color) for i in range(32): - darken_factor = (32 - i) / 32.0 - darkened_colors = blend_colors(dark, colors, darken_factor) - output_colormap(generate_colormap(darkened_colors, palette)) + darken_factor = (32 - i) / 32.0 + darkened_colors = blend_colors(dark, colors, darken_factor) + output_colormap(generate_colormap(darkened_colors, palette)) # Inverse color map for invulnerability effect. inverse_colors = invert_colors(palette) @@ -227,4 +242,3 @@ output_colormap(generate_colormap(inverse_colors, palette)) # strictly unneeded, though some utilities (SLADE) do not detect a # lump as a COLORMAP unless it is the right length. output_colormap(generate_colormap(dark, palette)) - diff --git a/lumps/dmxgus/comparison.py b/lumps/dmxgus/comparison.py index f9ee2425..d182dc97 100644 --- a/lumps/dmxgus/comparison.py +++ b/lumps/dmxgus/comparison.py @@ -23,16 +23,18 @@ from config import * import midi -def instrument_num(instrument): - """Given a GUS patch name, get the MIDI instrument number. - For percussion instruments, the instrument number is offset - by 128. - """ - for key, name in GUS_INSTR_PATCHES.items(): - if name == instrument: - return key - raise Exception('Unknown instrument %s' % instrument) +def instrument_num(instrument): + """Given a GUS patch name, get the MIDI instrument number. + + For percussion instruments, the instrument number is offset + by 128. + """ + for key, name in GUS_INSTR_PATCHES.items(): + if name == instrument: + return key + raise Exception("Unknown instrument %s" % instrument) + pattern = midi.Pattern(resolution=48) track = midi.Track() @@ -40,62 +42,70 @@ track.append(midi.ControlChangeEvent(tick=0, channel=9, data=[7, 92])) time = 500 for group in SIMILAR_GROUPS: - # Don't bother with special effects. - #if group[0] == 'blank': - # continue + # Don't bother with special effects. + # if group[0] == 'blank': + # continue - print "Group: " - empty = True - for instrument in group: - inum = instrument_num(instrument) + print("Group: ") + empty = True + for instrument in group: + inum = instrument_num(instrument) - # For normal instruments, play a couple of different notes. - # For percussion instruments, play a couple of note on - # the percussion channel. - if inum <= 128: - print "\t%s (%i)" % (instrument, inum) - track.extend([ - midi.ProgramChangeEvent(tick=time, channel=0, - data=[inum]), - midi.NoteOnEvent(tick=0, channel=0, - velocity=92, pitch=midi.A_3), - midi.NoteOffEvent(tick=50, channel=0, - velocity=92, pitch=midi.A_3), - midi.NoteOnEvent(tick=0, channel=0, - velocity=92, pitch=midi.B_3), - midi.NoteOffEvent(tick=50, channel=0, - velocity=92, pitch=midi.B_3), - ]) - else: - print "\t%s (percussion %i)" % (instrument, inum-128) - track.extend([ - midi.NoteOnEvent(tick=time, channel=9, - velocity=92, pitch=inum-128), - midi.NoteOffEvent(tick=50, channel=9, - pitch=inum-128), - midi.NoteOnEvent(tick=0, channel=9, - velocity=92, pitch=inum-128), - midi.NoteOffEvent(tick=50, channel=9, - pitch=inum-128), - ]) + # For normal instruments, play a couple of different notes. + # For percussion instruments, play a couple of note on + # the percussion channel. + if inum <= 128: + print("\t%s (%i)" % (instrument, inum)) + track.extend( + [ + midi.ProgramChangeEvent(tick=time, channel=0, data=[inum]), + midi.NoteOnEvent( + tick=0, channel=0, velocity=92, pitch=midi.A_3 + ), + midi.NoteOffEvent( + tick=50, channel=0, velocity=92, pitch=midi.A_3 + ), + midi.NoteOnEvent( + tick=0, channel=0, velocity=92, pitch=midi.B_3 + ), + midi.NoteOffEvent( + tick=50, channel=0, velocity=92, pitch=midi.B_3 + ), + ] + ) + else: + print("\t%s (percussion %i)" % (instrument, inum - 128)) + track.extend( + [ + midi.NoteOnEvent( + tick=time, channel=9, velocity=92, pitch=inum - 128 + ), + midi.NoteOffEvent(tick=50, channel=9, pitch=inum - 128), + midi.NoteOnEvent( + tick=0, channel=9, velocity=92, pitch=inum - 128 + ), + midi.NoteOffEvent(tick=50, channel=9, pitch=inum - 128), + ] + ) - empty = False - time = 100 + empty = False + time = 100 - if not empty: - time = 500 + if not empty: + time = 500 # Four drumbeats indicate the end of the track and that the music is # going to loop. for i in range(4): - track.extend([ - midi.NoteOnEvent(tick=20, channel=9, velocity=92, pitch=35), - midi.NoteOffEvent(tick=20, channel=9, pitch=35), - ]) + track.extend( + [ + midi.NoteOnEvent(tick=20, channel=9, velocity=92, pitch=35), + midi.NoteOffEvent(tick=20, channel=9, pitch=35), + ] + ) track.append(midi.EndOfTrackEvent(tick=1)) pattern.append(track) # Save the pattern to disk midi.write_midifile("comparison.mid", pattern) - diff --git a/lumps/dmxgus/config.py b/lumps/dmxgus/config.py index 862379c1..0792b6c5 100644 --- a/lumps/dmxgus/config.py +++ b/lumps/dmxgus/config.py @@ -8,182 +8,182 @@ # that are loaded into the card. GUS_INSTR_PATCHES = { - 0: "acpiano", # #001 - Acoustic Grand Piano - 1: "britepno", # #002 - Bright Acoustic Piano - 2: "synpiano", # #003 - Electric Grand Piano - 3: "honky", # #004 - Honky-tonk Piano - 4: "epiano1", # #005 - Electric Piano 1 - 5: "epiano2", # #006 - Electric Piano 2 - 6: "hrpschrd", # #007 - Harpsichord - 7: "clavinet", # #008 - Clavi - 8: "celeste", # #009 - Celesta - 9: "glocken", # #010 - Glockenspiel - 10: "musicbox", # #011 - Music Box - 11: "vibes", # #012 - Vibraphone - 12: "marimba", # #013 - Marimba - 13: "xylophon", # #014 - Xylophone - 14: "tubebell", # #015 - Tubular Bells - 15: "santur", # #016 - Dulcimer - 16: "homeorg", # #017 - Drawbar Organ - 17: "percorg", # #018 - Percussive Organ - 18: "rockorg", # #019 - Rock Organ - 19: "church", # #020 - Church Organ - 20: "reedorg", # #021 - Reed Organ - 21: "accordn", # #022 - Accordion - 22: "harmonca", # #023 - Harmonica - 23: "concrtna", # #024 - Tango Accordion - 24: "nyguitar", # #025 - Acoustic Guitar (nylon) - 25: "acguitar", # #026 - Acoustic Guitar (steel) - 26: "jazzgtr", # #027 - Electric Guitar (jazz) - 27: "cleangtr", # #028 - Electric Guitar (clean) - 28: "mutegtr", # #029 - Electric Guitar (muted) - 29: "odguitar", # #030 - Overdriven Guitar - 30: "distgtr", # #031 - Distortion Guitar - 31: "gtrharm", # #032 - Guitar harmonics - 32: "acbass", # #033 - Acoustic Bass - 33: "fngrbass", # #034 - Electric Bass (finger) - 34: "pickbass", # #035 - Electric Bass (pick) - 35: "fretless", # #036 - Fretless Bass - 36: "slapbas1", # #037 - Slap Bass 1 - 37: "slapbas2", # #038 - Slap Bass 2 - 38: "synbass1", # #039 - Synth Bass 1 - 39: "synbass2", # #040 - Synth Bass 2 - 40: "violin", # #041 - Violin - 41: "viola", # #042 - Viola - 42: "cello", # #043 - Cello - 43: "contraba", # #044 - Contrabass - 44: "tremstr", # #045 - Tremolo Strings - 45: "pizzcato", # #046 - Pizzicato Strings - 46: "harp", # #047 - Orchestral Harp - 47: "timpani", # #048 - Timpani - 48: "marcato", # #049 - String Ensemble 1 - 49: "slowstr", # #050 - String Ensemble 2 - 50: "synstr1", # #051 - SynthStrings 1 - 51: "synstr2", # #052 - SynthStrings 2 - 52: "choir", # #053 - Choir Aahs - 53: "doo", # #054 - Voice Oohs - 54: "voices", # #055 - Synth Voice - 55: "orchhit", # #056 - Orchestra Hit - 56: "trumpet", # #057 - Trumpet - 57: "trombone", # #058 - Trombone - 58: "tuba", # #059 - Tuba - 59: "mutetrum", # #060 - Muted Trumpet - 60: "frenchrn", # #061 - French Horn - 61: "hitbrass", # #062 - Brass Section - 62: "synbras1", # #063 - SynthBrass 1 - 63: "synbras2", # #064 - SynthBrass 2 - 64: "sprnosax", # #065 - Soprano Sax - 65: "altosax", # #066 - Alto Sax - 66: "tenorsax", # #067 - Tenor Sax - 67: "barisax", # #068 - Baritone Sax - 68: "oboe", # #069 - Oboe - 69: "englhorn", # #070 - English Horn - 70: "bassoon", # #071 - Bassoon - 71: "clarinet", # #072 - Clarinet - 72: "piccolo", # #073 - Piccolo - 73: "flute", # #074 - Flute - 74: "recorder", # #075 - Recorder - 75: "woodflut", # #076 - Pan Flute - 76: "bottle", # #077 - Blown Bottle - 77: "shakazul", # #078 - Shakuhachi - 78: "whistle", # #079 - Whistle - 79: "ocarina", # #080 - Ocarina - 80: "sqrwave", # #081 - Lead 1 (square) - 81: "sawwave", # #082 - Lead 2 (sawtooth) - 82: "calliope", # #083 - Lead 3 (calliope) - 83: "chiflead", # #084 - Lead 4 (chiff) - 84: "charang", # #085 - Lead 5 (charang) - 85: "voxlead", # #086 - Lead 6 (voice) - 86: "lead5th", # #087 - Lead 7 (fifths) - 87: "basslead", # #088 - Lead 8 (bass + lead) - 88: "fantasia", # #089 - Pad 1 (new age) - 89: "warmpad", # #090 - Pad 2 (warm) - 90: "polysyn", # #091 - Pad 3 (polysynth) - 91: "ghostie", # #092 - Pad 4 (choir) - 92: "bowglass", # #093 - Pad 5 (bowed) - 93: "metalpad", # #094 - Pad 6 (metallic) - 94: "halopad", # #095 - Pad 7 (halo) - 95: "sweeper", # #096 - Pad 8 (sweep) - 96: "aurora", # #097 - FX 1 (rain) - 97: "soundtrk", # #098 - FX 2 (soundtrack) - 98: "crystal", # #099 - FX 3 (crystal) - 99: "atmosphr", # #100 - FX 4 (atmosphere) - 100: "freshair", # #101 - FX 5 (brightness) - 101: "unicorn", # #102 - FX 6 (goblins) - 102: "echovox", # #103 - FX 7 (echoes) - 103: "startrak", # #104 - FX 8 (sci-fi) - 104: "sitar", # #105 - Sitar - 105: "banjo", # #106 - Banjo - 106: "shamisen", # #107 - Shamisen - 107: "koto", # #108 - Koto - 108: "kalimba", # #109 - Kalimba - 109: "bagpipes", # #110 - Bag pipe - 110: "fiddle", # #111 - Fiddle - 111: "shannai", # #112 - Shanai - 112: "carillon", # #113 - Tinkle Bell - 113: "agogo", # #114 - Agogo - 114: "steeldrm", # #115 - Steel Drums - 115: "woodblk", # #116 - Woodblock - 116: "taiko", # #117 - Taiko Drum - 117: "toms", # #118 - Melodic Tom - 118: "syntom", # #119 - Synth Drum - 119: "revcym", # #120 - Reverse Cymbal - 120: "fx-fret", # #121 - Guitar Fret Noise - 121: "fx-blow", # #122 - Breath Noise - 122: "seashore", # #123 - Seashore - 123: "jungle", # #124 - Bird Tweet - 124: "telephon", # #125 - Telephone Ring - 125: "helicptr", # #126 - Helicopter - 126: "applause", # #127 - Applause - 127: "pistol", # #128 - Gunshot - 128: "blank", - 163: "kick1", # #35 Acoustic Bass Drum - 164: "kick2", # #36 Bass Drum 1 - 165: "stickrim", # #37 Side Stick - 166: "snare1", # #38 Acoustic Snare - 167: "claps", # #39 Hand Clap - 168: "snare2", # #40 Electric Snare - 169: "tomlo2", # #41 Low Floor Tom - 170: "hihatcl", # #42 Closed Hi Hat - 171: "tomlo1", # #43 High Floor Tom - 172: "hihatpd", # #44 Pedal Hi-Hat - 173: "tommid2", # #45 Low Tom - 174: "hihatop", # #46 Open Hi-Hat - 175: "tommid1", # #47 Low-Mid Tom - 176: "tomhi2", # #48 Hi-Mid Tom - 177: "cymcrsh1", # #49 Crash Cymbal 1 - 178: "tomhi1", # #50 High Tom - 179: "cymride1", # #51 Ride Cymbal 1 - 180: "cymchina", # #52 Chinese Cymbal - 181: "cymbell", # #53 Ride Bell - 182: "tamborin", # #54 Tambourine - 183: "cymsplsh", # #55 Splash Cymbal - 184: "cowbell", # #56 Cowbell - 185: "cymcrsh2", # #57 Crash Cymbal 2 - 186: "vibslap", # #58 Vibraslap - 187: "cymride2", # #59 Ride Cymbal 2 - 188: "bongohi", # #60 Hi Bongo - 189: "bongolo", # #61 Low Bongo - 190: "congahi1", # #62 Mute Hi Conga - 191: "congahi2", # #63 Open Hi Conga - 192: "congalo", # #64 Low Conga - 193: "timbaleh", # #65 High Timbale - 194: "timbalel", # #66 Low Timbale - 195: "agogohi", # #67 High Agogo - 196: "agogolo", # #68 Low Agogo - 197: "cabasa", # #69 Cabasa - 198: "maracas", # #70 Maracas - 199: "whistle1", # #71 Short Whistle - 200: "whistle2", # #72 Long Whistle - 201: "guiro1", # #73 Short Guiro - 202: "guiro2", # #74 Long Guiro - 203: "clave", # #75 Claves - 204: "woodblk1", # #76 Hi Wood Block - 205: "woodblk2", # #77 Low Wood Block - 206: "cuica1", # #78 Mute Cuica - 207: "cuica2", # #79 Open Cuica - 208: "triangl1", # #80 Mute Triangle - 209: "triangl2", # #81 Open Triangle + 0: "acpiano", # #001 - Acoustic Grand Piano + 1: "britepno", # #002 - Bright Acoustic Piano + 2: "synpiano", # #003 - Electric Grand Piano + 3: "honky", # #004 - Honky-tonk Piano + 4: "epiano1", # #005 - Electric Piano 1 + 5: "epiano2", # #006 - Electric Piano 2 + 6: "hrpschrd", # #007 - Harpsichord + 7: "clavinet", # #008 - Clavi + 8: "celeste", # #009 - Celesta + 9: "glocken", # #010 - Glockenspiel + 10: "musicbox", # #011 - Music Box + 11: "vibes", # #012 - Vibraphone + 12: "marimba", # #013 - Marimba + 13: "xylophon", # #014 - Xylophone + 14: "tubebell", # #015 - Tubular Bells + 15: "santur", # #016 - Dulcimer + 16: "homeorg", # #017 - Drawbar Organ + 17: "percorg", # #018 - Percussive Organ + 18: "rockorg", # #019 - Rock Organ + 19: "church", # #020 - Church Organ + 20: "reedorg", # #021 - Reed Organ + 21: "accordn", # #022 - Accordion + 22: "harmonca", # #023 - Harmonica + 23: "concrtna", # #024 - Tango Accordion + 24: "nyguitar", # #025 - Acoustic Guitar (nylon) + 25: "acguitar", # #026 - Acoustic Guitar (steel) + 26: "jazzgtr", # #027 - Electric Guitar (jazz) + 27: "cleangtr", # #028 - Electric Guitar (clean) + 28: "mutegtr", # #029 - Electric Guitar (muted) + 29: "odguitar", # #030 - Overdriven Guitar + 30: "distgtr", # #031 - Distortion Guitar + 31: "gtrharm", # #032 - Guitar harmonics + 32: "acbass", # #033 - Acoustic Bass + 33: "fngrbass", # #034 - Electric Bass (finger) + 34: "pickbass", # #035 - Electric Bass (pick) + 35: "fretless", # #036 - Fretless Bass + 36: "slapbas1", # #037 - Slap Bass 1 + 37: "slapbas2", # #038 - Slap Bass 2 + 38: "synbass1", # #039 - Synth Bass 1 + 39: "synbass2", # #040 - Synth Bass 2 + 40: "violin", # #041 - Violin + 41: "viola", # #042 - Viola + 42: "cello", # #043 - Cello + 43: "contraba", # #044 - Contrabass + 44: "tremstr", # #045 - Tremolo Strings + 45: "pizzcato", # #046 - Pizzicato Strings + 46: "harp", # #047 - Orchestral Harp + 47: "timpani", # #048 - Timpani + 48: "marcato", # #049 - String Ensemble 1 + 49: "slowstr", # #050 - String Ensemble 2 + 50: "synstr1", # #051 - SynthStrings 1 + 51: "synstr2", # #052 - SynthStrings 2 + 52: "choir", # #053 - Choir Aahs + 53: "doo", # #054 - Voice Oohs + 54: "voices", # #055 - Synth Voice + 55: "orchhit", # #056 - Orchestra Hit + 56: "trumpet", # #057 - Trumpet + 57: "trombone", # #058 - Trombone + 58: "tuba", # #059 - Tuba + 59: "mutetrum", # #060 - Muted Trumpet + 60: "frenchrn", # #061 - French Horn + 61: "hitbrass", # #062 - Brass Section + 62: "synbras1", # #063 - SynthBrass 1 + 63: "synbras2", # #064 - SynthBrass 2 + 64: "sprnosax", # #065 - Soprano Sax + 65: "altosax", # #066 - Alto Sax + 66: "tenorsax", # #067 - Tenor Sax + 67: "barisax", # #068 - Baritone Sax + 68: "oboe", # #069 - Oboe + 69: "englhorn", # #070 - English Horn + 70: "bassoon", # #071 - Bassoon + 71: "clarinet", # #072 - Clarinet + 72: "piccolo", # #073 - Piccolo + 73: "flute", # #074 - Flute + 74: "recorder", # #075 - Recorder + 75: "woodflut", # #076 - Pan Flute + 76: "bottle", # #077 - Blown Bottle + 77: "shakazul", # #078 - Shakuhachi + 78: "whistle", # #079 - Whistle + 79: "ocarina", # #080 - Ocarina + 80: "sqrwave", # #081 - Lead 1 (square) + 81: "sawwave", # #082 - Lead 2 (sawtooth) + 82: "calliope", # #083 - Lead 3 (calliope) + 83: "chiflead", # #084 - Lead 4 (chiff) + 84: "charang", # #085 - Lead 5 (charang) + 85: "voxlead", # #086 - Lead 6 (voice) + 86: "lead5th", # #087 - Lead 7 (fifths) + 87: "basslead", # #088 - Lead 8 (bass + lead) + 88: "fantasia", # #089 - Pad 1 (new age) + 89: "warmpad", # #090 - Pad 2 (warm) + 90: "polysyn", # #091 - Pad 3 (polysynth) + 91: "ghostie", # #092 - Pad 4 (choir) + 92: "bowglass", # #093 - Pad 5 (bowed) + 93: "metalpad", # #094 - Pad 6 (metallic) + 94: "halopad", # #095 - Pad 7 (halo) + 95: "sweeper", # #096 - Pad 8 (sweep) + 96: "aurora", # #097 - FX 1 (rain) + 97: "soundtrk", # #098 - FX 2 (soundtrack) + 98: "crystal", # #099 - FX 3 (crystal) + 99: "atmosphr", # #100 - FX 4 (atmosphere) + 100: "freshair", # #101 - FX 5 (brightness) + 101: "unicorn", # #102 - FX 6 (goblins) + 102: "echovox", # #103 - FX 7 (echoes) + 103: "startrak", # #104 - FX 8 (sci-fi) + 104: "sitar", # #105 - Sitar + 105: "banjo", # #106 - Banjo + 106: "shamisen", # #107 - Shamisen + 107: "koto", # #108 - Koto + 108: "kalimba", # #109 - Kalimba + 109: "bagpipes", # #110 - Bag pipe + 110: "fiddle", # #111 - Fiddle + 111: "shannai", # #112 - Shanai + 112: "carillon", # #113 - Tinkle Bell + 113: "agogo", # #114 - Agogo + 114: "steeldrm", # #115 - Steel Drums + 115: "woodblk", # #116 - Woodblock + 116: "taiko", # #117 - Taiko Drum + 117: "toms", # #118 - Melodic Tom + 118: "syntom", # #119 - Synth Drum + 119: "revcym", # #120 - Reverse Cymbal + 120: "fx-fret", # #121 - Guitar Fret Noise + 121: "fx-blow", # #122 - Breath Noise + 122: "seashore", # #123 - Seashore + 123: "jungle", # #124 - Bird Tweet + 124: "telephon", # #125 - Telephone Ring + 125: "helicptr", # #126 - Helicopter + 126: "applause", # #127 - Applause + 127: "pistol", # #128 - Gunshot + 128: "blank", + 163: "kick1", # #35 Acoustic Bass Drum + 164: "kick2", # #36 Bass Drum 1 + 165: "stickrim", # #37 Side Stick + 166: "snare1", # #38 Acoustic Snare + 167: "claps", # #39 Hand Clap + 168: "snare2", # #40 Electric Snare + 169: "tomlo2", # #41 Low Floor Tom + 170: "hihatcl", # #42 Closed Hi Hat + 171: "tomlo1", # #43 High Floor Tom + 172: "hihatpd", # #44 Pedal Hi-Hat + 173: "tommid2", # #45 Low Tom + 174: "hihatop", # #46 Open Hi-Hat + 175: "tommid1", # #47 Low-Mid Tom + 176: "tomhi2", # #48 Hi-Mid Tom + 177: "cymcrsh1", # #49 Crash Cymbal 1 + 178: "tomhi1", # #50 High Tom + 179: "cymride1", # #51 Ride Cymbal 1 + 180: "cymchina", # #52 Chinese Cymbal + 181: "cymbell", # #53 Ride Bell + 182: "tamborin", # #54 Tambourine + 183: "cymsplsh", # #55 Splash Cymbal + 184: "cowbell", # #56 Cowbell + 185: "cymcrsh2", # #57 Crash Cymbal 2 + 186: "vibslap", # #58 Vibraslap + 187: "cymride2", # #59 Ride Cymbal 2 + 188: "bongohi", # #60 Hi Bongo + 189: "bongolo", # #61 Low Bongo + 190: "congahi1", # #62 Mute Hi Conga + 191: "congahi2", # #63 Open Hi Conga + 192: "congalo", # #64 Low Conga + 193: "timbaleh", # #65 High Timbale + 194: "timbalel", # #66 Low Timbale + 195: "agogohi", # #67 High Agogo + 196: "agogolo", # #68 Low Agogo + 197: "cabasa", # #69 Cabasa + 198: "maracas", # #70 Maracas + 199: "whistle1", # #71 Short Whistle + 200: "whistle2", # #72 Long Whistle + 201: "guiro1", # #73 Short Guiro + 202: "guiro2", # #74 Long Guiro + 203: "clave", # #75 Claves + 204: "woodblk1", # #76 Hi Wood Block + 205: "woodblk2", # #77 Low Wood Block + 206: "cuica1", # #78 Mute Cuica + 207: "cuica2", # #79 Open Cuica + 208: "triangl1", # #80 Mute Triangle + 209: "triangl2", # #81 Open Triangle } # These are the data sizes of the patch files distributed with the @@ -192,70 +192,196 @@ GUS_INSTR_PATCHES = { # and check it is within the limit. PATCH_FILE_SIZES = { - "acbass": 5248, "accordn": 9616, "acguitar": 26080, - "acpiano": 32256, "agogo": 13696, "agogohi": 3488, - "agogolo": 3488, "altosax": 5616, "applause": 30064, - "atmosphr": 31360, "aurora": 31088, "bagpipes": 7760, - "banjo": 32016, "barisax": 10544, "basslead": 26496, - "bassoon": 8000, "belltree": 31888, "blank": 1520, - "bongohi": 3456, "bongolo": 4448, "bottle": 12368, - "bowglass": 24688, "britepno": 36000, "cabasa": 8448, - "calliope": 22992, "carillon": 5888, "castinet": 6016, - "celeste": 9936, "cello": 9120, "charang": 45056, - "chiflead": 31536, "choir": 22480, "church": 2144, - "claps": 5696, "clarinet": 9184, "clave": 2352, - "clavinet": 1440, "cleangtr": 22768, "concrtna": 8784, - "congahi1": 4224, "congahi2": 4704, "congalo": 4704, - "contraba": 4704, "cowbell": 3168, "crystal": 30224, - "cuica1": 9344, "cuica2": 12848, "cymbell": 17248, - "cymchina": 24112, "cymcrsh1": 31520, "cymcrsh2": 31040, - "cymride1": 17664, "cymride2": 17664, "cymsplsh": 31520, - "distgtr": 18848, "doo": 8464, "echovox": 14976, - "englhorn": 12096, "epiano1": 7344, "epiano2": 21936, - "fantasia": 23456, "fiddle": 5904, "flute": 6032, - "fngrbass": 9744, "frenchrn": 14128, "freshair": 28992, - "fretless": 2640, "fx-blow": 28688, "fx-fret": 13648, - "ghostie": 31488, "glocken": 5184, "gtrharm": 4928, - "guiro1": 4128, "guiro2": 9248, "halopad": 29984, - "harmonca": 7408, "harp": 11728, "helicptr": 25008, - "highq": 1808, "hihatcl": 4560, "hihatop": 20048, - "hihatpd": 1808, "hitbrass": 31520, "homeorg": 992, - "honky": 65680, "hrpschrd": 3584, "jazzgtr": 27712, - "jingles": 16944, "jungle": 13616, "kalimba": 2208, - "kick1": 4544, "kick2": 5024, "koto": 20832, - "lead5th": 6464, "maracas": 4560, "marcato": 61232, - "marimba": 2064, "metalpad": 30288, "metbell": 112, - "metclick": 112, "musicbox": 15312, "mutegtr": 17008, - "mutetrum": 9168, "nyguitar": 19200, "oboe": 3952, - "ocarina": 1616, "odguitar": 12640, "orchhit": 14208, - "percorg": 7520, "piccolo": 4320, "pickbass": 16416, - "pistol": 18144, "pizzcato": 19888, "polysyn": 30224, - "recorder": 2656, "reedorg": 1568, "revcym": 13536, - "rockorg": 30288, "santur": 21760, "sawwave": 27056, - "scratch1": 4384, "scratch2": 2288, "seashore": 31040, - "shakazul": 31136, "shaker": 3104, "shamisen": 13136, - "shannai": 9792, "sitar": 18288, "slap": 5856, - "slapbas1": 27872, "slapbas2": 20592, "slowstr": 18192, - "snare1": 8544, "snare2": 4096, "soundtrk": 19888, - "sprnosax": 7072, "sqrclick": 112, "sqrwave": 15056, - "startrak": 27376, "steeldrm": 11952, "stickrim": 2848, - "sticks": 4224, "surdo1": 9600, "surdo2": 9600, - "sweeper": 31216, "synbass1": 6160, "synbass2": 2928, - "synbras1": 30704, "synbras2": 30160, "synpiano": 5456, - "synstr1": 31216, "synstr2": 16416, "syntom": 30512, - "taiko": 18672, "tamborin": 8944, "telephon": 4416, - "tenorsax": 8448, "timbaleh": 5264, "timbalel": 9728, - "timpani": 7072, "tomhi1": 6576, "tomhi2": 6560, - "tomlo1": 6560, "tomlo2": 9600, "tommid1": 6560, - "tommid2": 6560, "toms": 6576, "tremstr": 61232, - "triangl1": 2224, "triangl2": 15792, "trombone": 12896, - "trumpet": 6608, "tuba": 5760, "tubebell": 9120, - "unicorn": 30096, "vibes": 10640, "vibslap": 9456, - "viola": 27952, "violin": 12160, "voices": 14976, - "voxlead": 14992, "warmpad": 18080, "whistle": 5872, - "whistle1": 2000, "whistle2": 928, "woodblk": 3680, - "woodblk1": 2352, "woodblk2": 3680, "woodflut": 1936, - "xylophon": 9376, + "acbass": 5248, + "accordn": 9616, + "acguitar": 26080, + "acpiano": 32256, + "agogo": 13696, + "agogohi": 3488, + "agogolo": 3488, + "altosax": 5616, + "applause": 30064, + "atmosphr": 31360, + "aurora": 31088, + "bagpipes": 7760, + "banjo": 32016, + "barisax": 10544, + "basslead": 26496, + "bassoon": 8000, + "belltree": 31888, + "blank": 1520, + "bongohi": 3456, + "bongolo": 4448, + "bottle": 12368, + "bowglass": 24688, + "britepno": 36000, + "cabasa": 8448, + "calliope": 22992, + "carillon": 5888, + "castinet": 6016, + "celeste": 9936, + "cello": 9120, + "charang": 45056, + "chiflead": 31536, + "choir": 22480, + "church": 2144, + "claps": 5696, + "clarinet": 9184, + "clave": 2352, + "clavinet": 1440, + "cleangtr": 22768, + "concrtna": 8784, + "congahi1": 4224, + "congahi2": 4704, + "congalo": 4704, + "contraba": 4704, + "cowbell": 3168, + "crystal": 30224, + "cuica1": 9344, + "cuica2": 12848, + "cymbell": 17248, + "cymchina": 24112, + "cymcrsh1": 31520, + "cymcrsh2": 31040, + "cymride1": 17664, + "cymride2": 17664, + "cymsplsh": 31520, + "distgtr": 18848, + "doo": 8464, + "echovox": 14976, + "englhorn": 12096, + "epiano1": 7344, + "epiano2": 21936, + "fantasia": 23456, + "fiddle": 5904, + "flute": 6032, + "fngrbass": 9744, + "frenchrn": 14128, + "freshair": 28992, + "fretless": 2640, + "fx-blow": 28688, + "fx-fret": 13648, + "ghostie": 31488, + "glocken": 5184, + "gtrharm": 4928, + "guiro1": 4128, + "guiro2": 9248, + "halopad": 29984, + "harmonca": 7408, + "harp": 11728, + "helicptr": 25008, + "highq": 1808, + "hihatcl": 4560, + "hihatop": 20048, + "hihatpd": 1808, + "hitbrass": 31520, + "homeorg": 992, + "honky": 65680, + "hrpschrd": 3584, + "jazzgtr": 27712, + "jingles": 16944, + "jungle": 13616, + "kalimba": 2208, + "kick1": 4544, + "kick2": 5024, + "koto": 20832, + "lead5th": 6464, + "maracas": 4560, + "marcato": 61232, + "marimba": 2064, + "metalpad": 30288, + "metbell": 112, + "metclick": 112, + "musicbox": 15312, + "mutegtr": 17008, + "mutetrum": 9168, + "nyguitar": 19200, + "oboe": 3952, + "ocarina": 1616, + "odguitar": 12640, + "orchhit": 14208, + "percorg": 7520, + "piccolo": 4320, + "pickbass": 16416, + "pistol": 18144, + "pizzcato": 19888, + "polysyn": 30224, + "recorder": 2656, + "reedorg": 1568, + "revcym": 13536, + "rockorg": 30288, + "santur": 21760, + "sawwave": 27056, + "scratch1": 4384, + "scratch2": 2288, + "seashore": 31040, + "shakazul": 31136, + "shaker": 3104, + "shamisen": 13136, + "shannai": 9792, + "sitar": 18288, + "slap": 5856, + "slapbas1": 27872, + "slapbas2": 20592, + "slowstr": 18192, + "snare1": 8544, + "snare2": 4096, + "soundtrk": 19888, + "sprnosax": 7072, + "sqrclick": 112, + "sqrwave": 15056, + "startrak": 27376, + "steeldrm": 11952, + "stickrim": 2848, + "sticks": 4224, + "surdo1": 9600, + "surdo2": 9600, + "sweeper": 31216, + "synbass1": 6160, + "synbass2": 2928, + "synbras1": 30704, + "synbras2": 30160, + "synpiano": 5456, + "synstr1": 31216, + "synstr2": 16416, + "syntom": 30512, + "taiko": 18672, + "tamborin": 8944, + "telephon": 4416, + "tenorsax": 8448, + "timbaleh": 5264, + "timbalel": 9728, + "timpani": 7072, + "tomhi1": 6576, + "tomhi2": 6560, + "tomlo1": 6560, + "tomlo2": 9600, + "tommid1": 6560, + "tommid2": 6560, + "toms": 6576, + "tremstr": 61232, + "triangl1": 2224, + "triangl2": 15792, + "trombone": 12896, + "trumpet": 6608, + "tuba": 5760, + "tubebell": 9120, + "unicorn": 30096, + "vibes": 10640, + "vibslap": 9456, + "viola": 27952, + "violin": 12160, + "voices": 14976, + "voxlead": 14992, + "warmpad": 18080, + "whistle": 5872, + "whistle1": 2000, + "whistle2": 928, + "woodblk": 3680, + "woodblk1": 2352, + "woodblk2": 3680, + "woodflut": 1936, + "xylophon": 9376, } # Groups of "similar sounding" instruments. The first instrument in each @@ -273,78 +399,163 @@ PATCH_FILE_SIZES = { # (see table above of patch sizes). SIMILAR_GROUPS = [ - # Pianos. - ('synpiano', 'acpiano', 'britepno', 'honky'), - ('glocken', 'epiano1', 'epiano2', 'celeste'), - # Harpsichord sounds noticeably different to pianos: - ('hrpschrd', 'clavinet'), - # Xylophone etc. - ('marimba', 'musicbox', 'vibes', 'xylophon', 'tubebell', 'carillon', - 'santur', 'kalimba'), - # Organs. - ('homeorg', 'percorg', 'rockorg', 'church', 'reedorg'), - # Accordion/Harmonica: - ('accordn', 'harmonca', 'concrtna'), - # Guitars. - ('nyguitar', 'acguitar', 'jazzgtr'), - # Overdriven/distortion guitars sound different. Besides, we - # definitely want at least one of these. - ('odguitar', 'distgtr', 'gtrharm', 'cleangtr', 'bagpipes'), - # Basses. - ('synbass2', 'acbass', 'fngrbass', 'pickbass', 'basslead', 'fretless'), - ('synbass1', 'slapbas1', 'slapbas2', 'mutegtr'), - # Violin and similar string instruments. - ('violin', 'viola', 'cello', 'contraba', 'pizzcato', 'harp'), - # Other stringed (?) - ('synstr2', 'slowstr', 'marcato', 'synstr1', 'choir', 'doo', 'voices', - 'orchhit', 'polysyn', 'bowglass', 'tremstr', - 'fantasia', 'warmpad', 'ghostie', 'metalpad', 'sweeper', 'halopad'), - # Trumpet and other brass. - ('trumpet', 'trombone', 'tuba', 'mutetrum', 'frenchrn', 'hitbrass', - 'synbras1', 'synbras2'), - # Reed instruments. - ('altosax', 'sprnosax', 'tenorsax', 'barisax', 'oboe', 'englhorn', - 'bassoon', 'clarinet'), - # Pipe instruments. - ('recorder', 'flute', 'piccolo', 'woodflut', 'bottle', 'shakazul', - 'whistle', 'ocarina', 'fiddle', 'shannai', 'calliope', 'chiflead', - 'charang'), - # Leads: - ('sqrwave', 'sawwave', 'voxlead', 'lead5th'), - # Odd stringed instruments. - ('sitar', 'banjo', 'shamisen', 'koto'), - - # Percussion sounds. - - # Kick: - ('kick2', 'taiko', 'kick1'), - # Conga: - ('congahi2', 'congahi1', 'congalo'), - # Snare drums: - ('snare2', 'claps', 'snare1'), - # Toms: - ('tomlo1', 'toms', 'syntom', 'tomlo2', 'tommid1', 'tommid2', 'tomhi2', - 'tomhi1', 'timpani'), - # Cymbal crash: - ('cymsplsh', 'cymcrsh2', 'cymcrsh1', 'revcym', 'cymchina'), - # Cymbal ride: - ('cymride1', 'cymride2', 'cymbell', 'hihatop'), - # Hi-hat: - ('hihatpd', 'hihatcl'), - # Metallic sounding: - ('bongohi', 'bongolo', 'timbaleh', 'timbalel', 'cowbell'), - # Click: - ('stickrim', 'woodblk1', 'woodblk2', 'woodblk', 'clave'), - - # Random instruments we don't include unless they're popular enough. - ('blank', - # Special effects: - 'unicorn', 'soundtrk', 'aurora', 'crystal', 'atmosphr', 'freshair', - 'echovox', 'startrak', 'fx-fret', 'fx-blow', 'seashore', 'jungle', - 'telephon', 'helicptr', 'applause', 'pistol', - # Percussion: - 'cabasa', 'whistle1', 'whistle2', 'vibslap', 'maracas', 'guiro1', - 'guiro2', 'cuica1', 'cuica2', 'steeldrm', 'agogohi', 'agogolo', - 'agogo', 'triangl1', 'triangl2' , 'tamborin'), + # Pianos. + ("synpiano", "acpiano", "britepno", "honky"), + ("glocken", "epiano1", "epiano2", "celeste"), + # Harpsichord sounds noticeably different to pianos: + ("hrpschrd", "clavinet"), + # Xylophone etc. + ( + "marimba", + "musicbox", + "vibes", + "xylophon", + "tubebell", + "carillon", + "santur", + "kalimba", + ), + # Organs. + ("homeorg", "percorg", "rockorg", "church", "reedorg"), + # Accordion/Harmonica: + ("accordn", "harmonca", "concrtna"), + # Guitars. + ("nyguitar", "acguitar", "jazzgtr"), + # Overdriven/distortion guitars sound different. Besides, we + # definitely want at least one of these. + ("odguitar", "distgtr", "gtrharm", "cleangtr", "bagpipes"), + # Basses. + ("synbass2", "acbass", "fngrbass", "pickbass", "basslead", "fretless"), + ("synbass1", "slapbas1", "slapbas2", "mutegtr"), + # Violin and similar string instruments. + ("violin", "viola", "cello", "contraba", "pizzcato", "harp"), + # Other stringed (?) + ( + "synstr2", + "slowstr", + "marcato", + "synstr1", + "choir", + "doo", + "voices", + "orchhit", + "polysyn", + "bowglass", + "tremstr", + "fantasia", + "warmpad", + "ghostie", + "metalpad", + "sweeper", + "halopad", + ), + # Trumpet and other brass. + ( + "trumpet", + "trombone", + "tuba", + "mutetrum", + "frenchrn", + "hitbrass", + "synbras1", + "synbras2", + ), + # Reed instruments. + ( + "altosax", + "sprnosax", + "tenorsax", + "barisax", + "oboe", + "englhorn", + "bassoon", + "clarinet", + ), + # Pipe instruments. + ( + "recorder", + "flute", + "piccolo", + "woodflut", + "bottle", + "shakazul", + "whistle", + "ocarina", + "fiddle", + "shannai", + "calliope", + "chiflead", + "charang", + ), + # Leads: + ("sqrwave", "sawwave", "voxlead", "lead5th"), + # Odd stringed instruments. + ("sitar", "banjo", "shamisen", "koto"), + # Percussion sounds. + # Kick: + ("kick2", "taiko", "kick1"), + # Conga: + ("congahi2", "congahi1", "congalo"), + # Snare drums: + ("snare2", "claps", "snare1"), + # Toms: + ( + "tomlo1", + "toms", + "syntom", + "tomlo2", + "tommid1", + "tommid2", + "tomhi2", + "tomhi1", + "timpani", + ), + # Cymbal crash: + ("cymsplsh", "cymcrsh2", "cymcrsh1", "revcym", "cymchina"), + # Cymbal ride: + ("cymride1", "cymride2", "cymbell", "hihatop"), + # Hi-hat: + ("hihatpd", "hihatcl"), + # Metallic sounding: + ("bongohi", "bongolo", "timbaleh", "timbalel", "cowbell"), + # Click: + ("stickrim", "woodblk1", "woodblk2", "woodblk", "clave"), + # Random instruments we don't include unless they're popular enough. + ( + "blank", + # Special effects: + "unicorn", + "soundtrk", + "aurora", + "crystal", + "atmosphr", + "freshair", + "echovox", + "startrak", + "fx-fret", + "fx-blow", + "seashore", + "jungle", + "telephon", + "helicptr", + "applause", + "pistol", + # Percussion: + "cabasa", + "whistle1", + "whistle2", + "vibslap", + "maracas", + "guiro1", + "guiro2", + "cuica1", + "cuica2", + "steeldrm", + "agogohi", + "agogolo", + "agogo", + "triangl1", + "triangl2", + "tamborin", + ), ] - diff --git a/lumps/dmxgus/gather_stats.py b/lumps/dmxgus/gather_stats.py index fe1ab634..fe87e744 100644 --- a/lumps/dmxgus/gather_stats.py +++ b/lumps/dmxgus/gather_stats.py @@ -12,43 +12,44 @@ import midi import sys + def get_instr_stats(filename): - """Get a set of instruments used by the specified MIDI file.""" - result = set() - midfile = midi.read_midifile(filename) + """Get a set of instruments used by the specified MIDI file.""" + result = set() + midfile = midi.read_midifile(filename) - for track in midfile: - for event in track: - if isinstance(event, midi.ProgramChangeEvent) \ - and event.channel != 9: - instr = event.data[0] - result.add(instr) - # Percussion: - if isinstance(event, midi.NoteOnEvent) \ - and event.channel == 9: - instr = event.data[0] + 128 - result.add(instr) + for track in midfile: + for event in track: + if ( + isinstance(event, midi.ProgramChangeEvent) + and event.channel != 9 + ): + instr = event.data[0] + result.add(instr) + # Percussion: + if isinstance(event, midi.NoteOnEvent) and event.channel == 9: + instr = event.data[0] + 128 + result.add(instr) + + return result - return result total_stats = [0] * 217 for filename in sys.argv[1:]: - print "Processing %s" % filename - stats = get_instr_stats(filename) - print sorted(stats) - for instrument in stats: - total_stats[instrument] += 1 + print("Processing %s" % filename) + stats = get_instr_stats(filename) + print(sorted(stats)) + for instrument in stats: + total_stats[instrument] += 1 with open("stats.py", "w") as f: - f.write("# Instrument stats, autogenerated by gather_stats.py\n\n") - f.write("INSTRUMENT_STATS = [\n\t") - - for index, stat in enumerate(total_stats): - f.write("% 5i," % stat) - if (index % 10) == 9: - f.write("\n\t") - - f.write("\n]\n") + f.write("# Instrument stats, autogenerated by gather_stats.py\n\n") + f.write("INSTRUMENT_STATS = [\n\t") + for index, stat in enumerate(total_stats): + f.write("% 5i," % stat) + if (index % 10) == 9: + f.write("\n\t") + f.write("\n]\n") diff --git a/lumps/dmxgus/gen-ultramid b/lumps/dmxgus/gen-ultramid index 096d9614..eaa44787 100755 --- a/lumps/dmxgus/gen-ultramid +++ b/lumps/dmxgus/gen-ultramid @@ -23,174 +23,185 @@ HEADER_TEXT = """ # bug in Doom's DMX sound library. """ + def normalize_stats(stats): - """Normalize the gathered instrument statistics. + """Normalize the gathered instrument statistics. - Percussion instruments tend to be more widely used, which can give - them a disproportionate priority. Therefore, generate a "normalized" - set of statistics that adjust the percussion instruments to be - roughly equal to the main instruments. - """ - main_stats = stats[0:128] - perc_stats = stats[128:] - main_av = sum(main_stats) / len(main_stats) - perc_av = sum(perc_stats) / (len(perc_stats) - 35) + Percussion instruments tend to be more widely used, which can give + them a disproportionate priority. Therefore, generate a "normalized" + set of statistics that adjust the percussion instruments to be + roughly equal to the main instruments. + """ + main_stats = stats[0:128] + perc_stats = stats[128:] + main_av = sum(main_stats) / len(main_stats) + perc_av = sum(perc_stats) / (len(perc_stats) - 35) - def adjusted_priority(stat): - return (float(stat) * main_av) / perc_av + def adjusted_priority(stat): + return (float(stat) * main_av) / perc_av - adjusted_perc_stats = map(adjusted_priority, perc_stats) + adjusted_perc_stats = map(adjusted_priority, perc_stats) + + return main_stats + list(adjusted_perc_stats) - return main_stats + list(adjusted_perc_stats) def ranked_patches(): - """Get a list of GUS patches ranked by priority. + """Get a list of GUS patches ranked by priority. - This uses the gathered statistics about use of different instruments - in Doom WADs and orders them by benefit/cost ratio (where cost is - the size that the instrument file will occupy in RAM). - """ - adjusted_stats = normalize_stats(stats.INSTRUMENT_STATS) - result = [] - for instr_id, name in config.GUS_INSTR_PATCHES.items(): - priority = float(adjusted_stats[instr_id]) \ - / config.PATCH_FILE_SIZES[name] - result.append((priority, name)) + This uses the gathered statistics about use of different instruments + in Doom WADs and orders them by benefit/cost ratio (where cost is + the size that the instrument file will occupy in RAM). + """ + adjusted_stats = normalize_stats(stats.INSTRUMENT_STATS) + result = [] + for instr_id, name in config.GUS_INSTR_PATCHES.items(): + priority = ( + float(adjusted_stats[instr_id]) / config.PATCH_FILE_SIZES[name] + ) + result.append((priority, name)) + + return list(map(lambda x: x[1], reversed(sorted(result)))) - return list(map(lambda x: x[1], reversed(sorted(result)))) def midi_instr_for_name(name): - """Given a GUS patch name, find the associated MIDI instrument.""" - for instr_id, instr_name in config.GUS_INSTR_PATCHES.items(): - if name == instr_name: - return instr_id - raise KeyError("Unknown instrument: %s" % name) + """Given a GUS patch name, find the associated MIDI instrument.""" + for instr_id, instr_name in config.GUS_INSTR_PATCHES.items(): + if name == instr_name: + return instr_id + raise KeyError("Unknown instrument: %s" % name) + def patchset_size(mapping): - """Given an instrument-instrument mapping patch set, calculate its - size. - """ - result = 0 - for i1, i2 in mapping.items(): - if i1 == i2: - name = config.GUS_INSTR_PATCHES[i1] - result += config.PATCH_FILE_SIZES[name] - return result + """Given an instrument-instrument mapping patch set, calculate its + size. + """ + result = 0 + for i1, i2 in mapping.items(): + if i1 == i2: + name = config.GUS_INSTR_PATCHES[i1] + result += config.PATCH_FILE_SIZES[name] + return result + def patchset_to_string(mapping): - result = [] - for i1, i2 in mapping.items(): - if i1 == i2: - name = config.GUS_INSTR_PATCHES[i1] - result.append("%s (%i bytes)" % ( - name, config.PATCH_FILE_SIZES[name])) - return "\n".join(result) + result = [] + for i1, i2 in mapping.items(): + if i1 == i2: + name = config.GUS_INSTR_PATCHES[i1] + result.append( + "%s (%i bytes)" % (name, config.PATCH_FILE_SIZES[name]) + ) + return "\n".join(result) + def mapping_for_size(size): - """Select a set of patches for the given RAM size. + """Select a set of patches for the given RAM size. - Args: - size: Size in bytes of the GUS RAM. - Returns: - Dictionary mapping from instrument number to instrument number. - An instrument that maps to itself is included in the output. - """ - # Leave some extra space. The ultramid.ini distributed with the - # GUS drivers says this: - # The libraries are built in such a way as to leave 8K+32bytes - # after the patches are loaded for digital audio. - size -= 32*1024 + 8 + Args: + size: Size in bytes of the GUS RAM. + Returns: + Dictionary mapping from instrument number to instrument number. + An instrument that maps to itself is included in the output. + """ + # Leave some extra space. The ultramid.ini distributed with the + # GUS drivers says this: + # The libraries are built in such a way as to leave 8K+32bytes + # after the patches are loaded for digital audio. + size -= 32 * 1024 + 8 - # Get a list of patches sorted by decreasing priority. - patches = ranked_patches() + # Get a list of patches sorted by decreasing priority. + patches = ranked_patches() - # Start by processing the similarity groups and pointing all - # instruments in a group to their group leader. - result = {} + # Start by processing the similarity groups and pointing all + # instruments in a group to their group leader. + result = {} - for group in config.SIMILAR_GROUPS: - leader = group[0] - leader_index = midi_instr_for_name(leader) - for patch in group: - patch_index = midi_instr_for_name(patch) - result[patch_index] = leader_index + for group in config.SIMILAR_GROUPS: + leader = group[0] + leader_index = midi_instr_for_name(leader) + for patch in group: + patch_index = midi_instr_for_name(patch) + result[patch_index] = leader_index - # We now have a mapping that should cover every instrument with - # a fallback. Go through the patches in order of priority and add - # patches that will fit. - curr_size = patchset_size(result) - assert curr_size < size, \ - "Minimal config for %s will not fit in RAM! (%i):\n%s" % \ - (size, curr_size, patchset_to_string(result)) + # We now have a mapping that should cover every instrument with + # a fallback. Go through the patches in order of priority and add + # patches that will fit. + curr_size = patchset_size(result) + assert curr_size < size, ( + "Minimal config for %s will not fit in RAM! (%i):\n%s" + % (size, curr_size, patchset_to_string(result)) + ) - for patch in patches: - patch_index = midi_instr_for_name(patch) - patch_size = config.PATCH_FILE_SIZES[patch] + for patch in patches: + patch_index = midi_instr_for_name(patch) + patch_size = config.PATCH_FILE_SIZES[patch] - if result[patch_index] != patch_index \ - and curr_size + patch_size < size: - result[patch_index] = patch_index - curr_size += patch_size + if ( + result[patch_index] != patch_index + and curr_size + patch_size < size + ): + result[patch_index] = patch_index + curr_size += patch_size + + return result - return result def instrument_patches(mappings): - """Returns list of MIDI instruments in an appropriate order for output. + """Returns list of MIDI instruments in an appropriate order for output. - The ordering in the output file is important, because of a bug in the - DMX sound library; when patches are shared between instruments it is - only possible to refer to instruments listed earlier in the file. + The ordering in the output file is important, because of a bug in the + DMX sound library; when patches are shared between instruments it is + only possible to refer to instruments listed earlier in the file. - Args: - mappings: List of mappings from instrument ID to leader instrument. - Yields: - A tuple containing each MIDI instrument number and patch file name - to load. - """ - done_instr_ids = set() - # Make multiple passes until we've done all the instruments. - while len(done_instr_ids) < len(config.GUS_INSTR_PATCHES): - made_progress = False - for instr_id, name in sorted(config.GUS_INSTR_PATCHES.items()): - for mapping in mappings: - mapped_instr_id = mapping[instr_id] - if (instr_id != mapped_instr_id and - mapped_instr_id not in done_instr_ids): - break - else: - if instr_id not in done_instr_ids: - yield instr_id, name - done_instr_ids.add(instr_id) - made_progress = True + Args: + mappings: List of mappings from instrument ID to leader instrument. + Yields: + A tuple containing each MIDI instrument number and patch file name + to load. + """ + done_instr_ids = set() + # Make multiple passes until we've done all the instruments. + while len(done_instr_ids) < len(config.GUS_INSTR_PATCHES): + made_progress = False + for instr_id, name in sorted(config.GUS_INSTR_PATCHES.items()): + for mapping in mappings: + mapped_instr_id = mapping[instr_id] + if ( + instr_id != mapped_instr_id + and mapped_instr_id not in done_instr_ids + ): + break + else: + if instr_id not in done_instr_ids: + yield instr_id, name + done_instr_ids.add(instr_id) + made_progress = True - assert made_progress, ( - "infinite loop while producing patches list") + assert made_progress, "infinite loop while producing patches list" if len(sys.argv) != 2: - print("Usage: %s " % sys.argv[0]) - sys.exit(1) + print("Usage: %s " % sys.argv[0]) + sys.exit(1) mappings = ( - mapping_for_size(256 * 1024), - mapping_for_size(512 * 1024), - mapping_for_size(768 * 1024), - mapping_for_size(1024 * 1024) + mapping_for_size(256 * 1024), + mapping_for_size(512 * 1024), + mapping_for_size(768 * 1024), + mapping_for_size(1024 * 1024), ) with open(sys.argv[1], "w") as output: - output.write(HEADER_TEXT.lstrip()) - - for instr_id, name in instrument_patches(mappings): - line = "%i, %i, %i, %i, %i, %s" % ( - instr_id, - mappings[0][instr_id], - mappings[1][instr_id], - mappings[2][instr_id], - mappings[3][instr_id], - name - ) - - output.write(line + "\n") + output.write(HEADER_TEXT.lstrip()) + for instr_id, name in instrument_patches(mappings): + line = "%i, %i, %i, %i, %i, %s" % ( + instr_id, + mappings[0][instr_id], + mappings[1][instr_id], + mappings[2][instr_id], + mappings[3][instr_id], + name, + ) + output.write(line + "\n") diff --git a/lumps/genmidi/a2i-to-sbi b/lumps/genmidi/a2i-to-sbi index b1611b69..42702950 100755 --- a/lumps/genmidi/a2i-to-sbi +++ b/lumps/genmidi/a2i-to-sbi @@ -6,10 +6,11 @@ import sbi_file import sys if len(sys.argv) != 3: - print >> sys.stderr, "Usage: %s " \ - % sys.argv[0] - sys.exit(-1) + print( + "Usage: %s " % sys.argv[0], + file=sys.stderr, + ) + sys.exit(-1) data = a2i_file.read(sys.argv[1]) sbi_file.write(sys.argv[2], data) - diff --git a/lumps/genmidi/a2i_file.py b/lumps/genmidi/a2i_file.py index 89d452cb..d09bc180 100644 --- a/lumps/genmidi/a2i_file.py +++ b/lumps/genmidi/a2i_file.py @@ -9,208 +9,216 @@ import struct HEADER_STRING = "_A2ins_" + class BitReader: - def __init__(self, data): - self.data = data - self.index = 0 - self.byte = 0 - self.byte_bit = 0 + def __init__(self, data): + self.data = data + self.index = 0 + self.byte = 0 + self.byte_bit = 0 - def read_byte(self): - if self.index >= len(self.data): - raise IndexError("Reached end of decompress stream " + - "(%i bytes)" % len(self.data)) - result, = struct.unpack("B", self.data[self.index:self.index+1]) - self.index += 1 - return result + def read_byte(self): + if self.index >= len(self.data): + raise IndexError( + "Reached end of decompress stream " + + "(%i bytes)" % len(self.data) + ) + result, = struct.unpack("B", self.data[self.index : self.index + 1]) + self.index += 1 + return result - def read_bit(self): - if self.byte_bit <= 0: - self.byte = self.read_byte() - self.byte_bit = 7 - else: - self.byte_bit -= 1 + def read_bit(self): + if self.byte_bit <= 0: + self.byte = self.read_byte() + self.byte_bit = 7 + else: + self.byte_bit -= 1 - if (self.byte & (1 << self.byte_bit)) != 0: - result = 1 - else: - result = 0 + if (self.byte & (1 << self.byte_bit)) != 0: + result = 1 + else: + result = 0 - return result + return result - def read_bits(self, n): - result = 0 + def read_bits(self, n): + result = 0 - for i in range(n): - result = (result << 1) + self.read_bit() + for i in range(n): + result = (result << 1) + self.read_bit() + + return result - return result def read_gamma(reader): - result = 1 + result = 1 - while True: - result = (result << 1) | reader.read_bit() + while True: + result = (result << 1) | reader.read_bit() - if reader.read_bit() == 0: - break + if reader.read_bit() == 0: + break + + return result - return result def decompress(data, data_len): - reader = BitReader(data) - result = [] - lwm = 0 - last_offset = 0 + reader = BitReader(data) + result = [] + lwm = 0 + last_offset = 0 - # First byte is an implied straight copy. - result.append(reader.read_byte()) + # First byte is an implied straight copy. + result.append(reader.read_byte()) - while True: - if reader.read_bit(): - if reader.read_bit(): - if reader.read_bit(): - # 111 = Copy byte from history, - # up to 15 bytes back. - #print "111 copy" + while True: + if reader.read_bit(): + if reader.read_bit(): + if reader.read_bit(): + # 111 = Copy byte from history, + # up to 15 bytes back. + # print "111 copy" - offset = reader.read_bits(4) + offset = reader.read_bits(4) - if offset == 0: - result.append(0) - else: - b = result[len(result) - offset] - result.append(b) + if offset == 0: + result.append(0) + else: + b = result[len(result) - offset] + result.append(b) - lwm = 0 - else: - #print "110 copy" - # 110 = Copy 2-3 bytes from - # further back in history + lwm = 0 + else: + # print "110 copy" + # 110 = Copy 2-3 bytes from + # further back in history - offset = reader.read_byte() - count = 2 + (offset & 0x01) - offset = offset >> 1 + offset = reader.read_byte() + count = 2 + (offset & 0x01) + offset = offset >> 1 - if offset == 0: - break + if offset == 0: + break - index = len(result) - offset - for i in range(count): - result += result[index:index+1] - index += 1 + index = len(result) - offset + for i in range(count): + result += result[index : index + 1] + index += 1 - last_offset = offset - lwm = 1 - else: - # 10 = Copy from further away... + last_offset = offset + lwm = 1 + else: + # 10 = Copy from further away... - offset = read_gamma(reader) + offset = read_gamma(reader) - if lwm == 0 and offset == 2: - #print "10 copy type 1" - count = read_gamma(reader) - index = len(result) - last_offset - for i in range(count): - result += result[index:index+1] - index += 1 - else: - #print "10 copy type 2" - if lwm == 0: - offset -= 3 - else: - offset -= 2 + if lwm == 0 and offset == 2: + # print "10 copy type 1" + count = read_gamma(reader) + index = len(result) - last_offset + for i in range(count): + result += result[index : index + 1] + index += 1 + else: + # print "10 copy type 2" + if lwm == 0: + offset -= 3 + else: + offset -= 2 - offset = offset * 256 \ - + reader.read_byte() + offset = offset * 256 + reader.read_byte() - count = read_gamma(reader) + count = read_gamma(reader) - if offset >= 32000: - count += 1 - if offset >= 1280: - count += 1 - if offset < 128: - count += 2 + if offset >= 32000: + count += 1 + if offset >= 1280: + count += 1 + if offset < 128: + count += 2 - index = len(result) - offset - for i in range(count): - result += result[index:index+1] - index += 1 + index = len(result) - offset + for i in range(count): + result += result[index : index + 1] + index += 1 - last_offset = offset + last_offset = offset - lwm = 1 + lwm = 1 - else: - #print "Single byte output" + else: + # print "Single byte output" - # 0 = Straight-through byte copy. - result.append(reader.read_byte()) - lwm = 0 + # 0 = Straight-through byte copy. + result.append(reader.read_byte()) + lwm = 0 - #print "len: %i" % len(result) + # print "len: %i" % len(result) + + return struct.pack("%iB" % len(result), *result) - return struct.pack("%iB" % len(result), *result) FIELDS = [ - "m_am_vibrato_eg", - "c_am_vibrato_eg", - "m_ksl_volume", - "c_ksl_volume", - "m_attack_decay", - "c_attack_decay", - "m_sustain_release", - "c_sustain_release", - "m_waveform", - "c_waveform", - "feedback_fm", - "panning", - "finetune", - "voice_type", + "m_am_vibrato_eg", + "c_am_vibrato_eg", + "m_ksl_volume", + "c_ksl_volume", + "m_attack_decay", + "c_attack_decay", + "m_sustain_release", + "c_sustain_release", + "m_waveform", + "c_waveform", + "feedback_fm", + "panning", + "finetune", + "voice_type", ] + def decode_type_9(data): - compressed_len, = struct.unpack("\n" % sys.argv[0]) - sys.exit(-1) + sys.stderr.write("Usage: %s \n" % sys.argv[0]) + sys.exit(-1) instruments = genmidi.read(sys.argv[1]) @@ -64,22 +68,21 @@ percussion = instruments[128:] print("INSTRUMENTS = [") for i in range(len(main_instrs)): - instr = main_instrs[i] - filename = "instr%03i.sbi" % (i+1) - filename2 = "instr%03i-2.sbi" % (i+1) - dump_instrument(filename, filename2, instr) - print_instr_def(filename, filename2, instr) + instr = main_instrs[i] + filename = "instr%03i.sbi" % (i + 1) + filename2 = "instr%03i-2.sbi" % (i + 1) + dump_instrument(filename, filename2, instr) + print_instr_def(filename, filename2, instr) print("]") print("") print("PERCUSSION = [") for i in range(len(percussion)): - instr = percussion[i] - filename = "perc%02i.sbi" % (i+35) - filename2 = "perc%02i-2.sbi" % (i+35) - dump_instrument(filename, filename2, instr) - print_instr_def(filename, filename2, instr) + instr = percussion[i] + filename = "perc%02i.sbi" % (i + 35) + filename2 = "perc%02i-2.sbi" % (i + 35) + dump_instrument(filename, filename2, instr) + print_instr_def(filename, filename2, instr) print("]") - diff --git a/lumps/genmidi/genmidi.py b/lumps/genmidi/genmidi.py index 934def5c..ae81fc7e 100644 --- a/lumps/genmidi/genmidi.py +++ b/lumps/genmidi/genmidi.py @@ -14,171 +14,181 @@ INSTR_DATA_LEN = 36 INSTR_NAME_LEN = 32 FLAG_FIXED_PITCH = 0x0001 -FLAG_TWO_VOICE = 0x0004 +FLAG_TWO_VOICE = 0x0004 -KSL_MASK = 0xc0 -VOLUME_MASK = 0x3f +KSL_MASK = 0xC0 +VOLUME_MASK = 0x3F # Order of fields in GENMIDI data structures. GENMIDI_FIELDS = [ - "m_am_vibrato_eg", - "m_attack_decay", - "m_sustain_release", - "m_waveform", - "m_ksl", - "m_volume", - "feedback_fm", - "c_am_vibrato_eg", - "c_attack_decay", - "c_sustain_release", - "c_waveform", - "c_ksl", - "c_volume", - "null", - "note_offset" + "m_am_vibrato_eg", + "m_attack_decay", + "m_sustain_release", + "m_waveform", + "m_ksl", + "m_volume", + "feedback_fm", + "c_am_vibrato_eg", + "c_attack_decay", + "c_sustain_release", + "c_waveform", + "c_ksl", + "c_volume", + "null", + "note_offset", ] # Encode a single voice of an instrument to binary. + def encode_voice(data, offset): - result = dict(data) + result = dict(data) - result["m_ksl"] = data["m_ksl_volume"] & KSL_MASK - result["m_volume"] = data["m_ksl_volume"] & VOLUME_MASK - result["c_ksl"] = data["c_ksl_volume"] & KSL_MASK - result["c_volume"] = data["c_ksl_volume"] & VOLUME_MASK + result["m_ksl"] = data["m_ksl_volume"] & KSL_MASK + result["m_volume"] = data["m_ksl_volume"] & VOLUME_MASK + result["c_ksl"] = data["c_ksl_volume"] & KSL_MASK + result["c_volume"] = data["c_ksl_volume"] & VOLUME_MASK - result["null"] = 0 - result["note_offset"] = offset + result["null"] = 0 + result["note_offset"] = offset + + return struct.pack( + "> sys.stderr, "%s: %s" % (filename, message) + def opl2_warning(message): + print("%s: %s" % (filename, message), file=sys.stderr) - # CHA,B control stereo, but are ignored on OPL2, so it's no problem: - #if (data["feedback_fm"] & 0xf0) != 0: - # opl2_warning("Cannot use CHA,B,C,D: %02x" % data["feedback_fm"]) + # CHA,B control stereo, but are ignored on OPL2, so it's no problem: + # if (data["feedback_fm"] & 0xf0) != 0: + # opl2_warning("Cannot use CHA,B,C,D: %02x" % data["feedback_fm"]) + + if data["m_waveform"] > 3: + opl2_warning( + "Modulator uses waveform %i: only 0-3 supported" + % data["m_waveform"] + ) + if data["c_waveform"] > 3: + opl2_warning( + "Carrier uses waveform %i: only 0-3 supported" % data["c_waveform"] + ) - if data["m_waveform"] > 3: - opl2_warning("Modulator uses waveform %i: only 0-3 supported" % - data["m_waveform"]) - if data["c_waveform"] > 3: - opl2_warning("Carrier uses waveform %i: only 0-3 supported" % - data["c_waveform"]) def load_instrument(filename): - # As a hack, a literal dictionary of the values can be specified - # in place of a filename. + # As a hack, a literal dictionary of the values can be specified + # in place of a filename. - if isinstance(filename, dict): - return filename + if isinstance(filename, dict): + return filename - filename = os.path.join("instruments", filename) + filename = os.path.join("instruments", filename) - if filename.endswith(".a2i"): - result = a2i_file.read(filename) - elif filename.endswith(".sbi"): - result = sbi_file.read(filename) - else: - raise Exception("Unknown instrument file type: '%s'" % filename) + if filename.endswith(".a2i"): + result = a2i_file.read(filename) + elif filename.endswith(".sbi"): + result = sbi_file.read(filename) + else: + raise Exception("Unknown instrument file type: '%s'" % filename) - check_opl2(filename, result) + check_opl2(filename, result) + + return result - return result class Instrument: - def __init__(self, file1, file2=None, off1=0, off2=0, note=None): - self.voice1 = load_instrument(file1) + def __init__(self, file1, file2=None, off1=0, off2=0, note=None): + self.voice1 = load_instrument(file1) - if file2 is not None: - self.voice2 = load_instrument(file2) - else: - self.voice2 = None + if file2 is not None: + self.voice2 = load_instrument(file2) + else: + self.voice2 = None + + self.fixed_note = note + self.offset1 = off1 + self.offset2 = off2 - self.fixed_note = note - self.offset1 = off1 - self.offset2 = off2 NullInstrument = Instrument("dummy.sbi") if __name__ == "__main__": - for filename in sys.argv[1:]: - Instrument(filename) - + for filename in sys.argv[1:]: + Instrument(filename) diff --git a/lumps/genmidi/midi.py b/lumps/genmidi/midi.py index 704a1a94..38b55287 100644 --- a/lumps/genmidi/midi.py +++ b/lumps/genmidi/midi.py @@ -10,46 +10,58 @@ # D in Octave 0: O0.D # E in Octave 2: O2.E -class Octave: - def __init__(self, base): - self.C = base - self.Cs = base + 1 - self.Db = base + 1 - self.D = base + 2 - self.Ds = base + 3 - self.Eb = base + 3 - self.E = base + 4 - self.F = base + 5 - self.Fs = base + 6 - self.Gb = base + 6 - self.G = base + 7 - self.Gs = base + 8 - self.Ab = base + 8 - self.A = base + 9 - self.As = base + 10 - self.Bb = base + 10 - self.B = base + 11 -On5 = Octave(0) # Octave -5 -On4 = Octave(12) # Octave -4 -On3 = Octave(24) # Octave -3 -On2 = Octave(36) # Octave -2 -On1 = Octave(48) # Octave -1 -O0 = Octave(60) # Octave 0 -O1 = Octave(72) # Octave 1 -O2 = Octave(84) # Octave 2 -O3 = Octave(96) # Octave 3 -O4 = Octave(108) # Octave 4 -O5 = Octave(120) # Octave 5 +class Octave: + def __init__(self, base): + self.C = base + self.Cs = base + 1 + self.Db = base + 1 + self.D = base + 2 + self.Ds = base + 3 + self.Eb = base + 3 + self.E = base + 4 + self.F = base + 5 + self.Fs = base + 6 + self.Gb = base + 6 + self.G = base + 7 + self.Gs = base + 8 + self.Ab = base + 8 + self.A = base + 9 + self.As = base + 10 + self.Bb = base + 10 + self.B = base + 11 + + +On5 = Octave(0) # Octave -5 +On4 = Octave(12) # Octave -4 +On3 = Octave(24) # Octave -3 +On2 = Octave(36) # Octave -2 +On1 = Octave(48) # Octave -1 +O0 = Octave(60) # Octave 0 +O1 = Octave(72) # Octave 1 +O2 = Octave(84) # Octave 2 +O3 = Octave(96) # Octave 3 +O4 = Octave(108) # Octave 4 +O5 = Octave(120) # Octave 5 # Given a MIDI note number, return a note definition in terms of the # constants above. + def def_for_note(note): - OCTAVES = [ "On5", "On4", "On3", "On2", "On1", - "O0", "O1", "O2", "O3", "O4", "O5" ] - NOTES = [ "C", "Cs", "D", "Ds", "E", "F", "Fs", - "G", "Gs", "A", "As", "B" ] - - return "%s.%s" % (OCTAVES[note // 12], NOTES[note % 12]) + OCTAVES = [ + "On5", + "On4", + "On3", + "On2", + "On1", + "O0", + "O1", + "O2", + "O3", + "O4", + "O5", + ] + NOTES = ["C", "Cs", "D", "Ds", "E", "F", "Fs", "G", "Gs", "A", "As", "B"] + return "%s.%s" % (OCTAVES[note // 12], NOTES[note % 12]) diff --git a/lumps/genmidi/mkgenmidi b/lumps/genmidi/mkgenmidi index 9b9db247..f5d4ac3c 100755 --- a/lumps/genmidi/mkgenmidi +++ b/lumps/genmidi/mkgenmidi @@ -5,10 +5,9 @@ import genmidi import sys if len(sys.argv) != 2: - print >> sys.stderr, "Usage: %s " % sys.argv[0] - sys.exit(-1) + print("Usage: %s " % sys.argv[0], file=sys.stderr) + sys.exit(-1) from config import INSTRUMENTS, PERCUSSION genmidi.write(sys.argv[1], INSTRUMENTS + PERCUSSION) - diff --git a/lumps/genmidi/sbi_file.py b/lumps/genmidi/sbi_file.py index ffa410f0..823a6d83 100644 --- a/lumps/genmidi/sbi_file.py +++ b/lumps/genmidi/sbi_file.py @@ -10,49 +10,51 @@ import sys HEADER_VALUE = "SBI\x1a" FIELDS = [ - "m_am_vibrato_eg", - "c_am_vibrato_eg", - "m_ksl_volume", - "c_ksl_volume", - "m_attack_decay", - "c_attack_decay", - "m_sustain_release", - "c_sustain_release", - "m_waveform", - "c_waveform", - "feedback_fm" + "m_am_vibrato_eg", + "c_am_vibrato_eg", + "m_ksl_volume", + "c_ksl_volume", + "m_attack_decay", + "c_attack_decay", + "m_sustain_release", + "c_sustain_release", + "m_waveform", + "c_waveform", + "feedback_fm", ] + def read(filename): - with open(filename, "rb") as f: - data = f.read() + with open(filename, "rb") as f: + data = f.read() - header, name = struct.unpack("4s32s", data[0:36]) - header = header.decode("ascii") + header, name = struct.unpack("4s32s", data[0:36]) + header = header.decode("ascii") - if header != HEADER_VALUE: - raise Exception("Invalid header for SBI file!") + if header != HEADER_VALUE: + raise Exception("Invalid header for SBI file!") - instr_data = data[36:] - result = { "name": name.decode("ascii").rstrip("\0") } + instr_data = data[36:] + result = {"name": name.decode("ascii").rstrip("\0")} - for i in range(len(FIELDS)): - result[FIELDS[i]], = struct.unpack("B", instr_data[i:i+1]) + for i in range(len(FIELDS)): + result[FIELDS[i]], = struct.unpack("B", instr_data[i : i + 1]) + + return result - return result def write(filename, data): - with open(filename, "wb") as f: - f.write(struct.pack("4s", HEADER_VALUE.encode("ascii"))) - f.write(struct.pack("32s", data["name"].encode("ascii"))) + with open(filename, "wb") as f: + f.write(struct.pack("4s", HEADER_VALUE.encode("ascii"))) + f.write(struct.pack("32s", data["name"].encode("ascii"))) + + for field in FIELDS: + f.write(struct.pack("B", data[field])) + for x in range(16 - len(FIELDS)): + f.write(struct.pack("B", 0)) - for field in FIELDS: - f.write(struct.pack("B", data[field])) - for x in range(16 - len(FIELDS)): - f.write(struct.pack("B", 0)) if __name__ == "__main__": - for filename in sys.argv[1:]: - print(filename) - print(read(filename)) - + for filename in sys.argv[1:]: + print(filename) + print(read(filename)) diff --git a/lumps/playpal/playpal b/lumps/playpal/playpal index 3ee6109d..14d1e21c 100755 --- a/lumps/playpal/playpal +++ b/lumps/playpal/playpal @@ -33,73 +33,82 @@ R3 = 1.0 / 3 R6 = 1.0 / 6 PI = 3.141592 + def ihs_to_rgb(i, h, s): - i = (i * 422) / 255 - h = (h * 2 * PI) / 255 - s = (s * 208.2066) / 255 + i = (i * 422) / 255 + h = (h * 2 * PI) / 255 + s = (s * 208.2066) / 255 - b, x = s * math.cos(h), s * math.sin(h) + b, x = s * math.cos(h), s * math.sin(h) + + return ( + R3 * i - R6 * b - R2 * x, + R3 * i - R6 * b + R2 * x, + R3 * i + R6 * 2 * b, + ) - return (R3 * i - R6 * b - R2 * x, - R3 * i - R6 * b + R2 * x, - R3 * i + R6 * 2 * b) # New palette builder + def make_pal_range(i, h, s, n): - map_function = lambda x: ihs_to_rgb(i * (n - x) / n, - h, - s * (n - x) / n), + map_function = (lambda x: ihs_to_rgb(i * (n - x) / n, h, s * (n - x) / n),) + + return map(map_function, range(n)) - return map(map_function, range(n)) # Very crude traversal of the IHS colour ball -def make_palette_new(): - result = [] - - result += make_pal_range(255, 0, 0, 32) - for i in range(7): - result += make_pal_range(127, 171 + (i + 1) * 256 / 7, 255, 16) - - for i in range(7): - result += make_pal_range(256, (i + 1) * 256 / 7, 127, 16) +def make_palette_new(): + result = [] + + result += make_pal_range(255, 0, 0, 32) + + for i in range(7): + result += make_pal_range(127, 171 + (i + 1) * 256 / 7, 255, 16) + + for i in range(7): + result += make_pal_range(256, (i + 1) * 256 / 7, 127, 16) + # Return palette read from named file + def read_palette(filename): - f = open(filename, "rb") + f = open(filename, "rb") - colors = [] + colors = [] - for i in range(256): - data = f.read(3) - color = struct.unpack("BBB", data) + for i in range(256): + data = f.read(3) + color = struct.unpack("BBB", data) - colors.append(color) + colors.append(color) - f.close() + f.close() + + return colors - return colors def make_palette(filename): - if filename is None: - return make_palette_new - else: - return read_palette(filename) + if filename is None: + return make_palette_new + else: + return read_palette(filename) + # Old palette builder -#sub make_pal_range($$$$$$) -#{ +# sub make_pal_range($$$$$$) +# { # my ($rs,$gs,$bs,$re,$ge,$be) = @_; # return map { my $e = $_/16; my $s = 1-$e; # [$rs*$s + $re*$e, $gs*$s + $ge*$e, $bs*$s + $be * $e] } (1..16); -#} +# } # -#sub make_palette () -#{ +# sub make_palette () +# { # my @p = ( # make_pal_range(0,0,0,0,0,0), # hmmm # make_pal_range(255,255,255,255,0,0), # pinks @@ -118,57 +127,61 @@ def make_palette(filename): # make_pal_range(0,0,0,0,0,0), # hmmm # make_pal_range(0,0,0,0,0,0)); # hmmm # return \@p; -#} +# } # Now the PLAYPAL stuff - take the main palette and construct biased versions # for the palette translation stuff # Bias an entire palette + def bias_palette_towards(palette, target, p): + def bias_rgb(rgb): + r = [] - def bias_rgb(rgb): - r = [] + for i in range(3): + r.append(rgb[i] * (1 - p) + target[i] * p) - for i in range(3): - r.append(rgb[i] * (1 - p) + target[i] * p) + return r - return r + return map(bias_rgb, palette) - return map(bias_rgb, palette) # Encode palette in the 3-byte RGB triples format expected by the engine + def clamp_pixval(v): - if v < 0: - return 0 - elif v > 255: - return 255 - else: - return int(v) + if v < 0: + return 0 + elif v > 255: + return 255 + else: + return int(v) + def output_palette(pal): - for color in palette: - color = tuple(map(clamp_pixval, color)) - - encoded = struct.pack("BBB", *color) - os.write(sys.stdout.fileno(), encoded) + for color in palette: + color = tuple(map(clamp_pixval, color)) + + encoded = struct.pack("BBB", *color) + os.write(sys.stdout.fileno(), encoded) + # Main program - make a base palette, then do the biased versions if len(sys.argv) < 2: - print("Usage: %s > playpal.lmp" % sys.argv[0]) - sys.exit(1) + print("Usage: %s > playpal.lmp" % sys.argv[0]) + sys.exit(1) base_pal = read_palette(sys.argv[1]) # From st_stuff.c, Copyright 1999 id Software, license GPL -#define STARTREDPALS 1 -#define STARTBONUSPALS 9 -#define NUMREDPALS 8 -#define NUMBONUSPALS 4 -#define RADIATIONPAL 13 +# define STARTREDPALS 1 +# define STARTBONUSPALS 9 +# define NUMREDPALS 8 +# define NUMBONUSPALS 4 +# define RADIATIONPAL 13 palettes = [] @@ -179,21 +192,20 @@ palettes.append(base_pal) # STARTREDPALS for i in range(8): - p = (i + 1) / 8.0 + p = (i + 1) / 8.0 - palettes.append(bias_palette_towards(base_pal, (255, 0, 0), p)) + palettes.append(bias_palette_towards(base_pal, (255, 0, 0), p)) # STARTBONUSPALS for i in range(4): - p = (i + 1) * 0.4 / 4 + p = (i + 1) * 0.4 / 4 - palettes.append(bias_palette_towards(base_pal, (128, 128, 128), p)) + palettes.append(bias_palette_towards(base_pal, (128, 128, 128), p)) # RADIATIONPAL palettes.append(bias_palette_towards(base_pal, (0, 255, 0), 0.2)) for palette in palettes: - output_palette(palette) - + output_palette(palette) diff --git a/lumps/textures/build-textures b/lumps/textures/build-textures index ce4225a8..498e4a97 100755 --- a/lumps/textures/build-textures +++ b/lumps/textures/build-textures @@ -46,224 +46,240 @@ PATCH_NAME_RE = re.compile(r"\s*\*\s+([\w-]+)\s+(-?\d+)\s+(-?\d+)") Texture = collections.namedtuple("Texture", ["w", "h", "patches"]) TexturePatch = collections.namedtuple("TexturePatch", ["pname", "x", "y"]) + class TextureSet(collections.OrderedDict): - def __init__(self, pnames): - """Initialize a new set of textures. + def __init__(self, pnames): + """Initialize a new set of textures. - Args: - pnames: List of PNAMES to use for the textures. New - patches will be added to this list as the texture - set is extended. - """ - super(TextureSet, self).__init__() - self.pnames = pnames + Args: + pnames: List of PNAMES to use for the textures. New + patches will be added to this list as the texture + set is extended. + """ + super(TextureSet, self).__init__() + self.pnames = pnames - def pname_index(self, pname): - """Get the index into the PNAMES list for the given patch. + def pname_index(self, pname): + """Get the index into the PNAMES list for the given patch. - If the patch is not in the list, it will be added to the - list. + If the patch is not in the list, it will be added to the + list. - Args: - pname: Name of the patch to look up. - Returns: - Index into the PNAMES list where this patch can be found. - """ - pname = pname.upper() - try: - return self.pnames.index(pname) - except ValueError: - self.pnames.append(pname) - return len(self.pnames) - 1 + Args: + pname: Name of the patch to look up. + Returns: + Index into the PNAMES list where this patch can be found. + """ + pname = pname.upper() + try: + return self.pnames.index(pname) + except ValueError: + self.pnames.append(pname) + return len(self.pnames) - 1 - def add_texture(self, name, width=0, height=0): - """Add a new texture to the set. + def add_texture(self, name, width=0, height=0): + """Add a new texture to the set. - If a texture is already defined with the given name, the - current definition is erased (though the ordering of - textures is maintained). + If a texture is already defined with the given name, the + current definition is erased (though the ordering of + textures is maintained). - Args: - name: Name of the texture. - width: Width of the texture in pixels. - height: Height of the texture in pixels. - """ - name = name.upper() - self[name] = Texture(width, height, []) + Args: + name: Name of the texture. + width: Width of the texture in pixels. + height: Height of the texture in pixels. + """ + name = name.upper() + self[name] = Texture(width, height, []) - def add_texture_patch(self, txname, patch, x, y): - """Add a patch to the given texture. + def add_texture_patch(self, txname, patch, x, y): + """Add a patch to the given texture. - Args: - txname: Name of the texture. - patch: Name of the patch to add. - x: X offset for the patch in pixels. - y: Y offset for the patch in pixels. - """ - txname = txname.upper() - texture = self[txname] - tp = TexturePatch(self.pname_index(patch), x, y) - texture.patches.append(tp) + Args: + txname: Name of the texture. + patch: Name of the patch to add. + x: X offset for the patch in pixels. + y: Y offset for the patch in pixels. + """ + txname = txname.upper() + texture = self[txname] + tp = TexturePatch(self.pname_index(patch), x, y) + texture.patches.append(tp) - def write_texture_lump(self, filename): - """Build the texture lump and output to a file. + def write_texture_lump(self, filename): + """Build the texture lump and output to a file. - Args: - filename: Path to file in which to store the resulting - lump. - """ - with open(filename, "wb") as out: - # Header indicating number of textures: - out.write(struct.pack(" 8: + raise NamesFileError( + "Invalid name in %s: %s" % (filename, line) + ) + if line != "": + result.append(line.upper()) + return result - The names are a maximum of 8 characters long. - Args: - filename: Name of the file from which to read names. - Returns: - List of name strings, all in upper case. - """ - with open(filename) as f: - result = [] - for line in f: - line = COMMENT_RE.sub('', line).strip() - if len(line) > 8: - raise NamesFileError( - 'Invalid name in %s: %s' % (filename, line)) - if line != '': - result.append(line.upper()) - return result def write_names_file(names, filename, sprites_dir): - """Write a list of names to a file. + """Write a list of names to a file. + + Args: + names: List of names to write. + filename: Filename to write them to. + sprites_dir: Path to directory containing sprites. If a file is found + in this directory matching a patch name, that patch will not be + included in the outputted list. + """ + with open(filename, "w") as f: + for name in names: + filename = "%s.png" % name.lower() + sprite_path = os.path.join(sprites_dir, filename) + if not os.path.exists(sprite_path): + f.write("%s\n" % name) - Args: - names: List of names to write. - filename: Filename to write them to. - sprites_dir: Path to directory containing sprites. If a file is found - in this directory matching a patch name, that patch will not be - included in the outputted list. - """ - with open(filename, "w") as f: - for name in names: - filename = "%s.png" % name.lower() - sprite_path = os.path.join(sprites_dir, filename) - if not os.path.exists(sprite_path): - f.write("%s\n" % name) def load_compat_textures(textures, compat_file): - """Pre-populate a texture set from a compatibility file. + """Pre-populate a texture set from a compatibility file. - Args: - textures: TextureSet to populate. - compat_file: Path to text file containing list of textures. If - None, do not do anything. - """ - if compat_file is None: - return + Args: + textures: TextureSet to populate. + compat_file: Path to text file containing list of textures. If + None, do not do anything. + """ + if compat_file is None: + return + + for texture in read_names_file(compat_file): + textures.add_texture(texture) - for texture in read_names_file(compat_file): - textures.add_texture(texture) class TextureConfigError(Exception): - pass + pass + def parse_textures(stream): - """Parse texture config from the given input stream. + """Parse texture config from the given input stream. - Args: - stream: Input stream from which to read lines of input. - Yields: - A tuple for each parsed texture, containing: - Texture name - Width - Height - List of tuples representing each patch, where each contains: - Patch name - X offset - Y offset - """ - current_texture = None - current_patches = [] - linenum = 0 - for line in sys.stdin: - line = COMMENT_RE.sub('', line).strip() - linenum += 1 + Args: + stream: Input stream from which to read lines of input. + Yields: + A tuple for each parsed texture, containing: + Texture name + Width + Height + List of tuples representing each patch, where each contains: + Patch name + X offset + Y offset + """ + current_texture = None + current_patches = [] + linenum = 0 + for line in sys.stdin: + line = COMMENT_RE.sub("", line).strip() + linenum += 1 - match = TEXTURE_NAME_RE.match(line) - if match: - if current_texture is not None: - yield current_texture + match = TEXTURE_NAME_RE.match(line) + if match: + if current_texture is not None: + yield current_texture - current_patches = [] - current_texture = ( - match.group(1), # Texture name - int(match.group(2)), # Width - int(match.group(3)), # Height - current_patches, # List of patches - ) - continue + current_patches = [] + current_texture = ( + match.group(1), # Texture name + int(match.group(2)), # Width + int(match.group(3)), # Height + current_patches, # List of patches + ) + continue - match = PATCH_NAME_RE.match(line) - if match and current_texture: - current_patches.append(( - match.group(1), # Patch name - int(match.group(2)), # X offset - int(match.group(3)), # Y offset - )) - continue + match = PATCH_NAME_RE.match(line) + if match and current_texture: + current_patches.append( + ( + match.group(1), # Patch name + int(match.group(2)), # X offset + int(match.group(3)), # Y offset + ) + ) + continue - if line != '': - raise TextureConfigError( - 'input:%i:Invalid config line: %s' % - (linenum, line)) + if line != "": + raise TextureConfigError( + "input:%i:Invalid config line: %s" % (linenum, line) + ) + + # Last texture: + if current_texture is not None: + yield current_texture - # Last texture: - if current_texture is not None: - yield current_texture def write_pnames_lump(pnames, filename): - """Write a PNAMES list to a file. + """Write a PNAMES list to a file. + + Args: + pnames: List of strings containing patch names. + filename: Output filename. + """ + with open(filename, "wb") as out: + out.write(struct.pack("II", chunk[1]) self.has_offset = True + def main(): dirname = path.split(path.abspath(__file__))[0] graphics = [] files = [] if len(sys.argv) < 2: - print("This script takes the offsets stored in the \"grAb\" chunk of " + - "the specified PNGs and adjusts the graphics offsets in the build_cfg file.\n") + print( + 'This script takes the offsets stored in the "grAb" chunk of ' + + "the specified PNGs and adjusts the graphics offsets in the build_cfg file.\n" + ) print("Usage:\n\t fix-sprite-offsets [names] [...]\n") - print("example: \n\tfix-sprite-offsets sprites/vilea1.png sprites/vileb1.png") + print( + "example: \n\tfix-sprite-offsets sprites/vilea1.png sprites/vileb1.png" + ) print("You can also use wildcards:") print("\t fix-sprite-offsets sprites/vile*.png") exit() @@ -40,7 +46,7 @@ def main(): for filepath in files: if not path.isfile(filepath): - print("Could not find" + filepath +", skipping...") + print("Could not find" + filepath + ", skipping...") elif path.splitext(filepath)[1] == ".png": graphics.append(Graphic(filepath)) for graphic in graphics: @@ -56,7 +62,11 @@ def main(): if graphic.has_offset is True: thing = line.split() if len(thing) > 0 and thing[0] == graphic.name: - new_string = "%s\t%i\t%i" %(graphic.name, graphic.xoffset, graphic.yoffset) + new_string = "%s\t%i\t%i" % ( + graphic.name, + graphic.xoffset, + graphic.yoffset, + ) comments = line.split(";") if len(comments) > 1: new_string += "\t;" + "".join(comments[1:]) @@ -72,5 +82,6 @@ def main(): f.writelines(newlines) f.close() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/scripts/fix-map-names b/scripts/fix-map-names index f839d345..45fefdb9 100755 --- a/scripts/fix-map-names +++ b/scripts/fix-map-names @@ -22,16 +22,16 @@ import sys # Globals -args = {} # Command line arguments. -error_count = 0 -fixes_needed = 0 -freedoom_1_re = re.compile(r"^C(\d)M(\d)$") # FD #1 maps -freedoom_dm_re = re.compile(r"^DM(\d\d)$") # FD DM maps -header_shown = False -ignored_wads = set(["dummy.wad", "test_levels.wad"]) -last_error = None -map_name_re = re.compile(r"^((E\dM\d)|(MAP\d\d))$") -output_line = "%-17s %-9s %-7s %s" +args = {} # Command line arguments. +error_count = 0 +fixes_needed = 0 +freedoom_1_re = re.compile(r"^C(\d)M(\d)$") # FD #1 maps +freedoom_dm_re = re.compile(r"^DM(\d\d)$") # FD DM maps +header_shown = False +ignored_wads = set(["dummy.wad", "test_levels.wad"]) +last_error = None +map_name_re = re.compile(r"^((E\dM\d)|(MAP\d\d))$") +output_line = "%-17s %-9s %-7s %s" # Functions @@ -44,6 +44,7 @@ def error(msg): if msg: error_count += 1 + # Given WAD path 'wad' return the expected map name as a function of the # filename. def get_expected_map_name(wad): @@ -61,31 +62,51 @@ def get_expected_map_name(wad): else: return None + # Parse the command line arguments and store the result in 'args'. def parse_args(): global args parser = argparse.ArgumentParser( description="Fix map names in WAD files.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) # The following is sorted by long argument. - parser.add_argument("-f", "--force", action="store_true", - help="Force. Fix map name regardless of the existing map name.") - parser.add_argument("-q", "--quiet", action="store_true", - help="Quiet (minimum output).") - parser.add_argument("-r", "--recursive", action="store_true", - help="Recurse into directories.") - parser.add_argument("-t", "--test", action="store_true", - help="Test mode. Don't make any changes.") - parser.add_argument("paths", metavar="PATH", nargs="+", - help="WAD paths, files and directories.") + parser.add_argument( + "-f", + "--force", + action="store_true", + help="Force. Fix map name regardless of the existing map name.", + ) + parser.add_argument( + "-q", "--quiet", action="store_true", help="Quiet (minimum output)." + ) + parser.add_argument( + "-r", + "--recursive", + action="store_true", + help="Recurse into directories.", + ) + parser.add_argument( + "-t", + "--test", + action="store_true", + help="Test mode. Don't make any changes.", + ) + parser.add_argument( + "paths", + metavar="PATH", + nargs="+", + help="WAD paths, files and directories.", + ) args = parser.parse_args() return args + # Process path 'path' which is at depth 'depth'. If 'depth' is 0 then this is # a top level path passed in on the command line. def process_path(path, depth): @@ -103,11 +124,13 @@ def process_path(path, depth): if (not depth) or path.lower().endswith(".wad"): process_wad(path) + # Process the paths passed in on the command line. def process_paths(): for path in args.paths: process_path(path, 0) + # Process WAD path 'wad'. def process_wad(wad): global header_shown @@ -141,7 +164,8 @@ def process_wad(wad): # The first lump in the directory, which should be the 0 byte map # name one. lump_data_offset, lump_size, lump_name = struct.unpack( - " " + command) os.system(command) + # Find the version to build: version = os.getenv("VERSION") @@ -22,7 +24,7 @@ version = os.getenv("VERSION") if version is None: sys.stderr.write("Version not specified for release\n") sys.exit(1) -if version[0] is 'v': +if version[0] is "v": # Strip the leading 'v' from versioning version = version[1:] diff --git a/scripts/music-duplicates b/scripts/music-duplicates index ac83135b..54b41764 100755 --- a/scripts/music-duplicates +++ b/scripts/music-duplicates @@ -10,65 +10,71 @@ import os import re import sys -PHASE1_MATCH_RE = re.compile(r'(e\dm\d)', re.I) -PHASE2_MATCH_RE = re.compile(r'(map\d\d)', re.I) -FREEDM_MATCH_RE = re.compile(r'(dm\d\d)', re.I) +PHASE1_MATCH_RE = re.compile(r"(e\dm\d)", re.I) +PHASE2_MATCH_RE = re.compile(r"(map\d\d)", re.I) +FREEDM_MATCH_RE = re.compile(r"(dm\d\d)", re.I) + def get_music_tracks(): - """Returns a dictionary mapping from MIDI file SHA1 + """Returns a dictionary mapping from MIDI file SHA1 to a list of game tracks that use that MIDI.""" - result = {} - musics_path = os.path.join(os.path.dirname(sys.argv[0]), '../musics') - for mus in glob('%s/*.mid' % musics_path): - with open(mus, 'rb') as f: - contents = f.read() - m = hashlib.sha1() - m.update(contents) - digest = m.digest() - basename = os.path.basename(mus) - result.setdefault(digest, []).append(basename) - return result + result = {} + musics_path = os.path.join(os.path.dirname(sys.argv[0]), "../musics") + for mus in glob("%s/*.mid" % musics_path): + with open(mus, "rb") as f: + contents = f.read() + m = hashlib.sha1() + m.update(contents) + digest = m.digest() + basename = os.path.basename(mus) + result.setdefault(digest, []).append(basename) + return result + def get_prime_track(tracks): - """Given a list of tracks that all use the same MIDI, find the + """Given a list of tracks that all use the same MIDI, find the "prime" one (the one that isn't a reuse/duplicate).""" - # We have almost all Phase 2 tracks fulfilled. So if the same - # track is used in Phase 1 and Phase 2, or Phase 2 and FreeDM, - # the Phase 2 track is probably the leader. - phase2_tracks = [x for x in tracks if PHASE2_MATCH_RE.search(x)] - if len(phase2_tracks) == 1: - return phase2_tracks[0] + # We have almost all Phase 2 tracks fulfilled. So if the same + # track is used in Phase 1 and Phase 2, or Phase 2 and FreeDM, + # the Phase 2 track is probably the leader. + phase2_tracks = [x for x in tracks if PHASE2_MATCH_RE.search(x)] + if len(phase2_tracks) == 1: + return phase2_tracks[0] - # FreeDM music has been hand-picked. So if it is used for both - # Phase 1 and FreeDM, assume it's probably a FreeDM track. - freedm_tracks = [x for x in tracks if FREEDM_MATCH_RE.search(x)] - if len(freedm_tracks) == 1: - return freedm_tracks[0] + # FreeDM music has been hand-picked. So if it is used for both + # Phase 1 and FreeDM, assume it's probably a FreeDM track. + freedm_tracks = [x for x in tracks if FREEDM_MATCH_RE.search(x)] + if len(freedm_tracks) == 1: + return freedm_tracks[0] + + # We're out of options. Pick the first one in the list. + # print "Warning: Don't know which of %s is the leader." % tracks + return sorted(tracks)[0] - # We're out of options. Pick the first one in the list. - #print "Warning: Don't know which of %s is the leader." % tracks - return sorted(tracks)[0] def find_missing_tracks(tracks): - """Given a dictionary of tracks, get a list of "missing" tracks.""" - result = [] - for midi, tracks in tracks.items(): - if len(tracks) < 2: - continue - prime_track = get_prime_track(tracks) - result.extend(x for x in tracks if x != prime_track) - return result + """Given a dictionary of tracks, get a list of "missing" tracks.""" + result = [] + for midi, tracks in tracks.items(): + if len(tracks) < 2: + continue + prime_track = get_prime_track(tracks) + result.extend(x for x in tracks if x != prime_track) + return result + def tracks_matching_regexp(tracks, regexp): - return set([x for x in tracks if regexp.search(x)]) + return set([x for x in tracks if regexp.search(x)]) + def print_report(title, tracks): - if len(tracks) == 0: - return - print(title) - for track in sorted(tracks): - print('\t%s' % track.replace('.mid', '').upper()) - print('') + if len(tracks) == 0: + return + print(title) + for track in sorted(tracks): + print("\t%s" % track.replace(".mid", "").upper()) + print("") + missing_tracks = set(find_missing_tracks(get_music_tracks())) phase1_tracks = tracks_matching_regexp(missing_tracks, PHASE1_MATCH_RE) @@ -76,8 +82,8 @@ phase2_tracks = tracks_matching_regexp(missing_tracks, PHASE2_MATCH_RE) freedm_tracks = tracks_matching_regexp(missing_tracks, FREEDM_MATCH_RE) other_tracks = missing_tracks - phase1_tracks - phase2_tracks - freedm_tracks -print('=== Missing tracks (tracks currently using duplicates):\n') -print_report('Phase 1 tracks:', phase1_tracks) -print_report('Phase 2 tracks:', phase2_tracks) -print_report('FreeDM tracks:', freedm_tracks) -print_report('Other tracks:', other_tracks) +print("=== Missing tracks (tracks currently using duplicates):\n") +print_report("Phase 1 tracks:", phase1_tracks) +print_report("Phase 2 tracks:", phase2_tracks) +print_report("FreeDM tracks:", freedm_tracks) +print_report("Other tracks:", other_tracks)