# # MIT License # # Copyright (c) 2016-2020 Christian-E! / Ten by Ten Software # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # # # This script will convert the program list for # Nord Electro 4, 6 # Nord Stage 2, 2EX, 3 # Nord Lead A1 (programs and performances) # as exported from # Nord Sound Manager 6.86 build 734_12 [OSX Intel] # into a grid-style bank and program table. # import argparse import sys from xml.etree import ElementTree parser = argparse.ArgumentParser(description='Creates a Program Reference Card for Nord Keyboards. This version of the script supports at minimum the following models: Electro 4, 6; Lead A1; Stage 2, 2EX, 3.') parser.add_argument('-o', '--outputFile', type=str, help='the output HTML file') parser.add_argument('-r', '--reverse', action='store_const', const=1, default=0, help='print the program pages in reverse order (from high to low)') parser.add_argument('-R', '--rotate', action='store_const', const=1, default=0, help='rotate the program page rows and columns') parser.add_argument('-t', '--title', type=str, help='an optional title to print above each bank') parser.add_argument('-v', '--verbose', action='store_const', const=1, default=0, help='print the sample name(s) or organ model below the program name') parser.add_argument('inputFile', type=str, help='the input Nord Sound Manager Program HTML file') parser.add_argument('--eurostile', action='store_const', const=1, default=0, help='use Eurostile Extd font for titles and banks') args = parser.parse_args() # print(args) xml = '' # read the input HTML filename = args.inputFile f = open(filename) html = f.read() # reduce the HTML to just the table tableBegin = html.find('') tableEnd = html.find('
',tableBegin) html = html[tableBegin:tableEnd+8] # remove unquoted styling html = html.replace(' class=odd','') # escape ampersands (assuming there are no escaped ones already) html = html.replace('&','&') # fix the header lines; they end in instead of lfixth = html.rsplit('') for line in lfixth: tdIndex = line.find('') if tdIndex == -1: xml += line else: if tdIndex == len(line)-6: xml += '' + line.rstrip('\n') + '' else: xml += '' + line[:tdIndex] + '' + line[tdIndex+6:] # parse the table table = ElementTree.XML(xml) # see how many columns are in each row of the table and use this to try to detect the Nord model rows = iter(table) numCols = len(next(rows)) # headers = [col.text for col in next(rows)] # Electro 4 & Stage write location as "page:program" (01:1, 01:2, 02:1, 02:2...) # but Electro 6 & Stage 3 skip the colon: "pageprogram" (11, 12, 21, 22 ...) locationFormat = '{:02d}:{}' bankColumn = 1 locationColumn = 2 nameColumn = 3 # ----- Electro 4 ----- # 32 pages of 4 programs # table data: 8 columns # 2:location (page:program) # 3:name # 4:category # 5:version # 6:sample if numCols == 8: numPages = 32 numBanks = 1 numPrograms = 4 numColumns = 4 hasNumberedBanks = False verboseValues = {6} # ----- Electro 6 ----- # 26 banks of 16 programs # table data: 9 columns # 1:bank # 2:location (pageprogram) # 3:name # 4:category # 5:version # 6:piano # 7:sample if numCols == 9: numPages = 4 numBanks = 26 numPrograms = 4 numColumns = 4 hasNumberedBanks = False verboseValues = {6,7} locationFormat = '{}{}' # ----- Stage 2 ----- # 2 banks with 20 pages of 5 programs # table data: 11 columns # 1:bank # 2:location (page:program) # 3:name # 4:category # 5:version # 6:piano A # 7:sample A # 8:piano B # 9:sample B if numCols == 11: numPages = 20 numBanks = 4 numPrograms = 5 numColumns = 5 hasNumberedBanks = False verboseValues = {6,7,8,9} # ----- Stage 3 ----- # detect location as "pageprogram" (11, 12, 21, 22 ...) in the first entry if table[1][locationColumn].text.find(':') == -1: numPages = 5 numBanks = 26 locationFormat = '{}{}' # ----- Lead A1 ----- # 4 banks of 50 performances or 8 banks of 50 programs # table data: 7 columns # 1:bank A-D or 1-8 # 2:location (program) # 3:name # 4:category # 5:version if numCols == 7: numPages = 1 numPrograms = 50 numColumns = 5 verboseValues = {4} hasNumberedBanks = bool(html.find('Bank 1') != -1) if hasNumberedBanks: numBanks = 8 else: numBanks = 4 # this is a guess for NL4, NL2X if html.find('51') != -1: numPrograms = 100 # TODO refactor so Lead A1 can be rotated if args.rotate and numPages>1: numPages,numPrograms = numPrograms,numPages # function to find values based on bank and location def findValues(table, bankName, location): rows = iter(table) for row in rows: if row[locationColumn].text == location and row[bankColumn].text == bankName: return [col.text for col in row] # function to see if there are any values for this bank def isBankEmpty(table, bankName): rows = iter(table) for row in rows: if row[bankColumn].text == bankName: return False return True # build new html html = '\n' html += '\n' if args.title: html += '' + args.title + '' html += '\n' html += '\n' for bank in range(0,numBanks): if numBanks>1: if hasNumberedBanks: bankName = 'Bank {:X}'.format(bank+1) else: bankName = 'Bank {:c}'.format(bank+0x41) # 0x41 is 'A' else: bankName = 'Program' # save space if this bank is empty if isBankEmpty(table,bankName): continue if args.title: html += '

' + args.title + '

\n' html += '
' if numBanks>1: html += '

' + bankName + '

\n' html += '
\n' for p in range(0,numPages): page = p+1 if args.reverse: page = numPages-p html += '' for program in range(1,numPrograms+1): if numPages>1: if args.rotate: location = locationFormat.format(program,page) else: location = locationFormat.format(page,program) else: location = '{}'.format(program) name = '' samp = '' cat = '' values = findValues(table,bankName,location) if values: name = values[nameColumn] html += '' else: html += '
' # + samp + '
' for v in verboseValues: html += values[v] if len(verboseValues)>1: html += '
' html += '' html += '' if numPages==1 and (program % numColumns) == 0: html += '\n' html += '\n' html += '
' + location + '
' + name # for organs, show the model. otherwise, show the sample name, or category or something if args.verbose and values: cat = values[4] if cat == 'B3' or cat == 'Farf' or cat == 'Vx': html += '

' + cat + '
' # programs and bank divs if bank < numBanks-1: html += '
' html += '' # optionally write directly to a file if args.outputFile: outHtmlFile = open(args.outputFile, 'w') outHtmlFile.write(html) else: print(html) # This was my first Python script. Thanks for watching!