From ee2f3b8db7c4a3a82f60e83917887bcb8bd33c41 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 30 Nov 2010 13:32:06 -0500 Subject: [PATCH] Cleanup to more Pythonic sytax. --- nwc2ly.py | 2834 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 1773 insertions(+), 1061 deletions(-) mode change 100644 => 100755 nwc2ly.py diff --git a/nwc2ly.py b/nwc2ly.py old mode 100644 new mode 100755 index 028cc45..8aaf9e4 --- a/nwc2ly.py +++ b/nwc2ly.py @@ -1,1061 +1,1773 @@ -import binascii, sys, zlib, traceback - -shortcopyleft = """ -nwc2ly - Converts NWC(v 1.75) to LY fileformat -Copyright (C) 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -""" - -## -# most infomation obtained about the nwc format -# is by using noteworthycomposer and the somewhat like the 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 Sig, Triplets, Experimental chords -# 0.6 17 April 2005 Compressed NWC file Supported! -# 0.7 19 April 2005 Version detection, Header -# 20 April 2005 BUGS 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 -# -## -# TODO -# Proper syntax and structure for staffs, lyrics, layering -# version 1.7 Support -# nwc2ly in lilytool -# -# Piano Staff -# Chords -# Pedals -# Midi Instruments -# Visability -# Lyrics -# Context Staff -# Staff layering / Merging -## -# -# BUGS text markups, chords, fermata -###### -# -# cd /cygdrive/c/development/nwc2ly -# $ python nwc2ly.py lvb7th1\ uncompressed.nwc test.ly > convert.log -####### - -nwc2lyversion = '0.9.0' -############ -# Options # -############ -debug = 0 - # You can use 0 for False and 1 for True -relativePitch = 1 -relativeDuration = 1 -barLinesComments = 10 # Comments for every x lines # section/line comment - -insertBeaming = 1 -insertSteming = 1 -#insertText = 0 - -nwcversion = 1.75 -############## - -args = len(sys.argv) -if args<2: - print "Syntax: python nwc2ly.py nwcfile [lyfile]" - sys.exit() -nwcfile = sys.argv[1] # 'simple1.nwc' #'simple2.nwc' 'simple3.nwc' 'bbc5-1-org.nwc' bbc5-1-nop. - -if args<3: - lyfile = '' # 'test.ly' -else: - lyfile = sys.argv[2] - - - -def getFileFormat(nwcData): - #'[NoteWorthy ArtWare]' - #'[NoteWorthy Composer]' - nwcData.seek(0) - company = readLn(nwcData) - nwcData.seek(2,1) # pad - product = readLn(nwcData) - version = readLn(nwcData) - version = ord(version[0]) * 0.01 + ord(version[1]) - nwcversion = version - print 'NWC file version', nwcversion, 'detected!' - pad(nwcData,2) # times saved - name1 = readLn(nwcData) - name2 = readLn(nwcData) - - pad(nwcData,8) - huh = nwcData.read(1) - pad(nwcData,1) - -def getFileInfo(nwcData): - title = readLn(nwcData) - author = readLn(nwcData) - copyright1 = readLn(nwcData) - copyright2 = readLn(nwcData) - comments = readLn(nwcData) - - header = "\\header {" - header += "\n\ttitle = \"%s\"" % title - header += "\n\tenteredby = \"%s\"" % author - header += "\n\tcopyright = \"%s\"" % copyright1 - header += "\n\tfooter = \"%s\"" % copyright2 - header += "\n\t%%{ %s %%}" % comments - header += "\n}" - - # TO ADD IN DEFAULT BLANK FEILDS TO KEY IN - print 'title,author,copyright1,copyright2,comments: ', (title,author,copyright1,copyright2,comments) - return header - -# Page Setup -def getPageSetup(nwcData): - # ?? - getMargins(nwcData) - #getContents(nwcData) - #getOptions(nwcData) - getFonts(nwcData) - - -def getMargins(nwcData): - readTill(nwcData,'\x01') - pad(nwcData,1) - # get string size 43 - margins = readLn(nwcData) - print 'margins ', margins - #mirrorMargines - #UOM - return - -def getOptions(nwcData): - # page numbering, from - # title page info - # extend last system - # incrase note spacing for larger note duration - # staff size - # staff labels (none, first systems, top systems, all systems - # measure numbers none, plain, circled, boxed - # measure start - return - -def getFonts(nwcData): - # 12 Times - readTill(nwcData,'\xFF') - n = nwcData.read(1) - pad(nwcData,1) - for i in range (12): - # Font Name - font = readLn (nwcData) - - # 00 - - # Style 'Regular' 'Italic' 'Bold' 'Bold Italic' - style = ord(nwcData.read(1)) & 3 - - # Size - size = ord(nwcData.read(1)) - - ## 00 - nwcData.seek(1,1) - - # Typeface - # 00 Western, b1 Hebrew - typeface = nwcData.read(1) - - print 'Font detected' , font, 'size',size, 'style ', style, ' typeface',typeface - - -def findNoOfStaff(nwcData): - # Infomation on Staffs \x08 00 00 FF 00 00 n - data = 0; - - readTill(nwcData,'\xFF') - print "Where am I? ", nwcData.tell() - - - nwcData.read(2) - - layering = nwcData.read(1) # FF or 00 - - noOfStaffs = ord(nwcData.read(1)) - nwcData.read(1) - print noOfStaffs, " noOfStaffs found" - return noOfStaffs - -def findStaffInfo(nwcData): - # Properties for Staff - # General - # Name. - # Group - # Ending Bar - # Visual - # Verticle Upper Size - # Verticle Lower Size - # Style - # Layer Next Staff - # Color - # Midi - # Part Volume - # Stereo Pan - # Transposition - # Muted - # PlayBack Device - # Playback Channel - # Instrument - # Patch Name - # Patch List Type - # Bank Select - # Controller0 - # Controller32 - # Staff Lyrics - # LineCount - # AlignSyllableRule - # StaffPlacementAligment - # StaffPlacementOffset - # StaffPropertiesVerticleSizeUpper - # StaffPropertiesVerticleSizeLower - - format = '' - staffName = readLn(nwcData) - format += "\\context Staff = %s " % staffName # or voice or lyrics - - groupName = readLn(nwcData) # HOW TO ORGANISE THEM?? - - endbar = ord(nwcData.read(1)) # & (2^3-1) - #print 'end ',endbar - endingBar = ending[endbar] # 10 --> OC for lyrics? 10000 1100 - - muted = ord(nwcData.read(1)) & 1 - nwcData.read(1) - - channel = ord(nwcData.read(1)) + 1 - nwcData.read(9) - - stafftype = staffType[ord(nwcData.read(1))&3] - nwcData.read(1) - - uppersize = 256 - ord(nwcData.read(1)) # - signed +1 )& 2^7-1 ) - readTill(nwcData,'\xFF') - - lowersize = ord(nwcData.read(1)) - ww = nwcData.read(1) - print '[uppersize,lowersize]',[uppersize,lowersize] - - noOfLines = ord(nwcData.read(1)) - print '[staffName,groupName,endingBar,stafftype,noOfLines]', [staffName,groupName,endingBar,stafftype,noOfLines] - - layer = ord(nwcData.read(1)) & 1 - - # signed transposition - # FF? - - partVolume = ord(nwcData.read(1)) - ord(nwcData.read(1)) - - stereoPan = ord(nwcData.read(1)) - if nwcversion == 1.7: - nwcData.read(2) - else: - nwcData.read(3) - - nwcData.read(2) - #lyrics = ord(nwcData.read(1)) & 1 - lyrics = readInt(nwcData) - noOfLyrics = readInt(nwcData) - - lyricsContent = '' - if lyrics: - lyricOptions = readInt(nwcData) - nwcData.read(3) - for i in range(noOfLyrics): - print 'looping ',i, 'where', nwcData.tell() - lyricsContent += '\\ \lyricmode { ' # lyrics - lyricsContent += getLyrics(nwcData) # - lyricsContent += '}' - nwcData.read(1) - - nwcData.read(1) - color = ord(nwcData.read(1)) & 3 #12 - - noOfTokens = readInt(nwcData) - print noOfTokens, " Tokens found", nwcData.tell() - return noOfTokens, format, lyricsContent - - - - - -def pad(nwcData, length): - nwcData.seek(length,1) - -def readTill(nwcData, delimit): - data = '' - value = '' - while data!=delimit: - value += data - data = nwcData.read(1) - - return value - -def readLn(nwcData): - return readTill (nwcData,'\x00') - # reads until 00 is hit - # od oa == \n - -def readInt(nwcData): - data = nwcData.read(2) - no = ord(data[0]) - no += ord(data[1]) * 256 - return no - - -def getLyrics(nwcData): - - data = '' - print 'reach' - blocks = ord(nwcData.read(1)) - if blocks==4: blocks = 1 - if blocks==8: blocks = 2 - if blocks == 0: return - - lyricsLen = readInt(nwcData) - - print 'blocks ',blocks, 'lyrics len', lyricsLen, 'at ', nwcData.tell() - - nwcData.read(1) - for i in range (blocks): - data += nwcData.read(1024) - - lyrics = data[1:lyricsLen-1] - print 'lyrics ', lyrics - return lyrics - -def getDuration(data): - durationBit = ord(data[2]) & 7 - durationDotBit = ord(data[6]) - - duration = durations[durationBit] - if (durationDotBit & 1<<2): - durationDot = '.' - elif (durationDotBit & 1): - durationDot = '..' - else : - durationDot = '' - return duration + durationDot - -def getKey(data): - data = binascii.hexlify(data) - - if (keysigs.has_key(data)): - return '\key ' + keysigs[data] - return '% unknown key' - -def getLocation(data): - offset = ord(data[8]); - if offset > 127 : - return 256-offset - if (ord(data[9])>>3 & 1): - return -offset - - return offset - #print 'offset ', offset - #print binascii.hexlify(data[8:9]) - -def getAccidental(data): - data = ord(data) - data = (data & 7 ) - return acdts[data] - -def getNote(data): - - # pitch - pitch = getLocation(data) - - # get Accidentals - accidental = getAccidental(data[9]) - - # get Relative Duration - duration = getDuration(data) - - # check stems - stem = ord(data[4]) - stem = (stem >> 4) & 3 - - # check beam - beam = ord(data[4]) & 3 - - # triplets - triplet = triplets [ord(data[4])>>2 & 3 ] - - # check tie - tie = '' - if ord(data[6]) >> 4 & 1: - tie = '~' - - staccato = (ord(data[6]) >> 1) & 1 - accent = (ord(data[6]) >> 5) & 1 - - tenuto = (ord(data[7]) >> 2) & 1 - slur = slurs[ord(data[7]) & 3 ] - grace = (ord(data[7]) >> 5) & 1 - - # check slur - slur = slurs[ord(data[7]) & 3 ] - - - #TODO should use a dictionary - return (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto) - -def getRelativePitch(lastPitch, pitch): - octave = '' - diff = pitch - lastPitch - if diff>3: - for i in range((diff-4 + 7)/7): - octave += "'" - if octave == '': - octave += "'" - elif diff<-3: - for i in range((-diff-4 + 7)/7): - octave += "," - if octave == '': - octave += "," - return octave - -def durVal(duration): - where = duration.find('.') - - if where>-1: - durVal = 128/int (duration[:where]) - nextVal = durVal - for i in range(len(duration)-where): - nextVal = nextVal / 2 - durVal += nextVal - - else: - durVal = 128/int (duration) - return durVal - -def processStaff(nwcData): - keysigCount =0 - timesigCount=0 - noteCount=0 - restCount=0 - staffCount=0 - barlineCount=0 - clefCount=0 - textCount =0 - dynamicCount = 0 - - lastPitch = scale.index('c') - lastDuration = 0 - lastClef = scale.index(clefs[0]) # index referenced to note of last clef - lastStem = 0 - lastTimesig = 1 - - lastKey = { 'c': '', #c - 'd': '', 'e': '', 'f': '', 'g': '', 'a': '', 'b': '' } - currentKey = lastKey.copy() - - data = 0 - token = 1 - - result = "" - result += "\n\t\\new Staff {\n\t\t" - lastChord = 0 - lastGrace = 0; - - (noOfTokens,format, lyrics) = findStaffInfo(nwcData); - if lyrics!='': - #result += '\\lyricmode { ' + lyrics + '}' - result += '%%Lyrics %%{ ' + lyrics + '%%}' - - if relativePitch: - result+="\\relative c {" - else : - result+=" {" - result+="\n\t\t\n\n\t\t" - result+= '\n\t %% Staff %s \n\t' % staff - - #print "00112233445566778899\n" - extra = '' - # the juice - - while data!="": - - token += 1 - if token==noOfTokens: - result += "\n\t\t}\n\t}\n\t" - print "going next staff! %s" % nwcData.tell() - break - - - if nwcversion==1.7: - nwcData.seek(2,1) - - data = nwcData.read(1) - #print 'test', data - - # clef - if data=='\x00': - data = nwcData.read(6) - clefCount += 1 - - key = ord(data[2]) & 3 - octave = ord(data[4]) & 3 - # print binascii.hexlify(data) , "CLEF? " - lastClef = scale.index(clefs[key]) + octaves[octave] - #TODO check for octave shifts _8 - lastClef += clefShift[octave] - result += '\clef "' + clefNames[key] + clefOctave[octave]+ '"\n\t\t' - - # key signature - elif data=='\x01': - data = nwcData.read(12) - keysigCount = keysigCount + 1 - - # - flatBits = ord(data[2]) - sharpBits = ord(data[4]) - - for note in lastKey.keys(): - noteInd = ['a','b','c','d','e','f','g'].index(note) - if (flatBits >> noteInd & 1): - lastKey[note] = 'es' - elif (sharpBits >> noteInd & 1): - lastKey[note] = 'is' - else: - lastKey[note] = '' - - currentKey = lastKey.copy() - result = result + getKey(data[1:5]) + "\n\t\t" - - #print "data", binascii.hexlify(data) - #print "flat", binascii.hexlify(flatBits) - #print "sharp", binascii.hexlify(data[4]) - #print getKey(data[1:5]) - - - # barline - elif data=='\x02': - data = nwcData.read(4) - barlineCount += 1 - - currentKey = lastKey.copy() - - result += "|\n\t\t" - if (barlineCount % barLinesComments == 0): - result += "\n\t\t% Bar " + str(barlineCount + 1) + "\n\t\t" - #print '.', - print "Bar ",barlineCount, " completed, " - - # timesig - elif data=='\x05': - data = nwcData.read(8) - timesigCount = timesigCount + 1 - beats = ord(data[2]) - beatValues = [ 1, 2, 4, 8 ,6, 32 ] - beatValue = beatValues[ord(data[4])] - timesig = str(beats) + "/" + str(beatValue) - lastTimesig = timesigValues[timesig] - result += "\\time " + timesig + " " - # Tempo - elif data=='\x06': - print "Tempo" - data = nwcData.read(7) - tempo = readLn(nwcData) - result += '\n\t\t%%tempo %s\n\t\t' % tempo - #tempoCount = tempoCount + 1 - - # note - elif data=='\x08': - data = nwcData.read(10) - noteCount = noteCount + 1 - - if debug: print binascii.hexlify(data) , noteCount , nwcData.tell() - (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto) = getNote(data) - - - articulate = '' - if staccato: articulate+= '-.' - if accent: articulate+= '->' - if tenuto: articulate+= '--' - - beam = beams[beam] - - chordMatters = '' - if lastChord>0 and beam==']' : - chordMatters = ' } >> ' - lastChord = 0 - elif lastChord>0: - - print 'CHORd', durVal(duration), lastChord - dur = durVal(duration) - #lastChord = 1.0/((1.0 / lastChord) - (1.0/durVal(duration))) - lastChord -= dur - - if lastChord <= 0 : - chordMatters = ' } >> ' - lastChord = 0 - - - - if not insertBeaming: beam = '' - - # pitch - pitch += lastClef - note = scale[pitch] - - - # get Accidentals - #print 'accidental',accidental - if (accidental!='auto'): - currentKey[note[0]] = accidental - accidental = currentKey[note[0]] - - if (relativePitch): - octave = getRelativePitch(lastPitch, pitch) - lastPitch = pitch - else: - octave = note[1:] - pitch = note[0] + accidental + octave - - # get Relative Duration - if (relativeDuration): - if (lastDuration==duration): - duration = '' - else: - lastDuration = duration - - # check stems - if insertSteming and (stem!= lastStem) : - lastStem = stem - stem = stems[stem] - else : - stem = '' - - # normal note - if extra!='': - extra = '-"' + extra + '"' - - if grace and not lastGrace: result += "\\acciaccatura { " - - if not grace and lastGrace: result += " } " - result += triplet[0] + stem + pitch + duration + articulate + extra - result += slur + tie + beam + triplet[1] +chordMatters + " " - - - # reset - lastGrace = grace - extra = '' - # rest - elif data=='\x09': - data = nwcData.read(10) - restCount = restCount + 1 - - # get Relative Duration - duration = getDuration(data) - if duration == '1': - duration = lastTimesig - if (relativeDuration): - if (lastDuration==duration): - duration = '' - else: - lastDuration = duration - - result = result + 'r' + duration + " " - - # text - elif data=='\x11': - textCount = textCount + 1 - data = nwcData.read(2) #pad - textpos = nwcData.read(1) - data = nwcData.read(2) #pad - text = '' - data = nwcData.read(1) - while data!='\x00': - text += data - data = nwcData.read(1) - - #if text.isdigit() : # check numbers - # text = "-\\markup -\\number "+ text - # #text = "-\\markup {\\number "+ text +"}" - #else : - # text = '-"' + text + '"' - - extra += ' ' + text - - # dynamics - elif data=='\x07': - dynamicCount = dynamicCount + 1 - data = nwcData.read(9) - # chord - elif data=='\x0A' or data=='\x12': - - data = nwcData.read(12) - - chordAmt = ord(data[10]) - chords = [] - chordDur = getDuration(data) - #print 'duration',chordDur - - #print "WARNING Chord support is experimental", chordDur, chordAmt - #print binascii.hexlify(data), 'barlines ', barlineCount - #print 'no. of notes in chord' , chordAmt - - chord1 = [] - chord2 = [] - - for i in range(chordAmt): - # rest or note - what = nwcData.read(1) - - data = nwcData.read(10) - ha = getNote(data) - #print 'data ', ha - (pitch, accidental, duration, stem, beam, triplet, slur, tie, grace, staccato, accent, tenuto) = ha - # add to list - if ha[2] == chordDur: - chord1.append( (pitch,accidental ) ) - if beam==0 or beam ==4 : - lastChord = 0 - - - else : # 2 voices - chord2.append( (pitch,accidental, duration) ) - lastChord = durVal(duration) - - - if len(chord2)==0: # 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 (relativePitch): - octave = getRelativePitch(lastPitch, pitch) - lastPitch = pitch - else: - octave = note[1:] - result += note[0] + accidental + octave + ' ' - result += '>' +chordDur +' ' - lastPitch = chord1[0][0] + lastClef - else: # 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 (relativePitch): - 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 (relativePitch): - octave = getRelativePitch(lastPitch, pitch) - lastPitch = pitch - else: - octave = note[1:] - result += note[0] + accidental + octave +chordDur + ' ' - if lastChord >0 : lastChord = durVal(duration) - durVal(chordDur) - if lastChord==0: result += ' } >> ' - # end 2 voices - lastDuration = chordDur - - # check if duration / stem / same - # < >duration ties,beam, slurs - - # Pedal - elif data=='\x0b': - print 'Ped ' - data = data = nwcData.read(5) - # midi control instruction / MPC - elif data=='\x0d': - print 'midi control instruction' - data = data = nwcData.read(36) - # fermata / Breath mark - elif data=='\x0E': - print "Fermata" - data = nwcData.read(6) - extra += "\\fermata" - - # Dynamics - elif data=='\x0f': - print "Dynamics" - data = nwcData.read(5) - - # Performance Style - elif data=='\x10': - print "Performance Style" - data = nwcData.read(5) - - # Instrument Patch - elif data=='\x04': - print "Instrument Patch" - data = nwcData.read(10) - - - # todo - else : - print "WARNING: Unrecognised token ",binascii.hexlify(data), " at #", nwcData.tell(), " at Token", token - - - - # output converted file? - print "\nStats" - print keysigCount, " keysigCount found" - print noteCount, " notes found" - print staffCount, " staffCount found" - print clefCount, " clefCount found" - print barlineCount, " barlineCount found" - print timesigCount, " timesigCount found" - print textCount, " textCount found" - print dynamicCount, " dynamicCount found" - print restCount, " restCount found" - - #print "\nLilypond format:\n" - return result - -# Variables -keysigs = { -'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' -} - -acdts = ( 'is', 'es', '' ,'isis', 'eses', 'auto' ) - -clefs = { 0 : "b'", - 1 : "d", - 2 : "c'", - 3 : "a'", - } - -clefNames = { 0: 'treble', - 1: 'bass', - 2: 'alto', - 3: 'tenor', - } -octaves = { 0: 0, 1:7, 2:-7 } -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''''", - ] - -stems = [ '\stemNeutral ', '\stemUp ', '\stemDown '] - -slurs = [ '', '(' , ')', '' ] - -triplets = [ - ('' , '' ), - ( '\\times 2/3 { ', '') , - ('' , '' ), - ('' , ' }' ), - ] - -durations = ( '1','2','4','8','16','32','64' ) - -barlines = ( - '|', # 'Single' - '||', # 'Double' - '.|', # SectionOpen - '|.', # SectionClose - '|:', # MasterRepeatOpen - ':|', # MasterRepeatClose - '|:', # LocalRepeatOpen - ':|', # LocalRepeatClose -) - -ending = ( -'|.', # SectionClose -':|', # MasterRepeatClose -'|', # 'Single' -'||', # 'Double' -'' # Open hidden -) - -staffType = ( -'Standard' , # Standard -'Upper Grand Staff' , # Upper Grand Staff -'Lower Grand Staff' , # Lower Grand Staff -'Orchestra' , # Orchestra -) - - -beams = [ '', '[', '',']' ] - - -# end ' \bar "|."' - -# Notation Properties -# extra accidental spacing -# extra note spacing -# muted -# no ledger lines -# slurdirection -# tiedirection -# lyricsyllable -# visability -# show printed -# item color - -#dynamics -# cmd = DynamicVariance -# Decrescendo \setTextCresc \< -# setTextCresc Crescendo \> setHairpinCresc -# Dynamics stop '\! ' -# style = ff pp - -clefOctave = [ '' , '^8', '_8' , '' ] -clefShift = [0,7,-7, 0] - - -# '#(set-accidental-style '#39'modern-cautionary)' -#(ly:set-point-and-click 'line-column) -#(set-global-staff-size 20) - -timesigValues = { - '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', - } - -print "python nwc2ly is running..." -try: - - nwcData = open( nwcfile,'rb') - - # check if its a readable nwc format - # compressed - [NWZ] - # uncompressed - [NoteWorthy ArtWare] [NoteWorthy Composer] - format = nwcData.read(5) - if format== '[NWZ]': - nwcData.seek(1,1) - print 'Compressed NWC detected!' - print 'Dumping to uncompressed NWC format and attemping conversion soon...' - uncompress = open ('uncompressed.nwc','wb') - uncompress.write(zlib.decompress(nwcData.read())) - uncompress.close() - print 'Inflating done. Now opening new file...' - nwcData.close() - nwcData = open( 'uncompressed.nwc','rb') - nwcData.seek(6) - elif format!= '[Note': - print 'Unknown format, please use an uncompress NWC format and try again.' - sys.exit() - - - - resultFile = '%% Generated from python nwc2ly converter v%s by Joshua Koo (joshuakoo@myrealbox.com)' % nwc2lyversion - resultFile += '\n\n\\version "2.4.0"' - resultFile += "\n" - - - # START WORK - getFileFormat(nwcData) - resultFile+=getFileInfo(nwcData) - resultFile+= "\n\n\\score {" - resultFile+= "\n\t<<\n\t\t" - - getPageSetup(nwcData) - - noOfStaffs = findNoOfStaff(nwcData); - - for staff in range(1,noOfStaffs+1): - print "\n\nWorking on Staff", staff - result = processStaff(nwcData) - #print result - resultFile += result - - resultFile+= "\n\t>>" - resultFile+= "\n\t\layout {}" - resultFile+= "\n\t\midi {}" - resultFile+= "\n}" - nwcData.close() - - if lyfile=='': - print 'Dumping output file to screen' - print resultFile - else : - write = open( lyfile ,'w') - write.write (resultFile) - write.close() - -except IOError: - print 'File does not exist or an IO error occurred' -except Exception, e: #KeyError - print "Error while reading data at ", nwcData.tell() ,"\n" - print 'Dumping whatever result first' - print resultFile - print result - print - traceback.print_exc() - -print -print -print "Please send all bugs and requests to joshuakoo@myrealbox.com" +#!/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() -- 2.26.2