Cleanup to more Pythonic sytax.
[nwc2ly.git] / nwc2ly.py
1 #!/usr/bin/env python
2 #
3 # Copyright (C)  2010  W.Trevor King (wking @ drexel.edu)
4 #                2005  Joshua Koo (joshuakoo @ myrealbox.com)
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19
20 """Convert NoteWorthy Composer's `nwc` to LilyPond's `ly` fileformat.
21
22 Most infomation obtained about the `nwc` format is by using
23 noteworthycomposer and the somewhat like the `French cafe method`_.
24
25 .. _French cafe method: http://samba.org/ftp/tridge/misc/french_cafe.txt
26
27 Revisions:
28
29 * 0.1
30
31   * 07 april 2005.  Initial hex parsing
32
33 * 0.2
34
35   * 13 april 2005.  Added multiple staff, keysig, dots, durations
36
37 * 0.3
38
39   * 14 april 2005.  Clef, key sig detection, absolute notes pitching
40
41 * 0.4
42
43   * 15 April 2005.  Relative pitchs, durations, accidentals, stem
44     up/down, beam, tie
45
46 * 0.5
47
48   * 16 April 2005.  Bug fixes, generate ly score, write to file, time
49     signature, triplets, experimental chords
50
51 * 0.6
52
53   * 17 April 2005.  Compressed NWC file supported!
54
55 * 0.7
56
57   * 19 April 2005.  Version detection, header
58   * 20 April 2005.  Bug fixes, small syntax changes
59   * 21 April 2005.  Still fixing aimlessly
60   * 23 April 2005.  Chords improvement
61   * 24 April 2005.  Staccato, accent, tenuto. dynamics, midi detection
62     (but unsupported)
63
64 * 0.8
65
66   * 24 April 2005.  Experimental lyrics support
67
68 * 0.9
69
70   * 29 April 2005.  Workround for ``\acciaccatura``, simple check full
71     bar rest adjustment
72
73 * 0.10
74
75   * 30 November 2010.  Cleanup to more Pythonic syntax.
76
77 TODO:
78
79 * http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Line-breaking
80 """
81
82 import logging
83 import pprint
84 import re
85 import struct
86 import sys
87 import zlib
88
89
90 __version__ = '0.10'
91
92 LOG = logging
93
94
95 # define useful structures and tools to make parsing easier
96
97 def _decode_string(string):
98     """Decode NWC text from ISO-8859-1 to Unicode.
99
100     Based on the contents of a few example files, NWC uses
101     `ISO-8859-1`_ to encode text with the private use character
102     `\\x92` representing the right single quote (see `ISO-6492`_).
103
104     .. _ISO-8859-1: http://en.wikipedia.org/wiki/ISO/IEC_8859
105     .. _ISO-6492: http://en.wikipedia.org/wiki/ISO_6429
106
107     >>> _decode_string('Someone\\x92s \\xc9tude in \\xa9')
108     u'Someone\u2019s \\xc9tude in \\xa9'
109     """
110     string = unicode(string, 'iso-8859-1')
111     string = string.replace(r'\r\n', '\n')
112     return string.replace(u'\x92', u'\u2019')
113
114
115 class Enum (object):
116     """An enumerated type (or any type requiring post-processing).
117
118     >>> class E (Enum):
119     ...     _list = ['A', 'B', 'C']
120     >>> E.process(1)
121     'B'
122     """
123     type = 'B'
124     _list = []
125
126     @classmethod
127     def process(self, value):
128         try:
129             return self._list[value]
130         except IndexError:  # if _list is a list
131             raise ValueError('index %d out of range for %s'
132                              % (value, self._list))
133         except KeyError:  # if _list is a dict
134             raise ValueError('key %s not in %s'
135                              % (value, self._list))
136
137
138 class BoolEnum (Enum):
139     _list = [False, True]
140
141
142 class Pow2Enum (Enum):
143     _list = [1, 2, 4, 8, 16, 32, 64]
144
145
146 class BitField (list):
147     """Allow clean access into bit fields.
148
149     Initialize with a sequence of `(name, bit_size)` pairs.
150
151     When packing into a `Structure`, the type should be set to one of
152     the standard unsigned integer type characters.  The total bit size
153     must match the size of the type specifier.
154
155     If you want, you may add an `Enum` subclass as a third tuple
156     element `(name, bit_size, enum)` to postprocess that bit field
157     value.  The first bit fields will recieve the most significant
158     bits.
159
160     >>> b = BitField('B', ('a', 4), ('b', 3), ('c', 1, BoolEnum))
161     >>> a = ord('\\x2f')
162     >>> bin(a)
163     '0b101111'
164     >>> b.parse('B', a)
165     [('a', 2), ('b', 7), ('c', True)]
166
167     `BitField` instances will raise exceptions if you try to parse the
168     wrong type.
169
170     >>> b.parse('C', a)
171     Traceback (most recent call last):
172       ...
173     AssertionError: C
174
175     Or if the type bit size doesn't match the field total.
176
177     >>> b = BitField('B', ('a', 4), ('b', 3))
178     Traceback (most recent call last):
179     ...
180     AssertionError: only assigned 7 of 8 bits
181     """
182     _uint_types = ['B', 'H', 'I', 'L', 'Q']
183
184     def __init__(self, type, *fields):
185         assert type in self._uint_types, type
186         self._type = type
187         self._fields = list(fields)
188         field_bits = 0
189         for i,field in enumerate(self._fields):
190             if len(field) == 2:
191                 self._fields[i] = (field[0], field[1], None)
192             field_bits += field[1]
193         num_bytes = self._uint_types.index(type) + 1
194         self._num_bits = 8 * num_bytes
195         self._max_value = (1 << self._num_bits) - 1
196         assert field_bits == self._num_bits, (
197             'only assigned %d of %d bits' % (field_bits, self._num_bits))
198
199     def parse(self, type, value):
200         assert type == self._type, type
201         assert value >= 0, value
202         assert value <= self._max_value, value
203         results = []
204         top_offset = self._num_bits
205         for name,bits,enum in self._fields:
206             bottom_offset = top_offset - bits
207             mask = (1 << bits) - 1
208             v = (value >> bottom_offset) & mask
209             if enum:
210                 v = enum.process(v)
211             results.append((name, v))
212             top_offset -= bits
213         assert top_offset == 0, top_offset
214         return results
215
216
217 class Structure (dict):
218     """Extend `struct.Struct` to support additional types.
219
220     Additional types:
221
222     ====  ========================
223     `S`   null-terminated string
224     -     `Enum` instance
225     -     nested `Structure` class
226     ====  ========================
227
228     You can also use `BitField` instances as names.
229
230     >>> class E (Enum):
231     ...     _list = ['A', 'B', 'C', 'D', 'E', 'F']
232     >>> class S (Structure):
233     ...     _fields = [(BitField('B',
234     ...                          ('bf1', 4),
235     ...                          ('bf2', 3),
236     ...                          ('bf3', 1, BoolEnum)), 'B'),
237     ...                ('string1', 'S'), ('uint16', 'H'),
238     ...                ('string3', 'S'), ('enum1', E)]
239     >>> buffer = '\\x2fHello\\x00\\x02\\x03World\\x00\\x04ABC'
240     >>> s = S(buffer)
241     >>> for n,t in s._fields:
242     ...     if isinstance(n, BitField):
243     ...         for n2,bits,enum in n._fields:
244     ...             print n2, s[n2]
245     ...     else:
246     ...         print n, s[n]
247     ... # doctest: +REPORT_UDIFF
248     bf1 2
249     bf2 7
250     bf3 True
251     string1 Hello
252     uint16 515
253     string3 World
254     enum1 E
255     >>> s.size
256     16
257     >>> buffer[s.size:]
258     'ABC'
259
260     >>> class N (Structure):
261     ...     _fields = [('string1', 'S'), ('s', S), ('string2', 'S')]
262     >>> n_buffer = 'Fun\\x00%sDEF\\x00GHI' % buffer
263     >>> n = N(n_buffer)
264     >>> pprint.pprint(n)
265     {'s': {'bf1': 2,
266            'bf2': 7,
267            'bf3': True,
268            'enum1': 'E',
269            'string1': u'Hello',
270            'string3': u'World',
271            'uint16': 515},
272      'string1': u'Fun',
273      'string2': u'ABCDEF'}
274     >>> n.size
275     27
276     >>> n_buffer[n.size:]
277     'GHI'
278
279     Note that `ctypes.Structure` is similar to `struct.Struct`, but I
280     found it more difficult to work with.  Neither one supports the
281     variable-length, null-terminated strings we need.
282     """
283     _byte_order = '>'  # big-endian
284     _string_decoder = staticmethod(_decode_string)
285     _fields = []       # sequence of (name, type) pairs
286
287     def __init__(self, buffer=None, offset=0):
288         super(Structure, self).__init__()
289         self._parsers = []
290         self._post_processors = []
291         f = []
292         for name,_type in self._fields:
293             if self._is_subclass(_type, Enum):
294                 _type = _type.type
295             if self._is_subclass(_type, Structure) or _type in ['S']:
296                 if f:
297                     self._parsers.append(
298                         self._create_struct_parser(''.join(f)))
299                     f = []
300                 if self._is_subclass(_type, Structure):
301                     self._parsers.append(_type)
302                 elif _type == 'S':
303                     self._parsers.append(self._string_parser)
304             else:
305                 f.append(_type)
306         if f:
307             self._parsers.append(self._create_struct_parser(''.join(f)))
308
309         if buffer:
310             self.unpack_from(buffer, offset)
311
312     def _is_subclass(self, obj, _class):
313         """
314         >>> s = Structure()
315         >>> s._is_subclass('c', Structure)
316         False
317         >>> s._is_subclass(Structure, Structure)
318         True
319         """
320         try:
321             return issubclass(obj, _class)
322         except TypeError:
323             return False
324
325     def _create_struct_parser(self, format):
326         LOG.debug('%s: initialize struct parser for %s%s'
327                   % (self.__class__.__name__, self._byte_order, format))
328         return struct.Struct('%s%s' % (self._byte_order, format))
329
330     def _string_parser(self, buffer, offset=0):
331         size = buffer[offset:].find('\x00')
332         string = buffer[offset:offset+size]
333         if self._string_decoder:
334             string = self._string_decoder(string)
335         return ((string,), size+1)
336
337     def unpack_from(self, buffer, offset=0):
338         self._results = []
339         self.size = 0
340         for parser in self._parsers:
341             if self._is_subclass(parser, Structure):
342                 parser = parser()
343             if hasattr(parser, 'unpack_from'):
344                 results = parser.unpack_from(buffer, offset)
345                 size = parser.size
346             else:
347                 results,size = parser(buffer, offset)
348             self._results.extend(results)
349             self.size += size
350             offset += size
351         self._results = tuple(self._results)
352         for i,result in enumerate(self._results):
353             name,_type = self._fields[i]
354             if self._is_subclass(_type, Enum):
355                 result = _type.process(result)
356             if isinstance(name, BitField):
357                 for n,r in name.parse(_type, result):
358                     LOG.debug("%s['%s'] = %s" % (
359                             self.__class__.__name__, n, repr(r)))
360                     self[n] = r
361             else:
362                 LOG.debug("%s['%s'] = %s" % (
363                         self.__class__.__name__, name, repr(result)))
364                 self[name] = result
365         return (self,)
366
367
368 # define the `nwc` file format
369
370
371 class NWCSongInfo (Structure):
372     _fields = [
373         ('title', 'S'),
374         ('author', 'S'),
375         #('lyricist, 'S'),  # later versions?  In example/2.0-1.nwctxt
376         ('copyright1', 'S'),
377         ('copyright2', 'S'),
378         ('comments', 'S'),
379         ]
380
381     def ly_text(self):
382         """
383         http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Creating-titles#Creating-titles
384         """
385         lines = [r'\header {']
386         for element,field in [('title', 'title'), ('composer', 'author'),
387                               ('copyright', 'copyright1')]:
388             if self[field]:
389                 lines.append('\t%s = "%s"' % (element, self[field]))
390         if self['copyright2']:
391             lines.append('\t%%{ %s %%}' % self['copyright2'])
392         if self['comments']:
393             lines.append('\t%%{ %s %%}' % self['comments'])
394         lines.append('}')
395         return '\n'.join(lines)
396
397
398 class NWCFontStyleEnum (Enum):
399     _list = ('regular', 'italic', 'bold', 'bold_italic')
400
401
402 class NWCTypefaceEnum (Enum):
403     _list = {
404         0: 'western',
405         1: 'unknown',
406         177: 'hebrew',}
407
408
409 class NWCFont (Structure):
410     _fields = [
411         ('name', 'S'),
412         (BitField('B',
413                   ('unknown2', 6),
414                   ('style', 2, NWCFontStyleEnum)),
415          'B'),
416         ('size', 'B'),
417         ('unknown3', 'B'),
418         ('typeface', NWCTypefaceEnum),
419         ]
420
421
422 class NWCPageSetup (Structure):
423     _fields = [
424         ('ny_', 'S'),
425         ('f2', 'S'),
426         ('unknown01', 'B'),
427         ('unknown02', 'B'),
428         ('unknown03', 'B'),
429         ('unknown04', 'B'),
430         ('margins', 'S'),
431         ('unknown05', 'B'),
432         ('unknown06', 'B'),
433         ('unknown07', 'B'),
434         ('unknown08', 'B'),
435         ('unknown09', 'B'),
436         ('unknown10', 'B'),
437         ('unknown11', 'B'),
438         ('unknown12', 'B'),
439         ('unknown13', 'B'),
440         ('unknown14', 'B'),
441         ('unknown15', 'B'),
442         ('unknown16', 'B'),
443         ('unknown17', 'B'),
444         ('unknown18', 'B'),
445         ('unknown19', 'B'),
446         ('unknown20', 'B'),
447         ('unknown21', 'B'),
448         ('unknown22', 'B'),
449         ('unknown23', 'B'),
450         ('unknown24', 'B'),
451         ('unknown25', 'B'),
452         ('unknown26', 'B'),
453         ('unknown27', 'B'),
454         ('unknown28', 'B'),
455         ('unknown29', 'B'),
456         ('unknown30', 'B'),
457         ('unknown31', 'B'),
458         ('unknown32', 'B'),
459         ('unknown33', 'B'),
460         ('unknown34', 'B'),
461         ('unknown35', 'B'),
462         ('unknown36', 'B'),
463         ('unknown37', 'B'),
464         ('unknown38', 'B'),
465         ('unknown39', 'B'),
466         ('unknown40', 'B'),
467         ('unknown41', 'B'),
468         ('unknown42', 'B'),
469         ('staff_italic', NWCFont),
470         ('staff_bold', NWCFont),
471         ('staff_lyric', NWCFont),
472         ('page_title', NWCFont),
473         ('page_text', NWCFont),
474         ('page_small', NWCFont),
475         ('user1', NWCFont),
476         ('user2', NWCFont),
477         ('user3', NWCFont),
478         ('user4', NWCFont),
479         ('user5', NWCFont),
480         ('user6', NWCFont),
481         ]
482
483
484 class NWCFileHead (Structure):
485     _fields = [
486         ('type', 'S'),
487         ('unknown01', 'B'),
488         ('unknown02', 'B'),
489         ('product', 'S'),
490         ('minor_version', 'B'), ('major_version', 'B'),
491         ('unknown03', 'B'),
492         ('unknown04', 'B'),  #('num_saves', 'B'), ???
493         ('unknown05', 'B'),
494         ('unknown06', 'B'),
495         ('na', 'S'),  # what does this mean?
496         ('name1', 'S'), # what does this mean?
497         ('unknown07', 'B'),
498         ('unknown08', 'B'),
499         ('unknown09', 'B'),
500         ('unknown10', 'B'),
501         ('unknown11', 'B'),
502         ('unknown12', 'B'),
503         ('unknown13', 'B'),
504         ('unknown14', 'B'),
505         ('unknown15', 'B'),
506         ('unknown16', 'B'),
507         ('song_info', NWCSongInfo),
508         ('page_setup', NWCPageSetup),
509         ]
510
511
512 class NWCStaffSet (Structure):
513     _fields = [
514         ('ff', 'B'),
515         ('unknown1', 'B'),
516         ('unknown2', 'B'),
517         ('unknown3', 'B'),
518         ('num_staves', 'B'),
519         ('unknown5', 'B'),
520         ]
521
522
523 class FinalBarEnum (Enum):
524     _list = ('section_close', 'master_repeat_close', 'single', 'double',
525              'invisible')
526
527
528 class StaffTypeEnum (Enum):
529     _list = ('standard', 'upper_grand', 'lower_grand', 'orchestra')
530
531
532 class NWCStaff (Structure):
533     _fields = [
534         ('name', 'S'),
535         ('group', 'S'),
536         (BitField('B',
537                   ('unknown01', 5),
538                   ('final_bar', 3, FinalBarEnum)),
539          'B'),
540         (BitField('B',
541                   ('unknown02', 7),
542                   ('muted', 1, BoolEnum)),
543          'B'),
544         ('unknown01', 'B'),
545         ('playback_channel', 'B'),  #+1
546         ('unknown02', 'B'),
547         ('unknown03', 'B'),
548         ('unknown04', 'B'),
549         ('unknown05', 'B'),
550         ('unknown06', 'B'),
551         ('unknown07', 'B'),
552         ('unknown08', 'B'),
553         ('unknown09', 'B'),
554         ('unknown10', 'B'),
555         (BitField('B',
556                   ('unknown11', 6),
557                   ('type', 2, StaffTypeEnum)),
558          'B'),
559         ('unknown11', 'B'),
560         ('vertical_size_upper', 'B'),  # signed?  256 - ord(nwcData.read(1))  # - signed +1 )& 2^7-1 )
561
562         #... to ff
563
564         ('vertical_size_lower', 'B'),
565         ('unknown12', 'B'),  # ww?
566         ('line_count', 'B'),
567         ('layer', 'B'),  # &1
568         ('unknown12', 'B'),
569         ('unknown13', 'B'),
570         ('unknown14', 'B'),
571         ('unknown15', 'B'),
572         ('part_volume', 'B'),
573         ('unknown16', 'B'),
574         ('stereo_pan', 'B'),
575         ('unknown17', 'B'),
576         ('unknown18', 'B'),
577         ('has_lyrics', 'B'),
578         ('num_lyrics', 'H'),
579
580
581         #('visible', 'B'),
582         #('boundary_top', 'B'),
583         #('boundary_bottom', 'B'),
584         #('lines', 'H'),
585         #('style', 'B'),
586         #('color', 'B'),
587         #('playback_device', 'B'),
588         #('transposition', 'B'),
589         #('dynamic_velocity', 'S'),
590         #('patch_name', 'B'),
591         #('patch_list_type', 'B'),
592         #('bank_select', 'B'),
593         #('controller0', 'B'),
594         #('controller32', 'B'),
595         #('align_syllable_rule', 'B'),
596         #('staff_alignment', 'B'),
597         #('staff_offset', 'B'),
598         ]
599
600     def _initialize_bar_items(self):
601         return {
602             'bar_comment_interval': 5,  # HACK
603             'clef': None,
604             'key_sig': None,
605             'time_sig': None,
606             'last_bar': None,
607             'bar_index': 0,
608             'bar': [],
609             'bar_beats': 0,
610             'previous_note': None,
611             }
612
613     def _add_bar_token(self, bar_items, token, token_index):
614         next_note = None
615         for t in self['tokens'][token_index+1:]:
616             if isinstance(t, (NWCToken_note,
617                               NWCToken_chord1)):
618                 next_note = t
619                 break
620         if isinstance(token, (NWCToken_text)):
621             bar_items['bar'].append(token.ly_text())
622             return
623         bar_items['bar'].append(token.ly_text(
624                 clef=bar_items['clef'],
625                 key=bar_items['key_sig'],
626                 previous_note=bar_items['previous_note'],
627                 next_note=next_note))
628         bar_items['bar_beats'] += _nwc_duration_value(token)
629         bar_items['previous_note'] = token
630
631     def _format_bar(self, bar_items, bar_token=None):
632         """
633         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Durations
634         """
635         if bar_token:
636             bar_items['bar'].append(bar_token.ly_text())
637         elif bar_items['bar']:
638             b = NWCToken_bar()
639             bar_items['bar'].append(
640                 b.ly_text(self['final_bar']))
641         else:  # empty final bar
642             return ''
643         if bar_items['time_sig']:
644             expected_bar_beats = bar_items['time_sig'].expected_bar_beats()
645         else:
646             expected_bar_beats = 1.0
647             LOG.warn('no time signature given for bar %d, guessing 4/4'
648                      % bar_items['bar_index']+1)
649         bar_fraction = float(bar_items['bar_beats'])/expected_bar_beats
650         assert bar_fraction < 1.01, (
651                 'too many beats (%f > %f) in bar %d' %
652                 (bar_items['bar_beats'], expected_bar_beats,
653                  bar_items['bar_index']+1))
654         if bar_fraction < 0.99:
655             bar_items['bar'].insert(0, self._partial(
656                     bar_items, expected_bar_beats, bar_fraction))
657         line = '\t%s' % ' '.join(bar_items['bar'])
658         bar_items['last_bar'] = bar_items['bar']
659         bar_items['bar'] = []
660         bar_items['bar_index'] += 1
661         bar_items['bar_beats'] = 0
662         bar_items['previous_note'] = None
663         if bar_items['bar_index'] % bar_items['bar_comment_interval'] == 0:
664             line += ('  %% bar %d' % bar_items['bar_index'])
665         return line
666
667     def _partial(self, bar_items, expected_bar_beats, bar_fraction):
668         """
669         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-rhythms#Upbeats
670         """
671         remaining_fraction = 1 - bar_fraction
672         divisor = 128
673         numerator = round(int(bar_fraction * expected_bar_beats * divisor))
674         while numerator % 2 == 0 and divisor >= 1:
675             numerator /= 2
676             divisor /= 2
677         LOG.debug('partial bar, %f < %f, %d/%d remaining' % (
678                 bar_items['bar_beats'], expected_bar_beats,
679                 numerator, divisor))
680         if numerator == 1:
681             duration = str(divisor)
682         else:
683             duration = '%d*%d' % (divisor, numerator)
684         return r'\partial %s' % duration
685
686     def ly_lines(self, voice=None):
687         """
688         http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Multiple-staves#index-_005cnew-Staff
689         """
690
691         lines = [
692             '%% %s' % self['name'],
693             r'\new Staff',
694             ]
695
696         #if relative_pitch:
697         #    result+="\\relative c {"
698         #else :
699         lines.append('{')
700
701         if voice:
702             lines[-1] = (r'\new Voice = "%s" ' % voice) + lines[-1]
703
704         LOG.info('format staff %s (%s)' % (self['name'], voice))
705         lines.extend([
706                 '\t\\autoBeamOff',
707                 ])
708         bar_items = self._initialize_bar_items()
709         for i,token in enumerate(self['tokens']):
710             LOG.info('format token %s' % type(token))
711             if isinstance(token, (NWCToken_clef,
712                                   NWCToken_key_sig,
713                                   NWCToken_time_sig,
714                                   NWCToken_tempo)):
715                 if isinstance(token, NWCToken_clef):
716                     bar_items['clef'] = token
717                 elif isinstance(token, NWCToken_key_sig):
718                     bar_items['key_sig'] = token
719                 elif isinstance(token, NWCToken_time_sig):
720                     bar_items['time_sig'] = token
721                 lines.append('\t%s' % token.ly_text())
722             elif isinstance(token, NWCToken_bar):
723                 lines.append(self._format_bar(bar_items, token))
724             elif isinstance(token, (NWCToken_note,  # rests are note instances
725                                     NWCToken_chord1,
726                                     NWCToken_text)):
727                 self._add_bar_token(bar_items, token, i)
728             elif isinstance(token, NWCToken_fermata):
729                 if bar_items['bar']:
730                     bar_items['bar'].append(token.ly_text())
731                 else:  # replace the previous \bar with a bar-fermata
732                     # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-text#Text-marks
733                     # except without an auto-generated bar, no line
734                     # shows up.  If we put the fermata before the bar,
735                     # it still ends up over the clef on the next line.
736                     # See bars 124 and 125 of example/1.75-2.nwc for
737                     # an example.
738                     bar_items['last_bar'].insert(
739                         -1,
740                          r'\mark \markup { \musicglyph #"scripts.ufermata" }')
741                     lines[-1] = ('\t%s' % ' '.join(bar_items['last_bar']))
742         lines.append(self._format_bar(bar_items))
743
744         lines.append('}')
745
746         for i,lyric_block in enumerate(self['lyrics']):
747             # http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Entering-lyrics
748             # http://kainhofer.com/~lilypond/ajax/user/lilypond/Stanzas.html
749             assert voice != None, 'lyrics must be attached to a named voice'
750             lines.append(r'\new Lyrics \lyricsto "%s" {' % voice)
751             first_line = lyric_block[0]
752             first_word = first_line[0]
753             if first_word[0].isdigit():
754                 stanza,words = first_word.split(' ', 1)
755                 first_line[0] = words
756                 lines.append('\t\\set stanza = #"%s "' % stanza)
757             for line in lyric_block:
758                 L = []
759                 for word in line:
760                     if not word.isalpha():
761                         word = '"%s"' % word
762                     L.append(word)
763                 lines.append('\t%s' % ' '.join(L))
764             lines.append('}')
765
766         return lines
767
768
769 class NWCLyricProperties (Structure):
770     _fields = []
771     for i in range(6):
772         _fields.append(('unknown%d' % (i+1), 'B'))
773
774
775 class NWCLyricBlockProperties (Structure):
776     #'\x00\x00\x00\x00\x00'
777     #'\x00\x04\x9f'\x00\x00\x01
778     _fields = [
779         ('num_blocks', 'B'),
780         ('len', 'B'),
781         ('unknown1', 'B'),
782         ('unknown2', 'B'),
783         ('unknown3', 'B'),
784         ]
785
786 class NWCLyric (Structure):
787     _fields = [
788         ('lyric', 'S'),
789         ]
790
791
792 class NWCStaffNoteProperties (Structure):
793     _fields = [
794         ('unknown1', 'B'),
795         ('color', 'B'),
796         ('num_tokens', 'H'),  # ?
797         ]
798     for i in range(1):
799         _fields.append(('unknown%d' % (i+1), 'B'))
800
801
802
803 # tokens
804
805
806 class NWCTokenEnum (Enum):
807     _list = ['clef', 'key_sig', 'bar', '3', 'instrument_patch', 'time_sig',
808              'tempo', 'dynamics1', 'note', 'rest', 'chord1', 'pedal', 12,
809              'midi_MPC', 'fermata', 'dynamics2', 'performance_style', 'text',
810              'chord2']
811
812     @classmethod
813     def process(self, value):
814         if value > len(self._list):
815             return 'STOP'
816         try:
817             return self._list[value]
818         except IndexError:
819             raise ValueError('index %d out of range for %s'
820                              % (value, self._list))
821
822
823 class NWCToken (Structure):
824     _fields = [
825         ('token', NWCTokenEnum),
826         ]
827
828
829 class NWCClefEnum (Enum):
830     _list = ['treble', 'bass', 'alto', 'tenor']
831
832
833 class NWCOctaveEnum (Enum):
834     _list = [0, 7, -7]
835
836
837 class NWCToken_clef (Structure):
838     _fields = [
839         ('unknown1', 'B'),
840         ('unknown2', 'B'),
841         (BitField('B',
842                   ('unknown3', 6),
843                   ('clef', 2, NWCClefEnum)),
844          'B'),
845         (BitField('B',
846                   ('unknown4', 6),
847                   ('octave', 2, NWCOctaveEnum)),
848          'B'),
849         ('unknown4', 'B'),
850         ('unknown5', 'B'),
851         ]
852
853     def ly_text(self):
854         """
855         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-pitches#Clef
856         """
857         return r'\clef %s' % self['clef']
858
859
860 class NWCToken_key_sig (Structure):
861     _fields = [
862         ('unknown1', 'B'),
863         ('flat_bits', 'H'),
864         ('sharp_bits', 'H'),
865         ('unknown2', 'B'),
866         ('unknown3', 'B'),
867         ('unknown4', 'B'),
868         ('unknown5', 'B'),
869         ('unknown6', 'B'),
870         ('unknown7', 'B'),
871         ('unknown8', 'B'),
872         ]
873     _sigs = {
874         '00000000': 'c \major % or a \minor',
875         '00000020': 'g \major % or e \minor',
876         '00000024': 'd \major % or b \minor',
877         '00000064': 'a \major % or fis \minor',
878         '0000006c': 'e \major % or cis \minor',
879         '0000006d': 'b \major % or gis \minor',
880         '0000007d': 'fis \major % or dis \minor',
881         '0000007f': 'cis \major % or ais \minor',
882         '00020000': 'f \major % or d \minor',
883         '00120000': 'bes \major % or g \minor',
884         '00130000': 'ees \major % or c \minor',
885         '001b0000': 'aes \major % or f \minor',
886         '005b0000': 'des \major % or bes \minor',
887         '005f0000': 'ges \major % or ees \minor',
888         '007f0000': 'ces \major % or a \minor',
889         }
890
891     def ly_text(self):
892         """
893         http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Accidentals-and-key-signatures#index-_005ckey
894         """
895         sig = '%04x%04x' % (self['flat_bits'], self['sharp_bits'])
896         return '\key %s' % self._sigs[sig]
897
898     def accidental(self, pitch):
899         """Return the appropriate accidental for an in-key note.
900         """
901         index = ord(pitch[0].lower()) - ord('a')
902         if self['flat_bits'] >> index & 1:
903             return 'flat'
904         elif self['sharp_bits'] >> index & 1:
905             return 'sharp'
906         return 'natural'
907
908
909 class NWCToken_bar (Structure):
910     _fields = [
911         ('unknown1', 'B'),
912         ('unknown2', 'B'),
913         ('unknown3', 'B'),
914         ('unknown4', 'B'),
915         ]
916
917     _bars = {
918         'invisible': '',
919         'single': '|',
920         'double': '||',
921         'section_open': '.|',
922         'section_close': '|.',
923         # use \repeat for repeats
924         }
925
926     def ly_text(self, type=None):
927         """
928         http://www.noteworthysoftware.com/composer/faq/102.htm
929         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Bars#Bar-lines
930         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Repeats
931         """
932         if type == None:
933             type = 'single'  # TODO: detect bar type
934         return r'\bar "%s"' % self._bars[type]
935         #|Bar|Style:SectionClose|SysBreak:Y
936
937
938 class NWCToken_3 (Structure):
939     _fields = []
940     for i in range(4):
941         _fields.append(('unknown%d' % (i+1), 'B'))
942
943
944 class NWCToken_instrument_patch (Structure):
945     _fields = []
946     for i in range(10):
947         _fields.append(('unknown%d' % (i+1), 'B'))
948
949     def ly_text(self):
950         """
951         http://kainhofer.com/~lilypond/ajax/user/lilypond/Creating-MIDI-files.html#index-instrument-names-1
952         http://kainhofer.com/~lilypond/ajax/user/lilypond/MIDI-instruments.html#MIDI-instruments
953         """
954         return r'\set Staff.midiInstrument = #"%s"' % 'cello'
955
956
957 class NWCToken_time_sig (Structure):
958     _fields = [
959         ('unknown1', 'B'),
960         ('unknown2', 'B'),
961         ('beats', 'B'),
962         ('unknown3', 'B'),
963         ('beat_value', Pow2Enum),
964         ('unknown6', 'B'),
965         ('unknown7', 'B'),
966         ('unknown8', 'B'),
967         ]
968
969     _sigs = {
970         '4/4': '1', '3/4': '2.', '2/4': '2',  '1/4': '1',
971         '1/8': '8', '2/8': '4',  '3/8': '4.', '6/8': '2.',
972         '4/8': '2', '9/8': '12', '12/8': '1',
973         '2/2': '1', '4/2': '0',  '1/2': '2',
974         }
975
976     def ly_text(self):
977         """
978         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Displaying-rhythms#Time-signature
979         """
980         return r'\time %s/%s' % (self['beats'], self['beat_value'])
981
982     def expected_bar_beats(self):
983         return float(self['beats'])/self['beat_value']
984
985
986 class NWCToken_tempo (Structure):
987     _fields = [
988         ('unknown1', 'B'),
989         ('unknown2', 'B'),
990         ('unknown3', 'B'),
991         ('unknown4', 'B'),
992         ('beats_per_minute', 'B'),
993         ('unknown6', 'B'),
994         ('unknown7', 'B'),
995         ('text', 'S'),
996         ]
997
998     def ly_text(self, time_sig=None):
999         """
1000         http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-parts#Metronome-marks
1001         """
1002         if time_sig == None:
1003             beats_per_measure = 4
1004         else:
1005             beats_per_measure = time_sig['']
1006         return r'\tempo "%s" %s = %s' % (
1007             self['text'], beats_per_measure, self['beats_per_minute'])
1008
1009
1010 class NWCToken_dynamics1 (Structure):
1011     _fields = [
1012         ('unknown1', 'B'),
1013         ('unknown2', 'B'),
1014         ('unknown3', 'B'),
1015         ('unknown4', 'B'),
1016         ('unknown5', 'B'),
1017         ('unknown6', 'B'),
1018         ('unknown7', 'B'),
1019         ('unknown8', 'B'),
1020         ('unknown9', 'B'),
1021         ]
1022
1023
1024 class NWCAccidentalEnum (Enum):
1025     """
1026     http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-pitches#Accidentals
1027     """
1028     _list = ['sharp', 'flat', 'natural', 'double_sharp', 'double_flat', 'auto']
1029
1030
1031 def _nwc_duration_dot(struct):
1032     if struct['dot']:
1033         return '.'
1034     elif struct['ddot']:
1035         return '..'
1036     return ''
1037
1038 def _nwc_duration(struct):
1039     return '%s%s' % (struct['duration'], _nwc_duration_dot(struct))
1040
1041 def _nwc_duration_value(struct):
1042     v = 1.0 / struct['duration']
1043     if struct['grace']:
1044         return 0
1045     if struct['dot']:
1046         v *= 1.5
1047     if struct['ddot']:
1048         v *= 1.75
1049     if struct['triplet'] != 'none':
1050         v *= 2./3
1051     return v
1052
1053
1054 class NWCStemEnum (Enum):
1055     _list = ('neutral', 'up', 'down')
1056     _ly_text_list = ('\stemNeutral', '\stemUp', '\stemDown')
1057
1058
1059 class NWCTripletEnum (Enum):
1060     _list = ('none', 'start', 'inside', 'stop')
1061
1062
1063 class NWCBeamEnum (Enum):
1064     _list = ('none', 'start', 'inside', 'stop', 4, 5, 6, 7)
1065
1066
1067 class NWCSlurEnum (Enum):
1068     _list = ('none', 'start' , 'stop', 'inside')
1069
1070
1071 class NWCToken_note (Structure):
1072     _fields = [
1073         ('unknown1', 'B'),
1074         ('unknown2', 'B'),
1075         ('duration', Pow2Enum),
1076         ('unknown3', 'B'),
1077         (BitField('B',
1078                   ('unknown4', 2),
1079                   ('stem', 2, NWCStemEnum),
1080                   ('triplet', 2, NWCTripletEnum),
1081                   ('beam', 2, NWCBeamEnum)),
1082          'B'),
1083         ('unknown6', 'B'),
1084         (BitField('B',
1085                   ('unknown5', 2),
1086                   ('accent', 1, BoolEnum),
1087                   ('tie', 1, BoolEnum),
1088                   ('unknown6', 1),
1089                   ('dot', 1, BoolEnum),
1090                   ('staccato', 1, BoolEnum),
1091                   ('ddot', 1, BoolEnum)),
1092          'B'),
1093         (BitField('B',
1094                   ('unknown7', 2),
1095                   ('grace', 1),
1096                   ('unknown8', 2),
1097                   ('tenuto', 1, BoolEnum),
1098                   ('slur', 2, NWCSlurEnum),
1099                   ),
1100          'B'),
1101         ('pitch', 'b'),
1102         (BitField('B',
1103                   ('unknown9', 4),
1104                   ('pitch1', 1, BoolEnum),  # some kind of pitch adjustment?
1105                   ('accidental', 3, NWCAccidentalEnum),
1106                   ),
1107          'B'),
1108         ]
1109
1110     _scale = (  # this list is taken from lilycomp
1111         "c,,,","d,,,","e,,,","f,,,","g,,,","a,,,","b,,,",
1112         "c,,","d,,","e,,","f,,","g,,","a,,","b,,",
1113         "c,","d,","e,","f,","g,","a,","b,",
1114         "c","d","e","f","g","a","b",
1115         "c'","d'","e'","f'","g'","a'","b'",
1116         "c''","d''","e''","f''","g''","a''","b''",
1117         "c'''","d'''","e'''","f'''","g'''","a'''","b'''",
1118         "c''''","d''''","e''''","f''''","g''''","a''''","b''''",
1119         )
1120
1121     _clef_offsets = {
1122         'treble': "b'",
1123         'bass': 'd',
1124         'alto': "c'",
1125         'tenor': "a'",
1126         }
1127
1128     _accidentals = {
1129         'natural': '',
1130         'sharp': 'is',
1131         'flat': 'es',
1132         'double_sharp': 'isis',
1133         'double_flat': 'eses',
1134         'semi_sharp': 'ih',
1135         'semi_flat': 'eh',
1136         'sesqui_sharp': 'isih',
1137         'sesqui_flat': 'eseh'
1138         }
1139
1140     _ornaments = {
1141         # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Attached-to-notes#Articulations-and-ornamentations
1142         # http://lilypond.org/doc/v2.11/Documentation/user/lilypond/List-of-articulations
1143         'accent': '->',  # r'\accent',
1144         'marcato': '-^',  #r'\marcato',
1145         'staccatissimo': '-|',  # r'\staccatissimo',
1146         'espressivo': r'\espressivo',
1147         'staccato': '-.',  # r'\staccato',
1148         'tenuto': '--',  # r'\tenuto',
1149         'portato': '-_',  # r'\portato',
1150         'upbow': r'\upbow',
1151         'downbow': r'\downbow',
1152         'flageolet': r'\flageolet',
1153         'thumb': r'\thumb',
1154         'lheel': r'\lheel',
1155         'rheel': r'\rheel',
1156         'ltoe': r'\ltoe',
1157         'rtoe': r'\rtoe',
1158         'open': r'\open',
1159         'stopped': '-+',  #r'\stopped',
1160         'turn': r'\turn',
1161         'reverseturn': r'\reverseturn',
1162         'trill': r'\trill',
1163         'prall': r'\prall',
1164         'mordent': r'\mordent',
1165         'prallprall': r'\prallprall',
1166         'prallmordent': r'\prallmordent',
1167         'upprall': r'\upprall',
1168         'downprall': r'\downprall',
1169         'upmordent': r'\upmordent',
1170         'downmordent': r'\downmordent',
1171         'pralldown': r'\pralldown',
1172         'prallup': r'\prallup',
1173         'lineprall': r'\lineprall',
1174         'signumconguruentiae': r'\signumconguruentiae',
1175         'shortfermata': r'\shortfermata',
1176         'fermata': r'\fermata',
1177         'longfermata': r'\longfermata',
1178         'verylongfermata': r'\verylongfermata',
1179         'segno': r'\segno',
1180         'coda': r'\coda',
1181         'varcoda': r'\varcoda',
1182         }
1183
1184     _slurs = {
1185         'none': '',
1186         'start': r'\(',
1187         'stop': r'\)',
1188         'inside': '',
1189         }
1190
1191     def ly_text(self, clef=None, key=None,
1192                 previous_note=None, next_note=None,
1193                 in_chord=False):
1194         """
1195         http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Curves#Phrasing-slurs
1196         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Ties
1197         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Writing-rhythms#Tuplets
1198         http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Special-rhythmic-concerns#Grace-notes
1199         """
1200         if clef:
1201             clef_name = clef['clef']
1202         else:
1203             clef_name = 'treble'
1204         clef_offset = self._scale.index(self._clef_offsets[clef_name])
1205         pitch = self._scale[-self['pitch'] + clef_offset]
1206
1207         if self['accidental'] == 'auto':
1208             if key:
1209                 accidental = key.accidental(pitch)
1210             else:
1211                 accidental = 'natural'
1212         else:
1213             accidental = self['accidental']
1214         note = [pitch[0], self._accidentals[accidental], pitch[1:]]
1215
1216         if in_chord:
1217             return ''.join(note)
1218
1219         note.append(_nwc_duration(self))
1220
1221         for key in ['accent', 'staccato', 'tenuto']:
1222             if self[key]:
1223                 note.append(self._ornaments[key])
1224
1225         note.append(self._slurs[self['slur']])
1226
1227         if self['tie']:
1228             note.append('~')
1229
1230         if self['beam'] == 'start':
1231             note.append('[')
1232         elif self['beam'] == 'stop':
1233             note.append(']')
1234
1235         if self['triplet'] == 'start':
1236             note.insert(0, r'\times 2/3 { ')
1237         elif self['triplet'] == 'stop':
1238             note.append(' }')
1239
1240         if self['grace']:
1241             if previous_note == None or not previous_note['grace']:
1242                 note.insert(0, r'\acciaccatura { ')
1243             if next_note == None or not next_note['grace']:
1244                 note.append(' }')
1245
1246         return ''.join(note)
1247
1248
1249 class NWCToken_rest (NWCToken_note):
1250     def ly_text(self, **kwargs):
1251         rest = ['r%s' % _nwc_duration(self)]
1252         if self['triplet'] == 'start':
1253             rest.insert(0, r'\times 2/3 { ')
1254         elif self['triplet'] == 'stop':
1255             rest.append(' }')
1256
1257         return ''.join(rest)
1258
1259
1260 class NWCToken_chord1 (Structure):
1261     _fields = NWCToken_note._fields + [
1262         ('num_notes', 'B'),
1263         ('unknown12', 'B'),
1264         ]
1265
1266     def unpack_from(self, buffer, offset=0):
1267         super(NWCToken_chord1, self).unpack_from(buffer, offset)
1268         chords,size = self._unpack_chords_from(buffer, offset + self.size)
1269         self['chords'] = chords
1270         self.size += size
1271
1272     def _unpack_chords_from(self, buffer, offset=0):
1273         size = 0
1274         chord1 = []
1275         chord2 = []
1276
1277         token = NWCToken()
1278         size = 0
1279         for i in range(self['num_notes']):
1280             token.unpack_from(buffer, offset + size)
1281             size += token.size
1282             assert token['token'] in ['rest', 'note'], token
1283
1284             structure = globals()['NWCToken_%s' % token['token']]
1285             t = structure(buffer, offset + size)
1286             size += t.size
1287
1288             if _nwc_duration(t) == _nwc_duration(self):
1289                 chord1.append(t)
1290             else : # 2 voices
1291                 chord2.append(t)
1292
1293         chords = [chord1]
1294         if chord2:
1295             chords.append(chord2)
1296         return (chords, size)
1297
1298     def ly_text(self, **kwargs):
1299         """
1300         http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Combining-notes-into-chords#Combining-notes-into-chords
1301         """
1302         notes = []
1303         for chord in self['chords']:
1304             for token in chord:
1305                 if isinstance(token, NWCToken_note):
1306                     notes.append(token.ly_text(in_chord=True, **kwargs))
1307         chord = ['<%s>' % ' '.join(notes)]
1308         chord.append(_nwc_duration(self))
1309         if self['tie']:
1310             chord.append('~')
1311
1312         if self['beam'] == 'start':
1313             chord.append('[')
1314         elif self['beam'] == 'stop':
1315             chord.append(']')
1316
1317         if self['triplet'] == 'start':
1318             chord.insert(0, r'\times 2/3 { ')
1319         elif self['triplet'] == 'stop':
1320             chord.append(' }')
1321
1322         return ''.join(chord)
1323 #        if chord2:  # 2 voices
1324 #            result += ' << '
1325 #            for i in range(len(chord2)):
1326 #                (pitch,accidental,duration ) = chord2[i]
1327 #                pitch += lastClef
1328 #                note = SCALE[pitch]
1329 #
1330 #                if (accidental!='auto'):
1331 #                    currentKey[note[0]] = accidental
1332 #                accidental = currentKey[note[0]]
1333 #
1334 #                if (relative_pitch):
1335 #                    octave = getRelativePitch(lastPitch, pitch)
1336 #                    lastPitch = pitch
1337 #                else:
1338 #                    octave = note[1:]
1339 #                result += note[0] + accidental + octave + duration + ' '
1340 #
1341 #            result += " \\\\ {"
1342 #
1343 #            for i in range(len(chord1)):
1344 #                (pitch,accidental ) = chord1[i]
1345 #                pitch += lastClef
1346 #                note = SCALE[pitch]
1347 #
1348 #                if (accidental!='auto'):
1349 #                    currentKey[note[0]] = accidental
1350 #                accidental = currentKey[note[0]]
1351 #
1352 #                if (relative_pitch):
1353 #                    octave = getRelativePitch(lastPitch, pitch)
1354 #                    lastPitch = pitch
1355 #                else:
1356 #                    octave = note[1:]
1357 #                result += note[0] + accidental + octave +chordDur + ' '
1358 #            if lastChord >0 : lastChord = _nwc_duration_value(duration) - _nwc_duration_value(chordDur)
1359 #            if lastChord==0: result += ' } >> '
1360 #            # end 2 voices
1361 #        else:  # block chord
1362 #            result += ' <'
1363 #            for i in range(len(chord1)):
1364 #                (pitch,accidental ) = chord1[i]
1365 #                pitch += lastClef
1366 #                note = SCALE[pitch]
1367 #
1368 #                if (accidental!='auto'):
1369 #                    currentKey[note[0]] = accidental
1370 #                accidental = currentKey[note[0]]
1371 #
1372 #                if (relative_pitch):
1373 #                    octave = getRelativePitch(lastPitch, pitch)
1374 #                    lastPitch = pitch
1375 #                else:
1376 #                    octave = note[1:]
1377 #                result += note[0] + accidental + octave + ' '
1378 #            result += '>' +chordDur +' '
1379 #            lastPitch = chord1[0][0] + lastClef
1380 #        lastDuration = chordDur
1381
1382
1383 class NWCToken_pedal (Structure):
1384     _fields = []
1385     for i in range(5):
1386         _fields.append(('unknown%d' % (i+1), 'B'))
1387
1388
1389 class NWCToken_midi_MPC (Structure):
1390     _fields = []
1391     for i in range(36):
1392         _fields.append(('unknown%d' % (i+1), 'B'))
1393
1394
1395 class NWCToken_fermata (Structure):
1396     _fields = []
1397     for i in range(6):
1398         _fields.append(('unknown%d' % (i+1), 'B'))
1399
1400     def ly_text(self):
1401         """
1402         http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Other-uses-for-tweaks#index-fermata_002c-implementing-in-MIDI
1403         """
1404         return r'\fermata'
1405
1406
1407 class NWCToken_dynamics2 (Structure):
1408     _fields = []
1409     for i in range(5):
1410         _fields.append(('unknown%d' % (i+1), 'B'))
1411
1412
1413 class NWCToken_performance_style (Structure):
1414     _fields = []
1415     for i in range(5):
1416         _fields.append(('unknown%d' % (i+1), 'B'))
1417
1418
1419 class NWCToken_text (Structure):
1420     _fields = [
1421         ('unknown1', 'B'),
1422         ('unknown2', 'B'),
1423         ('textpos', 'B'),
1424         ('unknown3', 'B'),
1425         ('unknown4', 'B'),
1426         ('text', 'S'),
1427         ]
1428
1429     def ly_text(self):
1430         """
1431         http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-text
1432         http://lilypond.org/doc/v2.12/Documentation/user/lilypond/Writing-text#Text-marks
1433         """
1434         #if text.isdigit() : # check numbers
1435         #    text = "-\\markup -\\number "+ text
1436         #    #text = "-\\markup {\\number "+ text +"}"
1437         #else :
1438         #    text = '-"' + text + '"'
1439         #extra += ' ' + text
1440         return r'\mark "%s"' % self['text']
1441
1442
1443 class NWCToken_chord2 (NWCToken_chord1):
1444     pass
1445
1446
1447 def _get_type_and_type_tail(nwc_buffer):
1448     """
1449     >>> _get_type_and_type_tail('[NWZ]...\\n[bla bla]...')
1450     ('NWZ', '...\\n[bla bla]...')
1451     >>> _get_type_and_type_tail('[NoteWorthy ArtWare]...\\n[bla bla]...')
1452     ('NoteWorthy ArtWare', '...\\n[bla bla]...')
1453     """
1454     m = re.match('\[([^]]*)\]', nwc_buffer)
1455     _type = m.group(1)
1456     tail = nwc_buffer[len(m.group(0)):]
1457     return (_type, tail)
1458
1459 def _normalize_nwc(nwc_buffer):
1460     """
1461     Tested in `parse_nwc()` doctests.
1462     """
1463     _type,tail = _get_type_and_type_tail(nwc_buffer)
1464     if _type == 'NWZ':
1465         LOG.debug('compressed nwc detected, decompressing.')
1466         uncompressed_nwc_buffer = zlib.decompress(tail[1:])
1467         return uncompressed_nwc_buffer
1468     return nwc_buffer
1469
1470 def _get_staff(nwc_buffer, offset, n):
1471     orig_offset = offset
1472     staff = NWCStaff(nwc_buffer, offset)
1473     offset += staff.size
1474     if staff['num_lyrics'] > 0:
1475         assert staff['has_lyrics'] == 1, pprint.pformat(staff)
1476         staff['lyric_properties'] = NWCLyricProperties(nwc_buffer, offset)
1477         offset += staff['lyric_properties'].size
1478     staff['lyrics'] = []
1479     for i in range(staff['num_lyrics']):
1480         p = NWCLyricBlockProperties(nwc_buffer, offset)
1481         offset += p.size
1482         length = 1024 * (p['num_blocks']/4) - 1
1483         #p['len']
1484         lines = re.split(
1485             '\r|\n',
1486             _decode_string(nwc_buffer[offset:offset+length]))
1487         block = []
1488         for line in lines:
1489             block.append([word.strip() for word in line.rstrip('\x00').split('\x00')])
1490             LOG.debug('lyric line %s' % block[-1])
1491         staff['lyrics'].append(block)
1492         offset += length
1493     # possible if lyric byte read
1494     staff['note_properties'] = NWCStaffNoteProperties(nwc_buffer, offset)
1495     offset += staff['note_properties'].size
1496
1497     staff['tokens'] = []
1498     token = NWCToken()
1499     oo = offset
1500     for i in range(10000000):  # num_tokens
1501         try:
1502             token.unpack_from(nwc_buffer, offset)
1503         except struct.error:
1504             LOG.warn('ran off end of file')
1505             return (staff, offset-orig_offset)
1506         if token['token'] == 'STOP':
1507             break
1508         offset += token.size
1509         LOG.debug('token: %s' % token['token'])
1510         if type(token['token']) == int:
1511             raise ValueError(token['token'])
1512         structure = globals()['NWCToken_%s' % token['token']]
1513         t = structure(nwc_buffer, offset)
1514         offset += t.size
1515         staff['tokens'].append(t)
1516     LOG.debug('%d tokens, %d bytes (chords count as one)' % (i, offset - oo))
1517     return (staff, offset-orig_offset)
1518
1519 def parse_nwc(nwc_buffer):
1520     """Parse a NoteWorthy Composer `nwc` file.
1521
1522     >>> import os.path
1523     >>> with open(os.path.join('example', '1.75-1.nwc'), 'rb') as f:
1524     ...     nwc_buffer = f.read()
1525     >>> nwc_buffer  # doctest: +ELLIPSIS
1526     '[NWZ]...'
1527     >>> n = parse_nwc(nwc_buffer)
1528     >>> n['_debug']['nwc_buffer']  # doctest: +ELLIPSIS
1529     '[NoteWorthy ArtWare]...'
1530     >>> d = n.pop('_debug')
1531     >>> pprint.pprint(n)  # doctest: +ELLIPSIS, +REPORT_UDIFF
1532     {'major_version': 1,
1533      'minor_version': 75,
1534      'na': u'N/A',
1535      'name1': u'Abwhir',
1536      'page_setup': {'f2': u'F2',
1537                     'margins': u'1.00000000 1.00000000 1.00000000 1.00000000',
1538                     'ny_': u'NN_',
1539                     'page_small': {'name': u'Times New Roman',
1540                                    'size': 10,
1541                                    'style': 0,
1542                                    'unknown3': 0,
1543                                    'unknown4': 0},
1544                     'page_text': {'name': u'Times New Roman',
1545                                   'size': 12,
1546                                   'style': 0,
1547                                   'unknown3': 0,
1548                                   'unknown4': 0},
1549                     'page_title': {'name': u'Times New Roman',
1550                                    'size': 24,
1551                                    'style': 1,
1552                                    'unknown3': 0,
1553                                    'unknown4': 0},
1554                     'staff_bold': {'name': u'Times New Roman',
1555                                    'size': 10,
1556                                    'style': 1,
1557                                    'unknown3': 0,
1558                                    'unknown4': 0},
1559                     'staff_italic': {'name': u'Times New Roman',
1560                                      'size': 12,
1561                                      'style': 3,
1562                                      'unknown3': 0,
1563                                      'unknown4': 0},
1564                     'staff_lyric': {'name': u'Times New Roman',
1565                                     'size': 15,
1566                                     'style': 0,
1567                                     'unknown3': 0,
1568                                     'unknown4': 0},
1569                     ...,
1570                     'user1': {'name': u'Times New Roman',
1571                               'size': 12,
1572                               'style': 0,
1573                               'unknown3': 0,
1574                               'unknown4': 0},
1575                     'user2': {'name': u'Times New Roman',
1576                               'size': 12,
1577                               'style': 0,
1578                               'unknown3': 0,
1579                               'unknown4': 0},
1580                     'user3': {'name': u'Times New Roman',
1581                               'size': 12,
1582                               'style': 0,
1583                               'unknown3': 0,
1584                               'unknown4': 0},
1585                     'user4': {'name': u'Times New Roman',
1586                               'size': 12,
1587                               'style': 0,
1588                               'unknown3': 0,
1589                               'unknown4': 0},
1590                     'user5': {'name': u'Times New Roman',
1591                               'size': 8,
1592                               'style': 0,
1593                               'unknown3': 0,
1594                               'unknown4': 0},
1595                     'user6': {'name': u'Times New Roman',
1596                               'size': 8,
1597                               'style': 0,
1598                               'unknown3': 0,
1599                               'unknown4': 0}},
1600      'product': u'[NoteWorthy Composer]',
1601      'song_info': {'author': u'George Job Elvey, 1858',
1602                    'comments': u'Source: The United Methodist Hymnal (Nashville, Tennessee: The United Methodist Publishing House, 1989), # 694.',
1603                    'copyright1': u'Public Domain',
1604                    'copyright2': u'Courtesy of the Cyber Hymnal (http://www.cyberhymnal.org)',
1605                    'title': u'St. George\u2019s Windsor, 77.77 D'},
1606      'staff': [{'ff': 255,
1607                 'group': u'Standard',
1608                 'has_lyrics': 0,
1609                 'lyrics': [],
1610                 'name': u'Unnamed-000',
1611                 'note_properties': {'color': 0, 'num_tokens': 0, 'unknown1': 0},
1612                 'num_lyrics': 0,
1613                 'tokens': [{'flat_bits': 2,
1614                             'sharp_bits': 0,
1615                             ...},
1616                            {'tempo': u'',
1617                             ...},
1618                            {'beat_value': 4,
1619                             'beats': 4,
1620                             ...},
1621                            ...],
1622                 'unknown01': 0,
1623                 ...}],
1624      'type': u'[NoteWorthy ArtWare]',
1625      'unknown01': 0,
1626      ...}
1627     """
1628     LOG.info('parsing nwc')
1629     n = {'_debug': {}}
1630     nwc_buffer = _normalize_nwc(nwc_buffer)
1631     n['_debug']['nwc_buffer'] = nwc_buffer
1632
1633     head = NWCFileHead(nwc_buffer)
1634     offset = head.size
1635     n.update(head)
1636
1637     assert head['major_version'] == 1, head['major_version']
1638     assert head['minor_version'] == 75, head['minor_version']
1639
1640     #o = nwc_buffer[offset:].find('\xff')
1641     #if o < 0:
1642     #    LOG.error('could not find staff section')
1643     #    raise NotImplementedError
1644     #offset += o
1645     #LOG.warn('skip to staves with offset %d (skipped %d, %s)'
1646     #          % (offset, o, repr(nwc_buffer[offset:offset+10])))
1647
1648     n['staff_set'] = NWCStaffSet(nwc_buffer, offset)
1649     offset += n['staff_set'].size
1650
1651     n['staff'] = []
1652     for i in range(n['staff_set']['num_staves']):
1653         LOG.info('process staff %d' % i)
1654         staff,size = _get_staff(nwc_buffer, offset, n)
1655         offset += size
1656         if staff == None:
1657             break
1658         n['staff'].append(staff)
1659
1660     return n
1661
1662
1663 def ly_text(nwc):
1664     """
1665     >>> import os.path
1666     >>> with open(os.path.join('example', '1.75-1.nwc'), 'rb') as f:
1667     ...     nwc_buffer = f.read()
1668     >>> nwc = parse_nwc(nwc_buffer)
1669     >>> print ly_text(nwc)  # doctest: +ELLIPSIS
1670     '[NoteWorthy ArtWare]...'
1671
1672     http://lilypond.org/doc/v2.11/Documentation/user/lilypond/Creating-MIDI-files
1673     """
1674     if nwc['type'] not in ['[NoteWorthy ArtWare]', '[NoteWorthy Composer]']:
1675         raise ValueError('unknown file type: %s' % nwc['type'])
1676
1677     #LOG.debug(pprint.pformat(nwc, width=70))
1678     LOG.info('format header')
1679
1680     lines = [
1681         '%% Generated with %s converter v%s' % ('nwc2ly.py', __version__),
1682         r'\version "2.12.2"',  # http://lilypond.org/doc/v2.12/Documentation/user/lilypond-learning/Version-number#Version-number
1683         ]
1684
1685     lines.extend([nwc['song_info'].ly_text(), ''])
1686
1687     lines.extend([
1688             '',
1689             r'\score {',
1690             '\t<<',
1691             ])
1692
1693     for i,staff in enumerate(nwc['staff']):
1694         LOG.info('format staff %d' % i)
1695         voice = chr(i+ord('a'))
1696         if i == 1 and False:
1697             for token in staff['tokens']:
1698                 if isinstance(token, NWCToken_clef):
1699                     token['clef'] = 'bass'
1700         lines.extend(['\t\t%s' % line for line in staff.ly_lines(voice=voice)])
1701
1702     LOG.info('format footer')
1703     lines.extend([
1704             '\t>>',
1705             ''
1706             '\t\layout {}',
1707             '\t\midi {}',
1708             '}',
1709             ''])
1710     return '\n'.join(lines)
1711
1712
1713 def test():
1714     import doctest
1715     return doctest.testmod()
1716
1717
1718 if __name__ == '__main__':
1719     from optparse import OptionParser
1720
1721     usage = "nwc2ly.py [options] uncompressed.nwc test.ly > convert.log"
1722     p = OptionParser(usage=usage)
1723     p.add_option('--debug', dest='debug', action='store_true',
1724                  help='Enable verbose logging')
1725     p.add_option('--absolute-pitch', dest='relative_pitch', default=True,
1726                  action='store_false',
1727                  help='')
1728     p.add_option('--absolute-duration', dest='relative_duration', default=True,
1729                  action='store_false',
1730                  help='')
1731     p.add_option('--bar-comments', dest='bar_comments', default=10,
1732                  type='int',
1733                  help='Comments for every x lines, section/line comment??')
1734     p.add_option('--no-beaming', dest='insert_beaming', default=True,
1735                  action='store_false', help='')
1736     p.add_option('--no-stemming', dest='insert_stemming', default=True,
1737                  action='store_false', help='')
1738     p.add_option('--no-text', dest='insert_text', default=True,
1739                  action='store_false', help='Currently a no-op')
1740     p.add_option('--test', dest='test', action='store_true',
1741                  help='Run internal tests and exit')
1742
1743     options,args = p.parse_args()
1744
1745     if options.debug:
1746         logging.basicConfig(level=logging.DEBUG)
1747     else:
1748         logging.basicConfig(level=logging.INFO)
1749
1750     if options.test:
1751         results = test()
1752         sys.exit(min(results.failed, 127))
1753
1754     #options? lvb7th1
1755     nwc_file = args[0]
1756     if len(args) > 1:
1757         ly_file = args[1]
1758     else:
1759         ly_file = None
1760
1761     with open(nwc_file, 'rb') as f:
1762         nwc_buffer = f.read()
1763     nwc = parse_nwc(nwc_buffer)
1764
1765     if ly_file:
1766         f = open(ly_file, 'w')
1767     else:
1768         f = sys.stdout
1769
1770     f.write(ly_text(nwc).encode('utf-8'))
1771
1772     if ly_file:
1773         f.close()