#!/usr/bin/env python # Transpose chord files in Adam's reasonably-regular format. import cgi, sys, os, re, getopt, StringIO # Testcases are transposed up two semitones; None means it shouldn't parse. testcases = [ # Things that shouldn't parse ("blah", None), ("Colorado", None), ("Jackdaws love my big sphinx of quartz", None), # Non-chords ("--", "--"), ("NC", "NC"), ("A -- C", "B -- D"), ("A NC C", "B NC D"), # Note names ("A", "B"), ("A#", "C "), ("G#", "Bb"), ("Bb", "C "), # Chord names ("Am", "Bm"), ("A7", "B7"), ("Adim", "Bdim"), ("Asus4", "Bsus4"), ("A-5", "B-5"), # Chord suffixes ("A/C", "B/D"), ("D D/C", "E E/D"), ("D /C", "E /D"), # Spacing ("B B B", "Db Db Db"), ("B B B", "Db Db Db"), ("Bb Bb Bb", "C C C "), #("B B Bb", "Db Db C "), -- it's not smart enough to do this yet ("\tA C", " B D"), ] safe_fn_re = re.compile(r'[a-z0-9][a-z0-9_.-]*', re.I) notes = ["Ab", "A", "Bb", "B", "C", "Db", "D", "Eb", "E", "F", "F#", "G"] def parse_note(s): if len(s) > 2: raise ValueError("bad note") try: n = notes.index(s) except ValueError: n = -1 if n != -1: return n elif len(s) > 1 and s[-1] == "b": return parse_note(s[0]) - 1 elif len(s) > 1 and s[-1] == "#": return parse_note(s[0]) + 1 else: raise ValueError("bad note") magic_chord_names = ["NC", "--", "-"] def parse_chord(s): if s == "Colorado": raise ValueError("Colorado is not a chord") if s in magic_chord_names: return (0, s, None) i = s.find("/") if i == -1: bass = None else: if s[i + 1:] == "NB": bass = "NB" else: bass = parse_note(s[i + 1:]) s = s[:i] if i == 0: return (None, s, bass) if len(s) > 2 and s[2] in "b#": root = parse_note(s[:3]) s = s[3:] elif len(s) > 1 and s[1] in "b#": root = parse_note(s[:2]) s = s[2:] elif len(s) > 0: root = parse_note(s[0]) s = s[1:] else: raise ValueError("nothing left to parse") return (root, s, bass) def format_note(n): if n is None: return "" return notes[n % 12] def format_chord(root, name, bass): if name in magic_chord_names: return name s = format_note(root) + name if bass is not None: if bass == "NB": s += "/NB" else: s += "/" + format_note(bass) return s def format_nash_note(n, key, roman): if n is None: return "" root = key[0] if key[1] == "m": root += 3 if roman: nash_notes = ["I", "bII", "II", "bIII", "III", "IV", "bV", "V", "bVI", "VI", "bVII", "VII"] else: nash_notes = ["1", "b2", "2", "b3", "3", "4", "b5", "5", "b6", "6", "b7", "7"] return nash_notes[(n - root) % 12] def format_nash(root, name, bass, key, roman): if name in magic_chord_names: return name s = format_nash_note(root, key, roman) + name if bass is not None: if bass == "NB": s += "/NB" else: s += "/" + format_nash_note(bass, key, roman) return s def split_pos(s): out = [] i = 0 last = -1 while i < len(s) + 1: if i == len(s) or s[i] == " ": if last != -1: out.append((last, s[last:i])) last = -1 else: if last == -1: last = i i += 1 return out def insert_space(s, pos, n): i = pos while i < len(s): if s[i] == " ": break i += 1 if i == len(s): return s else: return s[:i] + (" " * n) + s[i:] def transpose_file(f, key = 0, roman = False, nash = False): lines = [s[:-1].expandtabs() for s in f.readlines()] song_key = None chord_lines = [] for i in range(len(lines)): cs = lines[i].strip().split() if len(cs) == 2 and cs[0] == "Key:": try: song_key = parse_chord(cs[1]) (r, n, b) = song_key r += key if b is not None and b != "NB": b += key lines[i] = "Key: " + format_chord(r, n, b) except ValueError: pass is_chord = (cs != []) for c in cs: try: (r, n, b) = parse_chord(c) except ValueError: is_chord = 0 break chord_lines.append(is_chord) if song_key is None or key != 0: nash = 0 for i in range(len(lines)): if not chord_lines[i]: continue cs = split_pos(lines[i]) adjust = 0 for pos, chord in cs: pos += adjust (root, name, bass) = parse_chord(chord) if root is not None: root += key if bass is not None and bass != "NB": bass += key if nash: new_chord = format_nash(root, name, bass, song_key, roman) else: new_chord = format_chord(root, name, bass) while len(new_chord) < len(chord): new_chord += " " n = len(new_chord) - len(chord) if n > 0: needed = 0 for c in lines[i][pos + len(chord):pos + len(new_chord) + 1]: if c != " ": needed = 1 if not needed: n = 0 if n > 0: lines[i] = insert_space(lines[i], pos, n) if i + 1 < len(lines) and not chord_lines[i + 1]: lines[i + 1] = insert_space(lines[i + 1], pos, n) adjust += n lines[i] = lines[i][:pos] + new_chord + lines[i][pos + len(new_chord):] return song_key, zip(lines, chord_lines) def die(s): sys.stdout.write("
Error: " + cgi.escape(s) + "