2828if sys .version_info < (3 ,4 ):
2929 raise SystemError ('Must be using Python 3.4 or higher' )
3030
31- import io
31+ import os
3232import re
3333import shlex
34- from subprocess import Popen , check_output , PIPE
34+ from subprocess import check_output , Popen
3535
3636
3737def _make_scales (notes ):
@@ -44,11 +44,9 @@ def _make_scales(notes):
4444 return res
4545
4646
47- class Sound :
47+ class Sound ( object ) :
4848 """
49- Sound-related functions. The class has only static methods and is not
50- intended for instantiation. It can beep, play wav files, or convert text to
51- speech.
49+ Support beep, play wav files, or convert text to speech.
5250
5351 Note that all methods of the class spawn system processes and return
5452 subprocess.Popen objects. The methods are asynchronous (they return
@@ -75,8 +73,21 @@ class Sound:
7573
7674 channel = None
7775
78- @staticmethod
79- def beep (args = '' ):
76+ # play_types
77+ PLAY_WAIT_FOR_COMPLETE = 0
78+ PLAY_NO_WAIT_FOR_COMPLETE = 1
79+ PLAY_LOOP = 2
80+
81+ PLAY_TYPES = (
82+ PLAY_WAIT_FOR_COMPLETE ,
83+ PLAY_NO_WAIT_FOR_COMPLETE ,
84+ PLAY_LOOP
85+ )
86+
87+ def _validate_play_type (self , play_type ):
88+ assert play_type in self .PLAY_TYPES , "Invalid play_type %s, must be one of %s" % (play_type , ',' .join (self .PLAY_TYPES ))
89+
90+ def beep (self , args = '' ):
8091 """
8192 Call beep command with the provided arguments (if any).
8293 See `beep man page`_ and google `linux beep music`_ for inspiration.
@@ -87,8 +98,7 @@ def beep(args=''):
8798 with open (os .devnull , 'w' ) as n :
8899 return Popen (shlex .split ('/usr/bin/beep %s' % args ), stdout = n )
89100
90- @staticmethod
91- def tone (* args ):
101+ def tone (self , * args ):
92102 """
93103 .. rubric:: tone(tone_sequence)
94104
@@ -134,7 +144,7 @@ def beep_args(frequency=None, duration=None, delay=None):
134144
135145 return args
136146
137- return Sound .beep (' -n ' .join ([beep_args (* t ) for t in tone_sequence ]))
147+ return self .beep (' -n ' .join ([beep_args (* t ) for t in tone_sequence ]))
138148
139149 if len (args ) == 1 :
140150 return play_tone_sequence (args [0 ])
@@ -143,32 +153,75 @@ def beep_args(frequency=None, duration=None, delay=None):
143153 else :
144154 raise Exception ("Unsupported number of parameters in Sound.tone()" )
145155
146- @staticmethod
147- def play (wav_file ):
156+ def play_tone (self , frequency , duration_ms , delay_ms = 100 , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
157+ self ._validate_play_type (play_type )
158+ self .set_volume (volume )
159+
160+ if play_type == Sound .PLAY_WAIT_FOR_COMPLETE :
161+ play = self .tone ([(frequency , duration_ms , delay_ms )])
162+ play .wait ()
163+
164+ elif play_type == Sound .PLAY_NO_WAIT_FOR_COMPLETE :
165+ return self .tone ([(frequency , duration_ms , delay_ms )])
166+
167+ elif play_type == Sound .PLAY_LOOP :
168+ while True :
169+ play = self .tone ([(frequency , duration_ms , delay_ms )])
170+ play .wait ()
171+
172+ def play (self , wav_file , play_type = PLAY_WAIT_FOR_COMPLETE ):
148173 """
149174 Play wav file.
150175 """
176+ self ._validate_play_type (play_type )
177+
151178 with open (os .devnull , 'w' ) as n :
152- return Popen (shlex .split ('/usr/bin/aplay -q "%s"' % wav_file ), stdout = n )
153179
154- @staticmethod
155- def speak (text , espeak_opts = '-a 200 -s 130' ):
180+ if play_type == Sound .PLAY_WAIT_FOR_COMPLETE :
181+ pid = Popen (shlex .split ('/usr/bin/aplay -q "%s"' % wav_file ), stdout = n )
182+ pid .wait ()
183+
184+ # Do not wait, run in the background
185+ elif play_type == Sound .PLAY_NO_WAIT_FOR_COMPLETE :
186+ return Popen (shlex .split ('/usr/bin/aplay -q "%s"' % wav_file ), stdout = n )
187+
188+ elif play_type == Sound .PLAY_LOOP :
189+ while True :
190+ pid = Popen (shlex .split ('/usr/bin/aplay -q "%s"' % wav_file ), stdout = n )
191+ pid .wait ()
192+
193+ def play_file (self , wav_file , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
194+ self .set_volume (volume )
195+ self .play (wav_file , play_type )
196+
197+ def speak (self , text , espeak_opts = '-a 200 -s 130' , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
156198 """
157199 Speak the given text aloud.
158200 """
201+ self ._validate_play_type (play_type )
202+ self .set_volume (volume )
203+
159204 with open (os .devnull , 'w' ) as n :
160- cmd_line = '/usr/bin/espeak --stdout {0} "{1}"' .format (espeak_opts , text )
161- espeak = Popen (shlex .split (cmd_line ), stdout = PIPE )
162- play = Popen (['/usr/bin/aplay' , '-q' ], stdin = espeak .stdout , stdout = n )
163- return espeak
205+ cmd_line = '/usr/bin/espeak --stdout {0} "{1}" | /usr/bin/aplay -q' .format (espeak_opts , text )
206+
207+ if play_type == Sound .PLAY_WAIT_FOR_COMPLETE :
208+ play = Popen (cmd_line , stdout = n , shell = True )
209+ play .wait ()
210+
211+ elif play_type == Sound .PLAY_NO_WAIT_FOR_COMPLETE :
212+ return Popen (cmd_line , stdout = n , shell = True )
213+
214+ elif play_type == Sound .PLAY_LOOP :
215+ while True :
216+ play = Popen (cmd_line , stdout = n , shell = True )
217+ play .wait ()
164218
165- @staticmethod
166- def _get_channel ():
219+ def _get_channel (self ):
167220 """
168221 :return: the detected sound channel
169222 :rtype: str
170223 """
171- if Sound .channel is None :
224+ if self .channel is None :
172225 # Get default channel as the first one that pops up in
173226 # 'amixer scontrols' output, which contains strings in the
174227 # following format:
@@ -178,14 +231,13 @@ def _get_channel():
178231 out = check_output (['amixer' , 'scontrols' ]).decode ()
179232 m = re .search ("'(?P<channel>[^']+)'" , out )
180233 if m :
181- Sound .channel = m .group ('channel' )
234+ self .channel = m .group ('channel' )
182235 else :
183- Sound .channel = 'Playback'
236+ self .channel = 'Playback'
184237
185- return Sound .channel
238+ return self .channel
186239
187- @staticmethod
188- def set_volume (pct , channel = None ):
240+ def set_volume (self , pct , channel = None ):
189241 """
190242 Sets the sound volume to the given percentage [0-100] by calling
191243 ``amixer -q set <channel> <pct>%``.
@@ -195,13 +247,12 @@ def set_volume(pct, channel=None):
195247 """
196248
197249 if channel is None :
198- channel = Sound ._get_channel ()
250+ channel = self ._get_channel ()
199251
200252 cmd_line = '/usr/bin/amixer -q set {0} {1:d}%' .format (channel , pct )
201253 Popen (shlex .split (cmd_line )).wait ()
202254
203- @staticmethod
204- def get_volume (channel = None ):
255+ def get_volume (self , channel = None ):
205256 """
206257 Gets the current sound volume by parsing the output of
207258 ``amixer get <channel>``.
@@ -211,7 +262,7 @@ def get_volume(channel=None):
211262 """
212263
213264 if channel is None :
214- channel = Sound ._get_channel ()
265+ channel = self ._get_channel ()
215266
216267 out = check_output (['amixer' , 'get' , channel ]).decode ()
217268 m = re .search ('\[(?P<volume>\d+)%\]' , out )
@@ -220,8 +271,7 @@ def get_volume(channel=None):
220271 else :
221272 raise Exception ('Failed to parse output of `amixer get {}`' .format (channel ))
222273
223- @classmethod
224- def play_song (cls , song , tempo = 120 , delay = 50 ):
274+ def play_song (self , song , tempo = 120 , delay = 50 ):
225275 """ Plays a song provided as a list of tuples containing the note name and its
226276 value using music conventional notation instead of numerical values for frequency
227277 and duration.
@@ -296,26 +346,26 @@ def beep_args(note, value):
296346 Returns:
297347 str: the arguments to be passed to the beep command
298348 """
299- freq = Sound ._NOTE_FREQUENCIES [note .upper ()]
349+ freq = self ._NOTE_FREQUENCIES [note .upper ()]
300350 if '/' in value :
301351 base , factor = value .split ('/' )
302- duration = meas_duration * Sound ._NOTE_VALUES [base ] / float (factor )
352+ duration = meas_duration * self ._NOTE_VALUES [base ] / float (factor )
303353 elif '*' in value :
304354 base , factor = value .split ('*' )
305- duration = meas_duration * Sound ._NOTE_VALUES [base ] * float (factor )
355+ duration = meas_duration * self ._NOTE_VALUES [base ] * float (factor )
306356 elif value .endswith ('.' ):
307357 base = value [:- 1 ]
308- duration = meas_duration * Sound ._NOTE_VALUES [base ] * 1.5
358+ duration = meas_duration * self ._NOTE_VALUES [base ] * 1.5
309359 elif value .endswith ('3' ):
310360 base = value [:- 1 ]
311- duration = meas_duration * Sound ._NOTE_VALUES [base ] * 2 / 3
361+ duration = meas_duration * self ._NOTE_VALUES [base ] * 2 / 3
312362 else :
313- duration = meas_duration * Sound ._NOTE_VALUES [value ]
363+ duration = meas_duration * self ._NOTE_VALUES [value ]
314364
315365 return '-f %d -l %d -D %d' % (freq , duration , delay )
316366
317- return Sound .beep (' -n ' .join (
318- [beep_args (note , value ) for note , value in song ]
367+ return self .beep (' -n ' .join (
368+ [beep_args (note , value ) for ( note , value ) in song ]
319369 ))
320370
321371 #: Note frequencies.
0 commit comments