Source

nwc2ly / nwc2ly.py

Full commit
#! /usr/bin/env python
# -*- coding: utf8 -*-
#
# Noteworthy Composer Text Format Converter
# (Korean version)
#
# The binary nwc format may have incompatible changes
# when the version of nwc is updated, but nwctxt can be
# readable rather stably.
import sys, codecs

position_bases = {
    'Treble': 67,  # 가온 다 오른쪽 첫번째 B(시)음
    'Bass': 49,    # 가온 다 왼쪽 첫번째 D(레)음
}
# We cannot distinguish major and minor keys currently.
keysig_map = {
    '0': 'c \major',
    '#1': 'g \major',
    '#2': 'd \major',
    '#3': 'a \major',
    '#4': 'e \major',
    '#5': 'b \major',
    '#6': 'fis \major',
    '#7': 'cis \major',
    'b1': 'f \major',
    'b2': 'bes \major',
    'b3': 'ees \major',
    'b4': 'aes \major',
    'b5': 'des \major',
    'b6': 'ges \major',
    'b7': 'ces \major',
}
accidental_map = {
    '#': 'is',
    'x': 'isis',
    'b': 'es',
    'v': 'eses',
}
barline_map = {
    'Single': '|',
    'Double': '||',
    'SectionOpen': '.|',
    'SecionClose': '|.',
    'MasterRepeatOpen': '|:',
    'MasterRepeatClose': ':|',
    'CloseRepeatOpen': '|:',
    'CloseRepeatClose': ':|',
}

# Assumptions:
# - All staves have the same time signature and the same key signature.
#   (including intermediate changes)
#   Exceptions may apply for percussion staves.
# - All staves share a single global tempo.
# - 

def usage():
    # TODO
    print 'blah blah'

def parse_parts(parts):
    data = {}
    for item in parts:
        k, v = item.split(':')
        if v == 'Y':
            v = True
        elif v == 'N':
            v = False
        elif v.startswith('"') and v.endswith('"'):
            v = v[1:-1].replace('\\r\\n', '\r\n').decode('cp949')
        data[k] = v
    return data

def parse_opts(value):
    data = {}
    for item in value.split(','):
        try:
            k, v = item.split('=')
        except ValueError:
            k = item
            v = True
        data[k] = v
    return data

def get_absolute_octave(relative_pos, clef='Treble'):
    return position_bases[clef] + relative_pos

def get_relative_octave(pos, last_pos):
    octave = ''
    diff = pos - last_pos 
    if diff > 3:
        for i in range((diff - 4 + 7) / 7):
            octave += "'"
        if octave == '':
            octave += "'"
    elif diff < -3:
        for i in range((-diff - 4 + 7) / 7):
            octave += ","
        if octave == '':
            octave += ","
    return octave

def normalize_name(name):
    return name.replace(' ', '_')

def convert_key(keysig):
    sharps = keysig.count('#')
    flats = keysig.count('b')
    if sharps == 0 and flats == 0:
        return keysig_map['0']
    if sharps > 0 and flats == 0:
        return keysig_map['#' + str(sharps)]
    if sharps == 0 and flats > 0:
        return keysig_map['b' + str(flats)]
    else:
        print 'WARNING: unsupported keysignature (%s)' % keysig
    return None

def convert_in_context(ctx, token_type, token_data):
    # TODO: This structure can be used at Python scripts for the 'User Tools' feature in NWC 2.0 or later.
    pass

def convert_file(input_file, output_file):
    staff_began = False
    num_staves, num_processed_staves = 0, 0
    num_notes, num_bars = 0, 0
    output_file.write('\\version "2.12"\n')
    for line in input_file:
        if line.startswith('!'):  # comment lines
            continue
        parts = line.strip().split('|')
        token_type = parts[1]
        data = parse_parts(parts[2:])
        #print token_type, data
        if token_type == 'SongInfo':
            output_file.write('\\header {\n')
            output_file.write(''.join([
                '\ttitle = "%s"\n' % data['Title'],
                '\tenteredby = "%s"\n' % data['Author'],
                '\tcopyright = "%s"\n' % data['Copyright1'],
                '\tfooter = "%s"\n' % data['Copyright2'],
                '\t%%{ %s %%}\n' % data['Comments'],
            ]))
            output_file.write('\\}\n')
        if token_type == 'PgSetup':
            print 'skipping page setup...'
        if token_type == 'Font':
            print 'skipping font configuration...'
        if token_type == 'PgMargins':
            print 'skipping page margins...'
        if token_type == 'AddStaff':
            # followed by several StaffProperties lines and StaffInstrument line
            if staff_began:
                num_processed_staves += 1
                output_file.write('}\n')
                print 'Staff finished'
                print '  Number of bars : %d' % num_bars
                print '  Number of notes : %d' % num_notes
            staff_began = False
            num_staves += 1
            staff_name = data['Name']
            staff_label = data['Label']
            print 'Adding a staff (%s)' % staff_label
            staff_style = None
            staff_layered = None
            staff_endingbar = None
            staff_muted = None
            staff_midi_channel = None
            staff_lines = None
            num_bars, num_notes = 0, 0
        if token_type == 'StaffProperties':
            if staff_style is None:
                staff_style = data.get('Style', 'Standard')
            if staff_layered is None:
                staff_layered = data.get('Layer', False)
            if staff_endingbar is None:
                staff_endingbar = data.get('EndingBar', 'MasterClose')
            if staff_muted is None:
                staff_muted = data.get('Muted', False)
            if staff_midi_channel is None:
                staff_midi_channel = data.get('Channel', 1)  # TODO: support percussions?
        if token_type == 'StaffInstrument':
            pass
            if staff_midi_channel == 10 or staff_midi_channel == 13:
                print 'WARNING: unsupported staff (percussion channel)'
            else:
                staff_began = True
                output_file.write('new Staff {\n')
                last_clef = ''
                last_note_pos = ''
                last_dur = ''
        if token_type == 'Lyrics':
            pass
        if token_type.startswith('Lyric'):
            pass
        if staff_began:
            # context includes staff properties and fake/emulated surroundings
            # such as clef, timesig, keysig, and tempo.
            # TODO: convert_in_context(context, token_type, data)
            if token_type == 'Clef':
                pass
            if token_type == 'TimeSig':
                pass
            if token_type == 'Key':
                pass
            if token_type == 'Tempo':
                data.get('Text', '')
                pass
            if token_type == 'Rest':
                pass
            if token_type == 'Bar':
                pass
                num_bars += 1
            if token_type == 'Text':
                pass
            if token_type == 'Note':
                dur = data['Dur'].split(',')
                dur_opts = parse_opts(''.join(dur[1:]))
                pass
                num_notes += 1
            if token_type == 'Chord':
                pass
                num_notes += 1
            if token_type == 'Dynamic':
                data['Style']
                pass
            if token_type == 'SustainPedal':
                pass
            if token_type == 'PerformanceStyle':
                pass
            if token_type == 'Flow':
                pass
            if token_type == 'Text':
                pass
        else:
            continue
    if staff_began:
        num_processed_staves += 1
        output_file.write('}\n')
        print 'Staff finished'
        print '  Number of bars : %d' % num_bars
        print '  Number of notes : %d' % num_notes
    print 'Total number of staves found : %d' % num_staves
    print 'Number of processed staves : %d' % num_processed_staves

if __name__ == '__main__':
    if len(sys.argv) == 1:
        usage()
    else:
        input_file = open(sys.argv[1], 'r')
        try:
            output_file = codecs.open(sys.argv[2], 'w', encoding='utf8')
        except IndexError:
            output_file = sys.stdout

    convert_file(input_file, output_file)
    input_file.close()
    if output_file != sys.stdout:
        output_file.close()
    print 'Conversion complete!'