From 49d8f2f73a634c42ecaf109804090a02eef3b6d1 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 16 Feb 2021 18:13:26 -0800 Subject: [PATCH 1/8] Fixing spelling --- .../code/analytics/esp-eye/src/ImageHandler.cpp | 2 +- .../code/custom-vision/src/ImageHandler.cpp | 2 +- .../code/image-capture/src/ImageHandler.cpp | 2 +- .../code/pi-control/esp-eye/src/ImageHandler.cpp | 2 +- .../vision/manufacturing-part-check/steps/image-capture.md | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/labs/ai-edge/vision/manufacturing-part-check/code/analytics/esp-eye/src/ImageHandler.cpp b/labs/ai-edge/vision/manufacturing-part-check/code/analytics/esp-eye/src/ImageHandler.cpp index 76c0dad..8ea9a6f 100644 --- a/labs/ai-edge/vision/manufacturing-part-check/code/analytics/esp-eye/src/ImageHandler.cpp +++ b/labs/ai-edge/vision/manufacturing-part-check/code/analytics/esp-eye/src/ImageHandler.cpp @@ -64,7 +64,7 @@ bool ImageHandler::SavePhoto(camera_fb_t *frameBuffer, const char *fileName) // Checks if the photo has been saved by validating the file size bool ImageHandler::CheckPhoto(const char *fileName) { - // Open the file from teh SPIFFS file system + // Open the file from the SPIFFS file system File f_pic = SPIFFS.open(fileName); // Get the file size diff --git a/labs/ai-edge/vision/manufacturing-part-check/code/custom-vision/src/ImageHandler.cpp b/labs/ai-edge/vision/manufacturing-part-check/code/custom-vision/src/ImageHandler.cpp index 76c0dad..8ea9a6f 100644 --- a/labs/ai-edge/vision/manufacturing-part-check/code/custom-vision/src/ImageHandler.cpp +++ b/labs/ai-edge/vision/manufacturing-part-check/code/custom-vision/src/ImageHandler.cpp @@ -64,7 +64,7 @@ bool ImageHandler::SavePhoto(camera_fb_t *frameBuffer, const char *fileName) // Checks if the photo has been saved by validating the file size bool ImageHandler::CheckPhoto(const char *fileName) { - // Open the file from teh SPIFFS file system + // Open the file from the SPIFFS file system File f_pic = SPIFFS.open(fileName); // Get the file size diff --git a/labs/ai-edge/vision/manufacturing-part-check/code/image-capture/src/ImageHandler.cpp b/labs/ai-edge/vision/manufacturing-part-check/code/image-capture/src/ImageHandler.cpp index 76c0dad..8ea9a6f 100644 --- a/labs/ai-edge/vision/manufacturing-part-check/code/image-capture/src/ImageHandler.cpp +++ b/labs/ai-edge/vision/manufacturing-part-check/code/image-capture/src/ImageHandler.cpp @@ -64,7 +64,7 @@ bool ImageHandler::SavePhoto(camera_fb_t *frameBuffer, const char *fileName) // Checks if the photo has been saved by validating the file size bool ImageHandler::CheckPhoto(const char *fileName) { - // Open the file from teh SPIFFS file system + // Open the file from the SPIFFS file system File f_pic = SPIFFS.open(fileName); // Get the file size diff --git a/labs/ai-edge/vision/manufacturing-part-check/code/pi-control/esp-eye/src/ImageHandler.cpp b/labs/ai-edge/vision/manufacturing-part-check/code/pi-control/esp-eye/src/ImageHandler.cpp index 76c0dad..8ea9a6f 100644 --- a/labs/ai-edge/vision/manufacturing-part-check/code/pi-control/esp-eye/src/ImageHandler.cpp +++ b/labs/ai-edge/vision/manufacturing-part-check/code/pi-control/esp-eye/src/ImageHandler.cpp @@ -64,7 +64,7 @@ bool ImageHandler::SavePhoto(camera_fb_t *frameBuffer, const char *fileName) // Checks if the photo has been saved by validating the file size bool ImageHandler::CheckPhoto(const char *fileName) { - // Open the file from teh SPIFFS file system + // Open the file from the SPIFFS file system File f_pic = SPIFFS.open(fileName); // Get the file size diff --git a/labs/ai-edge/vision/manufacturing-part-check/steps/image-capture.md b/labs/ai-edge/vision/manufacturing-part-check/steps/image-capture.md index e94852c..cfa106b 100644 --- a/labs/ai-edge/vision/manufacturing-part-check/steps/image-capture.md +++ b/labs/ai-edge/vision/manufacturing-part-check/steps/image-capture.md @@ -77,7 +77,7 @@ To access the camera and host a web server, you can use some pre-existing Arduin ESP Async WebServer ``` - This will add the [Espressif ESP32 Camera driver](https://github.com/espressif/esp32-camera) and the [ESP Async WebServer](https://github.com/me-no-dev/ESPAsyncWebServer) libraries to the project. The ESP32 camera driver is a library to talk to the camera on the ESP-EYE board, and ht eESP Async WebServer allows you to run a web server on the ESP board. + This will add the [Espressif ESP32 Camera driver](https://github.com/espressif/esp32-camera) and the [ESP Async WebServer](https://github.com/me-no-dev/ESPAsyncWebServer) libraries to the project. The ESP32 camera driver is a library to talk to the camera on the ESP-EYE board, and the ESP Async WebServer allows you to run a web server on the ESP board. Next time the app is compiled, it will bring in these libraries and compile them into the build. @@ -362,7 +362,7 @@ Just like code for interacting with the camera, the code for interacting with th // Checks if the photo has been saved by validating the file size bool ImageHandler::CheckPhoto(const char *fileName) { - // Open the file from teh SPIFFS file system + // Open the file from the SPIFFS file system File f_pic = SPIFFS.open(fileName); // Get the file size From 0236acd7ceb3e07fec1ed0c300ecf1902b0494e4 Mon Sep 17 00:00:00 2001 From: Jim Bennett Date: Tue, 16 Feb 2021 18:13:39 -0800 Subject: [PATCH 2/8] First cut of TinyML lab --- .gitignore | 3 +- labs/README.md | 6 + labs/tiny-ml/audio-classifier/LICENSE | 21 + labs/tiny-ml/audio-classifier/README.md | 48 ++ .../code/audio-capture/.gitignore | 5 + .../audio-capture/.vscode/extensions.json | 7 + .../code/audio-capture/README.md | 30 ++ .../code/audio-capture/include/README | 39 ++ .../code/audio-capture/platformio.ini | 18 + .../code/audio-capture/src/main.cpp | 109 +++++ .../code/audio-capture/src/mic.h | 104 +++++ .../code/audio-capture/test/README | 11 + .../code/audio-classifier/.gitignore | 5 + .../audio-classifier/.vscode/extensions.json | 7 + .../code/audio-classifier/README.md | 24 + .../code/audio-classifier/include/README | 39 ++ .../code/audio-classifier/platformio.ini | 22 + .../code/audio-classifier/src/classifier.h | 4 + .../code/audio-classifier/src/main.cpp | 108 +++++ .../code/audio-classifier/src/mic.h | 103 +++++ .../code/audio-classifier/test/README | 11 + .../code/model-trainer/.gitignore | 121 +++++ .../model-trainer/.vscode/extensions.json | 7 + .../code/model-trainer/README.md | 26 ++ .../code/model-trainer/requirements.txt | 2 + .../code/model-trainer/train_classifier.py | 32 ++ .../vscode-platformio-new-capture-project.png | Bin 0 -> 60805 bytes .../audio-classifier/steps/audio-capture.md | 424 ++++++++++++++++++ .../steps/audio-classifier.md | 9 + .../audio-classifier/steps/train-model.md | 9 + 30 files changed, 1353 insertions(+), 1 deletion(-) create mode 100644 labs/tiny-ml/audio-classifier/LICENSE create mode 100644 labs/tiny-ml/audio-classifier/README.md create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/.gitignore create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/.vscode/extensions.json create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/README.md create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/include/README create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/platformio.ini create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/src/main.cpp create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/src/mic.h create mode 100644 labs/tiny-ml/audio-classifier/code/audio-capture/test/README create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/.gitignore create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/.vscode/extensions.json create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/README.md create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/include/README create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/platformio.ini create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/src/classifier.h create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/src/main.cpp create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/src/mic.h create mode 100644 labs/tiny-ml/audio-classifier/code/audio-classifier/test/README create mode 100644 labs/tiny-ml/audio-classifier/code/model-trainer/.gitignore create mode 100644 labs/tiny-ml/audio-classifier/code/model-trainer/.vscode/extensions.json create mode 100644 labs/tiny-ml/audio-classifier/code/model-trainer/README.md create mode 100644 labs/tiny-ml/audio-classifier/code/model-trainer/requirements.txt create mode 100644 labs/tiny-ml/audio-classifier/code/model-trainer/train_classifier.py create mode 100644 labs/tiny-ml/audio-classifier/images/vscode-platformio-new-capture-project.png create mode 100644 labs/tiny-ml/audio-classifier/steps/audio-capture.md create mode 100644 labs/tiny-ml/audio-classifier/steps/audio-classifier.md create mode 100644 labs/tiny-ml/audio-classifier/steps/train-model.md diff --git a/.gitignore b/.gitignore index aa6da67..32e6264 100644 --- a/.gitignore +++ b/.gitignore @@ -140,4 +140,5 @@ obj # Node stuff node_modules -~$*.docx \ No newline at end of file +~$*.docx +*.a diff --git a/labs/README.md b/labs/README.md index 7dfa8cf..195485f 100644 --- a/labs/README.md +++ b/labs/README.md @@ -22,6 +22,12 @@ These labs cover running AI workloads either in the cloud from the IoT device, o * [Assembly line QA](./ai-edge/vision/manufacturing-part-check/) - A prototype of an AI image classification based quality assurance tool for manufacturing showing how to detect broken parts on an assembly line using AI, controlled from another device * [Speech](./ai-edge/speech) - Speech to text, text to speech and speech translation using a Raspberry Pi and USB microphone/speaker +## TinyML + +These labs cover training tiny machine learning workloads and running them on embedded hardware such as Adruino microcontrollers. + +* [Audio classifier](./tiny-ml/audio-classifier/) - a TinyML lab to capture audio data, use it to train a model, then classify audio data using that model on an Arduino Nano 33 Sense BLE board. + ## Digital Agriculture These labs cover scenarios around digital agriculture. diff --git a/labs/tiny-ml/audio-classifier/LICENSE b/labs/tiny-ml/audio-classifier/LICENSE new file mode 100644 index 0000000..b341fdc --- /dev/null +++ b/labs/tiny-ml/audio-classifier/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jim Bennett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/labs/tiny-ml/audio-classifier/README.md b/labs/tiny-ml/audio-classifier/README.md new file mode 100644 index 0000000..c1b09e6 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/README.md @@ -0,0 +1,48 @@ +# TinyML Audio classifier + +This folder contains a lab with multiple parts working towards an audio classifier running on an Arduino Nano 33 BLE Sense microcontroller, taking advantage of the built-in microphone. + +> This lab was inspired by [Better word classification with Arduino Nano 33 BLE Sense and Machine Learning](https://eloquentarduino.github.io/2020/08/better-word-classification-with-arduino-33-ble-sense-and-machine-learning/) by [Eloquent Arduino](https://eloquentarduino.github.io/about-me/). + +| Author | [Jim Bennett](https://github.com/JimBobBennett) | +|:---|:---| +| Target platform | | +| Hardware required | | +| Software required | | +| Programming Language | | +| Prerequisites | Basic proficiency in using VS Code, C++ and Python.
If you want to learn Python, check out these free resources:
| +| Date | February 2021 | +| Learning Objectives | | +| Time to complete | 1 hours | + +## The lab parts + +This lab has the following parts + +1. Program the Arduino device for audio capture and capture training data +1. Train a ML model using the training data +1. Program the Arduino device to classify audio + +## Audio classification + +Audo classification is the process of classifying a sound based on labelled training data. For example - you could train a model by using multiple recordings of someone saying the word "Yes" labelled as `Yes`, and multiple recordings of someone saying the word "No" labelled as `No`. The model could then take a new sound recording and classify it as either `Yes` or `No`. + +This lab starts by coding the Arduino to record multiple samples that are labelled, then these labelled samples are used to train a model, which is then add to device code that runs on the microcontroller to classify new audio data. + +The classifier you will create here needs at least 2 labels in the model, and will pick the most probable one to classify the audio. + +## TinyML + +TinyML is the coming together of machine learning and embedded systems. It involves training ML models that are tiny - that is substantially smaller than the models historically created, and able to run on microcontrollers with limited memory and power. It was originally defined as ML models that can run using less than 1mW of power, but has become a general term for running ML on microcontrollers. + +Microcontrollers have memory limits usually in the kilobytes, meaning traditional ML models that are many megabytes in size cannot even be installed on the device, let alone run. By using TinyML models you can bring the world of ML to a microcontroller. An audio classifier (a model that can distinguish between multiple sounds) or a wake word model (a model that can detect one specific sound, such as the command to wake a smart device up), for example, can be compressed to less than 20KB using TinyML. + +There is a trade off - with smaller models you lose accuracy, but this is an acceptable tradeoff for the advantages of smaller, low powered models. For example, if you were creating a voice activated smart device you would want it to consume as little power as possible waiting for a wake word, and only then powering up to listen for more instructions and using bigger models, or even sending data to the cloud. If you are using audio to classify animal behavior on a smart collar, you want long battery life and the device to be as small and light-weight as possible to reduce the inconvenience to the animal being tracked. + +In this lab, the device in question is an [Arduino Nano 33 BLE Sense microcontroller](https://store.arduino.cc/usa/nano-33-ble-sense) - a board that has a built in microphone that can be used to detect and classify audio signals. It has 256KB of memory available. + +## Labs + +1. [Create the audio capture tool](./steps/audio-capture.md) +1. [Train the TinyML model](./steps/train-model.md) +1. [Create the audio classifier](./steps/audio-classifier.md) diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/.gitignore b/labs/tiny-ml/audio-classifier/code/audio-capture/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/.vscode/extensions.json b/labs/tiny-ml/audio-classifier/code/audio-capture/.vscode/extensions.json new file mode 100644 index 0000000..0f0d740 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/README.md b/labs/tiny-ml/audio-classifier/code/audio-capture/README.md new file mode 100644 index 0000000..1bcde42 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/README.md @@ -0,0 +1,30 @@ +# Audio Capture + +This folder contains a [PlatformIO](https://platformio.org/platformio-ide) project to build an audio capture app that runs on an [Arduino Nano 33 BLE Sense](https://store.arduino.cc/usa/nano-33-ble-sense) board. + +You can read more about this project in the [top level README.md file](../README.md). + +To build and deploy this project, you will need to open this folder in [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=academic-7372-jabenn) with the [PlatformIO extension](https://platformio.org/platformio-ide) installed. You will then be able to build the project and upload to a Arduino Nano 33 BLE Sense board. + +## Building this project + +This project has a library dependency on the ARM CMSIS static library. You will need to download this from the ARM GitHub repo. + +* Head to [the CMSIS GCC libs folder in the GitHub repo](https://github.com/ARM-software/CMSIS_5/tree/5.7.0/CMSIS/DSP/Lib/GCC) +* Download `libarm_cortexM4l_math.a` from that repo page +* Add the file to the root folder of this project +* Build and upload as normal + +## Running the project + +Once the project is running on the board, it will listen for audio and output RMS values to the serial port. + +* Connect to the serial monitor to view the audio values +* Make the one of the relevant noises into the microphone of the board. Pause after each noise and you will see a line output to the serial monitor. +* Repeat 15-30 times for that one noise +* Copy the values from the serial monitor into a CSV file named using the name of the label (for eample if you were capturing numbers you would put the data for one into `one.csv`, for two into `two.csv` and so on). +* Clear the serial output and repeat the above steps for all the noises you are interested in + +## Processing the output + +Refer to the [top level README.md file](../README.md) for instructions on how to process and use the output to classify noises. diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/include/README b/labs/tiny-ml/audio-classifier/code/audio-capture/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/platformio.ini b/labs/tiny-ml/audio-classifier/code/audio-capture/platformio.ini new file mode 100644 index 0000000..1b1b1f5 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nano33ble] +platform = nordicnrf52 +board = nano33ble +framework = arduino +build_flags = + -L. + -l arm_cortexM4l_math + -w diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/src/main.cpp b/labs/tiny-ml/audio-classifier/code/audio-capture/src/main.cpp new file mode 100644 index 0000000..b27ccfb --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/src/main.cpp @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Audio Capture + * + * This program listens on the microphone capturing audio data. + * Instead of capturing raw data, it captures root mean squared values allowing + * audio to be reduced to a few values instead of thousands of values every second. + * These values are then output to the serial port - one line per audio sample. + * + * This data can then be used to train a TinyML model to classify audio. + */ + +#include "mic.h" + +// Settings for the audio +// Try tuning these if you need different results +// 128 samples is enough for 2 seconds of audio - it's captured at 64 samples per second +#define SAMPLES 128 +#define GAIN (1.0f / 50) +#define SOUND_THRESHOLD 1000 + +// An array of the audio samples +float features[SAMPLES]; + +// A wrapper for the microphone +Mic mic; + +/** + * @brief PDM callback to update the data in the mic object + */ +void onAudio() +{ + mic.update(); +} + +/** + * @brief Read given number of samples from mic + * @return True if there is enough data captured from the microphone, + * otherwise False + */ +bool recordAudioSample() +{ + // Check the microphone class as captured enough data + if (mic.hasData() && mic.pop() > SOUND_THRESHOLD) + { + // Loop through the samples, waiting to capture data if needed + for (int i = 0; i < SAMPLES; i++) + { + while (!mic.hasData()) + delay(1); + + // Add the features to the array + features[i] = mic.pop() * GAIN; + } + + // Return that we have features + return true; + } + + // Return that we don't have enough data yet + return false; +} + +/** + * @brief Sets up the serial port and the microphone + */ +void setup() +{ + // Start the serial connection so the captured audio data can be output + Serial.begin(115200); + + // Set up the microphone callback + PDM.onReceive(onAudio); + + // Start listening + mic.begin(); + + // Wait 3 seconds for everything to get started + delay(3000); +} + +/** + * @brief Runs continuously capturing audio data and writing it to + * the serial port + */ +void loop() +{ + // wait for audio data + if (recordAudioSample()) + { + // print the audio data to serial port + for (int i = 0; i < SAMPLES; i++) + { + Serial.print(features[i], 6); + + // Seperate the audio values with commas, at the last value + // send a newline + Serial.print(i == SAMPLES - 1 ? '\n' : ','); + } + + // Wait between samples + delay(1000); + } + + // Sleep to allow background microphone processing + delay(20); +} diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/src/mic.h b/labs/tiny-ml/audio-classifier/code/audio-capture/src/mic.h new file mode 100644 index 0000000..3bb0db8 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/src/mic.h @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include +#include + +// The size of the data coming in from the microphone +#define MICROPHONE_BUFFER_SIZE_IN_WORDS (256U) +#define MICROPHONE_BUFFER_SIZE_IN_BYTES (MICROPHONE_BUFFER_SIZE_IN_WORDS * sizeof(int16_t)) + +/** + * @brief A helper class for accessing the BLEs microphone. This reads + * samples from the microphone at 16KHz and computes the root mean squared value. + * This value is then retrieved which resets the value so it can be recalculated + * from more incomng data. + * + * This allows for a small data set that represents a few seconds of sound data. + * This is not enough to reproduce the audio, but is enough to train or use a + * TinyML model. + */ +class Mic +{ +public: + /** + * @brief Initializes the Mic class + */ + Mic() : _hasData(false) + { + } + + /** + * @brief Setup the PDM library to access the microphone at a sample rate of 16KHz + */ + void begin() + { + PDM.begin(1, 16000); + PDM.setGain(20); + } + + /** + * @brief Gets if the microphone has new data + * @return true if the microphone has new data, otherwise false + */ + bool hasData() + { + return _hasData; + } + + /** + * @brief Get the root mean squared data value from the microphone + * @return The root mean square value of the last data point from the microphone + */ + int16_t data() + { + return _rms; + } + + /** + * @brief Gets the last root mean squared value and resets the calculation + * @return The last root mean square value + */ + int16_t pop() + { + int16_t rms = data(); + + reset(); + + return rms; + } + + /** + * @brief Reads the audio data from the PDM object and calculates the + * root mean square value + */ + void update() + { + // Get the available bytes from the microphone + int bytesAvailable = PDM.available(); + + // Check we have a full buffers worth + if (bytesAvailable == MICROPHONE_BUFFER_SIZE_IN_BYTES) + { + // Read from the buffer + int16_t _buffer[MICROPHONE_BUFFER_SIZE_IN_WORDS]; + + _hasData = true; + PDM.read(_buffer, bytesAvailable); + + // Calculate a running root mean square value + arm_rms_q15((q15_t *)_buffer, MICROPHONE_BUFFER_SIZE_IN_WORDS, (q15_t *)&_rms); + } + } + + /** + * @brief Mark data as read + */ + void reset() + { + _hasData = false; + } + +private: + int16_t _rms; + bool _hasData; +}; \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/audio-capture/test/README b/labs/tiny-ml/audio-classifier/code/audio-capture/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-capture/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/.gitignore b/labs/tiny-ml/audio-classifier/code/audio-classifier/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/.vscode/extensions.json b/labs/tiny-ml/audio-classifier/code/audio-classifier/.vscode/extensions.json new file mode 100644 index 0000000..0f0d740 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/README.md b/labs/tiny-ml/audio-classifier/code/audio-classifier/README.md new file mode 100644 index 0000000..3a7b19b --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/README.md @@ -0,0 +1,24 @@ +# Audio Classifier + +This folder contains a [PlatformIO](https://platformio.org/platformio-ide) project to build an audio classifier app that runs on an [Arduino Nano 33 BLE Sense](https://store.arduino.cc/usa/nano-33-ble-sense) board. + +You can read more about this project in the [top level README.md file](../README.md). + +To build and deploy this project, you will need to open this folder in [Visual Studio Code](https://code.visualstudio.com/?WT.mc_id=academic-7372-jabenn) with the [PlatformIO extension](https://platformio.org/platformio-ide) installed. You will then be able to build the project and upload to a Arduino Nano 33 BLE Sense board. + +## Building this project + +This project has a library dependency on the ARM CMSIS static library. You will need to download this from the ARM GitHub repo. + +* Head to [the CMSIS GCC libs folder in the GitHub repo](https://github.com/ARM-software/CMSIS_5/tree/5.7.0/CMSIS/DSP/Lib/GCC) +* Download `libarm_cortexM4l_math.a` from that repo page +* Add the file to the root folder of this project + +You will also need to add code to the [`classifier.h`](./src/classifier.h) file. This code is generated by the model training utility. Refer to the [top level README.md file](../README.md) for more details on creating the contents of this file. + +## Running the project + +Once the project is running on the board, it will listen for audio, classify based on a trained model, then output the label of the predicted value to the serial port. + +* Connect to the serial monitor to view the audio values +* Make the one of the relevant noises into the microphone of the board. Pause after each noise and you will see the predicted label for that sound output to the serial monitor. diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/include/README b/labs/tiny-ml/audio-classifier/code/audio-classifier/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/platformio.ini b/labs/tiny-ml/audio-classifier/code/audio-classifier/platformio.ini new file mode 100644 index 0000000..df9fd5e --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/platformio.ini @@ -0,0 +1,22 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:nano33ble] +platform = nordicnrf52 +board = nano33ble +framework = arduino +build_flags = + -L. + -l arm_cortexM4l_math + -w +lib_deps = + # RECOMMENDED + # Accept new functionality in a backwards compatible manner and patches + eloquentarduino/EloquentTinyML @ ^0.0.3 diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/src/classifier.h b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/classifier.h new file mode 100644 index 0000000..828b6bd --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/classifier.h @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Paste the output of the model trainer here \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/src/main.cpp b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/main.cpp new file mode 100644 index 0000000..2aba807 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/main.cpp @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Audio Classifier + * + * This program listens on the microphone capturing audio data. + * Instead of capturing raw data, it captures root mean squared values allowing + * audio to be reduced to a few values instead of thousands of values every second. + * These values are then output to the serial port - one line per audio sample. + * + * This data is then run through a TinyML classifier to classify the sound based on + * a trained model using code pasted into the classifier.h header file. + */ + +#include "mic.h" +#include "classifier.h" + +// Settings for the audio +// These need to match the settings you used in the audio capture program +// 128 samples is enough for 2 seconds of audio - it's captured at 64 samples per second +#define SAMPLES 128 +#define GAIN (1.0f / 50) +#define SOUND_THRESHOLD 1000 + +// An array of the audio samples +float features[SAMPLES]; + +// A wrapper for the microphone +Mic mic; + +// The classifier +Eloquent::ML::Port::SVM clf; + +/** + * @brief PDM callback to update the data in the mic object + */ +void onAudio() +{ + mic.update(); +} + +/** + * @brief Read given number of samples from mic + * @return True if there is enough data captured from the microphone, + * otherwise False + */ +bool recordAudioSample() +{ + // Check the microphone class as captured enough data + if (mic.hasData() && mic.pop() > SOUND_THRESHOLD) + { + // Loop through the samples, waiting to capture data if needed + for (int i = 0; i < SAMPLES; i++) + { + while (!mic.hasData()) + delay(1); + + // Add the features to the array + features[i] = mic.pop() * GAIN; + } + + // Return that we have features + return true; + } + + // Return that we don't have enough data yet + return false; +} + +/** + * @brief Sets up the serial port and the microphone + */ +void setup() +{ + // Start the serial connection so the captured audio data can be output + Serial.begin(115200); + + // Set up the microphone callback + PDM.onReceive(onAudio); + + // Start listening + mic.begin(); + + // Wait 3 seconds for everything to get started + delay(3000); +} + +/** + * @brief Runs continuously capturing audio data and classifying it, + * writing the predicted label to the serial port + */ +void loop() +{ + // wait for audio data + if (recordAudioSample()) + { + // Write out the classification to the serial port + Serial.print("Sound number: "); + Serial.println(clf.predictLabel(features)); + + // Wait between samples + delay(1000); + } + + // Sleep to allow background microphone processing + delay(20); +} diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/src/mic.h b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/mic.h new file mode 100644 index 0000000..56a4043 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/src/mic.h @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +#include +#include + +#define MICROPHONE_BUFFER_SIZE_IN_WORDS (256U) +#define MICROPHONE_BUFFER_SIZE_IN_BYTES (MICROPHONE_BUFFER_SIZE_IN_WORDS * sizeof(int16_t)) + +/** + * @brief A helper class for accessing the BLEs microphone. This reads + * samples from the microphone at 16KHz and computes the root mean squared value. + * This value is then retrieved which resets the value so it can be recalculated + * from more incomng data. + * + * This allows for a small data set that represents a few seconds of sound data. + * This is not enough to reproduce the audio, but is enough to train or use a + * TinyML model. + */ +class Mic +{ +public: + /** + * @brief Initializes the Mic class + */ + Mic() : _hasData(false) + { + } + + /** + * @brief Setup the PDM library to access the microphone at a sample rate of 16KHz + */ + void begin() + { + PDM.begin(1, 16000); + PDM.setGain(20); + } + + /** + * @brief Gets if the microphone has new data + * @return true if the microphone has new data, otherwise false + */ + bool hasData() + { + return _hasData; + } + + /** + * @brief Get the root mean squared data value from the microphone + * @return The root mean square value of the last data point from the microphone + */ + int16_t data() + { + return _rms; + } + + /** + * @brief Gets the last root mean squared value and resets the calculation + * @return The last root mean square value + */ + int16_t pop() + { + int16_t rms = data(); + + reset(); + + return rms; + } + + /** + * @brief Reads the audio data from the PDM object and calculates the + * root mean square value + */ + void update() + { + // Get the available bytes from the microphone + int bytesAvailable = PDM.available(); + + // Check we have a full buffers worth + if (bytesAvailable == MICROPHONE_BUFFER_SIZE_IN_BYTES) + { + // Read from the buffer + int16_t _buffer[MICROPHONE_BUFFER_SIZE_IN_WORDS]; + + _hasData = true; + PDM.read(_buffer, bytesAvailable); + + // Calculate a running root mean square value + arm_rms_q15((q15_t *)_buffer, MICROPHONE_BUFFER_SIZE_IN_WORDS, (q15_t *)&_rms); + } + } + + /** + * @brief Mark data as read + */ + void reset() + { + _hasData = false; + } + +private: + int16_t _rms; + bool _hasData; +}; \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/audio-classifier/test/README b/labs/tiny-ml/audio-classifier/code/audio-classifier/test/README new file mode 100644 index 0000000..b94d089 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/audio-classifier/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/labs/tiny-ml/audio-classifier/code/model-trainer/.gitignore b/labs/tiny-ml/audio-classifier/code/model-trainer/.gitignore new file mode 100644 index 0000000..ffc839f --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/model-trainer/.gitignore @@ -0,0 +1,121 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# Junit test results (used by VSTS) +junit + +# Coverage results +coverage/ + +# Pycharm +.idea/ + +# vim +*.swp + +# Certificates +*.pem +demoCA/ +*.rnd \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/model-trainer/.vscode/extensions.json b/labs/tiny-ml/audio-classifier/code/model-trainer/.vscode/extensions.json new file mode 100644 index 0000000..90e4b09 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/model-trainer/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-python.vscode-pylance" + ] +} diff --git a/labs/tiny-ml/audio-classifier/code/model-trainer/README.md b/labs/tiny-ml/audio-classifier/code/model-trainer/README.md new file mode 100644 index 0000000..1576b7f --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/model-trainer/README.md @@ -0,0 +1,26 @@ +# Audio model trainer + +This Python program takes the output from the [audio capture program](../audio-capture) and uses it to train a TinyML model that can then be used in the the [audio classifier program](../audio-classifier). + +## Setting up the Python environment + +* Create a virtual environment for this folder using Python 3.8 or above +* Install the pip packages in the `requirements.txt` file + + > If you are using a new Apple Silicon based Mac, then the packages may not install as at the time of writing there is no supported Scikit-Learn package for the Mac. Instead you will need to use MiniForge. Refer to [this blog post](https://dev.to/jimbobbennett/installing-scikit-learn-on-an-apple-m1-114d) for instructions. +* Copy the CSV files created from the [audio capture program](../audio-capture) into the [`data`](./data) folder. These files should be named for the relevant label for the audio data. +* Run the `train_classifier.py` file + +The model will be trained, and the output sent to the console. + +```output +(.venv) ➜ model-trainer git:(master) ✗ python train_classifier.py +Accuracy 1.0 +Exported classifier to plain C +#pragma once +#include +namespace Eloquent { +} +``` + +Copy everything from `#pragma once` to the end of the output and paste it into the [`classifier.h`](../audio-classifier/src/classifier.h) header file in the audio classifier project. \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/model-trainer/requirements.txt b/labs/tiny-ml/audio-classifier/code/model-trainer/requirements.txt new file mode 100644 index 0000000..cfc4179 --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/model-trainer/requirements.txt @@ -0,0 +1,2 @@ +scikit-learn +micromlgen \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/code/model-trainer/train_classifier.py b/labs/tiny-ml/audio-classifier/code/model-trainer/train_classifier.py new file mode 100644 index 0000000..dd6af2c --- /dev/null +++ b/labs/tiny-ml/audio-classifier/code/model-trainer/train_classifier.py @@ -0,0 +1,32 @@ +import numpy as np +from os.path import basename +from glob import glob +from sklearn.svm import SVC +from micromlgen import port +from sklearn.model_selection import train_test_split + +def load_features(folder): + dataset = None + classmap = {} + for class_idx, filename in enumerate(glob('%s/*.csv' % folder)): + class_name = basename(filename)[:-4] + classmap[class_idx] = class_name + samples = np.loadtxt(filename, dtype=float, delimiter=',') + labels = np.ones((len(samples), 1)) * class_idx + samples = np.hstack((samples, labels)) + dataset = samples if dataset is None else np.vstack((dataset, samples)) + return dataset, classmap + +np.random.seed(0) +dataset, classmap = load_features('data') +X, y = dataset[:, :-1], dataset[:, -1] + +# this line is for testing your accuracy only: once you're satisfied with the results, set test_size to 1 +X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) + +clf = SVC(kernel='poly', degree=2, gamma=0.1, C=100) +clf.fit(X_train, y_train) + +print('Accuracy', clf.score(X_test, y_test)) +print('Exported classifier to plain C') +print(port(clf, classmap=classmap)) \ No newline at end of file diff --git a/labs/tiny-ml/audio-classifier/images/vscode-platformio-new-capture-project.png b/labs/tiny-ml/audio-classifier/images/vscode-platformio-new-capture-project.png new file mode 100644 index 0000000000000000000000000000000000000000..7e7710772cba28ec97b634bf9cec8901b9657afa GIT binary patch literal 60805 zcmbrmWn9}$&^B7!-6nrjoTC@+D6NQn6E-8&R1NipSj?;sW5y@S|=hXt4LpmdXf9}rH; z5+d(_<3tDG4`ODTQs#1U@94nq;om`oTE2t+Qw97Y1i#+BgUW??2L*mY{3*+Y{O4Op z#ayU=-b3vEsYtEhg!1m4&^swHVO4jC<19EswZ7>A&g=EE+DR{^2yEH_92l`a{VF57 zvx|ePB^)p2;#|#6M0jbuIP$hY$+Mt!uTbx+46nQ2poDkQ-GMA+6r6XNHDB&JYCgVY zr8#l@eBE6j36I|{$A=901~7#GzYiije|K5}GKC$uTfWnw0Um%pD%DtLf*@u&^FvL7 zpz#7VSsb7A5UEt-auaAm`ZY{1=bO+a4W%E&mQ|0SM-sToJ>|buK+Jzi9C^0zY-F}4 zsOMjM7ogv&k7E052=yHFWShx!->#jeLE+2b4q7!Xzfk)10u_YF{`@Gx{^pnX5D;xU zX>|{zB>gH`1^j>am-zPQi?FSfeqG-K_sp#sWRg0Uafj_4R}8 z3;X}62CnU&DBcM_D-SIfpn>l zZoFCe+lxFQ#0N!clsvV6Cj%ey%X>)i-0^yhhF8LWr>RrwgCX=`0iYJ~ujHFHqF`y3 zIJV~v{?m))AF)3Lsr&z(-^{Onq*)PPeYF0o5b9{K*oh&NbpKZie}-2B<9}~2F`)@r zXT8AG;C8nD(XrLysom#ke6U{U2g?7|nSTv|f2#f7$Mg3byB+X;Z?S&FT=tz~8N3T8 z>uoD$!o0+nU9?tD|{2P%C`B=T>*{_Mqn0;cNw?H-*9Zq~L2g^dYH1)@<rvgGu2w#rN}-Zh^2Ujs`vtlyGY`6Z~Z$x{?tE5O^x28?*VnnxjNS9CwFZ z$Wt=UxdTIy9RgvIoi*y(=%gTnIZpzRnCy4@qn+Fx`TVup%%gScZPGCNFC0g)ibX?F zn+|4*TKGJ!8i&X2o;ux4Lb& zd&6xz3i{Etx;(HLta}Aw@mago$`!kIN7EcL0ey@Xva+qbj`v~o6oAqUyLRsB(D@3T zqm381Yd5zwpA^qLSTvGjE7i&^Hq+6~SA70u9D4QDI@=j-V7OZ%m7MEL--u|tq60uy zE`vkuQaBidCYQxCXEv13r{}&EOh^uTf%%vIWln=-1wbIhv2YK~5$M43_}-`!zsC7sWsdIT_F!E4kpvbE$2p!1ouJ{Ewv+X+1ytm!619J*%C z4ey@05|x3I)utJj!`UGmI@LjT+f~gr?w7|qH|F;9)|F*y+C z+50{`1J!9*f9)zme=i-UJP{+g)Q?c@)_Y8rNA%LUWV*5ZuZ;;UP7zr<^-`n&%?W|0 z@|J_28A*!mH}g8u5$9Gf4|_SDYc~sZPA@PiDf*sLtP|fEGoEC-!=srkurCFIVHiB` zkL~{5y0Rj<-Vc|yPq!zB;luGCGQ>iyn)tnL8}md%n`O1wS|XX6xjqkJEfy@RxF7U# zd0qBKvpgPAJn*^!R|#EvTz81mI_&+e7f!W9IS)&UvX#$MElAy7HN*SL92sw%!RBTAhs!L=eGEGDay+jaNqSB(C=ql+r&iWj zDD%B>Zu?ij{+2+H=9>GCoHWTroRTD0Q6buO-JmZFUiIbD3{STQN1oUFdH4ZnLywyf+>mg5wjgsCjQm=&5%s$~kcqIl zf%~h+-3W8X{#p3;8n64N;UH3BMK$R^2Wn?pw9v_NT_38(cbXaZ2GvY_lYtnear-68 zl;IxpCFNEpBQ#Y+flz8S;p}rZ4C(HSK#AbYLJv#W>8>S?xNj6W^B` zjT>a)Z1d5~68*}jvz86Lwfc&A9bFNtv>@j#oczu0k$C=4%oyYCgD%EYUCYJXy!VrS z-zYMfzkBD)rhnRK{fu-Ihm(9nC7-#uys@r-rqGs5uZF6cjLs8ye;x}-5ODu(B$Byp z--2{^N`e$8iihriTsL$8>1_G9!@Ok-GlD@g_Zu&R)G-X`4vD!Hy$SVf#t{KSf9=-F z?aRF-uShukj(W4(hOdN`yhZ5xpzmMXKm*A|Lp;W3zINmem2wosZqBbQ{g)IJ< zcv6VT?j>}Aaa4;NUmMm4rE0o%9CvXviv2^c4O^tT{NbTxR)>?fzYd+!sg|QZq+gY3 zRGw1FrsgVCJroUimw8kO<6ayh%VyV){^mBC zg7Ti5nks>Qx1$p96>V4R!S1->pX1;D_Xi;wrJ6<0L)7&#j{Ei(s zh557@dwcq@U_C&|=mye_@o{>^b?YffjbjHhg?IrteyTZW(pf<_&vo$`OK|^MA!PA) z+n-E+Bm3ARN%ZVb`4!{QJr1=d?Qc)nfC{W}?84kXUyek6T&b0E$D3A~i+~sif|G*Y zLpOdD#T=uSOE1u;Zto%HRLdS5p;8#YVb+)Bm+eB58KTD`lk@0%ng;6eGEHGp=aqtw zftJiAC!TTtkIw*wgLtQ11kCUq^mqvp-(DOVO|ogKR=xqcptVD35nd0O)u+qvj2~>f z(=UT`eLa+cDM5@EgINZZ`uU~zOpCs)=4Y)>G1dSn+g2HAe&;QE^#}>u`J9?hpQPk* zVX7BjGVHpu>(K)MhuGbpyh}nZ$>L2YpFQ@n-00Fk_-EfDwmN#fM-0q6{`EgTqC(`$ zeUOMr2vfkIl%uCqv#4qnv3$NqNU&R_F}_nNH7OCw6@lf~=Oc|n=M$1jDX4Heny=8E z&=+hP&*DXj4|AXlzJGqS7UcB0wHYWwAO6mGRR1-xXtFz5L|<|eNVU!=`B@&Ib#s_? zhRdiAT2C>(c;0S5x=~@KB%3SMP+>7(RMy-nQ28l4_jC@@9nZ|;4BZ<^(crhpkzspu z5$Wd=cl1;)%nn-eeZG)L(QlQJ>wVglDM=^<;UE-#B7y@HjD<&hA}Q^0p{Ec$@{l?g zV5syp{6?*iU9i>j;X*>Usd_gkX?KdVBJt_7BiDl}pPb~W8m>n*8Kl@ftg!0QY{^Nf zdf6e^Gd;@oee3{g)tD>4Vc$71d!VWPE_g~R&bkf#cb~wLRq5yx_hPxF_0{W9OcCepsx!r9g1aN@`%#|;Nj4uX~K!Jg|_;VnEBoU z_-OC4K{9Q{5|Yp@&zB>?(0moSLfI5A5<^hXbUE%Vi|Ijraa1`2s&(2f93sNKpZ`)V4r!^DJ7LeTGhkSY8wYGmIaa@wnCCoh`S!QsXga|Z=JGv?tYR_3Q zDP*^b_@Yu0;`2%J>2Vr^;5}YMV7RF-61{`CEIIXA)-43=*7*ST+-vk%`VOH(ZJ3*b zYo3RKZ&T2haHkyx4douX`exrG!dok>M55f|i>MRg)DBT_mv5hICR+Bg{l;HrYf_%J zLUWQv*W72KPmd})<~g7z8Ycc8P9g8=?P@{rSIdy(28${NRBZGw;~$6}7HW+*7dj;*W7&s%6IY8f+^vn9me>>5-t~iYAQWXwD=a)*{}3*`>NGGpv4R_?GKcoeY1;)>E}g^SZq3brZhfef<8RbH zLjXZ8G-b&qRhQyzGwq|Ce?Ob{F-9mJ>GL|K)~!H63g$OW;(w;|4=zH41|g*~pARja zSRL!uXX!|UxAtq*xk&0`1;O8HaUmF!4ZNNl$stdcdLRGo4MLQx51s4rW|3CCPR;ah zyd5I6EY=CHx=f^Tl>E-3kF^j{tWw@O~ZfU``Tb|%wNl%J^%iH zRRn_rFk~x?|LN^3zz~}5Yjn{~|6j4>A;4nELkzki{r$0|4j5UG*4eRaWBpeF{wm+V zy{NpaUhDq*<7rsP*ReG4u=?G#ng4dQiA}b-k!cBii2d#B{}xmTwz=l(iu%I=H_mto zKHlTdR53Z+tPhR=zqkT&Ht{3TGnEpG<2RF6S66;b=qW@WR9};O7<6>Q&fihVq?VrE zhW2mu&pBTg;Hr!l37)YC{P;P&+f#5}nC#n1St<;=j{yDG&;l%pT?V1-&h~`dDCNzFgm56k--1FF8^0Kp3hG zV)=M&`(++1aa}!thc3dyY@mh3o|B*{MNyuz8mGwI1<0IpVwEsCa9*_y2eE_~0%r z-R&!^|5w}KwMnd7q5hw-_WW6|J%QH$*|6XS|IZGZ^;DkgYh4RvYo2o)GPH*UctV!YMVq=G&r7gsrfSD`uy5r2FBWK!2pxz((B99 znt|WTuIt(Qi(A>RD-?gfpm$ekP5XN;&XiALRC}XoAK03xA15~+y$+@e{|DF^h#~en zYEI-%6QESPJzm=3uwC7EJzY$j-BP(+TWxY$yEGe-A8M1o%P7}vnvN!9UuM)#XkFu4 zd#n;aUM;I^|NVG#^cI^DLgafnM12~`+-|@B`mxTi2NxXn@PK4bM61@o=7ZoK6KA^v zKKoihz;Ax{V}tR7V~T(%YzmVe4;~iQ4l1cY`P(?HYmC>qKipa>tI6s!%+;FH1b+&M zF$C@XZz{R$V||ZZoD{I*L@~j=#O(KU!pZ5>FMBs!R@s=z?UZffGSKF`_IPvpBUZps zLGY!PUafT2VFE3iMJj4IiB@IpPl#4;v$V}K66E>(Yd|*`5htLiaA*RbYtW?K`$4xW z0A`|CfCLPO8bPCx3T&5ZRQ_f&8+I12%jp-%Y=61c;q-guOPe@QgJ3iG11F_(+5vr* zt37Jy9wbP8u7gY+`yzJ1Sl~5K>;e;mPyFp76zqGD>TZby>rUl=)&uG`kq;;z^?<># z!nG{D7S{!tmXV{c^>!Plf1*^*r-`N0&Ah*Q@K_R$z6Jq0D#H1Fym&A!EJLB!VP?`7c&qak;GBrl516)IwaF; zxDxb+*sj!%O}VvO&-@&to$)vDeJC8s;H

sXzIZo(W<-e%bDetgG1nI$R)?xEh3j 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 PlatformIO is a self-contained embedded development environment. If you have the Arduino extension installed, it can clash with PlatformIO, so make sure to uninstall the Arduino extension before using PlatformIO. + +## Create the project + +Once you have PlatformIO installed, it can be used to write the audio capture tool to gather data for classification. + +### Create the PlatformIO project + +1. From VS Code, select the **PlatformIO** tab from the side menu + + ![The VS Code Platform IO menu](../../../images/vscode-platformio-menu.png) + +1. The *PlatformIO: Quick Access* bar will appear. Select *PIO Home->Open* + + ![The platform IO home open option](../../../images/vscode-platformio-home-open.png) + +1. From the home screen, select **+ New Project** from the *Quick Access* section + + ![The new platform button](../../../images/vscode-platformio-new-project-button.png) + +1. Name the project `audio-capture` + +1. For the *Board*, type in `Arduinno Nano 33 BLE` and select the `Arduinno Nano 33 BLE` board + +1. Set the *Framework* to **Arduino** + +1. For the location, either leave the default option checked, or uncheck it and select a location for the project. If you hover over the question mark you can see the default project location. + +1. Select the **Finish** button + + ![The completed project options](../images/vscode-platformio-new-capture-project.png) + +PlatformIO will take some time to download the components it needs to use the board, and create the project. Once done, the project will be opened in the VS Code window. + +### Navigate the project + +The PlatformIO project contains a number of files and folders: + +* `.pio` - this folder contains files used by PlatformIO such as the output of the build and other temporary files. You should never need to read or edit any files in this folder. This folder would not normally be checked in to source code control. +* `.vscode` - this folder contains configuration information used by VS Code and may be specific to your PC or Mac, such as paths to library files. You should never need to read or edit any files in this folder. +* `include` - this folder is for any header files for your project that you would want to expose to anyone using your code. This is normally when building libraries for consumption by other developers, and won't be used when writing code for a board. +* `lib` - this folder is for any additional libraries you want to use in your project. PlatformIO has a library manager than can import third party libraries, but if you want to use an additional libraries you need to add them to this folder. +* `src` - this folder is where the source code for your app lives. It will have a single file in it `main.cpp`. This file contains the code for an empty Arduino project. +* `test` - this folder is where you would put any unit test you write against your code. +* `.gitignore` - this is a [`gitignore`](https://git-scm.com/docs/gitignore) file that defines what will be ignored if you add this project to Git source code control. +* `platform.ini` - this file defines the configuration for the PlatformIO project including the board types (PlatformIO projects can target multiple boards), any additional libraries to use, and any build flags to control the code compilation. + +This project uses the [Arduino framework](https://www.arduino.cc), an open source framework for software and hardware. When writing arduino code, the core flow is around two methods that are defined in your main source code file. In a PlatformIO project, this file is called `main.cpp`. + +* `setup()` - the setup method is called once and only once when the board starts up. This function is used to do one-time setup, such as configuring the microphone or seial port connection. +* `loop()` - the loop method is called after setup, and once the method completes it is called again and again. All the time the board is running, this method will be called as soon as it completes. + +![The flow of an Arduino app](../../../images/arduino-setup-loop-flow.png) + +## Write the code + +The code needs to listen to the microphone and capture data, saving it out in a way that can be used to train a classifier. + +Traditionally, audio data is sampled at a rate of many of thousands of values a second - for example low quality audio is sampled at 16KHz, or 16 thousand values every second. This would give a huge amount of inputs to our model, meaning we'd need a large model - something TinyML is trying to avoid. Instead the audio can be averaged out into smaller samples, only a few a second. This gives a less accurate representation of the audio data, but is good enough to distinguish between different sounds. This is done by calculating the root mean square (RMS) value of the sampled input for a period of time. + +### Import a library + +The code to calculate the RMS value is at the time of writing not shipped with the code to support this Arduino board in PlatformIO. You can get round this by downloading a static library with the implementation that can be linked into your code. + +1. Head to the [ARM CMSIS GitHub repo compiled static libraries page](https://github.com/ARM-software/CMSIS_5/tree/5.7.0/CMSIS/DSP/Lib/GCC) +1. Download the `libarm_cortexM4l_math.a` file and add it to the root folder of the project. + > There are a number of libraries with similar names targetting different M chips, so make sure to download the correct one +1. Open the `platformio.ini` file from the root of the project in VS Code +1. Add the following to the end of the `[env:nano33ble]` section to tell the linker to look for libraries in the root and link this file when building the project: + + ```sh + build_flags = + -L. + -l arm_cortexM4l_math + -w + ``` + + The `-L.` flag tells the linker to look for libraries in the root folder. + + The `-l arm_cortexM4l_math` flag tells the linker to link the `libarm_cortexM4l_math.a` library + + The `-w` flag ignores warnings that come from the Arduino libraries. Although it is generally good practice to find and fix all warnings, in this case the Arduino libraries have warnings that can make it harder to catch errors in your code, so it is easier to ignore them + +### Add code to read from the microphone + +To keep the code cleaner, the code to read from the microphone can be added to a separate file, and this file re-used later when building the audio classifier. + +1. In VS Code, create a new file in the `src` folder called `mic.h`. This file will contain code to read the data from the microphone using a standard Arduino library, and calculate the RMS value of periods of audio. + +1. Add the following code to this file: + + ```cpp + #include + #include + + // The size of the data coming in from the microphone + #define MICROPHONE_BUFFER_SIZE_IN_WORDS (256U) + #define MICROPHONE_BUFFER_SIZE_IN_BYTES (MICROPHONE_BUFFER_SIZE_IN_WORDS * sizeof(int16_t)) + + /** + * @brief A helper class for accessing the BLEs microphone. This reads + * samples from the microphone at 16KHz and computes the root mean squared value. + * This value is then retrieved which resets the value so it can be recalculated + * from more incomng data. + * + * This allows for a small data set that represents a few seconds of sound data. + * This is not enough to reproduce the audio, but is enough to train or use a + * TinyML model. + */ + class Mic + { + public: + /** + * @brief Initializes the Mic class + */ + Mic() : _hasData(false) + { + } + + /** + * @brief Setup the PDM library to access the microphone at a sample rate of 16KHz + */ + void begin() + { + PDM.begin(1, 16000); + PDM.setGain(20); + } + + /** + * @brief Gets if the microphone has new data + * @return true if the microphone has new data, otherwise false + */ + bool hasData() + { + return _hasData; + } + + /** + * @brief Get the root mean squared data value from the microphone + * @return The root mean square value of the last data point from the microphone + */ + int16_t data() + { + return _rms; + } + + /** + * @brief Gets the last root mean squared value and resets the calculation + * @return The last root mean square value + */ + int16_t pop() + { + int16_t rms = data(); + reset(); + return rms; + } + + /** + * @brief Reads the audio data from the PDM object and calculates the + * root mean square value + */ + void update() + { + // Get the available bytes from the microphone + int bytesAvailable = PDM.available(); + + // Check we have a full buffers worth + if (bytesAvailable == MICROPHONE_BUFFER_SIZE_IN_BYTES) + { + // Read from the buffer + int16_t _buffer[MICROPHONE_BUFFER_SIZE_IN_WORDS]; + + _hasData = true; + PDM.read(_buffer, bytesAvailable); + + // Calculate a running root mean square value + arm_rms_q15((q15_t *)_buffer, MICROPHONE_BUFFER_SIZE_IN_WORDS, (q15_t *)&_rms); + } + } + + /** + * @brief Mark data as read + */ + void reset() + { + _hasData = false; + } + + private: + int16_t _rms; + bool _hasData; + }; + ``` + + This code defines a class called `Mic` that uses the [Arduino `PDM` library](https://www.arduino.cc/en/Reference/PDM) to get data from the microphone at a sample rate of 16KHz, then uses the `arm_math` library to calculate the RMS of the incoming data from the microphone buffer. + + The PDM buffer is read in blocks of 256 2-byte values, with each full buffer converted to a single RMS value. Once the buffer has been converted to a single RMS value, that value can be read and the next full buffer converted to a new value. + + A 256 value buffer at a sample rate of 16KHz is enough for 1/64th of a second, so 64 RMS value will be created for each second of audio. + + You can read the code and the comments to understand how each call works. + +### Add the program code + +The main program now needs to be written to read data from the microphone at regular intervals, then output the RMS values to the serial port. The data that is captured needs to be stored somewhere so that it can be used to train the model, and the easiest way to do this is to output it from the Arduino device to the serial port, then monitor that serial port using PlatformIO. + +1. Open the `main.cpp` file + +1. Replace the existing code in this file with the following: + + ```cpp + /** + * Audio Capture + * + * This program listens on the microphone capturing audio data. + * Instead of capturing raw data, it captures root mean squared values allowing + * audio to be reduced to a few values instead of thousands of values every second. + * These values are then output to the serial port - one line per audio sample. + * + * This data can then be used to train a TinyML model to classify audio. + */ + + #include "mic.h" + + // Settings for the audio + // Try tuning these if you need different results + // 128 samples is enough for 2 seconds of audio - it's captured at 64 samples per second + #define SAMPLES 128 + #define GAIN (1.0f / 50) + #define SOUND_THRESHOLD 1000 + + // An array of the audio samples + float features[SAMPLES]; + + // A wrapper for the microphone + Mic mic; + + /** + * @brief PDM callback to update the data in the mic object + */ + void onAudio() + { + mic.update(); + } + + /** + * @brief Read given number of samples from mic + * @return True if there is enough data captured from the microphone, + * otherwise False + */ + bool recordAudioSample() + { + // Check the microphone class as captured enough data + if (mic.hasData() && mic.pop() > SOUND_THRESHOLD) + { + // Loop through the samples, waiting to capture data if needed + for (int i = 0; i < SAMPLES; i++) + { + while (!mic.hasData()) + delay(1); + + // Add the features to the array + features[i] = mic.pop() * GAIN; + } + + // Return that we have features + return true; + } + + // Return that we don't have enough data yet + return false; + } + + /** + * @brief Sets up the serial port and the microphone + */ + void setup() + { + // Start the serial connection so the captured audio data can be output + Serial.begin(115200); + + // Set up the microphone callback + PDM.onReceive(onAudio); + + // Start listening + mic.begin(); + + // Wait 3 seconds for everything to get started + delay(3000); + } + + /** + * @brief Runs continuously capturing audio data and writing it to + * the serial port + */ + void loop() + { + // wait for audio data + if (recordAudioSample()) + { + // print the audio data to serial port + for (int i = 0; i < SAMPLES; i++) + { + Serial.print(features[i], 6); + + // Seperate the audio values with commas, at the last value + // send a newline + Serial.print(i == SAMPLES - 1 ? '\n' : ','); + } + + // Wait between samples + delay(1000); + } + + // Sleep to allow background microphone processing + delay(20); + } + ``` + + This code sets up the microphone in the `setup` function. It then registers for callbacks from the microphone when the 256 2-byte value buffer is full. + + Each time a callback is fired, the code will check to see if the RMS value exceeds a threshold and if it does start gathering data. This is done to ignore silence. + + Once sound is detected, 128 RMS values are read from the buffer - enough for 2 seconds of data. This is then written as comma-separated values to the serial port, with a new line after each 2-seconds of data. This means the output will be a valid CSV file, with one line per audio sample. + + > If you need different length audio recordings, then change the value of `SAMPLES` to 64 x the number of seconds. For example, for 4 seconds of data set it to 256. Be warned that the longer the audio, the bigger the model - too long will exceed the available RAM when building the final project. + +## Run the code + +Now the code is written, it can be deployed to the Arduino device and run to capture data. + +### Upload the code to the device + + 1. Connect your Arduino board to your PC or Mac using a USB cable + +1. From Visual Studio Code, launch the Command Palette. This is a pop-up menu that allows you to run actions from VS Code as well as any extensions installed. + + 1. If you are using Windows or Linux, press `Ctrl+Shift+p` + 1. If you are using macOS, press `Command+Shift+p` + +1. Search for `PlatformIO: Upload` by typing in the box, then select that option + + ![The upload option](../../../images/vscode-command-palette-platformio-upload.png) + +The relevant tooling will be installed, and the code will be compiled for the device. Once built, it will be uploaded to the Arduino. You will see the progress in the VS Code terminal. + +```output +Linking .pio/build/nano33ble/firmware.elf +Checking size .pio/build/nano33ble/firmware.elf +Building .pio/build/nano33ble/firmware.bin +Advanced Memory Usage is available via "PlatformIO Home > Project Inspect" +RAM: [== ] 16.8% (used 44080 bytes from 262144 bytes) +Flash: [= ] 8.2% (used 80236 bytes from 983040 bytes) +================================================ [SUCCESS] Took 12.28 seconds ================================================ +``` + +### Monitor the output + +The serial output of the Arduino is the USB cable that is used to connect it to your PC or Mac. You can monitor the data being sent using the PlatformIO serial monitor. + +1. Launch the serial monitor using one of these methods: + * Open the VS Code Command Palette, search for `PlatformIO: Serial Monitor` and select this option + + ![The serial monitor command palette option](../../../images/vscode-command-palette-platformio-serial-monitor.png) + + * Select the Serial Monitor button from the status bar + + ![The serial monitor button](../../../images/vscode-status-bar-platformio-serial-monitor-button.png) + +The serial monitor will listen for all messages from the device. You'll see nothing output to start with. + +### Capture audio data + +To train the model, you will need multiple audio samples for each label you want to classify - for example if you want to detect the difference between "Yes" and "No", you will need multiple samples for the word "Yes", and multiple for the word "No". Ideally you'd want between 15-30 samples per label. + +1. Make the relevant sound into the microphone on the Arduino device, either by speaking in to it, or playing the relevant audio. You will see a line of samples appear on the serial monitor output. + +1. Repeat this multiple times, slightly adjusting the position of the microphone relative to the audio. You want 15-30 samples. Each new sample will be a new line. + + ```output + > Executing task: pio device monitor < + + --- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time + --- More details at http://bit.ly/pio-monitor-filters + --- Miniterm on /dev/cu.usbmodem101 9600,8,N,1 --- + --- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- + 25.599998,42.820000,13.540000,31.559999,43.279999,45.919998,46.059998,44.919998,40.459999,29.840000,30.500000,27.799999,23.459999,19.139999,16.980000,9.559999,8.860000,6.260000,0.000000,3.620000,0.000000,3.620000,8.080000,11.440000,12.540000,15.339999,14.020000,14.020000,12.000000,10.860000,8.860000,6.260000,3.620000,0.000000,0.000000,0.000000,3.620000,3.620000,6.260000,8.860000,9.559999,10.860000,11.440000,12.540000,13.040000,13.540000,14.020000,14.920000,15.780000,16.180000,16.580000,16.580000,16.980000,16.980000,17.359999,16.980000,16.580000,16.180000,15.780000,14.920000,14.480000,13.540000,13.040000,12.540000,11.440000,10.860000,10.219999,9.559999,8.860000,8.080000,8.080000,7.240000,7.240000,6.260000,6.260000,5.100000,5.100000,5.100000,5.100000,3.620000,3.620000,3.620000,3.620000,3.620000,3.620000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000 + 38.299999,34.139999,31.139999,27.799999,19.480000,7.240000,7.240000,8.860000,17.719999,20.779999,23.740000,21.400000,21.100000,20.779999,18.459999,15.339999,10.860000,5.100000,3.620000,0.000000,0.000000,0.000000,0.000000,0.000000,3.620000,3.620000,5.100000,5.100000,6.260000,8.080000,9.559999,11.440000,12.540000,14.020000,14.480000,15.339999,15.780000,16.180000,16.980000,17.359999,17.719999,18.100000,17.719999,17.719999,16.980000,16.580000,15.780000,15.339999,14.480000,13.540000,12.540000,12.000000,10.860000,10.219999,8.860000,8.860000,8.080000,7.240000,7.240000,6.260000,5.100000,5.100000,5.100000,3.620000,3.620000,3.620000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000,0.000000 + ``` + +1. Copy all the data from the serial monitor into a text file. Save this as `