]>
vault307.fbx.one Git - Sensory_Wall.git/blob - adafruit_soundboard.py
1 # The MIT License (MIT)
3 # Copyright (c) 2017 Mike Mabey
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:
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
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
23 A MicroPython library for the Adafruit Sound Boards in UART mode!
25 This library has been adapted from the library written by Adafruit for Arduino,
26 available at https://github.com/adafruit/Adafruit_Soundboard_library.
28 * Author(s): Mike Mabey
31 from machine
import UART
, Pin
32 from sys
import platform
as BOARD
34 __author__
= 'Mike Mabey'
36 __copyright_
= 'Copyright 2017, Mike Mabey'
43 # Should be supported by all 3 supported boards
44 from utime
import sleep_ms
46 _supported
= ', '.join([ESP8266
, PYBOARD
, WIPY
])
47 raise OSError('Unsupported board. Currently only works with {}.'.format(_supported
))
49 SB_BAUD
= 9600 # Baud rate of the sound boards
57 """Control an Adafruit Sound Board via UART.
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.
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
73 def __init__(self
, uart_id
, rst_pin
=None, vol
=None, alt_get_files
=False, debug
=None, **uart_kwargs
):
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
78 :param rst_pin: Identifier for the pin (on the MicroPython board)
79 connected to the ``RST`` pin of the sound board. Valid identifiers
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
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`
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.
96 self
.toggle_debug(bool(debug
))
98 self
._uart
= UART(uart_id
, SB_BAUD
)
99 uart_kwargs
['baudrate'] = SB_BAUD
100 self
._uart
.init(**uart_kwargs
)
107 self
._cur
_track
= None
108 self
._reset
_attempted
= False
112 self
._sb
_rst
= None if rst_pin
is None else Pin(rst_pin
, mode
=Pin
.OPEN_DRAIN
, value
=1)
114 self
._sb
_rst
= None if rst_pin
is None else Pin(rst_pin
, mode
=Pin
.IN
)
119 self
.use_alt_get_files()
121 def _flush_uart_input(self
):
122 """Read any available data from the UART bus until none is left."""
123 while self
._uart
.any():
126 def _send_simple(self
, cmd
, check
=None, strip
=True):
127 """Send the command, optionally do a check on the output.
129 The sound board understands the following commands:
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)
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
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
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
))
161 # We need to gobble the return when there's more than one character in the command
162 self
._uart
.readline()
164 msg
= self
._uart
.readline()
167 assert isinstance(msg
, bytes)
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
176 return self
._send
_simple
(cmd
, check
)
181 self
._reset
_attempted
= True # We already sent a command successfully
183 if isinstance(check
, str):
184 return msg
.startswith(check
.encode())
185 elif isinstance(check
, bytes):
186 return msg
.startswith(check
)
188 return msg
.startswith(cmd
[0].encode())
192 """Return a ``list`` of the files on the sound board.
196 if self
._files
is None:
202 """Return a ``list`` of the files' sizes on the sound board.
204 .. seealso:: :meth:`use_alt_get_files`
208 if self
._sizes
is None:
212 def _get_files(self
):
213 """Ask the board for the files and their sizes, store the results."""
214 self
._flush
_uart
_input
()
218 self
._uart
.write('L\n')
221 while self
._uart
.any():
222 msg
= self
._uart
.readline().strip()
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
231 def _get_files_alt(self
):
232 """Play every track, get info from feedback."""
238 for i
in range(MAX_FILES
):
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
244 play
, track_num
, fname
= msg
.split(b
'\t')
245 self
._files
.append(fname
)
246 self
._track
[fname
] = i
249 sec
= self
.track_time()
251 self
._lengths
.append(sec
[1])
253 self
._lengths
.append(0)
254 size
= self
.track_size()
256 self
._sizes
.append(size
[1])
258 self
._sizes
.append(0)
264 """Return a ``list`` of the track lengths in seconds.
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.
274 if self
._lengths
is None:
278 def _get_lengths(self
):
279 """Store the length of each track."""
280 self
._get
_files
_alt
()
282 def file_name(self
, n
):
283 """Return the name of track ``n``.
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``.
295 def track_num(self
, file_name
):
296 """Return the track number of the given file name.
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.
304 return self
._track
[file_name
]
308 def play(self
, track
=None):
309 """Play a track on the board.
311 :param track: The index (``int``) or filename (``str``) of the track to
313 :type track: int or str
314 :return: If the command was successful.
317 if isinstance(track
, int):
320 elif isinstance(track
, str):
322 num
= self
.track_num(track
)
324 raise TypeError('You must specify a track by its number (int) or its name (str)')
326 if self
._send
_simple
('{}{}'.format(cmd
, track
), 'play'):
327 self
._cur
_track
= num
331 def play_now(self
, track
):
332 """Play a track on the board now, stopping current track if necessary.
334 :param track: The index (``int``) or filename (``str``) of the track to
336 :type track: int or str
337 :return: If the command was successful.
341 if not self
.play(track
):
342 # Playing the specified track failed, so just return False
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`.
358 if self
._cur
_vol
is None:
363 def vol(self
, new_vol
):
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.')
371 elif new_vol
> self
.vol
:
373 elif new_vol
< self
.vol
:
374 self
.vol_down(new_vol
)
376 def vol_up(self
, vol
=None):
377 """Turn volume up by 2 points, return current volume level [0-204].
379 :param int vol: Target volume. When not ``None``, volume will be turned
380 up to be greater than or equal to this value.
384 printif('Turning volume up')
386 self
._cur
_vol
= int(self
._send
_simple
('+'))
389 printif('{} is above maximum volume. Setting to {} instead.'.format(vol
, MAX_VOL
))
391 self
._cur
_vol
= MIN_VOL
- 1
394 while vol
> self
._cur
_vol
:
395 self
._cur
_vol
= int(self
._send
_simple
('+'))
399 def vol_down(self
, vol
=None):
400 """Turn volume down by 2 points, return current volume level [0-204].
402 :param int vol: Target volume. When not ``None``, volume will be turned
403 down to be less than or equal to this value.
407 printif('Turning volume down')
409 self
._cur
_vol
= int(self
._send
_simple
('-'))
411 self
._cur
_vol
= MAX_VOL
+ 1
413 printif('{} is below minimum volume. Setting to {} instead.'.format(vol
, MIN_VOL
))
417 while vol
< self
._cur
_vol
:
418 self
._cur
_vol
= int(self
._send
_simple
('-'))
423 """Pause playback, return if the command was successful.
427 return self
._send
_simple
('=', True)
430 """Continue playback, return if the command was successful.
434 return self
._send
_simple
('>', True)
437 """Stop playback, return if the command was successful.
441 return self
._send
_simple
('q', True)
443 def track_time(self
):
444 """Return the current position of playback and total time of track.
448 msg
= self
._send
_simple
('t')
454 current
, total
= msg
.decode('utf-8').split(':')
455 return int(current
), int(total
)
457 def track_size(self
):
458 """Return the remaining size and total size.
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
464 :return: Remaining track size and total size
467 msg
= self
._send
_simple
('s')
473 remaining
, total
= msg
.decode('utf-8').split('/')
474 return int(remaining
), int(total
)
477 """Reset the sound board.
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
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
489 `Soundboard Pinout <https://learn.adafruit.com/adafruit-audio-fx-sound-board/pinouts#uart-pins>`_
490 Documentation on the sound boards' pinouts.
493 :return: Whether the reset was successful. If the reset pin was not
494 initialized in the constructor, this will always return ``False``.
497 if self
._sb
_rst
is None:
498 # Don't attempt to restart the board if the reset pin wasn't initialized
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)
510 self
._sb
_rst
.mode(Pin
.OUT
)
512 self
._sb
_rst
.mode(Pin
.IN
)
514 sleep_ms(1000) # Give the board some time to boot
515 msg
= self
._uart
.readline().strip()
516 printif(msg
) # Blank line
518 msg
= self
._uart
.readline().strip()
519 printif(msg
) # Date and name
521 if not msg
.startswith('Adafruit FX Sound Board'):
526 msg
= self
._uart
.readline().strip()
527 printif(msg
) # FAT type
529 msg
= self
._uart
.readline().strip()
530 printif(msg
) # Number of files
532 # Reset volume level and current track
533 self
.vol
= self
._cur
_vol
534 self
._cur
_track
= None
538 def use_alt_get_files(self
, now
=False):
539 """Get list of track files using an alternate method.
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.
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).
553 self
._get
_files
= self
._get
_files
_alt
558 def toggle_debug(debug
=None):
559 """Turn on/off :obj:`DEBUG` flag.
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``.
573 def printif(*values
, **kwargs
):
574 """Print a message if :obj:`DEBUG` is set to ``True``."""
575 print(*values
, **kwargs
) if DEBUG
else None