posts:salt-stack: Add Salt post
[blog.git] / posts / abcplay / abcplay.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Play ABC_ files using abc2midi_ and timidity_.
20
21 For example::
22
23     abcplay.py my_collection.abc:3:7:8 another_collection.abc
24
25 An example of using timidity options would be slowing down a tune
26 while you are learning::
27
28     abcplay.py --timidity '--adjust-tempo 50' my_collection.abc:3
29
30 SIGINT (usually ^C) will kill the current tune and proceed the next
31 one.  Two SIGINTs in less than one second will kill the abcplay.py
32 process.  This should be familiar to users of mpg123_ or ogg123_.
33
34 You can also play LilyPond_ files (converted to MIDI via ``lilypond``)
35 by using the ``--ly`` option::
36
37     abcplay.py --ly somefile.ly anotherfile.ly
38
39 .. _abc2midi: http://abc.sourceforge.net/abcMIDI/
40 .. _timidity: http://timidity.sourceforge.net/
41 .. _mpg123: http://www.mpg123.org/
42 .. _ogg123: http://www.vorbis.com
43 .. _LilyPond: http://lilypond.org/
44 """
45
46 import shlex
47 from subprocess import Popen
48 from tempfile import mkstemp
49 from time import time
50 from os import remove
51 import os.path
52
53
54 class MIDIPlayer (object):
55     def __init__(self, to_midi_program=None,
56                  to_midi_options=None, timidity_options=None):
57         f,self._tempfile = mkstemp(prefix='abcplay-', suffix='.midi')
58         self._to_midi = [to_midi_program]
59         if to_midi_options:
60             self._to_midi.extend(to_midi_options)
61         self._timidity = ['timidity']
62         if timidity_options:
63             self._timidity.extend(timidity_options)
64         self._last_interrupt = 0
65         self._p = None
66
67     def cleanup(self):
68         remove(self._tempfile)
69
70     def play_files(self, filenames):
71         raise NotImplementedError()
72
73     def _return_after_interrupt(self):
74         t = time()
75         ret = t-self._last_interrupt < 1
76         self._last_interrupt = t
77         return ret
78
79     def play(self, filename, **kwargs):
80         self._convert_to_midi(filename, **kwargs)
81         self._play_midi()
82
83     def _convert_to_midi(self, filename, **kwargs):
84         raise NotImplementedError()
85
86     def _play_midi(self):
87         self._p = Popen(self._timidity + [self._tempfile])
88         self._p.wait()
89         self._p = None
90
91     def _kill_p(self):
92         if self._p != None:
93             try:
94                 self._p.terminate()
95             except OSError, e:
96                 if e.errno == 3:
97                     pass  # no such process
98                 else:
99                     raise
100             else:
101                 self._p.wait()
102             self._p = None
103
104
105 class ABCPlayer (MIDIPlayer):
106     refnum_sep = ':'
107
108     def __init__(self, abc2midi_options=None, timidity_options=None):
109         super(ABCPlayer, self).__init__(
110             'abc2midi', abc2midi_options, timidity_options)
111
112     def play_files(self, filenames):
113         for filename in filenames:
114             if self.refnum_sep in filename:
115                 fields = filename.split(self.refnum_sep)
116                 filename = fields[0]
117                 refnums = fields[1:]
118             else:
119                 refnums = list(self._refnums(filename))
120             while len(refnums) > 0:
121                 refnum = refnums.pop(0)
122                 try:
123                     self.play(filename, refnum=refnum)
124                 except KeyboardInterrupt:
125                     self._kill_p()
126                     if self._return_after_interrupt():
127                         return
128
129     def _refnums(self, filename):
130         with open(filename, 'r') as f:
131             for line in f:
132                 if line.startswith('X:'):
133                     yield int(line[len('X:'):])
134
135     def _convert_to_midi(self, filename, refnum):
136         self._p = Popen(
137             self._to_midi[:1] + [filename, str(refnum)] +
138             self._to_midi[1:] + ['-o', self._tempfile])
139         self._p.wait()
140         self._p = None
141
142
143 class LilyPondPlayer (MIDIPlayer):
144     def __init__(self, lilypond_options=None, timidity_options=None):
145         default_lilypond_options = [
146             '-dbackend=null',  # don't create a typeset version
147             '-ddelete-intermediate-files',  # clean up after ourselves
148             ]
149         if lilypond_options:
150             lilypond_options = default_lilypond_options + lilypond_options
151         else:
152             lilypond_options = default_lilypond_options
153         super(LilyPondPlayer, self).__init__(
154             'lilypond', lilypond_options, timidity_options)
155
156     def play_files(self, filenames):
157         for filename in filenames:
158             try:
159                 self.play(filename)
160             except KeyboardInterrupt:
161                 self._kill_p()
162                 if self._return_after_interrupt():
163                     return
164
165     def _convert_to_midi(self, filename):
166         ofilename,ext = os.path.splitext(self._tempfile)
167         assert ext == '.midi', ext  # lilypond adds suffix automatically
168         #-dmidi-extension=midi
169         self._p = Popen(
170             self._to_midi + ['-o', ofilename, filename])
171         self._p.wait()
172         self._p = None
173
174
175 if __name__ == '__main__':
176     import sys
177     import optparse
178
179     usage = '%prog [options] file[:refnum[:refnum:...]] ...'
180     epilog = __doc__
181     p = optparse.OptionParser(usage=usage, epilog=epilog)
182     p.format_epilog = lambda formatter: epilog+'\n'
183     p.add_option('-a', '--abc2midi', dest='abc2midi',
184                  help='Extra options to pass to abc2midi.')
185     p.add_option('-t', '--timidity', dest='timidity',
186                  help='Extra options to pass to timidity.')
187     p.add_option('-l', '--ly', dest='use_lilypond', action='store_true',
188                  help='Use LilyPond input instead of ABC.')
189     p.add_option('--lilypond', dest='lilypond',
190                  help='Extra options to pass to LilyPond.')
191     options,args = p.parse_args()
192     del p
193
194     abc2midi = options.abc2midi
195     if abc2midi:
196         abc2midi = shlex.split(abc2midi)
197     timidity = options.timidity
198     if timidity:
199         timidity = shlex.split(timidity)
200     lilypond = options.lilypond
201     if lilypond:
202         lilypond = shlex.split(lilypond)
203
204     if options.use_lilypond:
205         p = LilyPondPlayer(lilypond, timidity)
206     else:
207         p = ABCPlayer(abc2midi, timidity)
208     try:
209         p.play_files(args)
210     finally:
211         p.cleanup()