diff --git a/docs/audio.rst b/docs/audio.rst index c607101b4..986235f93 100644 --- a/docs/audio.rst +++ b/docs/audio.rst @@ -12,19 +12,36 @@ 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 +There are five 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:: +3. `Audio Recordings <#audiorecording-audiotrack-v2>`_, an object that can + be used to record audio from the microphone:: + + recording = audio.AudioRecording(duration=4000) + microphone.record_into(recording) + audio.play(recording) + +4. `Audio Tracks <#audiorecording-audiotrack-v2>`_, a way to point to a portion + of the data in an ``AudioRecording`` or a ``bytearray`` and/or modify it:: + + recording = audio.AudioRecording(duration=4000) + microphone.record(recording) + track = AudioTrack(recording)[1000:3000] + audio.play(track) + +5. `Audio Frames <#audioframe>`_, an instance or an iterable (like a list or + generator) of Audio Frames, which are lists of samples with values + from 0 to 255:: square_wave = audio.AudioFrame() for i in range(16): @@ -47,8 +64,13 @@ Functions 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 + - ``AudioRecording``: An instance of ``AudioRecording`` as described + in the `AudioRecording <#audiorecording-audiotrack-v2>`_ section + - ``AudioTrack``: An instance of ``AudioTrack`` as described in the + `AudioTrack <#audiorecording-audiotrack-v2>`_ section + - ``AudioFrame``: An instance or an iterable of ``AudioFrame`` + instances as described in the + `AudioFrame Technical Details <#technical-details>`_ section :param wait: If ``wait`` is ``True``, this function will block until the source is exhausted. @@ -69,6 +91,13 @@ Functions Stops all audio playback. +.. py:function:: sound_level() + + Get the sound pressure level produced by audio currently being played. + + :return: A representation of the output sound pressure level in the + range 0 to 255. + Built-in sounds **V2** ====================== @@ -215,6 +244,190 @@ Sound Effects Example .. include:: ../examples/soundeffects.py :code: python + +AudioRecording & AudioTrack **V2** +================================== + +To record and play back audio, we need a way to store the audio data and +the sampling rate that has been used to record it and play it back. + +Two new classes are introduced in micro:bit V2 for this purpose: + +- The ``AudioRecording`` class holds its own audio data and sampling rate. + It is initialised with a size defined in units of time, and it's the object + type that the ``microphone.record()`` function returns. +- The ``AudioTrack`` class contains its sampling rate, but does not hold its + own data. It instead points to a buffer externally created, + like an ``AudioRecording``, or a basic type like a ``bytearray``. + It's similar to a + `memoryview `_ + and it can be used to easily modify the audio data or chop into portions + of different sizes. + +AudioRecording +-------------- + +.. py:class:: + AudioRecording(duration, rate=7812) + + The ``AudioRecording`` object contains audio data and the sampling rate + associated to it. + + The size of the internal buffer will depend on the ``rate`` + (number of samples per second) and ``duration`` parameters. + The larger these values are, the more memory that will be used. + + :param duration: Indicates how many milliseconds of audio this + instance can store. + :param rate: The sampling rate at which data will be stored + via the microphone, or played via the ``audio.play()`` function. + + .. py:function:: set_rate(sample_rate) + + Configure the sampling rate associated with the data in the + ``AudioRecording`` instance. + + :param sample_rate: The sample rate to set. + + .. py:function:: get_rate() + + Return the configured sampling rate for this + ``AudioRecording`` instance. + + :return: The configured sample rate. + + .. py:function:: copy() + + :returns: a copy of the ``AudioRecording``. + + .. py:function:: track(start_ms=0, end_ms=-1) + + Create an `AudioTrack <#audio.AudioTrack>`_ instance from a portion of + the data in this ``AudioRecording`` instance. + + Out-of-range values will be truncated to the recording limits. + If ``end_ms`` is lower than ``start_ms``, an empty track will be + created. + + :param start_ms: Where to start of the track in milliseconds. + :param end_ms: The end of the track in milliseconds. + If the default value of ``-1`` is provided it will end the track + at the end of the AudioRecording. + +When an ``AudioRecording`` is used to record data from the microphone, +a higher sampling rate produces better sound quality, +but it also uses more memory. + +During playback, increasing the sampling rate speeds up the sound +and decreasing the sample rate slows it down. + +The data inside an ``AudioRecording`` is not easy to modify, so the +``AudioTrack`` class is provided to help access the audio data like a list. +The method ``AudioRecording.track()`` can be used to create an ``AudioTrack``, +and its arguments ``start_ms`` and ``end_ms`` can be used to slice portions +of the data. + +AudioTrack +---------- + +.. py:class:: + AudioTrack(buffer, rate=None) + + The ``AudioTrack`` object points to the data provided by the input buffer, + which can be an ``AudioRecording``, another ``AudioTrack``, + or a buffer-like object like a ``bytearray``. + + When the input buffer has an associated rate (e.g. an ``AudioRecording`` + or ``AudioTrack``), the rate is copied. If the buffer object does not have + a rate, the default value of 7812 is used. + + Changes to an ``AudioTrack`` rate won't affect the original source rate, + so multiple instances pointing to the same buffer can have different + rates and the original buffer rate would stay unmodified. + + :param buffer: The buffer containing the audio data. + :param rate: The sampling rate at which data will be stored + via the microphone, or played via the ``audio.play()`` function. + + .. py:function:: set_rate(sample_rate) + + Configure the sampling rate associated with the data in the + ``AudioTrack`` instance. + + :param sample_rate: The sample rate to set. + + .. py:function:: get_rate() + + Return the configured sampling rate for this ``AudioTrack`` instance. + + :return: The configured sample rate. + + .. py:function:: copyfrom(other) + + Overwrite the data in this ``AudioTrack`` with the data from another + ``AudioTrack``, ``AudioRecording``, or buffer-like object like + a ``bytearray`` instance. + + If the input buffer is smaller than the available space in this + instance, the rest of the data is left untouched. + If it is larger, it will stop copying once this instance is filled. + + :param other: Buffer-like instance from which to copy the data. + +An ``AudioTrack`` can be created from an ``AudioRecording``, another +``AudioTrack``, or a ``bytearray`` and individual bytes can be accessed and +modified like elements in a list:: + + my_track = AudioTrack(bytearray(100)) + # Create a square wave + half_length = len(my_track) // 2 + for i in range(half_length): + my_track[i] = 255 + for i in range(half_length, len(my_track)): + my_track[i] = 0 + + +Or smaller AudioTracks can be created using slices, useful to send them +via radio or serial:: + + recording = microphone.record(duration=2000) + track = AudioTrack(recording) + packet_size = 32 + for i in range(0, len(track), packet_size): + radio.send_bytes(track[i:i+packet_size]) + +Example +------- + +:: + + from microbit import * + + # An AudioRecording holds the audio data + recording = audio.AudioRecording(duration=4000) + + # AudioTracks point to a portion of the data in the AudioRecording + # We can obtain the an AudioTrack from the AudioRecording.track() method + first_half = recording.track(end_ms=2000) + # Or we can create an AudioTrack from an AudioRecording and slice it + full_track = audio.AudioTrack(recording) + second_half = full_track[full_track.length() // 2:] + + while True: + if button_a.is_pressed(): + # We can record directly inside the AudioRecording + microphone.record(recording) + if button_b.is_pressed(): + audio.play(recording, wait=False) + # The rate can be changed while playing + first_half.set_rate( + scale(accelerometer.get_x(), from_=(-1000, 1000), to=(3_000, 30_000)) + ) + if pin_logo.is_touched(): + # We can also play the AudioTrack pointing to the AudioRecording + audio.play(first_half) + + AudioFrame ========== @@ -241,9 +454,9 @@ Technical Details It is just here in case you wanted to know how it works. The ``audio`` module can consumes an iterable (sequence, like list or tuple, or -generator) of ``AudioFrame`` instances, each 32 samples at 7812.5 Hz, and uses -linear interpolation to output a PWM signal at 32.5 kHz, which gives tolerable -sound quality. +generator) of ``AudioFrame`` instances, each 32 samples at 7812.5 Hz, +which take just over 4 milliseconds to play each frame +(1/7812.5 * 32 = 0.004096 = 4096 microseconds). The function ``play`` fully copies all data from each ``AudioFrame`` before it calls ``next()`` for the next frame, so a sound source can use the same @@ -260,5 +473,8 @@ the buffer. This means that a sound source has under 4ms to compute the next AudioFrame Example ------------------ +Creating and populating ``AudioFrame`` iterables and generators with +different sound waveforms: + .. include:: ../examples/waveforms.py :code: python diff --git a/docs/microbit_micropython_api.rst b/docs/microbit_micropython_api.rst index 19362fdac..41cb187de 100644 --- a/docs/microbit_micropython_api.rst +++ b/docs/microbit_micropython_api.rst @@ -108,6 +108,16 @@ The Microphone is accessed via the `microphone` object:: set_threshold(128) # Returns a representation of the sound pressure level in the range 0 to 255. sound_level() + # Record audio into a new `AudioRecording` + recording = record(duration, rate=7812) + # Record audio into an existing `AudioRecording` + record_into(recording, wait=True) + # Returns `True` if the microphone is currently recording audio + is_recording() + # Stop any active audio recording + stop() + # Set the microphone sensitivity (also referred as gain) + set_sensitivity(microphone.SENSITIVITY_MEDIUM) Pins ---- diff --git a/docs/microphone.rst b/docs/microphone.rst index 88a847031..3818407c1 100644 --- a/docs/microphone.rst +++ b/docs/microphone.rst @@ -4,9 +4,9 @@ Microphone **V2** .. py:module:: microbit.microphone This object lets you access the built-in microphone available on the -micro:bit **V2**. It can be used to respond to sound. The microphone input -is located on the front of the board alongside a microphone activity LED, -which is lit when the microphone is in use. +micro:bit **V2**. It can be used to record and respond to sound. +The microphone input is located on the front of the board alongside a +microphone activity LED, which is lit when the microphone is in use. .. image:: microphone.png :width: 300px @@ -28,6 +28,56 @@ accessible via variables in ``microbit.SoundEvent``: - ``microbit.SoundEvent.LOUD``: Represents the transition of sound events, from ``quiet`` to ``loud`` like clapping or shouting. +Recording +========= + +The microphone can record audio into an :doc:`AudioRecording