Skip to content

Commit ef23296

Browse files
authored
EV3-G API Sound (#396)
Resolves issue #358
1 parent aeb0155 commit ef23296

File tree

1 file changed

+92
-42
lines changed

1 file changed

+92
-42
lines changed

ev3dev/sound.py

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828
if sys.version_info < (3,4):
2929
raise SystemError('Must be using Python 3.4 or higher')
3030

31-
import io
31+
import os
3232
import re
3333
import shlex
34-
from subprocess import Popen, check_output, PIPE
34+
from subprocess import check_output, Popen
3535

3636

3737
def _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

Comments
 (0)