Cleanup to more Pythonic sytax.
[nwc2ly.git] / nwc2ly.py
old mode 100644 (file)
new mode 100755 (executable)
index 028cc45..8aaf9e4
--- a/nwc2ly.py
+++ b/nwc2ly.py
-import binascii, sys, zlib, traceback \r
-\r
-shortcopyleft = """\r
-nwc2ly - Converts NWC(v 1.75) to LY fileformat\r
-Copyright (C) 2005  Joshua Koo (joshuakoo @ myrealbox.com)\r
-\r
-This program is free software; you can redistribute it and/or\r
-modify it under the terms of the GNU General Public License\r
-as published by the Free Software Foundation; either version 2\r
-of the License, or (at your option) any later version.\r
-\r
-This program is distributed in the hope that it will be useful,\r
-but WITHOUT ANY WARRANTY; without even the implied warranty of\r
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r
-GNU General Public License for more details.\r
-\r
-You should have received a copy of the GNU General Public License\r
-along with this program; if not, write to the Free Software\r
-Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.\r
-"""\r
-\r
-##\r
-# most infomation obtained about the nwc format \r
-# is by using noteworthycomposer and the somewhat like the french cafe method\r
-# (http://samba.org/ftp/tridge/misc/french_cafe.txt)\r
-#\r
-#\r
-## \r
-# Revisions\r
-# 0.1  07 april 2005 initial hex parsing;\r
-# 0.2  13 april 2005 added multiple staff, keysig, dots, durations\r
-# 0.3  14 april 2005 clef, key sig detection, absolute notes pitching\r
-# 0.4   15 April 2005 Relative Pitchs, Durations, Accidentals, Stem Up/Down, Beam, Tie\r
-# 0.5   16 April 2005 Bug fixes, Generate ly score , Write to file, Time Sig, Triplets, Experimental chords\r
-# 0.6  17 April 2005 Compressed NWC file Supported!\r
-# 0.7  19 April 2005 Version detection, Header \r
-#      20 April 2005 BUGS FiXes, small Syntax changes\r
-#      21 April 2005 Still fixing aimlessly \r
-#      23 April 2005 Chords Improvement\r
-#      24 April 2005 staccato, accent, tenuto. dynamics, midi detection (but unsupported)\r
-# 0.8  24 April 2005 Experimental Lyrics Support\r
-# 0.9  29 April 2005 Workround for \acciaccatura, simple Check full bar rest adjustment\r
-#      \r
-## \r
-# TODO\r
-# Proper syntax and structure for staffs, lyrics, layering\r
-# version 1.7 Support\r
-# nwc2ly in lilytool\r
-# \r
-# Piano Staff\r
-# Chords\r
-# Pedals\r
-# Midi Instruments\r
-# Visability\r
-# Lyrics\r
-# Context Staff\r
-# Staff layering / Merging\r
-##\r
-#\r
-# BUGS text markups, chords, fermata\r
-######\r
-# \r
-# cd /cygdrive/c/development/nwc2ly\r
-# $ python nwc2ly.py lvb7th1\ uncompressed.nwc test.ly > convert.log\r
-#######\r
-\r
-nwc2lyversion = '0.9.0'\r
-############\r
-# Options  #\r
-############\r
-debug = 0 \r
-       # You can use 0 for False and 1 for True\r
-relativePitch = 1\r
-relativeDuration = 1\r
-barLinesComments = 10 # Comments for every x lines     # section/line comment\r
-\r
-insertBeaming = 1\r
-insertSteming = 1\r
-#insertText = 0\r
-\r
-nwcversion = 1.75\r
-##############\r
-\r
-args =  len(sys.argv)\r
-if args<2:\r
-       print "Syntax: python nwc2ly.py nwcfile [lyfile]"\r
-       sys.exit()\r
-nwcfile = sys.argv[1] # 'simple1.nwc' #'simple2.nwc' 'simple3.nwc' 'bbc5-1-org.nwc' bbc5-1-nop.\r
-\r
-if args<3:\r
-       lyfile = ''  # 'test.ly'\r
-else:\r
-       lyfile = sys.argv[2]\r
-\r
-\r
-\r
-def getFileFormat(nwcData):\r
-       #'[NoteWorthy ArtWare]'\r
-       #'[NoteWorthy Composer]'\r
-       nwcData.seek(0)\r
-       company = readLn(nwcData)\r
-       nwcData.seek(2,1) # pad\r
-       product = readLn(nwcData)\r
-       version = readLn(nwcData)\r
-       version = ord(version[0]) * 0.01 + ord(version[1])\r
-       nwcversion = version\r
-       print 'NWC file version', nwcversion, 'detected!'\r
-       pad(nwcData,2) # times saved\r
-       name1 = readLn(nwcData)\r
-       name2 = readLn(nwcData)\r
-       \r
-       pad(nwcData,8)\r
-       huh = nwcData.read(1)\r
-       pad(nwcData,1)\r
-       \r
-def getFileInfo(nwcData):\r
-       title = readLn(nwcData)\r
-       author = readLn(nwcData)\r
-       copyright1 = readLn(nwcData)\r
-       copyright2 = readLn(nwcData)\r
-       comments = readLn(nwcData)\r
-       \r
-       header = "\\header {"\r
-       header += "\n\ttitle = \"%s\"" % title\r
-       header += "\n\tenteredby = \"%s\"" % author\r
-       header += "\n\tcopyright = \"%s\"" % copyright1\r
-       header += "\n\tfooter = \"%s\"" % copyright2\r
-       header += "\n\t%%{ %s %%}" % comments\r
-       header += "\n}"\r
-               \r
-       # TO ADD IN DEFAULT BLANK FEILDS TO KEY IN\r
-       print 'title,author,copyright1,copyright2,comments: ', (title,author,copyright1,copyright2,comments)\r
-       return header\r
-\r
-# Page Setup\r
-def getPageSetup(nwcData):\r
-       # ??\r
-       getMargins(nwcData)\r
-       #getContents(nwcData)\r
-       #getOptions(nwcData)\r
-       getFonts(nwcData)\r
-\r
-       \r
-def getMargins(nwcData):\r
-       readTill(nwcData,'\x01')\r
-       pad(nwcData,1)\r
-       # get string size 43\r
-       margins = readLn(nwcData)\r
-       print 'margins ', margins\r
-       #mirrorMargines\r
-       #UOM\r
-       return \r
-\r
-def getOptions(nwcData):\r
-       # page numbering, from\r
-       # title page info\r
-       # extend last system\r
-       # incrase note spacing for larger note duration\r
-       # staff size\r
-       # staff labels (none, first systems, top systems, all systems\r
-       # measure numbers none, plain, circled, boxed\r
-       # measure start\r
-       return\r
-\r
-def getFonts(nwcData):\r
-       # 12 Times\r
-       readTill(nwcData,'\xFF')\r
-       n = nwcData.read(1)\r
-       pad(nwcData,1)\r
-       for i in range (12):\r
-               # Font Name\r
-               font = readLn (nwcData)\r
-               \r
-               # 00\r
-               \r
-               # Style 'Regular' 'Italic' 'Bold' 'Bold Italic'\r
-               style = ord(nwcData.read(1)) & 3\r
-               \r
-               # Size\r
-               size = ord(nwcData.read(1))\r
-               \r
-               ## 00\r
-               nwcData.seek(1,1)\r
-               \r
-               # Typeface\r
-               # 00 Western, b1 Hebrew\r
-               typeface = nwcData.read(1)\r
-               \r
-               print 'Font detected' , font, 'size',size, 'style ', style, ' typeface',typeface\r
-               \r
-\r
-def findNoOfStaff(nwcData):\r
-       # Infomation on Staffs \x08 00 00 FF 00 00 n\r
-       data = 0;\r
-       \r
-       readTill(nwcData,'\xFF')\r
-       print "Where am I? ", nwcData.tell()\r
-       \r
-       \r
-       nwcData.read(2)\r
-       \r
-       layering = nwcData.read(1) # FF or 00\r
-       \r
-       noOfStaffs = ord(nwcData.read(1))\r
-       nwcData.read(1)\r
-       print noOfStaffs, " noOfStaffs found"\r
-       return noOfStaffs\r
-\r
-def findStaffInfo(nwcData):\r
-       # Properties for Staff\r
-               # General \r
-                       # Name.\r
-                       # Group\r
-                       # Ending Bar\r
-               # Visual\r
-                       # Verticle Upper Size\r
-                       # Verticle Lower Size\r
-                       # Style\r
-                       # Layer Next Staff\r
-                       # Color\r
-               # Midi\r
-                       # Part Volume\r
-                       # Stereo Pan\r
-                       # Transposition\r
-                       # Muted\r
-                       # PlayBack Device\r
-                       # Playback Channel\r
-               # Instrument\r
-                       # Patch Name\r
-                       # Patch List Type\r
-                       # Bank Select \r
-                       # Controller0\r
-                       # Controller32\r
-       # Staff Lyrics\r
-               # LineCount\r
-               # AlignSyllableRule\r
-               # StaffPlacementAligment\r
-               # StaffPlacementOffset\r
-               # StaffPropertiesVerticleSizeUpper\r
-               # StaffPropertiesVerticleSizeLower\r
-               \r
-       format = ''\r
-       staffName = readLn(nwcData)\r
-       format += "\\context Staff = %s " % staffName # or voice or lyrics \r
-       \r
-       groupName = readLn(nwcData) # HOW TO ORGANISE THEM??\r
-       \r
-       endbar = ord(nwcData.read(1)) # & (2^3-1)\r
-       #print 'end ',endbar\r
-       endingBar = ending[endbar] #  10 --> OC for lyrics?  10000 1100\r
-       \r
-       muted = ord(nwcData.read(1)) & 1\r
-       nwcData.read(1)\r
-       \r
-       channel = ord(nwcData.read(1)) + 1\r
-       nwcData.read(9)\r
-       \r
-       stafftype = staffType[ord(nwcData.read(1))&3]\r
-       nwcData.read(1)\r
-       \r
-       uppersize = 256 - ord(nwcData.read(1))  # - signed +1 )& 2^7-1 )\r
-       readTill(nwcData,'\xFF')\r
-       \r
-       lowersize = ord(nwcData.read(1))\r
-       ww = nwcData.read(1) \r
-       print '[uppersize,lowersize]',[uppersize,lowersize]\r
-       \r
-       noOfLines = ord(nwcData.read(1))\r
-       print '[staffName,groupName,endingBar,stafftype,noOfLines]', [staffName,groupName,endingBar,stafftype,noOfLines]\r
-       \r
-       layer = ord(nwcData.read(1)) & 1\r
-       \r
-       # signed transposition\r
-       # FF?\r
-       \r
-       partVolume = ord(nwcData.read(1))\r
-       ord(nwcData.read(1))\r
-       \r
-       stereoPan = ord(nwcData.read(1))\r
-       if nwcversion == 1.7:\r
-               nwcData.read(2)\r
-       else:\r
-               nwcData.read(3)\r
-       \r
-       nwcData.read(2)\r
-       #lyrics = ord(nwcData.read(1)) & 1\r
-       lyrics = readInt(nwcData)\r
-       noOfLyrics = readInt(nwcData)\r
-       \r
-       lyricsContent = ''\r
-       if lyrics:\r
-               lyricOptions = readInt(nwcData)\r
-               nwcData.read(3)\r
-               for i in range(noOfLyrics):\r
-                       print 'looping ',i, 'where', nwcData.tell()\r
-                       lyricsContent  += '\\ \lyricmode { ' # lyrics\r
-                       lyricsContent  += getLyrics(nwcData) #\r
-                       lyricsContent  += '}'\r
-               nwcData.read(1)\r
-       \r
-       nwcData.read(1)\r
-       color = ord(nwcData.read(1)) & 3 #12\r
-       \r
-       noOfTokens = readInt(nwcData)\r
-       print noOfTokens, " Tokens found", nwcData.tell()\r
-       return noOfTokens, format, lyricsContent\r
-       \r
-\r
-\r
-\r
-\r
-def pad(nwcData, length):\r
-       nwcData.seek(length,1)\r
-\r
-def readTill(nwcData, delimit):\r
-       data = ''\r
-       value = ''\r
-       while data!=delimit:\r
-               value += data\r
-               data = nwcData.read(1)\r
-               \r
-       return value\r
-       \r
-def readLn(nwcData):\r
-       return readTill (nwcData,'\x00')\r
-       # reads until 00 is hit\r
-       # od oa == \n\r
-\r
-def readInt(nwcData):\r
-       data = nwcData.read(2)\r
-       no = ord(data[0])\r
-       no += ord(data[1]) * 256\r
-       return no\r
-\r
-\r
-def getLyrics(nwcData):\r
-       \r
-       data = ''\r
-       print 'reach'\r
-       blocks = ord(nwcData.read(1))\r
-       if blocks==4: blocks = 1\r
-       if blocks==8: blocks = 2\r
-       if blocks == 0: return \r
-       \r
-       lyricsLen = readInt(nwcData)\r
-       \r
-       print 'blocks ',blocks, 'lyrics len', lyricsLen, 'at ', nwcData.tell()\r
-       \r
-       nwcData.read(1)\r
-       for i in range (blocks):\r
-               data += nwcData.read(1024)\r
-       \r
-       lyrics = data[1:lyricsLen-1]\r
-       print 'lyrics ', lyrics\r
-       return lyrics\r
-       \r
-def getDuration(data):\r
-       durationBit = ord(data[2]) & 7\r
-       durationDotBit = ord(data[6]) \r
-       \r
-       duration = durations[durationBit]\r
-       if (durationDotBit & 1<<2):\r
-               durationDot = '.'\r
-       elif (durationDotBit & 1):\r
-               durationDot = '..'\r
-       else :\r
-               durationDot = ''\r
-       return duration + durationDot\r
-\r
-def getKey(data):\r
-       data = binascii.hexlify(data)\r
-       \r
-       if (keysigs.has_key(data)):\r
-               return '\key ' + keysigs[data]\r
-       return '% unknown key'\r
-\r
-def getLocation(data):\r
-       offset = ord(data[8]);\r
-       if offset > 127 :\r
-               return 256-offset\r
-       if (ord(data[9])>>3 & 1):\r
-               return -offset\r
-       \r
-       return offset\r
-       #print 'offset ', offset\r
-       #print binascii.hexlify(data[8:9])\r
-\r
-def getAccidental(data):\r
-       data = ord(data)\r
-       data = (data & 7 )\r
-       return acdts[data]\r
-\r
-def getNote(data):\r
-       \r
-       # pitch\r
-       pitch = getLocation(data)\r
-       \r
-       # get Accidentals\r
-       accidental =  getAccidental(data[9])\r
-       \r
-       # get Relative Duration\r
-       duration = getDuration(data)\r
-       \r
-       # check stems\r
-       stem = ord(data[4])\r
-       stem = (stem >> 4) & 3\r
-       \r
-       # check beam\r
-       beam = ord(data[4]) & 3\r
-       \r
-       # triplets \r
-       triplet = triplets [ord(data[4])>>2 & 3 ]\r
-       \r
-       # check tie\r
-       tie = ''\r
-       if ord(data[6]) >> 4 & 1:\r
-               tie = '~'\r
-       \r
-       staccato = (ord(data[6]) >> 1) & 1\r
-       accent = (ord(data[6]) >> 5) & 1\r
-       \r
-       tenuto = (ord(data[7]) >> 2) & 1\r
-       slur = slurs[ord(data[7]) & 3 ]\r
-       grace = (ord(data[7]) >> 5) & 1\r
-       \r
-       # check slur\r
-       slur = slurs[ord(data[7]) & 3 ]\r
-       \r
-       \r
-       #TODO should use a dictionary\r
-       return (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto)\r
-\r
-def getRelativePitch(lastPitch, pitch):\r
-       octave = ''\r
-       diff = pitch - lastPitch\r
-       if diff>3:\r
-               for i in range((diff-4 + 7)/7):\r
-                       octave += "'"\r
-               if octave == '':\r
-                       octave += "'"\r
-       elif diff<-3:\r
-               for i in range((-diff-4 + 7)/7):\r
-                       octave += ","\r
-               if octave == '':\r
-                       octave += ","\r
-       return octave\r
-       \r
-def durVal(duration):\r
-       where = duration.find('.')\r
-       \r
-       if where>-1:\r
-               durVal = 128/int (duration[:where])\r
-               nextVal = durVal\r
-               for i in range(len(duration)-where):\r
-                       nextVal = nextVal / 2\r
-                       durVal += nextVal\r
-               \r
-       else:\r
-               durVal = 128/int (duration)\r
-       return durVal\r
-       \r
-def processStaff(nwcData):\r
-       keysigCount =0\r
-       timesigCount=0\r
-       noteCount=0\r
-       restCount=0\r
-       staffCount=0\r
-       barlineCount=0\r
-       clefCount=0\r
-       textCount =0\r
-       dynamicCount = 0\r
-       \r
-       lastPitch = scale.index('c')\r
-       lastDuration = 0\r
-       lastClef = scale.index(clefs[0])  # index referenced to note of last clef\r
-       lastStem = 0\r
-       lastTimesig = 1\r
-       \r
-       lastKey = { 'c': '', #c \r
-       'd': '', 'e': '', 'f': '', 'g': '', 'a': '', 'b': '' }\r
-       currentKey = lastKey.copy()\r
-       \r
-       data = 0\r
-       token = 1\r
-       \r
-       result = ""\r
-       result += "\n\t\\new Staff {\n\t\t"\r
-       lastChord = 0\r
-       lastGrace = 0;\r
-       \r
-       (noOfTokens,format, lyrics) = findStaffInfo(nwcData);\r
-       if lyrics!='':\r
-               #result += '\\lyricmode { ' + lyrics + '}'\r
-               result += '%%Lyrics %%{ ' + lyrics + '%%}'\r
-       \r
-       if relativePitch: \r
-               result+="\\relative c {"\r
-       else :\r
-               result+=" {"\r
-       result+="\n\t\t\n\n\t\t"\r
-       result+= '\n\t %% Staff %s \n\t' % staff \r
-       \r
-       #print "00112233445566778899\n"\r
-       extra = ''\r
-       # the juice\r
-       \r
-       while data!="":\r
-               \r
-               token += 1\r
-               if token==noOfTokens:\r
-                       result += "\n\t\t}\n\t}\n\t"\r
-                       print "going next staff! %s" % nwcData.tell()\r
-                       break\r
-               \r
-               \r
-               if nwcversion==1.7:\r
-                       nwcData.seek(2,1)\r
-               \r
-               data = nwcData.read(1)\r
-               #print 'test', data\r
-               \r
-               # clef\r
-               if data=='\x00':\r
-                       data = nwcData.read(6)\r
-                       clefCount += 1\r
-                       \r
-                       key = ord(data[2]) & 3  \r
-                       octave = ord(data[4]) & 3\r
-                       # print binascii.hexlify(data) , "CLEF? "\r
-                       lastClef = scale.index(clefs[key]) + octaves[octave]\r
-                       #TODO check for octave shifts _8\r
-                       lastClef += clefShift[octave]  \r
-                       result += '\clef "' + clefNames[key] + clefOctave[octave]+ '"\n\t\t'\r
-                       \r
-               # key signature\r
-               elif data=='\x01':\r
-                       data = nwcData.read(12)\r
-                       keysigCount = keysigCount + 1\r
-                       \r
-                       #\r
-                       flatBits = ord(data[2])\r
-                       sharpBits = ord(data[4])\r
-                       \r
-                       for note in lastKey.keys():\r
-                               noteInd = ['a','b','c','d','e','f','g'].index(note)\r
-                               if (flatBits >> noteInd & 1):\r
-                                       lastKey[note] = 'es'\r
-                               elif (sharpBits >> noteInd & 1):\r
-                                       lastKey[note] = 'is'\r
-                               else:\r
-                                       lastKey[note] = ''\r
-                       \r
-                       currentKey = lastKey.copy()\r
-                       result = result + getKey(data[1:5]) + "\n\t\t"\r
-                       \r
-                       #print "data", binascii.hexlify(data)\r
-                       #print "flat", binascii.hexlify(flatBits)\r
-                       #print "sharp", binascii.hexlify(data[4])\r
-                       #print getKey(data[1:5])\r
-                       \r
-               \r
-               # barline\r
-               elif data=='\x02':\r
-                       data = nwcData.read(4)\r
-                       barlineCount += 1\r
-                       \r
-                       currentKey = lastKey.copy()\r
-                       \r
-                       result += "|\n\t\t"\r
-                       if (barlineCount % barLinesComments == 0):\r
-                               result += "\n\t\t% Bar " + str(barlineCount + 1) + "\n\t\t" \r
-                               #print '.',\r
-                               print "Bar ",barlineCount, " completed, "\r
-               \r
-               # timesig\r
-               elif data=='\x05':\r
-                       data = nwcData.read(8)\r
-                       timesigCount = timesigCount + 1 \r
-                       beats = ord(data[2])\r
-                       beatValues = [ 1, 2, 4, 8 ,6, 32 ]\r
-                       beatValue = beatValues[ord(data[4])]\r
-                       timesig = str(beats) + "/"  + str(beatValue)\r
-                       lastTimesig = timesigValues[timesig]\r
-                       result += "\\time " + timesig + " "\r
-               # Tempo\r
-               elif data=='\x06':\r
-                       print "Tempo"\r
-                       data = nwcData.read(7)\r
-                       tempo = readLn(nwcData)\r
-                       result += '\n\t\t%%tempo %s\n\t\t' % tempo\r
-                       #tempoCount = tempoCount + 1    \r
-                       \r
-               # note\r
-               elif data=='\x08':\r
-                       data = nwcData.read(10)\r
-                       noteCount = noteCount + 1\r
-                       \r
-                       if debug: print binascii.hexlify(data) , noteCount , nwcData.tell()\r
-                       (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto) = getNote(data)\r
-                       \r
-                       \r
-                       articulate = ''\r
-                       if staccato: articulate+= '-.'\r
-                       if accent: articulate+= '->'\r
-                       if tenuto: articulate+= '--'\r
-                       \r
-                       beam = beams[beam]\r
-                       \r
-                       chordMatters = ''\r
-                       if lastChord>0 and beam==']' :\r
-                               chordMatters = ' } >> '\r
-                               lastChord = 0\r
-                       elif lastChord>0:\r
-                               \r
-                               print 'CHORd', durVal(duration), lastChord\r
-                               dur = durVal(duration)\r
-                               #lastChord = 1.0/((1.0 / lastChord) - (1.0/durVal(duration)))\r
-                               lastChord -= dur \r
-                               \r
-                               if lastChord <= 0 :\r
-                                       chordMatters = ' } >> '\r
-                                       lastChord = 0\r
-                                       \r
-                               \r
-                       \r
-                       if not insertBeaming: beam = ''\r
-                       \r
-                       # pitch\r
-                       pitch += lastClef\r
-                       note = scale[pitch]\r
-                       \r
-                       \r
-                       # get Accidentals\r
-                       #print 'accidental',accidental\r
-                       if (accidental!='auto'):\r
-                               currentKey[note[0]] = accidental\r
-                       accidental = currentKey[note[0]]\r
-                       \r
-                       if (relativePitch):\r
-                               octave = getRelativePitch(lastPitch, pitch)\r
-                               lastPitch = pitch\r
-                       else:\r
-                               octave = note[1:]\r
-                       pitch = note[0] + accidental + octave\r
-                       \r
-                       # get Relative Duration\r
-                       if (relativeDuration):\r
-                               if (lastDuration==duration):\r
-                                       duration = ''\r
-                               else:\r
-                                       lastDuration = duration\r
-                       \r
-                       # check stems\r
-                       if insertSteming and (stem!= lastStem) :\r
-                               lastStem = stem\r
-                               stem = stems[stem]\r
-                       else :\r
-                               stem = ''\r
-                       \r
-                       # normal note\r
-                       if extra!='':\r
-                               extra = '-"' + extra + '"'\r
-                       \r
-                       if grace and not lastGrace: result += "\\acciaccatura { "\r
-                       \r
-                       if not grace and lastGrace: result += " } "\r
-                       result += triplet[0] + stem + pitch + duration + articulate + extra \r
-                       result += slur + tie + beam  + triplet[1]  +chordMatters  +  " "\r
-                       \r
-                       \r
-                       # reset\r
-                       lastGrace = grace\r
-                       extra = ''\r
-               # rest\r
-               elif data=='\x09':\r
-                       data = nwcData.read(10)\r
-                       restCount = restCount + 1 \r
-                       \r
-                       # get Relative Duration\r
-                       duration = getDuration(data)\r
-                       if duration == '1':\r
-                               duration = lastTimesig\r
-                       if (relativeDuration):\r
-                               if (lastDuration==duration):\r
-                                       duration = ''\r
-                               else:\r
-                                       lastDuration = duration\r
-                                       \r
-                       result = result + 'r' + duration + " "\r
-\r
-               # text\r
-               elif data=='\x11':\r
-                       textCount = textCount + 1\r
-                       data = nwcData.read(2) #pad\r
-                       textpos = nwcData.read(1)\r
-                       data = nwcData.read(2) #pad\r
-                       text = ''\r
-                       data = nwcData.read(1)\r
-                       while data!='\x00':\r
-                               text += data\r
-                               data = nwcData.read(1)\r
-                       \r
-                       #if text.isdigit() : # check numbers\r
-                       #       text = "-\\markup -\\number "+ text \r
-                       #       #text = "-\\markup {\\number "+ text +"}"\r
-                       #else :\r
-                       #       text = '-"' + text + '"'\r
-                       \r
-                       extra += ' ' + text\r
-                       \r
-               # dynamics\r
-               elif data=='\x07':\r
-                       dynamicCount = dynamicCount + 1\r
-                       data = nwcData.read(9)\r
-               # chord\r
-               elif data=='\x0A' or data=='\x12':\r
-                       \r
-                       data = nwcData.read(12)\r
-                       \r
-                       chordAmt = ord(data[10])\r
-                       chords = []\r
-                       chordDur = getDuration(data)\r
-                       #print 'duration',chordDur\r
-                                               \r
-                       #print "WARNING Chord support is experimental", chordDur, chordAmt\r
-                       #print binascii.hexlify(data), 'barlines ', barlineCount\r
-                       #print 'no. of notes in chord' , chordAmt\r
-                       \r
-                       chord1 = []\r
-                       chord2 = []\r
-                       \r
-                       for i in range(chordAmt):\r
-                               # rest or note\r
-                               what = nwcData.read(1)\r
-                               \r
-                               data = nwcData.read(10)\r
-                               ha = getNote(data)\r
-                               #print 'data ', ha\r
-                               (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto) = ha\r
-                               # add to list\r
-                               if ha[2] == chordDur:\r
-                                       chord1.append( (pitch,accidental ) )\r
-                                       if beam==0 or beam ==4 :\r
-                                               lastChord = 0\r
-                       \r
-                                       \r
-                               else : # 2 voices\r
-                                       chord2.append(  (pitch,accidental, duration) )\r
-                                       lastChord = durVal(duration)  \r
-                                       \r
-                       \r
-                       if len(chord2)==0: # block chord\r
-                               result += ' <'\r
-                               for i in range(len(chord1)):\r
-                                       (pitch,accidental ) = chord1[i]\r
-                                       pitch += lastClef\r
-                                       note = scale[pitch]\r
-                                       \r
-                                       if (accidental!='auto'):\r
-                                               currentKey[note[0]] = accidental\r
-                                       accidental = currentKey[note[0]]\r
-                                       \r
-                                       if (relativePitch):\r
-                                               octave = getRelativePitch(lastPitch, pitch)\r
-                                               lastPitch = pitch\r
-                                       else:\r
-                                               octave = note[1:]\r
-                                       result += note[0] + accidental + octave + ' '\r
-                               result += '>' +chordDur +' '\r
-                               lastPitch = chord1[0][0] + lastClef\r
-                       else: # 2 voices\r
-                               result += ' << ' \r
-                               for i in range(len(chord2)):\r
-                                       (pitch,accidental,duration ) = chord2[i]\r
-                                       pitch += lastClef\r
-                                       note = scale[pitch]\r
-                                       \r
-                                       if (accidental!='auto'):\r
-                                               currentKey[note[0]] = accidental\r
-                                       accidental = currentKey[note[0]]\r
-                                       \r
-                                       if (relativePitch):\r
-                                               octave = getRelativePitch(lastPitch, pitch)\r
-                                               lastPitch = pitch\r
-                                       else:\r
-                                               octave = note[1:]\r
-                                       result += note[0] + accidental + octave + duration + ' '\r
-                               \r
-                               result += " \\\\ {"\r
-                               \r
-                               for i in range(len(chord1)):\r
-                                       (pitch,accidental ) = chord1[i]\r
-                                       pitch += lastClef\r
-                                       note = scale[pitch]\r
-                                       \r
-                                       if (accidental!='auto'):\r
-                                               currentKey[note[0]] = accidental\r
-                                       accidental = currentKey[note[0]]\r
-                                       \r
-                                       if (relativePitch):\r
-                                               octave = getRelativePitch(lastPitch, pitch)\r
-                                               lastPitch = pitch\r
-                                       else:\r
-                                               octave = note[1:]\r
-                                       result += note[0] + accidental + octave +chordDur + ' '\r
-                               if lastChord >0 : lastChord = durVal(duration) - durVal(chordDur)\r
-                               if lastChord==0: result += ' } >> '\r
-                               # end 2 voices\r
-                       lastDuration = chordDur\r
-                       \r
-                       # check if duration / stem  / same \r
-                               # < >duration ties,beam, slurs\r
-               \r
-               # Pedal\r
-               elif data=='\x0b':\r
-                       print 'Ped      '\r
-                       data = data = nwcData.read(5)\r
-               # midi control instruction / MPC\r
-               elif data=='\x0d':\r
-                       print 'midi control instruction'\r
-                       data = data = nwcData.read(36)\r
-               # fermata / Breath mark\r
-               elif data=='\x0E':\r
-                       print "Fermata"\r
-                       data = nwcData.read(6)\r
-                       extra += "\\fermata" \r
-               \r
-               # Dynamics\r
-               elif data=='\x0f':\r
-                       print "Dynamics"\r
-                       data = nwcData.read(5)\r
-               \r
-               # Performance Style\r
-               elif data=='\x10':\r
-                       print "Performance Style"\r
-                       data = nwcData.read(5)\r
-               \r
-               # Instrument Patch\r
-               elif data=='\x04':\r
-                       print "Instrument Patch"\r
-                       data = nwcData.read(10)\r
-               \r
-               \r
-               # todo\r
-               else :\r
-                       print "WARNING: Unrecognised token ",binascii.hexlify(data), " at #", nwcData.tell(), " at Token",  token\r
-                       \r
-                       \r
-                       \r
-       # output converted file?\r
-       print "\nStats"\r
-       print keysigCount, " keysigCount found"\r
-       print noteCount, " notes found"\r
-       print staffCount, " staffCount found"\r
-       print clefCount, " clefCount found"\r
-       print barlineCount, " barlineCount found"\r
-       print timesigCount, " timesigCount found"\r
-       print textCount, " textCount found"\r
-       print dynamicCount, " dynamicCount found"\r
-       print restCount, " restCount found"\r
-       \r
-       #print "\nLilypond format:\n"\r
-       return result\r
-       \r
-# Variables\r
-keysigs = {\r
-'00000000' : 'c \major % or a \minor' ,\r
-'00000020' : 'g \major % or e \minor' ,\r
-'00000024': 'd \major % or b \minor' ,\r
-'00000064' : 'a \major % or fis \minor' ,\r
-'0000006c' : 'e \major % or cis \minor' ,\r
-'0000006d' : 'b \major % or gis \minor' ,\r
-'0000007d' : 'fis \major % or dis \minor' ,\r
-'0000007f' : 'cis \major % or ais \minor' ,\r
-'00020000' : 'f \major % or d \minor' ,\r
-'00120000' : 'bes \major % or g \minor' ,\r
-'00130000' : 'ees \major % or c \minor' ,\r
-'001b0000' : 'aes \major % or f \minor' ,\r
-'005b0000' : 'des \major % or bes \minor' ,\r
-'005f0000' : 'ges \major % or ees \minor' ,\r
-'007f0000' : 'ces \major % or a \minor'\r
-}\r
-\r
-acdts = ( 'is', 'es', '' ,'isis', 'eses', 'auto'  ) \r
-\r
-clefs = { 0 : "b'",\r
-          1 : "d",\r
-          2 : "c'",\r
-          3 : "a'",\r
-        }\r
-\r
-clefNames = { 0: 'treble',\r
-          1: 'bass',\r
-          2: 'alto',\r
-          3: 'tenor',\r
-        }\r
-octaves = { 0: 0, 1:7, 2:-7 }\r
-scale = [   # this list is taken from lilycomp\r
-        "c,,,","d,,,","e,,,","f,,,","g,,,","a,,,","b,,,",\r
-        "c,,","d,,","e,,","f,,","g,,","a,,","b,,",\r
-        "c,","d,","e,","f,","g,","a,","b,",\r
-        "c","d","e","f","g","a","b",\r
-        "c'","d'","e'","f'","g'","a'","b'",\r
-        "c''","d''","e''","f''","g''","a''","b''",\r
-        "c'''","d'''","e'''","f'''","g'''","a'''","b'''",\r
-        "c''''","d''''","e''''","f''''","g''''","a''''","b''''",\r
-        ]\r
-\r
-stems = [ '\stemNeutral ', '\stemUp ', '\stemDown ']\r
-\r
-slurs = [ '', '(' , ')', '' ]\r
-\r
-triplets = [ \r
-       ('' , '' ),\r
-       ( '\\times 2/3 { ', '') ,\r
-       ('' , '' ),\r
-       ('' , ' }' ),\r
-       ]\r
-\r
-durations = ( '1','2','4','8','16','32','64' ) \r
-\r
-barlines = (\r
-     '|', # 'Single'\r
-     '||', # 'Double'\r
-     '.|', # SectionOpen\r
-     '|.', # SectionClose\r
-     '|:', # MasterRepeatOpen\r
-     ':|', # MasterRepeatClose\r
-     '|:', # LocalRepeatOpen\r
-     ':|', # LocalRepeatClose\r
-)\r
-\r
-ending = (\r
-'|.', # SectionClose\r
-':|', # MasterRepeatClose\r
-'|', # 'Single'\r
-'||', # 'Double'\r
-'' # Open hidden\r
-)\r
-\r
-staffType = (\r
-'Standard' , # Standard\r
-'Upper Grand Staff' , # Upper Grand Staff\r
-'Lower Grand Staff' , # Lower Grand Staff\r
-'Orchestra' , # Orchestra\r
-)\r
-\r
-\r
-beams = [ '', '[', '',']' ]\r
-\r
-\r
-# end ' \bar "|."'\r
-\r
-# Notation Properties\r
-# extra accidental spacing\r
-# extra note spacing\r
-# muted\r
-# no ledger lines\r
-# slurdirection\r
-# tiedirection\r
-# lyricsyllable\r
-# visability \r
-# show printed\r
-# item color\r
-\r
-#dynamics\r
-# cmd = DynamicVariance\r
-# Decrescendo \setTextCresc \<\r
-# setTextCresc Crescendo \>  setHairpinCresc\r
-# Dynamics stop '\! '\r
-# style = ff pp\r
-\r
-clefOctave = [ '' , '^8', '_8' , '' ]\r
-clefShift = [0,7,-7, 0]\r
-\r
-\r
-# '#(set-accidental-style '#39'modern-cautionary)'\r
-#(ly:set-point-and-click 'line-column)\r
-#(set-global-staff-size 20)\r
-\r
-timesigValues = { \r
-       '4/4' : '1', '3/4' : '2.', '2/4' : '2', '1/4' : '1',\r
-       '1/8' : '8', '2/8' : '4', '3/8' : '4.', '6/8' : '2.',\r
-       '4/8' : '2', '9/8' : '12', '12/8' : '1',\r
-       '2/2' : '1', '4/2' : '0', '1/2' : '2', \r
-        }\r
-\r
-print "python nwc2ly is running..."\r
-try:\r
-\r
-       nwcData = open( nwcfile,'rb')\r
-       \r
-       # check if its a readable nwc format\r
-       # compressed - [NWZ]\r
-       # uncompressed - [NoteWorthy ArtWare] [NoteWorthy Composer]\r
-       format = nwcData.read(5)\r
-       if format== '[NWZ]':\r
-               nwcData.seek(1,1)\r
-               print 'Compressed NWC detected!'\r
-               print 'Dumping to uncompressed NWC format and attemping conversion soon...'\r
-               uncompress = open ('uncompressed.nwc','wb')\r
-               uncompress.write(zlib.decompress(nwcData.read()))\r
-               uncompress.close()\r
-               print 'Inflating done. Now opening new file...'\r
-               nwcData.close()\r
-               nwcData = open( 'uncompressed.nwc','rb')\r
-               nwcData.seek(6)\r
-       elif format!= '[Note':\r
-               print 'Unknown format, please use an uncompress NWC format and try again.'  \r
-               sys.exit()\r
-       \r
-       \r
-       \r
-       resultFile = '%% Generated from python nwc2ly converter v%s by Joshua Koo (joshuakoo@myrealbox.com)' % nwc2lyversion\r
-       resultFile += '\n\n\\version "2.4.0"'\r
-       resultFile += "\n"\r
-       \r
-       \r
-       # START WORK\r
-       getFileFormat(nwcData)\r
-       resultFile+=getFileInfo(nwcData)\r
-       resultFile+= "\n\n\\score {"\r
-       resultFile+= "\n\t<<\n\t\t"\r
-       \r
-       getPageSetup(nwcData)\r
-       \r
-       noOfStaffs = findNoOfStaff(nwcData);\r
-       \r
-       for staff in range(1,noOfStaffs+1):\r
-               print "\n\nWorking on Staff", staff\r
-               result = processStaff(nwcData)\r
-               #print result\r
-               resultFile += result\r
-       \r
-       resultFile+= "\n\t>>"\r
-       resultFile+= "\n\t\layout {}"\r
-       resultFile+= "\n\t\midi {}"\r
-       resultFile+= "\n}"\r
-       nwcData.close()\r
-       \r
-       if lyfile=='':\r
-               print 'Dumping output file to screen'\r
-               print resultFile\r
-       else :\r
-               write = open( lyfile ,'w')\r
-               write.write (resultFile)\r
-               write.close()\r
-\r
-except IOError:\r
-       print 'File does not exist or an IO error occurred'\r
-except Exception, e: #KeyError\r
-       print "Error while reading data at ", nwcData.tell() ,"\n"\r
-       print 'Dumping whatever result first'\r
-       print resultFile\r
-       print result\r
-       print\r
-       traceback.print_exc()\r
-\r
-print\r
-print\r
-print "Please send all bugs and requests to joshuakoo@myrealbox.com"\r
+#!/usr/bin/env python
+#
+# Copyright (C)  2010  W.Trevor King (wking @ drexel.edu)
+#                2005  Joshua Koo (joshuakoo @ myrealbox.com)
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+"""Convert NoteWorthy Composer's `nwc` to LilyPond's `ly` fileformat.
+
+Most infomation obtained about the `nwc` format is by using
+noteworthycomposer and the somewhat like the `French cafe method`_.
+
+.. _French cafe method: http://samba.org/ftp/tridge/misc/french_cafe.txt
+
+Revisions:
+
+* 0.1
+
+  * 07 april 2005.  Initial hex parsing
+
+* 0.2
+
+  * 13 april 2005.  Added multiple staff, keysig, dots, durations
+
+* 0.3
+
+  * 14 april 2005.  Clef, key sig detection, absolute notes pitching
+
+* 0.4
+
+  * 15 April 2005.  Relative pitchs, durations, accidentals, stem
+    up/down, beam, tie
+
+* 0.5
+
+  * 16 April 2005.  Bug fixes, generate ly score, write to file, time
+    signature, triplets, experimental chords
+
+* 0.6
+
+  * 17 April 2005.  Compressed NWC file supported!
+
+* 0.7
+
+  * 19 April 2005.  Version detection, header
+  * 20 April 2005.  Bug fixes, small syntax changes
+  * 21 April 2005.  Still fixing aimlessly
+  * 23 April 2005.  Chords improvement
+  * 24 April 2005.  Staccato, accent, tenuto. dynamics, midi detection
+    (but unsupported)
+
+* 0.8
+
+  * 24 April 2005.  Experimental lyrics support
+
+* 0.9
+
+  * 29 April 2005.  Workround for ``\acciaccatura``, simple check full
+    bar rest adjustment
+
+* 0.10
+
+  * 30 November 2010.  Cleanup to more Pythonic syntax.
+
+TODO:
+
+* http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Line-breaking
+"""
+
+import logging
+import pprint
+import re
+import struct
+import sys
+import zlib
+
+
+__version__ = '0.10'
+
+LOG = logging
+
+
+# define useful structures and tools to make parsing easier
+
+def _decode_string(string):
+    """Decode NWC text from ISO-8859-1 to Unicode.
+
+    Based on the contents of a few example files, NWC uses
+    `ISO-8859-1`_ to encode text with the private use character
+    `\\x92` representing the right single quote (see `ISO-6492`_).
+
+    .. _ISO-8859-1: http://en.wikipedia.org/wiki/ISO/IEC_8859
+    .. _ISO-6492: http://en.wikipedia.org/wiki/ISO_6429
+
+    >>> _decode_string('Someone\\x92s \\xc9tude in \\xa9')
+    u'Someone\u2019s \\xc9tude in \\xa9'
+    """
+    string = unicode(string, 'iso-8859-1')
+    string = string.replace(r'\r\n', '\n')
+    return string.replace(u'\x92', u'\u2019')
+
+
+class Enum (object):
+    """An enumerated type (or any type requiring post-processing).
+
+    >>> class E (Enum):
+    ...     _list = ['A', 'B', 'C']
+    >>> E.process(1)
+    'B'
+    """
+    type = 'B'
+    _list = []
+
+    @classmethod
+    def process(self, value):
+        try:
+            return self._list[value]
+        except IndexError:  # if _list is a list
+            raise ValueError('index %d out of range for %s'
+                             % (value, self._list))
+        except KeyError:  # if _list is a dict
+            raise ValueError('key %s not in %s'
+                             % (value, self._list))
+
+
+class BoolEnum (Enum):
+    _list = [False, True]
+
+
+class Pow2Enum (Enum):
+    _list = [1, 2, 4, 8, 16, 32, 64]
+
+
+class BitField (list):
+    """Allow clean access into bit fields.
+
+    Initialize with a sequence of `(name, bit_size)` pairs.
+
+    When packing into a `Structure`, the type should be set to one of
+    the standard unsigned integer type characters.  The total bit size
+    must match the size of the type specifier.
+
+    If you want, you may add an `Enum` subclass as a third tuple
+    element `(name, bit_size, enum)` to postprocess that bit field
+    value.  The first bit fields will recieve the most significant
+    bits.
+
+    >>> b = BitField('B', ('a', 4), ('b', 3), ('c', 1, BoolEnum))
+    >>> a = ord('\\x2f')
+    >>> bin(a)
+    '0b101111'
+    >>> b.parse('B', a)
+    [('a', 2), ('b', 7), ('c', True)]
+
+    `BitField` instances will raise exceptions if you try to parse the
+    wrong type.
+
+    >>> b.parse('C', a)
+    Traceback (most recent call last):
+      ...
+    AssertionError: C
+
+    Or if the type bit size doesn't match the field total.
+
+    >>> b = BitField('B', ('a', 4), ('b', 3))
+    Traceback (most recent call last):
+    ...
+    AssertionError: only assigned 7 of 8 bits
+    """
+    _uint_types = ['B', 'H', 'I', 'L', 'Q']
+
+    def __init__(self, type, *fields):
+        assert type in self._uint_types, type
+        self._type = type
+        self._fields = list(fields)
+        field_bits = 0
+        for i,field in enumerate(self._fields):
+            if len(field) == 2:
+                self._fields[i] = (field[0], field[1], None)
+            field_bits += field[1]
+        num_bytes = self._uint_types.index(type) + 1
+        self._num_bits = 8 * num_bytes
+        self._max_value = (1 << self._num_bits) - 1
+        assert field_bits == self._num_bits, (
+            'only assigned %d of %d bits' % (field_bits, self._num_bits))
+
+    def parse(self, type, value):
+        assert type == self._type, type
+        assert value >= 0, value
+        assert value <= self._max_value, value
+        results = []
+        top_offset = self._num_bits
+        for name,bits,enum in self._fields:
+            bottom_offset = top_offset - bits
+            mask = (1 << bits) - 1
+            v = (value >> bottom_offset) & mask
+            if enum:
+                v = enum.process(v)
+            results.append((name, v))
+            top_offset -= bits
+        assert top_offset == 0, top_offset
+        return results
+
+
+class Structure (dict):
+    """Extend `struct.Struct` to support additional types.
+
+    Additional types:
+
+    ====  ========================
+    `S`   null-terminated string
+    -     `Enum` instance
+    -     nested `Structure` class
+    ====  ========================
+
+    You can also use `BitField` instances as names.
+
+    >>> class E (Enum):
+    ...     _list = ['A', 'B', 'C', 'D', 'E', 'F']
+    >>> class S (Structure):
+    ...     _fields = [(BitField('B',
+    ...                          ('bf1', 4),
+    ...                          ('bf2', 3),
+    ...                          ('bf3', 1, BoolEnum)), 'B'),
+    ...                ('string1', 'S'), ('uint16', 'H'),
+    ...                ('string3', 'S'), ('enum1', E)]
+    >>> buffer = '\\x2fHello\\x00\\x02\\x03World\\x00\\x04ABC'
+    >>> s = S(buffer)
+    >>> for n,t in s._fields:
+    ...     if isinstance(n, BitField):
+    ...         for n2,bits,enum in n._fields:
+    ...             print n2, s[n2]
+    ...     else:
+    ...         print n, s[n]
+    ... # doctest: +REPORT_UDIFF
+    bf1 2
+    bf2 7
+    bf3 True
+    string1 Hello
+    uint16 515
+    string3 World
+    enum1 E
+    >>> s.size
+    16
+    >>> buffer[s.size:]
+    'ABC'
+
+    >>> class N (Structure):
+    ...     _fields = [('string1', 'S'), ('s', S), ('string2', 'S')]
+    >>> n_buffer = 'Fun\\x00%sDEF\\x00GHI' % buffer
+    >>> n = N(n_buffer)
+    >>> pprint.pprint(n)
+    {'s': {'bf1': 2,
+           'bf2': 7,
+           'bf3': True,
+           'enum1': 'E',
+           'string1': u'Hello',
+           'string3': u'World',
+           'uint16': 515},
+     'string1': u'Fun',
+     'string2': u'ABCDEF'}
+    >>> n.size
+    27
+    >>> n_buffer[n.size:]
+    'GHI'
+
+    Note that `ctypes.Structure` is similar to `struct.Struct`, but I
+    found it more difficult to work with.  Neither one supports the
+    variable-length, null-terminated strings we need.
+    """
+    _byte_order = '>'  # big-endian
+    _string_decoder = staticmethod(_decode_string)
+    _fields = []       # sequence of (name, type) pairs
+
+    def __init__(self, buffer=None, offset=0):
+        super(Structure, self).__init__()
+        self._parsers = []
+        self._post_processors = []
+        f = []
+        for name,_type in self._fields:
+            if self._is_subclass(_type, Enum):
+                _type = _type.type
+            if self._is_subclass(_type, Structure) or _type in ['S']:
+                if f:
+                    self._parsers.append(
+                        self._create_struct_parser(''.join(f)))
+                    f = []
+                if self._is_subclass(_type, Structure):
+                    self._parsers.append(_type)
+                elif _type == 'S':
+                    self._parsers.append(self._string_parser)
+            else:
+                f.append(_type)
+        if f:
+            self._parsers.append(self._create_struct_parser(''.join(f)))
+
+        if buffer:
+            self.unpack_from(buffer, offset)
+
+    def _is_subclass(self, obj, _class):
+        """
+        >>> s = Structure()
+        >>> s._is_subclass('c', Structure)
+        False
+        >>> s._is_subclass(Structure, Structure)
+        True
+        """
+        try:
+            return issubclass(obj, _class)
+        except TypeError:
+            return False
+
+    def _create_struct_parser(self, format):
+        LOG.debug('%s: initialize struct parser for %s%s'
+                  % (self.__class__.__name__, self._byte_order, format))
+        return struct.Struct('%s%s' % (self._byte_order, format))
+
+    def _string_parser(self, buffer, offset=0):
+        size = buffer[offset:].find('\x00')
+        string = buffer[offset:offset+size]
+        if self._string_decoder:
+            string = self._string_decoder(string)
+        return ((string,), size+1)
+
+    def unpack_from(self, buffer, offset=0):
+        self._results = []
+        self.size = 0
+        for parser in self._parsers:
+            if self._is_subclass(parser, Structure):
+                parser = parser()
+            if hasattr(parser, 'unpack_from'):
+                results = parser.unpack_from(buffer, offset)
+                size = parser.size
+            else:
+                results,size = parser(buffer, offset)
+            self._results.extend(results)
+            self.size += size
+            offset += size
+        self._results = tuple(self._results)
+        for i,result in enumerate(self._results):
+            name,_type = self._fields[i]
+            if self._is_subclass(_type, Enum):
+                result = _type.process(result)
+            if isinstance(name, BitField):
+                for n,r in name.parse(_type, result):
+                    LOG.debug("%s['%s'] = %s" % (
+                            self.__class__.__name__, n, repr(r)))
+                    self[n] = r
+            else:
+                LOG.debug("%s['%s'] = %s" % (
+                        self.__class__.__name__, name, repr(result)))
+                self[name] = result
+        return (self,)
+
+
+# define the `nwc` file format
+
+
+class NWCSongInfo (Structure):
+    _fields = [
+        ('title', 'S'),
+        ('author', 'S'),
+        #('lyricist, 'S'),  # later versions?  In example/2.0-1.nwctxt
+        ('copyright1', 'S'),
+        ('copyright2', 'S'),
+        ('comments', 'S'),
+        ]
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Creating-titles#Creating-titles
+        """
+        lines = [r'\header {']
+        for element,field in [('title', 'title'), ('composer', 'author'),
+                              ('copyright', 'copyright1')]:
+            if self[field]:
+                lines.append('\t%s = "%s"' % (element, self[field]))
+        if self['copyright2']:
+            lines.append('\t%%{ %s %%}' % self['copyright2'])
+        if self['comments']:
+            lines.append('\t%%{ %s %%}' % self['comments'])
+        lines.append('}')
+        return '\n'.join(lines)
+
+
+class NWCFontStyleEnum (Enum):
+    _list = ('regular', 'italic', 'bold', 'bold_italic')
+
+
+class NWCTypefaceEnum (Enum):
+    _list = {
+        0: 'western',
+        1: 'unknown',
+        177: 'hebrew',}
+
+
+class NWCFont (Structure):
+    _fields = [
+        ('name', 'S'),
+        (BitField('B',
+                  ('unknown2', 6),
+                  ('style', 2, NWCFontStyleEnum)),
+         'B'),
+        ('size', 'B'),
+        ('unknown3', 'B'),
+        ('typeface', NWCTypefaceEnum),
+        ]
+
+
+class NWCPageSetup (Structure):
+    _fields = [
+        ('ny_', 'S'),
+        ('f2', 'S'),
+        ('unknown01', 'B'),
+        ('unknown02', 'B'),
+        ('unknown03', 'B'),
+        ('unknown04', 'B'),
+        ('margins', 'S'),
+        ('unknown05', 'B'),
+        ('unknown06', 'B'),
+        ('unknown07', 'B'),
+        ('unknown08', 'B'),
+        ('unknown09', 'B'),
+        ('unknown10', 'B'),
+        ('unknown11', 'B'),
+        ('unknown12', 'B'),
+        ('unknown13', 'B'),
+        ('unknown14', 'B'),
+        ('unknown15', 'B'),
+        ('unknown16', 'B'),
+        ('unknown17', 'B'),
+        ('unknown18', 'B'),
+        ('unknown19', 'B'),
+        ('unknown20', 'B'),
+        ('unknown21', 'B'),
+        ('unknown22', 'B'),
+        ('unknown23', 'B'),
+        ('unknown24', 'B'),
+        ('unknown25', 'B'),
+        ('unknown26', 'B'),
+        ('unknown27', 'B'),
+        ('unknown28', 'B'),
+        ('unknown29', 'B'),
+        ('unknown30', 'B'),
+        ('unknown31', 'B'),
+        ('unknown32', 'B'),
+        ('unknown33', 'B'),
+        ('unknown34', 'B'),
+        ('unknown35', 'B'),
+        ('unknown36', 'B'),
+        ('unknown37', 'B'),
+        ('unknown38', 'B'),
+        ('unknown39', 'B'),
+        ('unknown40', 'B'),
+        ('unknown41', 'B'),
+        ('unknown42', 'B'),
+        ('staff_italic', NWCFont),
+        ('staff_bold', NWCFont),
+        ('staff_lyric', NWCFont),
+        ('page_title', NWCFont),
+        ('page_text', NWCFont),
+        ('page_small', NWCFont),
+        ('user1', NWCFont),
+        ('user2', NWCFont),
+        ('user3', NWCFont),
+        ('user4', NWCFont),
+        ('user5', NWCFont),
+        ('user6', NWCFont),
+        ]
+
+
+class NWCFileHead (Structure):
+    _fields = [
+        ('type', 'S'),
+        ('unknown01', 'B'),
+        ('unknown02', 'B'),
+        ('product', 'S'),
+        ('minor_version', 'B'), ('major_version', 'B'),
+        ('unknown03', 'B'),
+        ('unknown04', 'B'),  #('num_saves', 'B'), ???
+        ('unknown05', 'B'),
+        ('unknown06', 'B'),
+        ('na', 'S'),  # what does this mean?
+        ('name1', 'S'), # what does this mean?
+        ('unknown07', 'B'),
+        ('unknown08', 'B'),
+        ('unknown09', 'B'),
+        ('unknown10', 'B'),
+        ('unknown11', 'B'),
+        ('unknown12', 'B'),
+        ('unknown13', 'B'),
+        ('unknown14', 'B'),
+        ('unknown15', 'B'),
+        ('unknown16', 'B'),
+        ('song_info', NWCSongInfo),
+        ('page_setup', NWCPageSetup),
+        ]
+
+
+class NWCStaffSet (Structure):
+    _fields = [
+        ('ff', 'B'),
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ('num_staves', 'B'),
+        ('unknown5', 'B'),
+        ]
+
+
+class FinalBarEnum (Enum):
+    _list = ('section_close', 'master_repeat_close', 'single', 'double',
+             'invisible')
+
+
+class StaffTypeEnum (Enum):
+    _list = ('standard', 'upper_grand', 'lower_grand', 'orchestra')
+
+
+class NWCStaff (Structure):
+    _fields = [
+        ('name', 'S'),
+        ('group', 'S'),
+        (BitField('B',
+                  ('unknown01', 5),
+                  ('final_bar', 3, FinalBarEnum)),
+         'B'),
+        (BitField('B',
+                  ('unknown02', 7),
+                  ('muted', 1, BoolEnum)),
+         'B'),
+        ('unknown01', 'B'),
+        ('playback_channel', 'B'),  #+1
+        ('unknown02', 'B'),
+        ('unknown03', 'B'),
+        ('unknown04', 'B'),
+        ('unknown05', 'B'),
+        ('unknown06', 'B'),
+        ('unknown07', 'B'),
+        ('unknown08', 'B'),
+        ('unknown09', 'B'),
+        ('unknown10', 'B'),
+        (BitField('B',
+                  ('unknown11', 6),
+                  ('type', 2, StaffTypeEnum)),
+         'B'),
+        ('unknown11', 'B'),
+        ('vertical_size_upper', 'B'),  # signed?  256 - ord(nwcData.read(1))  # - signed +1 )& 2^7-1 )
+
+        #... to ff
+
+        ('vertical_size_lower', 'B'),
+        ('unknown12', 'B'),  # ww?
+        ('line_count', 'B'),
+        ('layer', 'B'),  # &1
+        ('unknown12', 'B'),
+        ('unknown13', 'B'),
+        ('unknown14', 'B'),
+        ('unknown15', 'B'),
+        ('part_volume', 'B'),
+        ('unknown16', 'B'),
+        ('stereo_pan', 'B'),
+        ('unknown17', 'B'),
+        ('unknown18', 'B'),
+        ('has_lyrics', 'B'),
+        ('num_lyrics', 'H'),
+
+
+        #('visible', 'B'),
+        #('boundary_top', 'B'),
+        #('boundary_bottom', 'B'),
+        #('lines', 'H'),
+        #('style', 'B'),
+        #('color', 'B'),
+        #('playback_device', 'B'),
+        #('transposition', 'B'),
+        #('dynamic_velocity', 'S'),
+        #('patch_name', 'B'),
+        #('patch_list_type', 'B'),
+        #('bank_select', 'B'),
+        #('controller0', 'B'),
+        #('controller32', 'B'),
+        #('align_syllable_rule', 'B'),
+        #('staff_alignment', 'B'),
+        #('staff_offset', 'B'),
+        ]
+
+    def _initialize_bar_items(self):
+        return {
+            'bar_comment_interval': 5,  # HACK
+            'clef': None,
+            'key_sig': None,
+            'time_sig': None,
+            'last_bar': None,
+            'bar_index': 0,
+            'bar': [],
+            'bar_beats': 0,
+            'previous_note': None,
+            }
+
+    def _add_bar_token(self, bar_items, token, token_index):
+        next_note = None
+        for t in self['tokens'][token_index+1:]:
+            if isinstance(t, (NWCToken_note,
+                              NWCToken_chord1)):
+                next_note = t
+                break
+        if isinstance(token, (NWCToken_text)):
+            bar_items['bar'].append(token.ly_text())
+            return
+        bar_items['bar'].append(token.ly_text(
+                clef=bar_items['clef'],
+                key=bar_items['key_sig'],
+                previous_note=bar_items['previous_note'],
+                next_note=next_note))
+        bar_items['bar_beats'] += _nwc_duration_value(token)
+        bar_items['previous_note'] = token
+
+    def _format_bar(self, bar_items, bar_token=None):
+        """
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Durations
+        """
+        if bar_token:
+            bar_items['bar'].append(bar_token.ly_text())
+        elif bar_items['bar']:
+            b = NWCToken_bar()
+            bar_items['bar'].append(
+                b.ly_text(self['final_bar']))
+        else:  # empty final bar
+            return ''
+        if bar_items['time_sig']:
+            expected_bar_beats = bar_items['time_sig'].expected_bar_beats()
+        else:
+            expected_bar_beats = 1.0
+            LOG.warn('no time signature given for bar %d, guessing 4/4'
+                     % bar_items['bar_index']+1)
+        bar_fraction = float(bar_items['bar_beats'])/expected_bar_beats
+        assert bar_fraction < 1.01, (
+                'too many beats (%f > %f) in bar %d' %
+                (bar_items['bar_beats'], expected_bar_beats,
+                 bar_items['bar_index']+1))
+        if bar_fraction < 0.99:
+            bar_items['bar'].insert(0, self._partial(
+                    bar_items, expected_bar_beats, bar_fraction))
+        line = '\t%s' % ' '.join(bar_items['bar'])
+        bar_items['last_bar'] = bar_items['bar']
+        bar_items['bar'] = []
+        bar_items['bar_index'] += 1
+        bar_items['bar_beats'] = 0
+        bar_items['previous_note'] = None
+        if bar_items['bar_index'] % bar_items['bar_comment_interval'] == 0:
+            line += ('  %% bar %d' % bar_items['bar_index'])
+        return line
+
+    def _partial(self, bar_items, expected_bar_beats, bar_fraction):
+        """
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-rhythms#Upbeats
+        """
+        remaining_fraction = 1 - bar_fraction
+        divisor = 128
+        numerator = round(int(bar_fraction * expected_bar_beats * divisor))
+        while numerator % 2 == 0 and divisor >= 1:
+            numerator /= 2
+            divisor /= 2
+        LOG.debug('partial bar, %f < %f, %d/%d remaining' % (
+                bar_items['bar_beats'], expected_bar_beats,
+                numerator, divisor))
+        if numerator == 1:
+            duration = str(divisor)
+        else:
+            duration = '%d*%d' % (divisor, numerator)
+        return r'\partial %s' % duration
+
+    def ly_lines(self, voice=None):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Multiple-staves#index-_005cnew-Staff
+        """
+
+        lines = [
+            '%% %s' % self['name'],
+            r'\new Staff',
+            ]
+
+        #if relative_pitch:
+        #    result+="\\relative c {"
+        #else :
+        lines.append('{')
+
+        if voice:
+            lines[-1] = (r'\new Voice = "%s" ' % voice) + lines[-1]
+
+        LOG.info('format staff %s (%s)' % (self['name'], voice))
+        lines.extend([
+                '\t\\autoBeamOff',
+                ])
+        bar_items = self._initialize_bar_items()
+        for i,token in enumerate(self['tokens']):
+            LOG.info('format token %s' % type(token))
+            if isinstance(token, (NWCToken_clef,
+                                  NWCToken_key_sig,
+                                  NWCToken_time_sig,
+                                  NWCToken_tempo)):
+                if isinstance(token, NWCToken_clef):
+                    bar_items['clef'] = token
+                elif isinstance(token, NWCToken_key_sig):
+                    bar_items['key_sig'] = token
+                elif isinstance(token, NWCToken_time_sig):
+                    bar_items['time_sig'] = token
+                lines.append('\t%s' % token.ly_text())
+            elif isinstance(token, NWCToken_bar):
+                lines.append(self._format_bar(bar_items, token))
+            elif isinstance(token, (NWCToken_note,  # rests are note instances
+                                    NWCToken_chord1,
+                                    NWCToken_text)):
+                self._add_bar_token(bar_items, token, i)
+            elif isinstance(token, NWCToken_fermata):
+                if bar_items['bar']:
+                    bar_items['bar'].append(token.ly_text())
+                else:  # replace the previous \bar with a bar-fermata
+                    # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-text#Text-marks
+                    # except without an auto-generated bar, no line
+                    # shows up.  If we put the fermata before the bar,
+                    # it still ends up over the clef on the next line.
+                    # See bars 124 and 125 of example/1.75-2.nwc for
+                    # an example.
+                    bar_items['last_bar'].insert(
+                        -1,
+                         r'\mark \markup { \musicglyph #"scripts.ufermata" }')
+                    lines[-1] = ('\t%s' % ' '.join(bar_items['last_bar']))
+        lines.append(self._format_bar(bar_items))
+
+        lines.append('}')
+
+        for i,lyric_block in enumerate(self['lyrics']):
+            # http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Entering-lyrics
+            # http://kainhofer.com/~lilypond/ajax/user/lilypond/Stanzas.html
+            assert voice != None, 'lyrics must be attached to a named voice'
+            lines.append(r'\new Lyrics \lyricsto "%s" {' % voice)
+            first_line = lyric_block[0]
+            first_word = first_line[0]
+            if first_word[0].isdigit():
+                stanza,words = first_word.split(' ', 1)
+                first_line[0] = words
+                lines.append('\t\\set stanza = #"%s "' % stanza)
+            for line in lyric_block:
+                L = []
+                for word in line:
+                    if not word.isalpha():
+                        word = '"%s"' % word
+                    L.append(word)
+                lines.append('\t%s' % ' '.join(L))
+            lines.append('}')
+
+        return lines
+
+
+class NWCLyricProperties (Structure):
+    _fields = []
+    for i in range(6):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCLyricBlockProperties (Structure):
+    #'\x00\x00\x00\x00\x00'
+    #'\x00\x04\x9f'\x00\x00\x01
+    _fields = [
+        ('num_blocks', 'B'),
+        ('len', 'B'),
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ]
+
+class NWCLyric (Structure):
+    _fields = [
+        ('lyric', 'S'),
+        ]
+
+
+class NWCStaffNoteProperties (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('color', 'B'),
+        ('num_tokens', 'H'),  # ?
+        ]
+    for i in range(1):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+
+# tokens
+
+
+class NWCTokenEnum (Enum):
+    _list = ['clef', 'key_sig', 'bar', '3', 'instrument_patch', 'time_sig',
+             'tempo', 'dynamics1', 'note', 'rest', 'chord1', 'pedal', 12,
+             'midi_MPC', 'fermata', 'dynamics2', 'performance_style', 'text',
+             'chord2']
+
+    @classmethod
+    def process(self, value):
+        if value > len(self._list):
+            return 'STOP'
+        try:
+            return self._list[value]
+        except IndexError:
+            raise ValueError('index %d out of range for %s'
+                             % (value, self._list))
+
+
+class NWCToken (Structure):
+    _fields = [
+        ('token', NWCTokenEnum),
+        ]
+
+
+class NWCClefEnum (Enum):
+    _list = ['treble', 'bass', 'alto', 'tenor']
+
+
+class NWCOctaveEnum (Enum):
+    _list = [0, 7, -7]
+
+
+class NWCToken_clef (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        (BitField('B',
+                  ('unknown3', 6),
+                  ('clef', 2, NWCClefEnum)),
+         'B'),
+        (BitField('B',
+                  ('unknown4', 6),
+                  ('octave', 2, NWCOctaveEnum)),
+         'B'),
+        ('unknown4', 'B'),
+        ('unknown5', 'B'),
+        ]
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-pitches#Clef
+        """
+        return r'\clef %s' % self['clef']
+
+
+class NWCToken_key_sig (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('flat_bits', 'H'),
+        ('sharp_bits', 'H'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ('unknown4', 'B'),
+        ('unknown5', 'B'),
+        ('unknown6', 'B'),
+        ('unknown7', 'B'),
+        ('unknown8', 'B'),
+        ]
+    _sigs = {
+        '00000000': 'c \major % or a \minor',
+        '00000020': 'g \major % or e \minor',
+        '00000024': 'd \major % or b \minor',
+        '00000064': 'a \major % or fis \minor',
+        '0000006c': 'e \major % or cis \minor',
+        '0000006d': 'b \major % or gis \minor',
+        '0000007d': 'fis \major % or dis \minor',
+        '0000007f': 'cis \major % or ais \minor',
+        '00020000': 'f \major % or d \minor',
+        '00120000': 'bes \major % or g \minor',
+        '00130000': 'ees \major % or c \minor',
+        '001b0000': 'aes \major % or f \minor',
+        '005b0000': 'des \major % or bes \minor',
+        '005f0000': 'ges \major % or ees \minor',
+        '007f0000': 'ces \major % or a \minor',
+        }
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Accidentals-and-key-signatures#index-_005ckey
+        """
+        sig = '%04x%04x' % (self['flat_bits'], self['sharp_bits'])
+        return '\key %s' % self._sigs[sig]
+
+    def accidental(self, pitch):
+        """Return the appropriate accidental for an in-key note.
+        """
+        index = ord(pitch[0].lower()) - ord('a')
+        if self['flat_bits'] >> index & 1:
+            return 'flat'
+        elif self['sharp_bits'] >> index & 1:
+            return 'sharp'
+        return 'natural'
+
+
+class NWCToken_bar (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ('unknown4', 'B'),
+        ]
+
+    _bars = {
+        'invisible': '',
+        'single': '|',
+        'double': '||',
+        'section_open': '.|',
+        'section_close': '|.',
+        # use \repeat for repeats
+        }
+
+    def ly_text(self, type=None):
+        """
+        http://www.noteworthysoftware.com/composer/faq/102.htm
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Bars#Bar-lines
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Repeats
+        """
+        if type == None:
+            type = 'single'  # TODO: detect bar type
+        return r'\bar "%s"' % self._bars[type]
+        #|Bar|Style:SectionClose|SysBreak:Y
+
+
+class NWCToken_3 (Structure):
+    _fields = []
+    for i in range(4):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCToken_instrument_patch (Structure):
+    _fields = []
+    for i in range(10):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+    def ly_text(self):
+        """
+        http://kainhofer.com/~lilypond/ajax/user/lilypond/Creating-MIDI-files.html#index-instrument-names-1
+        http://kainhofer.com/~lilypond/ajax/user/lilypond/MIDI-instruments.html#MIDI-instruments
+        """
+        return r'\set Staff.midiInstrument = #"%s"' % 'cello'
+
+
+class NWCToken_time_sig (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('beats', 'B'),
+        ('unknown3', 'B'),
+        ('beat_value', Pow2Enum),
+        ('unknown6', 'B'),
+        ('unknown7', 'B'),
+        ('unknown8', 'B'),
+        ]
+
+    _sigs = {
+        '4/4': '1', '3/4': '2.', '2/4': '2',  '1/4': '1',
+        '1/8': '8', '2/8': '4',  '3/8': '4.', '6/8': '2.',
+        '4/8': '2', '9/8': '12', '12/8': '1',
+        '2/2': '1', '4/2': '0',  '1/2': '2',
+        }
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-rhythms#Time-signature
+        """
+        return r'\time %s/%s' % (self['beats'], self['beat_value'])
+
+    def expected_bar_beats(self):
+        return float(self['beats'])/self['beat_value']
+
+
+class NWCToken_tempo (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ('unknown4', 'B'),
+        ('beats_per_minute', 'B'),
+        ('unknown6', 'B'),
+        ('unknown7', 'B'),
+        ('text', 'S'),
+        ]
+
+    def ly_text(self, time_sig=None):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-parts#Metronome-marks
+        """
+        if time_sig == None:
+            beats_per_measure = 4
+        else:
+            beats_per_measure = time_sig['']
+        return r'\tempo "%s" %s = %s' % (
+            self['text'], beats_per_measure, self['beats_per_minute'])
+
+
+class NWCToken_dynamics1 (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('unknown3', 'B'),
+        ('unknown4', 'B'),
+        ('unknown5', 'B'),
+        ('unknown6', 'B'),
+        ('unknown7', 'B'),
+        ('unknown8', 'B'),
+        ('unknown9', 'B'),
+        ]
+
+
+class NWCAccidentalEnum (Enum):
+    """
+    http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-pitches#Accidentals
+    """
+    _list = ['sharp', 'flat', 'natural', 'double_sharp', 'double_flat', 'auto']
+
+
+def _nwc_duration_dot(struct):
+    if struct['dot']:
+        return '.'
+    elif struct['ddot']:
+        return '..'
+    return ''
+
+def _nwc_duration(struct):
+    return '%s%s' % (struct['duration'], _nwc_duration_dot(struct))
+
+def _nwc_duration_value(struct):
+    v = 1.0 / struct['duration']
+    if struct['grace']:
+        return 0
+    if struct['dot']:
+        v *= 1.5
+    if struct['ddot']:
+        v *= 1.75
+    if struct['triplet'] != 'none':
+        v *= 2./3
+    return v
+
+
+class NWCStemEnum (Enum):
+    _list = ('neutral', 'up', 'down')
+    _ly_text_list = ('\stemNeutral', '\stemUp', '\stemDown')
+
+
+class NWCTripletEnum (Enum):
+    _list = ('none', 'start', 'inside', 'stop')
+
+
+class NWCBeamEnum (Enum):
+    _list = ('none', 'start', 'inside', 'stop', 4, 5, 6, 7)
+
+
+class NWCSlurEnum (Enum):
+    _list = ('none', 'start' , 'stop', 'inside')
+
+
+class NWCToken_note (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('duration', Pow2Enum),
+        ('unknown3', 'B'),
+        (BitField('B',
+                  ('unknown4', 2),
+                  ('stem', 2, NWCStemEnum),
+                  ('triplet', 2, NWCTripletEnum),
+                  ('beam', 2, NWCBeamEnum)),
+         'B'),
+        ('unknown6', 'B'),
+        (BitField('B',
+                  ('unknown5', 2),
+                  ('accent', 1, BoolEnum),
+                  ('tie', 1, BoolEnum),
+                  ('unknown6', 1),
+                  ('dot', 1, BoolEnum),
+                  ('staccato', 1, BoolEnum),
+                  ('ddot', 1, BoolEnum)),
+         'B'),
+        (BitField('B',
+                  ('unknown7', 2),
+                  ('grace', 1),
+                  ('unknown8', 2),
+                  ('tenuto', 1, BoolEnum),
+                  ('slur', 2, NWCSlurEnum),
+                  ),
+         'B'),
+        ('pitch', 'b'),
+        (BitField('B',
+                  ('unknown9', 4),
+                  ('pitch1', 1, BoolEnum),  # some kind of pitch adjustment?
+                  ('accidental', 3, NWCAccidentalEnum),
+                  ),
+         'B'),
+        ]
+
+    _scale = (  # this list is taken from lilycomp
+        "c,,,","d,,,","e,,,","f,,,","g,,,","a,,,","b,,,",
+        "c,,","d,,","e,,","f,,","g,,","a,,","b,,",
+        "c,","d,","e,","f,","g,","a,","b,",
+        "c","d","e","f","g","a","b",
+        "c'","d'","e'","f'","g'","a'","b'",
+        "c''","d''","e''","f''","g''","a''","b''",
+        "c'''","d'''","e'''","f'''","g'''","a'''","b'''",
+        "c''''","d''''","e''''","f''''","g''''","a''''","b''''",
+        )
+
+    _clef_offsets = {
+        'treble': "b'",
+        'bass': 'd',
+        'alto': "c'",
+        'tenor': "a'",
+        }
+
+    _accidentals = {
+        'natural': '',
+        'sharp': 'is',
+        'flat': 'es',
+        'double_sharp': 'isis',
+        'double_flat': 'eses',
+        'semi_sharp': 'ih',
+        'semi_flat': 'eh',
+        'sesqui_sharp': 'isih',
+        'sesqui_flat': 'eseh'
+        }
+
+    _ornaments = {
+        # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Attached-to-notes#Articulations-and-ornamentations
+        # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/List-of-articulations
+        'accent': '->',  # r'\accent',
+        'marcato': '-^',  #r'\marcato',
+        'staccatissimo': '-|',  # r'\staccatissimo',
+        'espressivo': r'\espressivo',
+        'staccato': '-.',  # r'\staccato',
+        'tenuto': '--',  # r'\tenuto',
+        'portato': '-_',  # r'\portato',
+        'upbow': r'\upbow',
+        'downbow': r'\downbow',
+        'flageolet': r'\flageolet',
+        'thumb': r'\thumb',
+        'lheel': r'\lheel',
+        'rheel': r'\rheel',
+        'ltoe': r'\ltoe',
+        'rtoe': r'\rtoe',
+        'open': r'\open',
+        'stopped': '-+',  #r'\stopped',
+        'turn': r'\turn',
+        'reverseturn': r'\reverseturn',
+        'trill': r'\trill',
+        'prall': r'\prall',
+        'mordent': r'\mordent',
+        'prallprall': r'\prallprall',
+        'prallmordent': r'\prallmordent',
+        'upprall': r'\upprall',
+        'downprall': r'\downprall',
+        'upmordent': r'\upmordent',
+        'downmordent': r'\downmordent',
+        'pralldown': r'\pralldown',
+        'prallup': r'\prallup',
+        'lineprall': r'\lineprall',
+        'signumconguruentiae': r'\signumconguruentiae',
+        'shortfermata': r'\shortfermata',
+        'fermata': r'\fermata',
+        'longfermata': r'\longfermata',
+        'verylongfermata': r'\verylongfermata',
+        'segno': r'\segno',
+        'coda': r'\coda',
+        'varcoda': r'\varcoda',
+        }
+
+    _slurs = {
+        'none': '',
+        'start': r'\(',
+        'stop': r'\)',
+        'inside': '',
+        }
+
+    def ly_text(self, clef=None, key=None,
+                previous_note=None, next_note=None,
+                in_chord=False):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Curves#Phrasing-slurs
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Ties
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Tuplets
+        http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Special-rhythmic-concerns#Grace-notes
+        """
+        if clef:
+            clef_name = clef['clef']
+        else:
+            clef_name = 'treble'
+        clef_offset = self._scale.index(self._clef_offsets[clef_name])
+        pitch = self._scale[-self['pitch'] + clef_offset]
+
+        if self['accidental'] == 'auto':
+            if key:
+                accidental = key.accidental(pitch)
+            else:
+                accidental = 'natural'
+        else:
+            accidental = self['accidental']
+        note = [pitch[0], self._accidentals[accidental], pitch[1:]]
+
+        if in_chord:
+            return ''.join(note)
+
+        note.append(_nwc_duration(self))
+
+        for key in ['accent', 'staccato', 'tenuto']:
+            if self[key]:
+                note.append(self._ornaments[key])
+
+        note.append(self._slurs[self['slur']])
+
+        if self['tie']:
+            note.append('~')
+
+        if self['beam'] == 'start':
+            note.append('[')
+        elif self['beam'] == 'stop':
+            note.append(']')
+
+        if self['triplet'] == 'start':
+            note.insert(0, r'\times 2/3 { ')
+        elif self['triplet'] == 'stop':
+            note.append(' }')
+
+        if self['grace']:
+            if previous_note == None or not previous_note['grace']:
+                note.insert(0, r'\acciaccatura { ')
+            if next_note == None or not next_note['grace']:
+                note.append(' }')
+
+        return ''.join(note)
+
+
+class NWCToken_rest (NWCToken_note):
+    def ly_text(self, **kwargs):
+        rest = ['r%s' % _nwc_duration(self)]
+        if self['triplet'] == 'start':
+            rest.insert(0, r'\times 2/3 { ')
+        elif self['triplet'] == 'stop':
+            rest.append(' }')
+
+        return ''.join(rest)
+
+
+class NWCToken_chord1 (Structure):
+    _fields = NWCToken_note._fields + [
+        ('num_notes', 'B'),
+        ('unknown12', 'B'),
+        ]
+
+    def unpack_from(self, buffer, offset=0):
+        super(NWCToken_chord1, self).unpack_from(buffer, offset)
+        chords,size = self._unpack_chords_from(buffer, offset + self.size)
+        self['chords'] = chords
+        self.size += size
+
+    def _unpack_chords_from(self, buffer, offset=0):
+        size = 0
+        chord1 = []
+        chord2 = []
+
+        token = NWCToken()
+        size = 0
+        for i in range(self['num_notes']):
+            token.unpack_from(buffer, offset + size)
+            size += token.size
+            assert token['token'] in ['rest', 'note'], token
+
+            structure = globals()['NWCToken_%s' % token['token']]
+            t = structure(buffer, offset + size)
+            size += t.size
+
+            if _nwc_duration(t) == _nwc_duration(self):
+                chord1.append(t)
+            else : # 2 voices
+                chord2.append(t)
+
+        chords = [chord1]
+        if chord2:
+            chords.append(chord2)
+        return (chords, size)
+
+    def ly_text(self, **kwargs):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Combining-notes-into-chords#Combining-notes-into-chords
+        """
+        notes = []
+        for chord in self['chords']:
+            for token in chord:
+                if isinstance(token, NWCToken_note):
+                    notes.append(token.ly_text(in_chord=True, **kwargs))
+        chord = ['<%s>' % ' '.join(notes)]
+        chord.append(_nwc_duration(self))
+        if self['tie']:
+            chord.append('~')
+
+        if self['beam'] == 'start':
+            chord.append('[')
+        elif self['beam'] == 'stop':
+            chord.append(']')
+
+        if self['triplet'] == 'start':
+            chord.insert(0, r'\times 2/3 { ')
+        elif self['triplet'] == 'stop':
+            chord.append(' }')
+
+        return ''.join(chord)
+#        if chord2:  # 2 voices
+#            result += ' << '
+#            for i in range(len(chord2)):
+#                (pitch,accidental,duration ) = chord2[i]
+#                pitch += lastClef
+#                note = SCALE[pitch]
+#
+#                if (accidental!='auto'):
+#                    currentKey[note[0]] = accidental
+#                accidental = currentKey[note[0]]
+#
+#                if (relative_pitch):
+#                    octave = getRelativePitch(lastPitch, pitch)
+#                    lastPitch = pitch
+#                else:
+#                    octave = note[1:]
+#                result += note[0] + accidental + octave + duration + ' '
+#
+#            result += " \\\\ {"
+#
+#            for i in range(len(chord1)):
+#                (pitch,accidental ) = chord1[i]
+#                pitch += lastClef
+#                note = SCALE[pitch]
+#
+#                if (accidental!='auto'):
+#                    currentKey[note[0]] = accidental
+#                accidental = currentKey[note[0]]
+#
+#                if (relative_pitch):
+#                    octave = getRelativePitch(lastPitch, pitch)
+#                    lastPitch = pitch
+#                else:
+#                    octave = note[1:]
+#                result += note[0] + accidental + octave +chordDur + ' '
+#            if lastChord >0 : lastChord = _nwc_duration_value(duration) - _nwc_duration_value(chordDur)
+#            if lastChord==0: result += ' } >> '
+#            # end 2 voices
+#        else:  # block chord
+#            result += ' <'
+#            for i in range(len(chord1)):
+#                (pitch,accidental ) = chord1[i]
+#                pitch += lastClef
+#                note = SCALE[pitch]
+#
+#                if (accidental!='auto'):
+#                    currentKey[note[0]] = accidental
+#                accidental = currentKey[note[0]]
+#
+#                if (relative_pitch):
+#                    octave = getRelativePitch(lastPitch, pitch)
+#                    lastPitch = pitch
+#                else:
+#                    octave = note[1:]
+#                result += note[0] + accidental + octave + ' '
+#            result += '>' +chordDur +' '
+#            lastPitch = chord1[0][0] + lastClef
+#        lastDuration = chordDur
+
+
+class NWCToken_pedal (Structure):
+    _fields = []
+    for i in range(5):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCToken_midi_MPC (Structure):
+    _fields = []
+    for i in range(36):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCToken_fermata (Structure):
+    _fields = []
+    for i in range(6):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Other-uses-for-tweaks#index-fermata_002c-implementing-in-MIDI
+        """
+        return r'\fermata'
+
+
+class NWCToken_dynamics2 (Structure):
+    _fields = []
+    for i in range(5):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCToken_performance_style (Structure):
+    _fields = []
+    for i in range(5):
+        _fields.append(('unknown%d' % (i+1), 'B'))
+
+
+class NWCToken_text (Structure):
+    _fields = [
+        ('unknown1', 'B'),
+        ('unknown2', 'B'),
+        ('textpos', 'B'),
+        ('unknown3', 'B'),
+        ('unknown4', 'B'),
+        ('text', 'S'),
+        ]
+
+    def ly_text(self):
+        """
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-text
+        http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-text#Text-marks
+        """
+        #if text.isdigit() : # check numbers
+        #    text = "-\\markup -\\number "+ text
+        #    #text = "-\\markup {\\number "+ text +"}"
+        #else :
+        #    text = '-"' + text + '"'
+        #extra += ' ' + text
+        return r'\mark "%s"' % self['text']
+
+
+class NWCToken_chord2 (NWCToken_chord1):
+    pass
+
+
+def _get_type_and_type_tail(nwc_buffer):
+    """
+    >>> _get_type_and_type_tail('[NWZ]...\\n[bla bla]...')
+    ('NWZ', '...\\n[bla bla]...')
+    >>> _get_type_and_type_tail('[NoteWorthy ArtWare]...\\n[bla bla]...')
+    ('NoteWorthy ArtWare', '...\\n[bla bla]...')
+    """
+    m = re.match('\[([^]]*)\]', nwc_buffer)
+    _type = m.group(1)
+    tail = nwc_buffer[len(m.group(0)):]
+    return (_type, tail)
+
+def _normalize_nwc(nwc_buffer):
+    """
+    Tested in `parse_nwc()` doctests.
+    """
+    _type,tail = _get_type_and_type_tail(nwc_buffer)
+    if _type == 'NWZ':
+        LOG.debug('compressed nwc detected, decompressing.')
+        uncompressed_nwc_buffer = zlib.decompress(tail[1:])
+        return uncompressed_nwc_buffer
+    return nwc_buffer
+
+def _get_staff(nwc_buffer, offset, n):
+    orig_offset = offset
+    staff = NWCStaff(nwc_buffer, offset)
+    offset += staff.size
+    if staff['num_lyrics'] > 0:
+        assert staff['has_lyrics'] == 1, pprint.pformat(staff)
+        staff['lyric_properties'] = NWCLyricProperties(nwc_buffer, offset)
+        offset += staff['lyric_properties'].size
+    staff['lyrics'] = []
+    for i in range(staff['num_lyrics']):
+        p = NWCLyricBlockProperties(nwc_buffer, offset)
+        offset += p.size
+        length = 1024 * (p['num_blocks']/4) - 1
+        #p['len']
+        lines = re.split(
+            '\r|\n',
+            _decode_string(nwc_buffer[offset:offset+length]))
+        block = []
+        for line in lines:
+            block.append([word.strip() for word in line.rstrip('\x00').split('\x00')])
+            LOG.debug('lyric line %s' % block[-1])
+        staff['lyrics'].append(block)
+        offset += length
+    # possible if lyric byte read
+    staff['note_properties'] = NWCStaffNoteProperties(nwc_buffer, offset)
+    offset += staff['note_properties'].size
+
+    staff['tokens'] = []
+    token = NWCToken()
+    oo = offset
+    for i in range(10000000):  # num_tokens
+        try:
+            token.unpack_from(nwc_buffer, offset)
+        except struct.error:
+            LOG.warn('ran off end of file')
+            return (staff, offset-orig_offset)
+        if token['token'] == 'STOP':
+            break
+        offset += token.size
+        LOG.debug('token: %s' % token['token'])
+        if type(token['token']) == int:
+            raise ValueError(token['token'])
+        structure = globals()['NWCToken_%s' % token['token']]
+        t = structure(nwc_buffer, offset)
+        offset += t.size
+        staff['tokens'].append(t)
+    LOG.debug('%d tokens, %d bytes (chords count as one)' % (i, offset - oo))
+    return (staff, offset-orig_offset)
+
+def parse_nwc(nwc_buffer):
+    """Parse a NoteWorthy Composer `nwc` file.
+
+    >>> import os.path
+    >>> with open(os.path.join('example', '1.75-1.nwc'), 'rb') as f:
+    ...     nwc_buffer = f.read()
+    >>> nwc_buffer  # doctest: +ELLIPSIS
+    '[NWZ]...'
+    >>> n = parse_nwc(nwc_buffer)
+    >>> n['_debug']['nwc_buffer']  # doctest: +ELLIPSIS
+    '[NoteWorthy ArtWare]...'
+    >>> d = n.pop('_debug')
+    >>> pprint.pprint(n)  # doctest: +ELLIPSIS, +REPORT_UDIFF
+    {'major_version': 1,
+     'minor_version': 75,
+     'na': u'N/A',
+     'name1': u'Abwhir',
+     'page_setup': {'f2': u'F2',
+                    'margins': u'1.00000000 1.00000000 1.00000000 1.00000000',
+                    'ny_': u'NN_',
+                    'page_small': {'name': u'Times New Roman',
+                                   'size': 10,
+                                   'style': 0,
+                                   'unknown3': 0,
+                                   'unknown4': 0},
+                    'page_text': {'name': u'Times New Roman',
+                                  'size': 12,
+                                  'style': 0,
+                                  'unknown3': 0,
+                                  'unknown4': 0},
+                    'page_title': {'name': u'Times New Roman',
+                                   'size': 24,
+                                   'style': 1,
+                                   'unknown3': 0,
+                                   'unknown4': 0},
+                    'staff_bold': {'name': u'Times New Roman',
+                                   'size': 10,
+                                   'style': 1,
+                                   'unknown3': 0,
+                                   'unknown4': 0},
+                    'staff_italic': {'name': u'Times New Roman',
+                                     'size': 12,
+                                     'style': 3,
+                                     'unknown3': 0,
+                                     'unknown4': 0},
+                    'staff_lyric': {'name': u'Times New Roman',
+                                    'size': 15,
+                                    'style': 0,
+                                    'unknown3': 0,
+                                    'unknown4': 0},
+                    ...,
+                    'user1': {'name': u'Times New Roman',
+                              'size': 12,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0},
+                    'user2': {'name': u'Times New Roman',
+                              'size': 12,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0},
+                    'user3': {'name': u'Times New Roman',
+                              'size': 12,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0},
+                    'user4': {'name': u'Times New Roman',
+                              'size': 12,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0},
+                    'user5': {'name': u'Times New Roman',
+                              'size': 8,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0},
+                    'user6': {'name': u'Times New Roman',
+                              'size': 8,
+                              'style': 0,
+                              'unknown3': 0,
+                              'unknown4': 0}},
+     'product': u'[NoteWorthy Composer]',
+     'song_info': {'author': u'George Job Elvey, 1858',
+                   'comments': u'Source: The United Methodist Hymnal (Nashville, Tennessee: The United Methodist Publishing House, 1989), # 694.',
+                   'copyright1': u'Public Domain',
+                   'copyright2': u'Courtesy of the Cyber Hymnal (http://www.cyberhymnal.org)',
+                   'title': u'St. George\u2019s Windsor, 77.77 D'},
+     'staff': [{'ff': 255,
+                'group': u'Standard',
+                'has_lyrics': 0,
+                'lyrics': [],
+                'name': u'Unnamed-000',
+                'note_properties': {'color': 0, 'num_tokens': 0, 'unknown1': 0},
+                'num_lyrics': 0,
+                'tokens': [{'flat_bits': 2,
+                            'sharp_bits': 0,
+                            ...},
+                           {'tempo': u'',
+                            ...},
+                           {'beat_value': 4,
+                            'beats': 4,
+                            ...},
+                           ...],
+                'unknown01': 0,
+                ...}],
+     'type': u'[NoteWorthy ArtWare]',
+     'unknown01': 0,
+     ...}
+    """
+    LOG.info('parsing nwc')
+    n = {'_debug': {}}
+    nwc_buffer = _normalize_nwc(nwc_buffer)
+    n['_debug']['nwc_buffer'] = nwc_buffer
+
+    head = NWCFileHead(nwc_buffer)
+    offset = head.size
+    n.update(head)
+
+    assert head['major_version'] == 1, head['major_version']
+    assert head['minor_version'] == 75, head['minor_version']
+
+    #o = nwc_buffer[offset:].find('\xff')
+    #if o < 0:
+    #    LOG.error('could not find staff section')
+    #    raise NotImplementedError
+    #offset += o
+    #LOG.warn('skip to staves with offset %d (skipped %d, %s)'
+    #          % (offset, o, repr(nwc_buffer[offset:offset+10])))
+
+    n['staff_set'] = NWCStaffSet(nwc_buffer, offset)
+    offset += n['staff_set'].size
+
+    n['staff'] = []
+    for i in range(n['staff_set']['num_staves']):
+        LOG.info('process staff %d' % i)
+        staff,size = _get_staff(nwc_buffer, offset, n)
+        offset += size
+        if staff == None:
+            break
+        n['staff'].append(staff)
+
+    return n
+
+
+def ly_text(nwc):
+    """
+    >>> import os.path
+    >>> with open(os.path.join('example', '1.75-1.nwc'), 'rb') as f:
+    ...     nwc_buffer = f.read()
+    >>> nwc = parse_nwc(nwc_buffer)
+    >>> print ly_text(nwc)  # doctest: +ELLIPSIS
+    '[NoteWorthy ArtWare]...'
+
+    http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Creating-MIDI-files
+    """
+    if nwc['type'] not in ['[NoteWorthy ArtWare]', '[NoteWorthy Composer]']:
+        raise ValueError('unknown file type: %s' % nwc['type'])
+
+    #LOG.debug(pprint.pformat(nwc, width=70))
+    LOG.info('format header')
+
+    lines = [
+        '%% Generated with %s converter v%s' % ('nwc2ly.py', __version__),
+        r'\version "2.12.2"',  # http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Version-number#Version-number
+        ]
+
+    lines.extend([nwc['song_info'].ly_text(), ''])
+
+    lines.extend([
+            '',
+            r'\score {',
+            '\t<<',
+            ])
+
+    for i,staff in enumerate(nwc['staff']):
+        LOG.info('format staff %d' % i)
+        voice = chr(i+ord('a'))
+        if i == 1 and False:
+            for token in staff['tokens']:
+                if isinstance(token, NWCToken_clef):
+                    token['clef'] = 'bass'
+        lines.extend(['\t\t%s' % line for line in staff.ly_lines(voice=voice)])
+
+    LOG.info('format footer')
+    lines.extend([
+            '\t>>',
+            ''
+            '\t\layout {}',
+            '\t\midi {}',
+            '}',
+            ''])
+    return '\n'.join(lines)
+
+
+def test():
+    import doctest
+    return doctest.testmod()
+
+
+if __name__ == '__main__':
+    from optparse import OptionParser
+
+    usage = "nwc2ly.py [options] uncompressed.nwc test.ly > convert.log"
+    p = OptionParser(usage=usage)
+    p.add_option('--debug', dest='debug', action='store_true',
+                 help='Enable verbose logging')
+    p.add_option('--absolute-pitch', dest='relative_pitch', default=True,
+                 action='store_false',
+                 help='')
+    p.add_option('--absolute-duration', dest='relative_duration', default=True,
+                 action='store_false',
+                 help='')
+    p.add_option('--bar-comments', dest='bar_comments', default=10,
+                 type='int',
+                 help='Comments for every x lines, section/line comment??')
+    p.add_option('--no-beaming', dest='insert_beaming', default=True,
+                 action='store_false', help='')
+    p.add_option('--no-stemming', dest='insert_stemming', default=True,
+                 action='store_false', help='')
+    p.add_option('--no-text', dest='insert_text', default=True,
+                 action='store_false', help='Currently a no-op')
+    p.add_option('--test', dest='test', action='store_true',
+                 help='Run internal tests and exit')
+
+    options,args = p.parse_args()
+
+    if options.debug:
+        logging.basicConfig(level=logging.DEBUG)
+    else:
+        logging.basicConfig(level=logging.INFO)
+
+    if options.test:
+        results = test()
+        sys.exit(min(results.failed, 127))
+
+    #options? lvb7th1
+    nwc_file = args[0]
+    if len(args) > 1:
+        ly_file = args[1]
+    else:
+        ly_file = None
+
+    with open(nwc_file, 'rb') as f:
+        nwc_buffer = f.read()
+    nwc = parse_nwc(nwc_buffer)
+
+    if ly_file:
+        f = open(ly_file, 'w')
+    else:
+        f = sys.stdout
+
+    f.write(ly_text(nwc).encode('utf-8'))
+
+    if ly_file:
+        f.close()