2525
2626import sys
2727
28- if sys .version_info < (3 ,4 ):
28+ if sys .version_info < (3 , 4 ):
2929 raise SystemError ('Must be using Python 3.4 or higher' )
3030
3131import os
@@ -69,6 +69,10 @@ class Sound(object):
6969 ('G4', 'h'),
7070 ('D5', 'h')
7171 ))
72+
73+ In order to mimic EV3-G API parameters, durations used in methods
74+ exposed as EV3-G blocks for sound related operations are expressed
75+ as a float number of seconds.
7276 """
7377
7478 channel = None
@@ -85,7 +89,8 @@ class Sound(object):
8589 )
8690
8791 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 ))
92+ assert play_type in self .PLAY_TYPES , \
93+ "Invalid play_type %s, must be one of %s" % (play_type , ',' .join (str (t ) for t in self .PLAY_TYPES ))
8994
9095 def beep (self , args = '' ):
9196 """
@@ -131,16 +136,22 @@ def tone(self, *args):
131136 (392.00, 300, 150), (311.13, 250, 100), (466.16, 25, 100), (392, 700)
132137 ]).wait()
133138
139+ Have also a look at :py:meth:`play_song` for a more musician-friendly way of doing, which uses
140+ the conventional notation for notes and durations.
141+
134142 .. rubric:: tone(frequency, duration)
135143
136144 Play single tone of given frequency (Hz) and duration (milliseconds).
137145 """
138146 def play_tone_sequence (tone_sequence ):
139147 def beep_args (frequency = None , duration = None , delay = None ):
140148 args = ''
141- if frequency is not None : args += '-f %s ' % frequency
142- if duration is not None : args += '-l %s ' % duration
143- if delay is not None : args += '-D %s ' % delay
149+ if frequency is not None :
150+ args += '-f %s ' % frequency
151+ if duration is not None :
152+ args += '-l %s ' % duration
153+ if delay is not None :
154+ args += '-D %s ' % delay
144155
145156 return args
146157
@@ -153,10 +164,37 @@ def beep_args(frequency=None, duration=None, delay=None):
153164 else :
154165 raise Exception ("Unsupported number of parameters in Sound.tone()" )
155166
156- def play_tone (self , frequency , duration_ms , delay_ms = 100 , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
167+ def play_tone (self , frequency , duration , delay = 0.0 , volume = 100 ,
168+ play_type = PLAY_WAIT_FOR_COMPLETE ):
169+ """ Play a single tone, specified by its frequency, duration, volume and final delay.
170+
171+ Args:
172+ frequency (int): the tone frequency, in Hertz
173+ duration (float): tone duration, in seconds
174+ delay (float): delay after tone, in seconds (can be useful when chaining calls to ``play_tone``)
175+ volume (int): sound volume in percent (between 0 and 100)
176+ play_type (int): one off Sound.PLAY_xxx play types (wait, no wait, loop)
177+
178+ Returns:
179+ the sound playing subprocess PID when no wait play type is selected, None otherwise
180+
181+ Raises:
182+ ValueError: if invalid value for parameter(s)
183+ """
157184 self ._validate_play_type (play_type )
185+
186+ if duration <= 0 :
187+ raise ValueError ('invalid duration (%s)' % duration )
188+ if delay < 0 :
189+ raise ValueError ('invalid delay (%s)' % delay )
190+ if not 0 < volume <= 100 :
191+ raise ValueError ('invalid volume (%s)' % volume )
192+
158193 self .set_volume (volume )
159194
195+ duration_ms = int (duration * 1000 )
196+ delay_ms = int (delay * 1000 )
197+
160198 if play_type == Sound .PLAY_WAIT_FOR_COMPLETE :
161199 play = self .tone ([(frequency , duration_ms , delay_ms )])
162200 play .wait ()
@@ -169,9 +207,43 @@ def play_tone(self, frequency, duration_ms, delay_ms=100, volume=100, play_type=
169207 play = self .tone ([(frequency , duration_ms , delay_ms )])
170208 play .wait ()
171209
172- def play (self , wav_file , play_type = PLAY_WAIT_FOR_COMPLETE ):
210+ def play_note (self , note , duration , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
211+ """ Plays a note, given by its name as defined in ``_NOTE_FREQUENCIES``.
212+
213+ Args:
214+ note (str) the note symbol with its octave number
215+ duration (float): tone duration, in seconds
216+ volume (int) the play volume, in percent of maximum volume
217+ play_type (int) the type of play (wait, no wait, loop), as defined
218+ by the ``PLAY_xxx`` constants
219+
220+ Returns:
221+ the PID of the underlying beep command if no wait play type, None otherwise
222+
223+ Raises:
224+ ValueError: is invalid parameter (note, duration,...)
173225 """
174- Play wav file.
226+ self ._validate_play_type (play_type )
227+ try :
228+ freq = self ._NOTE_FREQUENCIES [note .upper ()]
229+ except KeyError :
230+ raise ValueError ('invalid note (%s)' % note )
231+ if duration <= 0 :
232+ raise ValueError ('invalid duration (%s)' % duration )
233+ if not 0 < volume <= 100 :
234+ raise ValueError ('invalid volume (%s)' % volume )
235+
236+ return self .play_tone (freq , duration = duration , volume = volume , play_type = play_type )
237+
238+ def play (self , wav_file , play_type = PLAY_WAIT_FOR_COMPLETE ):
239+ """ Play a sound file (wav format).
240+
241+ Args:
242+ wav_file (str): the sound file path
243+ play_type (int): one off Sound.PLAY_xxx play types (wait, no wait, loop)
244+
245+ Returns:
246+ subprocess.Popen: the spawn subprocess when no wait play type is selected, None otherwise
175247 """
176248 self ._validate_play_type (play_type )
177249
@@ -191,18 +263,40 @@ def play(self, wav_file, play_type=PLAY_WAIT_FOR_COMPLETE):
191263 pid .wait ()
192264
193265 def play_file (self , wav_file , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
266+ """ Play a sound file (wav format) at a given volume.
267+
268+ Args:
269+ wav_file (str): the sound file path
270+ volume (int) the play volume, in percent of maximum volume
271+ play_type (int): one off Sound.PLAY_xxx play types (wait, no wait, loop)
272+
273+ Returns:
274+ subprocess.Popen: the spawn subprocess when no wait play type is selected, None otherwise
275+ """
194276 self .set_volume (volume )
195277 self .play (wav_file , play_type )
196278
197279 def speak (self , text , espeak_opts = '-a 200 -s 130' , volume = 100 , play_type = PLAY_WAIT_FOR_COMPLETE ):
198- """
199- Speak the given text aloud.
280+ """ Speak the given text aloud.
281+
282+ Uses the ``espeak`` external command.
283+
284+ Args:
285+ text (str): the text to speak
286+ espeak_opts (str): espeak command options
287+ volume (int) the play volume, in percent of maximum volume
288+ play_type (int): one off Sound.PLAY_xxx play types (wait, no wait, loop)
289+
290+ Returns:
291+ subprocess.Popen: the spawn subprocess when no wait play type is selected, None otherwise
200292 """
201293 self ._validate_play_type (play_type )
202294 self .set_volume (volume )
203295
204296 with open (os .devnull , 'w' ) as n :
205- cmd_line = '/usr/bin/espeak --stdout {0} "{1}" | /usr/bin/aplay -q' .format (espeak_opts , text )
297+ cmd_line = '/usr/bin/espeak --stdout {0} "{1}" | /usr/bin/aplay -q' .format (
298+ espeak_opts , text
299+ )
206300
207301 if play_type == Sound .PLAY_WAIT_FOR_COMPLETE :
208302 play = Popen (cmd_line , stdout = n , shell = True )
@@ -218,8 +312,8 @@ def speak(self, text, espeak_opts='-a 200 -s 130', volume=100, play_type=PLAY_WA
218312
219313 def _get_channel (self ):
220314 """
221- :return: the detected sound channel
222- :rtype: str
315+ Returns:
316+ str: the detected sound channel
223317 """
224318 if self .channel is None :
225319 # Get default channel as the first one that pops up in
@@ -229,7 +323,7 @@ def _get_channel(self):
229323 # Simple mixer control 'Master',0
230324 # Simple mixer control 'Capture',0
231325 out = check_output (['amixer' , 'scontrols' ]).decode ()
232- m = re .search ("'(?P<channel>[^']+)'" , out )
326+ m = re .search (r "'(?P<channel>[^']+)'" , out )
233327 if m :
234328 self .channel = m .group ('channel' )
235329 else :
@@ -265,13 +359,13 @@ def get_volume(self, channel=None):
265359 channel = self ._get_channel ()
266360
267361 out = check_output (['amixer' , 'get' , channel ]).decode ()
268- m = re .search ('\[(?P<volume>\d+)%\]' , out )
362+ m = re .search (r '\[(?P<volume>\d+)%\]' , out )
269363 if m :
270364 return int (m .group ('volume' ))
271365 else :
272366 raise Exception ('Failed to parse output of `amixer get {}`' .format (channel ))
273367
274- def play_song (self , song , tempo = 120 , delay = 50 ):
368+ def play_song (self , song , tempo = 120 , delay = 0.05 ):
275369 """ Plays a song provided as a list of tuples containing the note name and its
276370 value using music conventional notation instead of numerical values for frequency
277371 and duration.
@@ -329,12 +423,21 @@ def play_song(self, song, tempo=120, delay=50):
329423 Args:
330424 song (iterable[tuple(str, str)]): the song
331425 tempo (int): the song tempo, given in quarters per minute
332- delay (int ): delay in ms between notes
426+ delay (float ): delay between notes (in seconds)
333427
334428 Returns:
335429 subprocess.Popen: the spawn subprocess
430+
431+ Raises:
432+ ValueError: if invalid note in song or invalid play parameters
336433 """
337- meas_duration = 60000 / tempo * 4
434+ if tempo <= 0 :
435+ raise ValueError ('invalid tempo (%s)' % tempo )
436+ if delay < 0 :
437+ raise ValueError ('invalid delay (%s)' % delay )
438+
439+ delay_ms = int (delay * 1000 )
440+ meas_duration_ms = 60000 / tempo * 4 # we only support 4/4 bars, hence "* 4"
338441
339442 def beep_args (note , value ):
340443 """ Builds the arguments string for producing a beep matching
@@ -349,24 +452,27 @@ def beep_args(note, value):
349452 freq = self ._NOTE_FREQUENCIES [note .upper ()]
350453 if '/' in value :
351454 base , factor = value .split ('/' )
352- duration = meas_duration * self ._NOTE_VALUES [base ] / float (factor )
455+ duration_ms = meas_duration_ms * self ._NOTE_VALUES [base ] / float (factor )
353456 elif '*' in value :
354457 base , factor = value .split ('*' )
355- duration = meas_duration * self ._NOTE_VALUES [base ] * float (factor )
458+ duration_ms = meas_duration_ms * self ._NOTE_VALUES [base ] * float (factor )
356459 elif value .endswith ('.' ):
357460 base = value [:- 1 ]
358- duration = meas_duration * self ._NOTE_VALUES [base ] * 1.5
461+ duration_ms = meas_duration_ms * self ._NOTE_VALUES [base ] * 1.5
359462 elif value .endswith ('3' ):
360463 base = value [:- 1 ]
361- duration = meas_duration * self ._NOTE_VALUES [base ] * 2 / 3
464+ duration_ms = meas_duration_ms * self ._NOTE_VALUES [base ] * 2 / 3
362465 else :
363- duration = meas_duration * self ._NOTE_VALUES [value ]
466+ duration_ms = meas_duration_ms * self ._NOTE_VALUES [value ]
364467
365- return '-f %d -l %d -D %d' % (freq , duration , delay )
468+ return '-f %d -l %d -D %d' % (freq , duration_ms , delay_ms )
366469
367- return self .beep (' -n ' .join (
368- [beep_args (note , value ) for (note , value ) in song ]
369- ))
470+ try :
471+ return self .beep (' -n ' .join (
472+ [beep_args (note , value ) for (note , value ) in song ]
473+ ))
474+ except KeyError as e :
475+ raise ValueError ('invalid note (%s)' % e )
370476
371477 #: Note frequencies.
372478 #:
0 commit comments