Skip to content

Commit bae544f

Browse files
authored
micropython support for Sound (#632)
Resolves #628
1 parent 56dc7ca commit bae544f

File tree

2 files changed

+92
-68
lines changed

2 files changed

+92
-68
lines changed

debian/changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
python-ev3dev2 (2.0.0~beta4) UNRELEASED; urgency=medium
22

33
[Daniel Walton]
4+
* micropython Sound support
45
* micropython support for LED animations
56
* StopWatch class
67
* Avoid race condition due to poll(None)

ev3dev2/sound.py

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

31+
from ev3dev2 import is_micropython
3132
import os
3233
import re
33-
import shlex
34-
from subprocess import check_output, Popen, PIPE
34+
35+
if not is_micropython():
36+
import shlex
37+
from subprocess import Popen, PIPE
3538

3639

3740
def _make_scales(notes):
@@ -44,15 +47,34 @@ def _make_scales(notes):
4447
return res
4548

4649

50+
51+
def get_command_processes(command):
52+
"""
53+
:param string command: a string of command(s) to run that may include pipes
54+
:return: a list of Popen objects
55+
"""
56+
57+
# We must split command into sub-commands to support pipes
58+
if "|" in command:
59+
command_parts = command.split("|")
60+
else:
61+
command_parts = [command]
62+
63+
processes = []
64+
65+
for command_part in command_parts:
66+
if processes:
67+
processes.append(Popen(shlex.split(command_part), stdin=processes[-1].stdout, stdout=PIPE, stderr=PIPE))
68+
else:
69+
processes.append(Popen(shlex.split(command_part), stdin=None, stdout=PIPE, stderr=PIPE))
70+
71+
return processes
72+
73+
4774
class Sound(object):
4875
"""
4976
Support beep, play wav files, or convert text to speech.
5077
51-
Note that all methods of the class spawn system processes and return
52-
subprocess.Popen objects. The methods are asynchronous (they return
53-
immediately after child process was spawned, without waiting for its
54-
completion), but you can call wait() on the returned result.
55-
5678
Examples::
5779
5880
# Play 'bark.wav':
@@ -92,6 +114,46 @@ def _validate_play_type(self, play_type):
92114
assert play_type in self.PLAY_TYPES, \
93115
"Invalid play_type %s, must be one of %s" % (play_type, ','.join(str(t) for t in self.PLAY_TYPES))
94116

117+
def _audio_command(self, command, play_type):
118+
if is_micropython():
119+
120+
if play_type == Sound.PLAY_WAIT_FOR_COMPLETE:
121+
os.system(command)
122+
123+
elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE:
124+
os.system('{} &'.format(command))
125+
126+
elif play_type == Sound.PLAY_LOOP:
127+
while True:
128+
os.system(command)
129+
130+
else:
131+
raise Exception("invalid play_type " % play_type)
132+
133+
return None
134+
135+
else:
136+
with open(os.devnull, 'w') as n:
137+
138+
if play_type == Sound.PLAY_WAIT_FOR_COMPLETE:
139+
processes = get_command_processes(command)
140+
processes[-1].communicate()
141+
processes[-1].wait()
142+
return None
143+
144+
elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE:
145+
processes = get_command_processes(command)
146+
return processes[-1]
147+
148+
elif play_type == Sound.PLAY_LOOP:
149+
while True:
150+
processes = get_command_processes(command)
151+
processes[-1].communicate()
152+
processes[-1].wait()
153+
154+
else:
155+
raise Exception("invalid play_type " % play_type)
156+
95157
def beep(self, args='', play_type=PLAY_WAIT_FOR_COMPLETE):
96158
"""
97159
Call beep command with the provided arguments (if any).
@@ -102,19 +164,12 @@ def beep(self, args='', play_type=PLAY_WAIT_FOR_COMPLETE):
102164
:param play_type: The behavior of ``beep`` once playback has been initiated
103165
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE``
104166
105-
:return: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
167+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
106168
107169
.. _`beep man page`: https://linux.die.net/man/1/beep
108170
.. _`linux beep music`: https://www.google.com/search?q=linux+beep+music
109171
"""
110-
with open(os.devnull, 'w') as n:
111-
subprocess = Popen(shlex.split('/usr/bin/beep %s' % args), stdout=n)
112-
if play_type == Sound.PLAY_WAIT_FOR_COMPLETE:
113-
subprocess.wait()
114-
return None
115-
else:
116-
return subprocess
117-
172+
return self._audio_command("/usr/bin/beep %s" % args, play_type)
118173

119174
def tone(self, *args, play_type=PLAY_WAIT_FOR_COMPLETE):
120175
"""
@@ -154,7 +209,7 @@ def tone(self, *args, play_type=PLAY_WAIT_FOR_COMPLETE):
154209
:param play_type: The behavior of ``tone`` once playback has been initiated
155210
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE``
156211
157-
:return: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
212+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
158213
159214
.. rubric:: tone(frequency, duration)
160215
@@ -166,7 +221,7 @@ def tone(self, *args, play_type=PLAY_WAIT_FOR_COMPLETE):
166221
:param play_type: The behavior of ``tone`` once playback has been initiated
167222
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_NO_WAIT_FOR_COMPLETE``
168223
169-
:return: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
224+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
170225
"""
171226
def play_tone_sequence(tone_sequence):
172227
def beep_args(frequency=None, duration=None, delay=None):
@@ -201,7 +256,7 @@ def play_tone(self, frequency, duration, delay=0.0, volume=100,
201256
:param play_type: The behavior of ``play_tone`` once playback has been initiated
202257
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP``
203258
204-
:return: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the PID of the underlying beep command; ``None`` otherwise
259+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the PID of the underlying beep command; ``None`` otherwise
205260
206261
:raises ValueError: if invalid parameter
207262
"""
@@ -231,7 +286,7 @@ def play_note(self, note, duration, volume=100, play_type=PLAY_WAIT_FOR_COMPLETE
231286
:param play_type: The behavior of ``play_note`` once playback has been initiated
232287
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP``
233288
234-
:return: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the PID of the underlying beep command; ``None`` otherwise
289+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the PID of the underlying beep command; ``None`` otherwise
235290
236291
:raises ValueError: is invalid parameter (note, duration,...)
237292
"""
@@ -257,34 +312,20 @@ def play_file(self, wav_file, volume=100, play_type=PLAY_WAIT_FOR_COMPLETE):
257312
:param play_type: The behavior of ``play_file`` once playback has been initiated
258313
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP``
259314
260-
:returns: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
315+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
261316
"""
262317
if not 0 < volume <= 100:
263318
raise ValueError('invalid volume (%s)' % volume)
264319

265320
if not wav_file.endswith(".wav"):
266321
raise ValueError('invalid sound file (%s), only .wav files are supported' % wav_file)
267322

268-
if not os.path.isfile(wav_file):
323+
if not os.path.exists(wav_file):
269324
raise ValueError("%s does not exist" % wav_file)
270325

271-
self.set_volume(volume)
272326
self._validate_play_type(play_type)
273-
274-
with open(os.devnull, 'w') as n:
275-
276-
if play_type == Sound.PLAY_WAIT_FOR_COMPLETE:
277-
pid = Popen(shlex.split('/usr/bin/aplay -q "%s"' % wav_file), stdout=n)
278-
pid.wait()
279-
280-
# Do not wait, run in the background
281-
elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE:
282-
return Popen(shlex.split('/usr/bin/aplay -q "%s"' % wav_file), stdout=n)
283-
284-
elif play_type == Sound.PLAY_LOOP:
285-
while True:
286-
pid = Popen(shlex.split('/usr/bin/aplay -q "%s"' % wav_file), stdout=n)
287-
pid.wait()
327+
self.set_volume(volume)
328+
return self._audio_command('/usr/bin/aplay -q "%s"' % wav_file, play_type)
288329

289330
def speak(self, text, espeak_opts='-a 200 -s 130', volume=100, play_type=PLAY_WAIT_FOR_COMPLETE):
290331
""" Speak the given text aloud.
@@ -298,33 +339,16 @@ def speak(self, text, espeak_opts='-a 200 -s 130', volume=100, play_type=PLAY_WA
298339
:param play_type: The behavior of ``speak`` once playback has been initiated
299340
:type play_type: ``Sound.PLAY_WAIT_FOR_COMPLETE``, ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` or ``Sound.PLAY_LOOP``
300341
301-
:returns: When ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
342+
:return: When python3 is used and ``Sound.PLAY_NO_WAIT_FOR_COMPLETE`` is specified, returns the spawn subprocess from ``subprocess.Popen``; ``None`` otherwise
302343
"""
303344
self._validate_play_type(play_type)
304345
self.set_volume(volume)
305-
306-
with open(os.devnull, 'w') as n:
307-
cmd_line = ['/usr/bin/espeak', '--stdout'] + shlex.split(espeak_opts) + [shlex.quote(text)]
308-
aplay_cmd_line = shlex.split('/usr/bin/aplay -q')
309-
310-
if play_type == Sound.PLAY_WAIT_FOR_COMPLETE:
311-
espeak = Popen(cmd_line, stdout=PIPE)
312-
play = Popen(aplay_cmd_line, stdin=espeak.stdout, stdout=n)
313-
play.wait()
314-
315-
elif play_type == Sound.PLAY_NO_WAIT_FOR_COMPLETE:
316-
espeak = Popen(cmd_line, stdout=PIPE)
317-
return Popen(aplay_cmd_line, stdin=espeak.stdout, stdout=n)
318-
319-
elif play_type == Sound.PLAY_LOOP:
320-
while True:
321-
espeak = Popen(cmd_line, stdout=PIPE)
322-
play = Popen(aplay_cmd_line, stdin=espeak.stdout, stdout=n)
323-
play.wait()
346+
cmd = "/usr/bin/espeak --stdout %s '%s' | /usr/bin/aplay -q" % (espeak_opts, text)
347+
return self._audio_command(cmd, play_type)
324348

325349
def _get_channel(self):
326350
"""
327-
:returns: The detected sound channel
351+
:return: The detected sound channel
328352
:rtype: string
329353
"""
330354
if self.channel is None:
@@ -334,10 +358,10 @@ def _get_channel(self):
334358
#
335359
# Simple mixer control 'Master',0
336360
# Simple mixer control 'Capture',0
337-
out = check_output(['amixer', 'scontrols']).decode()
338-
m = re.search(r"'(?P<channel>[^']+)'", out)
361+
out = os.popen('/usr/bin/amixer scontrols').read()
362+
m = re.search(r"'([^']+)'", out)
339363
if m:
340-
self.channel = m.group('channel')
364+
self.channel = m.group(1)
341365
else:
342366
self.channel = 'Playback'
343367

@@ -355,8 +379,7 @@ def set_volume(self, pct, channel=None):
355379
if channel is None:
356380
channel = self._get_channel()
357381

358-
cmd_line = '/usr/bin/amixer -q set {0} {1:d}%'.format(channel, pct)
359-
Popen(shlex.split(cmd_line)).wait()
382+
os.system('/usr/bin/amixer -q set {0} {1:d}%'.format(channel, pct))
360383

361384
def get_volume(self, channel=None):
362385
"""
@@ -370,10 +393,10 @@ def get_volume(self, channel=None):
370393
if channel is None:
371394
channel = self._get_channel()
372395

373-
out = check_output(['amixer', 'get', channel]).decode()
374-
m = re.search(r'\[(?P<volume>\d+)%\]', out)
396+
out = os.popen(['/usr/bin/amixer', 'get', channel]).read()
397+
m = re.search(r'\[(\d+)%\]', out)
375398
if m:
376-
return int(m.group('volume'))
399+
return int(m.group(1))
377400
else:
378401
raise Exception('Failed to parse output of `amixer get {}`'.format(channel))
379402

@@ -436,7 +459,7 @@ def play_song(self, song, tempo=120, delay=0.05):
436459
:param int tempo: the song tempo, given in quarters per minute
437460
:param float delay: delay between notes (in seconds)
438461
439-
:return: the spawn subprocess from ``subprocess.Popen``
462+
:return: When python3 is used the spawn subprocess from ``subprocess.Popen`` is returned; ``None`` otherwise
440463
441464
:raises ValueError: if invalid note in song or invalid play parameters
442465
"""

0 commit comments

Comments
 (0)