Skip to content

Commit

Permalink
docs: Add Sound Effects documentation. (#753)
Browse files Browse the repository at this point in the history
* docs: Add Sound Effects documentation.

* docs: Change proposed audio.Effect() to audio.SoundEffect()

* docs: Update the proposed default values for audio.SoundEffect().

* docs: Move the SoundEffects constants from `audio` to `audio.SoundEffect`.

* docs: Remove SoundEffect `preset` constructor parameter, and add `copy()` method.

* docs: Change SoundEffect() `interporlation` parameter to `shape`.

* docs: Remove mentions of SoundEffect presets, add ranges, example to file.
  • Loading branch information
microbit-carlos committed Sep 6, 2022
1 parent 77f6842 commit d591517
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 36 deletions.
221 changes: 185 additions & 36 deletions docs/audio.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,57 @@ Audio
This module allows you play sounds with the micro:bit.

By default sound output will be via the edge connector on pin 0 and the
:doc:`built-in speaker <speaker>` **V2**. You can connect wired headphones or
:doc:`built-in speaker <speaker>` (**V2**). You can connect wired headphones or
a speaker to pin 0 and GND on the edge connector to hear the sounds.

The ``audio`` module can be imported as ``import audio`` or accessed via
the ``microbit`` module as ``microbit.audio``.

There are three different kinds of audio sources that can be played using the
:py:meth:`audio.play` function:

1. `Built in sounds <#built-in-sounds-v2>`_ (**V2**),
e.g. ``audio.play(Sound.HAPPY)``
2. `Sound Effects <#sound-effects-v2>`_ (**V2**), a way to create custom sounds
by configuring its parameters::

my_effect = audio.SoundEffect(freq_start=400, freq_end=2500, duration=500)
audio.play(my_effect)

3. `Audio Frames <#audioframe>`_, an iterable (like a list or a generator)
of Audio Frames, which are lists of 32 samples with values from 0 to 255::

square_wave = audio.AudioFrame()
for i in range(16):
square_wave[i] = 0
square_wave[i + 16] = 255
audio.play([square_wave] * 64)


Functions
=========

.. py:function:: play(source, wait=True, pin=pin0, return_pin=None)
Play the source to completion.
Play the audio source to completion.

:param source: ``Sound``: The ``microbit`` module contains a list of
built-in sounds that your can pass to ``audio.play()``.
:param source: There are three types of data that can be used as a source:

- ``Sound``: The ``microbit`` module contains a list of
built-in sounds, e.g. ``audio.play(Sound.TWINKLE)``. A full list can
be found in the `Built in sounds <#built-in-sounds-v2>`_ section.
- ``SoundEffect``: A sound effect, or an iterable of sound effects,
created via the :py:meth:`audio.SoundEffect` class
- ``AudioFrame``: An iterable of ``AudioFrame`` instances as described
in the `AudioFrame Technical Details <#id2>`_ section

``AudioFrame``: The source agrument can also be an iterable
of ``AudioFrame`` elements as described below.
:param wait: If ``wait`` is ``True``, this function will block until the
source is exhausted.

:param pin: An optional argument to specify the output pin can be used to
override the default of ``pin0``. If we do not want any sound to play
we can use ``pin=None``.

:param return_pin: specifies a differential edge connector pin to connect
to an external speaker instead of ground. This is ignored for the **V2**
revision.
Expand All @@ -41,34 +69,9 @@ Functions
Stops all audio playback.

Classes
=======

.. py:class::
AudioFrame

An ``AudioFrame`` object is a list of 32 samples each of which is an unsigned byte
(whole number between 0 and 255).

It takes just over 4 ms to play a single frame.

.. py:function:: copyfrom(other)
Overwrite the data in this ``AudioFrame`` with the data from another
``AudioFrame`` instance.

:param other: ``AudioFrame`` instance from which to copy the data.


Using audio
===========

You will need a sound source, as input to the ``play`` function. You can use
the built-in sounds **V2** from the ``microbit`` module, ``microbit.Sound``, or
generate your own, like in ``examples/waveforms.py``.

Built-in sounds **V2**
----------------------
======================

The built-in sounds can be called using ``audio.play(Sound.NAME)``.

Expand All @@ -83,8 +86,154 @@ The built-in sounds can be called using ``audio.play(Sound.NAME)``.
* ``Sound.TWINKLE``
* ``Sound.YAWN``

Sounds Example
--------------

::

from microbit import *

while True:
if button_a.is_pressed() and button_b.is_pressed():
# When pressing both buttons only play via the edge connector
audio.play(Sound.HELLO, pin=pin0)
elif button_a.is_pressed():
# On button A play a sound and when it's done show an image
audio.play(Sound.HAPPY)
display.show(Image.HAPPY)
elif button_b.is_pressed():
# On button B play a sound and show an image at the same time
audio.play(Sound.TWINKLE, wait=False)
display.show(Image.BUTTERFLY)

sleep(500)
display.clear()


Sound Effects **V2**
====================

.. py:class::
SoundEffect(freq_start=500, freq_end=2500, duration=500, vol_start=255, vol_end=0, wave=WAVE_SQUARE, fx=FX_NONE, shape=SHAPE_LOG)

An ``SoundEffect`` instance represents a sound effect, composed by a set of
parameters configured via the constructor or attributes.

All the parameters are optional, with default values as shown above, and
they can all be modified via attributes of the same name. For example, we
can first create an effect ``my_effect = SoundEffect(duration=1000)``,
and then change its attributes ``my_effect.duration = 500``.

:param freq_start: Start frequency in Hertz (Hz), default: ``500``
:param freq_end: End frequency in Hertz (Hz), default: ``2500``
:param duration: Duration of the sound (ms), default: ``500``
:param vol_start: Start volume value, range 0-255, default: ``255``
:param vol_end: End volume value, range 0-255, default: ``0``
:param wave: Type of wave shape, one of these values: ``WAVE_SINE``,
``WAVE_SAWTOOTH``, ``WAVE_TRIANGLE``, ``WAVE_SQUARE``,
``WAVE_NOISE`` (randomly generated noise). Default: ``WAVE_SQUARE``
:param fx: Effect to add on the sound, one of the following values:
``FX_TREMOLO``, ``FX_VIBRATO``, ``FX_WARBLE``, or ``FX_NONE``.
Default: ``FX_NONE``
:param shape: The type of the interpolation curve between the start and end
frequencies, different wave shapes have different rates of change
in frequency. One of the following values: ``SHAPE_LINEAR``,
``SHAPE_CURVE``, ``SHAPE_LOG``. Default: ``SHAPE_LOG``

.. py:function:: copy()
:returns: A copy of the SoundEffect.

.. py:attribute:: freq_start
Start frequency in Hertz (Hz), a number between ``0`` and ``9999``.

.. py:attribute:: freq_end
End frequency in Hertz (Hz), a number between ``0`` and ``9999```.

.. py:attribute:: duration
Duration of the sound in milliseconds, a number between ``0`` and
``9999``.

.. py:attribute:: vol_start
Start volume value, a number between ``0`` and ``255``.

.. py:attribute:: vol_end
End volume value, a number between ``0`` and ``255``.

.. py:attribute:: wave
Type of wave shape, one of these values: ``WAVE_SINE``,
``WAVE_SAWTOOTH``, ``WAVE_TRIANGLE``, ``WAVE_SQUARE``,
``WAVE_NOISE`` (randomly generated noise).

.. py:attribute:: fx
Effect to add on the sound, one of the following values:
``FX_TREMOLO``, ``FX_VIBRATO``, ``FX_WARBLE``, or ``None``.

.. py:attribute:: shape
The type of interpolation curve between the start and end
frequencies, different wave shapes have different rates of change
in frequency. One of the following values: ``SHAPE_LINEAR``,
``SHAPE_CURVE``, ``SHAPE_LOG``.

The arguments used to create any Sound Effect,
can be inspected by looking at each of the SoundEffect instance attributes,
or by converting the instance into a string (which can be done via ``str()``
function, or by using a function that does the conversion automatically like
``print()``).

For example, with the :doc:`REPL </devguide/repl>` you can inspect the
default SoundEffects::

>>> print(audio.SoundEffect())
SoundEffect(freq_start=500, freq_end=2500, duration=500, vol_start=255, vol_end=0, wave=WAVE_SQUARE, fx=FX_NONE, shape=SHAPE_LOG)

This format is "human readable", which means it is easy for us to read,
and it looks very similar to the code needed to create that SoundEffect,
but it's not quite right. The ``repr()`` function can be used to create a
string of Python code that can be stored or transferred
(you could transmit sounds via micro:bit radio!) and be executed with the
``eval()`` function::

>>> from audio import SoundEffect
>>> sound_code = repr(SoundEffect())
>>> print(sound_code)
SoundEffect(500, 2500, 500, 255, 0, 3, 0, 18)
>>> eval("audio.play({})".format(sound_code))

Sound Effects Example
---------------------

.. include:: ../examples/soundeffects.py
:code: python

AudioFrame
==========

.. py:class::
AudioFrame

An ``AudioFrame`` object is a list of 32 samples each of which is an unsigned byte
(whole number between 0 and 255).

It takes just over 4 ms to play a single frame.

.. py:function:: copyfrom(other)
Overwrite the data in this ``AudioFrame`` with the data from another
``AudioFrame`` instance.

:param other: ``AudioFrame`` instance from which to copy the data.

Technical Details
=================
-----------------

.. note::
You don't need to understand this section to use the ``audio`` module.
Expand All @@ -104,11 +253,11 @@ samples. When reading reaches the start or the mid-point of the buffer, it
triggers a callback to fetch the next ``AudioFrame`` which is then copied into
the buffer. This means that a sound source has under 4ms to compute the next
``AudioFrame``, and for reliable operation needs to take less 2ms (which is
32000 cycles, so should be plenty).
32000 cycles in micro:bit V1 or 128000 in V2, so should be plenty).


Example
=======
AudioFrame Example
------------------

.. include:: ../examples/waveforms.py
:code: python
53 changes: 53 additions & 0 deletions examples/soundeffects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from microbit import *

# Play the default Sound Effect
audio.play(audio.SoundEffect())

# Create a new Sound Effect and immediately play it
audio.play(audio.SoundEffect(
freq_start=400,
freq_end=2000,
duration=500,
vol_start=100,
vol_end=255,
wave=audio.SoundEffect.WAVE_TRIANGLE,
fx=audio.SoundEffect.FX_VIBRATO,
shape=audio.SoundEffect.SHAPE_LOG
))

# Play a Sound Effect instance, modify an attribute, and play it again
my_effect = audio.SoundEffect(
freq_start=400,
freq_end=2000,
)
audio.play(my_effect)
my_effect.duration = 1000
audio.play(my_effect)

# You can also create a new effect based on an existing one, and modify
# any of its characteristics via arguments
my_modified_effect = my_effect.copy()
my_modified_effect.wave = audio.SoundEffect.WAVE_NOISE
audio.play(my_modified_effect)

# Use sensor data to modify and play an existing Sound Effect instance
my_effect.duration = 600
while True:
# int() might be temporarily neededhttps://github.com/microbit-foundation/micropython-microbit-v2/issues/121
my_effect.freq_start = int(scale(accelerometer.get_x(), from_=(-2000, 2000), to=(0, 9999)))
my_effect.freq_end = int(scale(accelerometer.get_y(), from_=(-2000, 2000), to=(0, 9999)))
audio.play(my_effect)

if button_a.is_pressed():
# Button A silences the micro:bit
speaker.off()
display.show(Image("09090:00000:00900:09990:00900"))
sleep(500)
elif button_b.is_pressed():
# On button B re-enable speaker & play an effect while showing an image
speaker.on()
audio.play(audio.SoundEffect(), wait=False)
display.show(Image.MUSIC_QUAVER)
sleep(500)

sleep(150)

0 comments on commit d591517

Please sign in to comment.