#
# 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 += '' + 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 + ' | '
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 += '
' # 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!