]> vault307.fbx.one Git - Sensory_Wall.git/blob - adafruit_soundboard.py
more sensory wall projects
[Sensory_Wall.git] / adafruit_soundboard.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2017 Mike Mabey
4 #
5 # Permission is hereby granted, free of charge, to any person obtaining a copy
6 # of this software and associated documentation files (the "Software"), to deal
7 # in the Software without restriction, including without limitation the rights
8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 # copies of the Software, and to permit persons to whom the Software is
10 # furnished to do so, subject to the following conditions:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 # THE SOFTWARE.
22 """
23 A MicroPython library for the Adafruit Sound Boards in UART mode!
24
25 This library has been adapted from the library written by Adafruit for Arduino,
26 available at https://github.com/adafruit/Adafruit_Soundboard_library.
27
28 * Author(s): Mike Mabey
29 """
30
31 from machine import UART, Pin
32 from sys import platform as BOARD
33
34 __author__ = 'Mike Mabey'
35 __license__ = 'MIT'
36 __copyright_ = 'Copyright 2017, Mike Mabey'
37
38 ESP8266 = 'esp8266'
39 PYBOARD = 'pyboard'
40 WIPY = 'wipy'
41
42 try:
43 # Should be supported by all 3 supported boards
44 from utime import sleep_ms
45 except ImportError:
46 _supported = ', '.join([ESP8266, PYBOARD, WIPY])
47 raise OSError('Unsupported board. Currently only works with {}.'.format(_supported))
48
49 SB_BAUD = 9600 # Baud rate of the sound boards
50 MIN_VOL = 0
51 MAX_VOL = 204
52 MAX_FILES = 25
53 DEBUG = False
54
55
56 class Soundboard:
57 """Control an Adafruit Sound Board via UART.
58
59 The :class:`Soundboard` class handles all communication with the sound
60 board via :ref:`UART <upy:machine.UART>`, making it easy to get
61 information about the sound files on the sound board and control playback.
62
63 If you need to reset the sound board from your MicroPython code, be
64 sure to provide the ``rst_pin`` parameter. The sound board sometimes gets
65 out of UART mode and reverts to the factory default of GPIO trigger
66 mode. When this happens, it will appear as if the sound board has
67 stoped working for no apparent reason. This library is designed to
68 automatically attempt resetting the board if a command fails, since
69 that is a common cause. So, it is a good idea to provide this
70 parameter.
71 """
72
73 def __init__(self, uart_id, rst_pin=None, vol=None, alt_get_files=False, debug=None, **uart_kwargs):
74 """
75 :param uart_id: ID for the :ref:`UART <upy:machine.UART>` bus to use.
76 Acceptable values vary by board. Check the documentation for your
77 board for more info.
78 :param rst_pin: Identifier for the pin (on the MicroPython board)
79 connected to the ``RST`` pin of the sound board. Valid identifiers
80 vary by board.
81 :param vol: Initial volume level to set. See :attr:`vol` for more info.
82 :type vol: int or float
83 :param bool alt_get_files: Uses an alternate method to get the list of
84 track file names. See :meth:`use_alt_get_files` method for more
85 info.
86 :param bool debug: When not None, will set the debug output flag to the
87 boolean value of this argument using the :meth:`toggle_debug`
88 method.
89 :param dict uart_kwargs: Additional values passed to the
90 :ref:`UART.init() <upy:machine.UART>` method of the UART bus object.
91 Acceptable values here also vary by board. It is not necessary to
92 include the baud rate among these keyword values, because it will
93 be set to ``SB_BAUD`` before the ``UART.init`` function is called.
94 """
95 if debug is not None:
96 self.toggle_debug(bool(debug))
97
98 self._uart = UART(uart_id, SB_BAUD)
99 uart_kwargs['baudrate'] = SB_BAUD
100 self._uart.init(**uart_kwargs)
101 self._files = None
102 self._sizes = None
103 self._lengths = None
104 self._track = {}
105
106 self._cur_vol = None
107 self._cur_track = None
108 self._reset_attempted = False
109
110 # Setup reset pin
111 if BOARD == PYBOARD:
112 self._sb_rst = None if rst_pin is None else Pin(rst_pin, mode=Pin.OPEN_DRAIN, value=1)
113 else:
114 self._sb_rst = None if rst_pin is None else Pin(rst_pin, mode=Pin.IN)
115
116 self.vol = vol
117
118 if alt_get_files:
119 self.use_alt_get_files()
120
121 def _flush_uart_input(self):
122 """Read any available data from the UART bus until none is left."""
123 while self._uart.any():
124 self._uart.read()
125
126 def _send_simple(self, cmd, check=None, strip=True):
127 """Send the command, optionally do a check on the output.
128
129 The sound board understands the following commands:
130
131 - ``L``: List files on the board
132 - ``#``: Play a file by number
133 - ``P``: Play a file by name
134 - ``+``: Volume up (range is 0-204, increments of 2)
135 - ``-``: Volume down
136 - ``=``: Pause playback
137 - ``>``: Un-pause playback
138 - ``q``: Stop playback
139 - ``t``: Give current position of playback and total time of track
140 - ``s``: Current track size and total size
141
142 :param cmd: Command to send over the UART bus. A newline character will
143 be appended to the command before sending it, so it's not necessary
144 to include one as part of the command.
145 :type cmd: str or bytes
146 :param check: Depending on the type of `check`, has three different
147 behaviors. When None (default), the return value will be whatever
148 the output from the command was. When a str or bytes, the return
149 value will be True/False, indicating whether the command output
150 starts with the value in `check`. When it otherwise evaluates to
151 True, return value will be True/False, indicating the output
152 started with the first character in `cmd`.
153 :type check: str or bytes or None or bool
154 :return: Varies depending on the value of `check`.
155 :rtype: bytes or bool
156 """
157 self._flush_uart_input()
158 cmd = cmd.strip() # Make sure there's not more than one newline
159 self._uart.write('{}\n'.format(cmd))
160 if len(cmd) > 1:
161 # We need to gobble the return when there's more than one character in the command
162 self._uart.readline()
163 try:
164 msg = self._uart.readline()
165 if strip:
166 msg = msg.strip()
167 assert isinstance(msg, bytes)
168 printif(msg)
169 except (AttributeError, AssertionError):
170 if self._reset_attempted:
171 # Only try resetting once
172 return False # TODO: Better way to handle failed commands? Too broad?
173 printif('Got back None from a command. Attempting to restart the board to put it in UART mode.')
174 self._reset_attempted = True
175 self.reset()
176 return self._send_simple(cmd, check)
177
178 if check is None:
179 return msg
180 else:
181 self._reset_attempted = True # We already sent a command successfully
182
183 if isinstance(check, str):
184 return msg.startswith(check.encode())
185 elif isinstance(check, bytes):
186 return msg.startswith(check)
187 elif check:
188 return msg.startswith(cmd[0].encode())
189
190 @property
191 def files(self):
192 """Return a ``list`` of the files on the sound board.
193
194 :rtype: list
195 """
196 if self._files is None:
197 self._get_files()
198 return self._files
199
200 @property
201 def sizes(self):
202 """Return a ``list`` of the files' sizes on the sound board.
203
204 .. seealso:: :meth:`use_alt_get_files`
205
206 :rtype: list
207 """
208 if self._sizes is None:
209 self._get_files()
210 return self._sizes
211
212 def _get_files(self):
213 """Ask the board for the files and their sizes, store the results."""
214 self._flush_uart_input()
215
216 self._files = []
217 self._sizes = []
218 self._uart.write('L\n')
219 sleep_ms(10)
220 i = 0
221 while self._uart.any():
222 msg = self._uart.readline().strip()
223 printif(msg)
224 fname, fsize = msg.split(b'\t')
225 fname = fname.decode()
226 self._files.append(fname)
227 self._sizes.append(int(fsize))
228 self._track[fname] = i
229 i += 1
230
231 def _get_files_alt(self):
232 """Play every track, get info from feedback."""
233 vol = self.vol
234 self.vol = 0
235 self._files = []
236 self._lengths = []
237 self._sizes = []
238 for i in range(MAX_FILES):
239 self.stop()
240 msg = self._send_simple('#{}'.format(i))
241 if msg.startswith(b'NoFile'):
242 # Playing track i failed, it must not be a valid track number
243 break
244 play, track_num, fname = msg.split(b'\t')
245 self._files.append(fname)
246 self._track[fname] = i
247
248 sleep_ms(50)
249 sec = self.track_time()
250 if sec:
251 self._lengths.append(sec[1])
252 else:
253 self._lengths.append(0)
254 size = self.track_size()
255 if size:
256 self._sizes.append(size[1])
257 else:
258 self._sizes.append(0)
259
260 self.vol = vol
261
262 @property
263 def lengths(self):
264 """Return a ``list`` of the track lengths in seconds.
265
266 .. note::
267
268 In my own testing of this method, the board always returns a value
269 of zero seconds for the length for every track, no matter if it's a
270 WAV or OGG file, short or long track.
271
272 :rtype: list
273 """
274 if self._lengths is None:
275 self._get_lengths()
276 return self._lengths
277
278 def _get_lengths(self):
279 """Store the length of each track."""
280 self._get_files_alt()
281
282 def file_name(self, n):
283 """Return the name of track ``n``.
284
285 :param int n: Index of a file on the sound board or ``False`` if the
286 track number doesn't exist.
287 :return: Filename of track ``n``.
288 :rtype: str or bool
289 """
290 try:
291 return self.files[n]
292 except IndexError:
293 return False
294
295 def track_num(self, file_name):
296 """Return the track number of the given file name.
297
298 :param str file_name: File name of the track. Should be one of the
299 values from the :attr:`files` property.
300 :return: The track number of the file name or ``False`` if not found.
301 :rtype: int or bool
302 """
303 try:
304 return self._track[file_name]
305 except KeyError:
306 return False
307
308 def play(self, track=None):
309 """Play a track on the board.
310
311 :param track: The index (``int``) or filename (``str``) of the track to
312 play.
313 :type track: int or str
314 :return: If the command was successful.
315 :rtype: bool
316 """
317 if isinstance(track, int):
318 cmd = '#'
319 num = track
320 elif isinstance(track, str):
321 cmd = 'P'
322 num = self.track_num(track)
323 else:
324 raise TypeError('You must specify a track by its number (int) or its name (str)')
325
326 if self._send_simple('{}{}'.format(cmd, track), 'play'):
327 self._cur_track = num
328 return True
329 return False
330
331 def play_now(self, track):
332 """Play a track on the board now, stopping current track if necessary.
333
334 :param track: The index (``int``) or filename (``str``) of the track to
335 play.
336 :type track: int or str
337 :return: If the command was successful.
338 :rtype: bool
339 """
340 self.stop()
341 if not self.play(track):
342 # Playing the specified track failed, so just return False
343 return False
344 return True
345
346 @property
347 def vol(self):
348 """Current volume.
349
350 This is implemented as a class property, so you can get and set its
351 value directly. When setting a new volume, you can use an ``int`` or a
352 ``float`` (assuming your board supports floats). When setting to an
353 ``int``, it should be in the range of 0-204. When set to a ``float``,
354 the value will be interpreted as a percentage of :obj:`MAX_VOL`.
355
356 :rtype: int
357 """
358 if self._cur_vol is None:
359 self.vol_down()
360 return self._cur_vol
361
362 @vol.setter
363 def vol(self, new_vol):
364 if new_vol is None:
365 return
366 if isinstance(new_vol, float):
367 new_vol = int(new_vol * MAX_VOL)
368 if not isinstance(new_vol, int):
369 printif('Invalid volume level. Try giving an int.')
370 return
371 elif new_vol > self.vol:
372 self.vol_up(new_vol)
373 elif new_vol < self.vol:
374 self.vol_down(new_vol)
375
376 def vol_up(self, vol=None):
377 """Turn volume up by 2 points, return current volume level [0-204].
378
379 :param int vol: Target volume. When not ``None``, volume will be turned
380 up to be greater than or equal to this value.
381 :rtype: int
382 """
383 global DEBUG
384 printif('Turning volume up')
385 if vol is None:
386 self._cur_vol = int(self._send_simple('+'))
387 return self._cur_vol
388 if vol > MAX_VOL:
389 printif('{} is above maximum volume. Setting to {} instead.'.format(vol, MAX_VOL))
390 vol = MAX_VOL
391 self._cur_vol = MIN_VOL - 1
392 db = DEBUG
393 DEBUG = False
394 while vol > self._cur_vol:
395 self._cur_vol = int(self._send_simple('+'))
396 DEBUG = db
397 return self._cur_vol
398
399 def vol_down(self, vol=None):
400 """Turn volume down by 2 points, return current volume level [0-204].
401
402 :param int vol: Target volume. When not ``None``, volume will be turned
403 down to be less than or equal to this value.
404 :rtype: int
405 """
406 global DEBUG
407 printif('Turning volume down')
408 if vol is None:
409 self._cur_vol = int(self._send_simple('-'))
410 return self._cur_vol
411 self._cur_vol = MAX_VOL + 1
412 if vol < MIN_VOL:
413 printif('{} is below minimum volume. Setting to {} instead.'.format(vol, MIN_VOL))
414 vol = MIN_VOL
415 db = DEBUG
416 DEBUG = False
417 while vol < self._cur_vol:
418 self._cur_vol = int(self._send_simple('-'))
419 DEBUG = db
420 return self._cur_vol
421
422 def pause(self):
423 """Pause playback, return if the command was successful.
424
425 :rtype: bool
426 """
427 return self._send_simple('=', True)
428
429 def unpause(self):
430 """Continue playback, return if the command was successful.
431
432 :rtype: bool
433 """
434 return self._send_simple('>', True)
435
436 def stop(self):
437 """Stop playback, return if the command was successful.
438
439 :rtype: bool
440 """
441 return self._send_simple('q', True)
442
443 def track_time(self):
444 """Return the current position of playback and total time of track.
445
446 :rtype: tuple
447 """
448 msg = self._send_simple('t')
449 if not msg:
450 return -1, -1
451 printif(len(msg))
452 if len(msg) != 11:
453 return False
454 current, total = msg.decode('utf-8').split(':')
455 return int(current), int(total)
456
457 def track_size(self):
458 """Return the remaining size and total size.
459
460 It seems the remaining track size refers to the number of bytes left
461 for the sound board to process before the playing of the track will be
462 over.
463
464 :return: Remaining track size and total size
465 :rtype: tuple
466 """
467 msg = self._send_simple('s')
468 if not msg:
469 return -1, -1
470 printif(len(msg))
471 if len(msg) != 21:
472 return False
473 remaining, total = msg.decode('utf-8').split('/')
474 return int(remaining), int(total)
475
476 def reset(self):
477 """Reset the sound board.
478
479 Soft reset the board by bringing the ``RST`` pin low momentarily (10
480 ms). This only has effect if the reset pin has been initialized in the
481 constructor.
482
483 Doing a soft reset on the board before doing any other actions can help
484 ensure that it has been started in UART control mode, rather than GPIO
485 trigger mode.
486
487 .. seealso::
488
489 `Soundboard Pinout <https://learn.adafruit.com/adafruit-audio-fx-sound-board/pinouts#uart-pins>`_
490 Documentation on the sound boards' pinouts.
491
492
493 :return: Whether the reset was successful. If the reset pin was not
494 initialized in the constructor, this will always return ``False``.
495 :rtype: bool
496 """
497 if self._sb_rst is None:
498 # Don't attempt to restart the board if the reset pin wasn't initialized
499 return False
500
501 if BOARD == PYBOARD:
502 self._sb_rst(0)
503 sleep_ms(10)
504 self._sb_rst(1)
505 else:
506 # Pin should already be in IN mode. This allows us to set the value we want to send, then switch to OUT mode
507 # just long enough for the value to take effect, and finally switch back to IN mode.
508 # self._sb_rst.value(0)
509 self._sb_rst(0)
510 self._sb_rst.mode(Pin.OUT)
511 sleep_ms(10)
512 self._sb_rst.mode(Pin.IN)
513
514 sleep_ms(1000) # Give the board some time to boot
515 msg = self._uart.readline().strip()
516 printif(msg) # Blank line
517
518 msg = self._uart.readline().strip()
519 printif(msg) # Date and name
520
521 if not msg.startswith('Adafruit FX Sound Board'):
522 return False
523
524 sleep_ms(250)
525
526 msg = self._uart.readline().strip()
527 printif(msg) # FAT type
528
529 msg = self._uart.readline().strip()
530 printif(msg) # Number of files
531
532 # Reset volume level and current track
533 self.vol = self._cur_vol
534 self._cur_track = None
535
536 return True
537
538 def use_alt_get_files(self, now=False):
539 """Get list of track files using an alternate method.
540
541 If the list of files is missing tracks you know are on the sound board,
542 try calling this method. It doesn't depend on the sound board's internal
543 command for returning a list of files. Instead, it plays each of the
544 tracks using their track numbers and gets the filename and size from
545 the output of the play command.
546
547 :param bool now: When set to ``True``, the alternate method of getting
548 the files list will be called immediately. Otherwise, the list of
549 files will be populated the next time the :attr:`files` property is
550 accessed (lazy loading).
551 :rtype: None
552 """
553 self._get_files = self._get_files_alt
554 if now:
555 self._get_files()
556
557 @staticmethod
558 def toggle_debug(debug=None):
559 """Turn on/off :obj:`DEBUG` flag.
560
561 :param debug: If None, the :obj:`DEBUG` flag will be toggled to have the
562 value opposite of its current value. Otherwise, :obj:`DEBUG` will be
563 set to the boolean value of ``debug``.
564 :rtype: None
565 """
566 global DEBUG
567 if debug is None:
568 DEBUG = not DEBUG
569 else:
570 DEBUG = bool(debug)
571
572
573 def printif(*values, **kwargs):
574 """Print a message if :obj:`DEBUG` is set to ``True``."""
575 print(*values, **kwargs) if DEBUG else None